diff options
author | Michael Davis | 2022-09-22 01:30:42 +0000 |
---|---|---|
committer | GitHub | 2022-09-22 01:30:42 +0000 |
commit | 6e168b5099c31d0681e3486d177a14d04dde4c20 (patch) | |
tree | 265724473244688631071f63e7745a35994de31f | |
parent | 1dd1476a9eea7f6cb7d66239782e032b3c8672aa (diff) |
Improve keymap errors from command typos (#3931)
* Improve keymap errors from command typos
Currently, opening helix with a config containing a bad command mapping
fails with a cryptic error. For example, say we have a config (bad.toml)
with a command name that doesn't exist:
[keys.normal]
b = "buffer_close" # should be ":buffer-close"
When we `hx -c bad.toml`, we get...
> Bad config: data did not match any variant of untagged enum KeyTrie for key `keys.normal` at line 1 column 1
> Press <ENTER> to continue with default config
This is because of the way that Serde tries to deserialize untagged
enums such as `helix_term::keymap::KeyTrie`. From the Serde docs[^1]:
> Serde will try to match the data against each variant in order and the
> first one that deserializes successfully is the one returned.
`MappableCommand::deserialize` fails (returns an Err variant) when a
command does not exist. Serde interprets this as the `KeyTrie::Leaf`
variant failing to match and declares that the input data doesn't
"match any variant of untagged enum KeyTrie."
Luckily the variants of KeyTrie are orthogonal in structure: we can tell
them apart by the type hints from a `serde::de::Visitor`. This change
uses a custom Deserialize implementation along with a Visitor that
discerns which variant of the KeyTrie applies. With this change, the
above failure becomes:
> Bad config: No command named 'buffer_close' for key `keys.normal.b` at line 2 column 5
> Press <ENTER> to continue with default config
We also provide more explicit information about the expectations on
the field. A config with an unexpected type produces a message with
that information and the expectation:
[keys.normal]
b = 1
> Bad config: invalid type: integer `1`, expected a command, list of commands, or sub-keymap for key `keys.normal.b` at line 2 column 5
> Press <ENTER> to continue with default config
[^1]: https://serde.rs/enum-representations.html#untagged
* Update helix-term/src/keymap.rs
Co-authored-by: Ivan Tham <pickfire@riseup.net>
Co-authored-by: Ivan Tham <pickfire@riseup.net>
-rw-r--r-- | helix-term/src/keymap.rs | 60 |
1 files changed, 58 insertions, 2 deletions
diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 59204889..088b3b6d 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -144,14 +144,70 @@ impl DerefMut for KeyTrieNode { } } -#[derive(Debug, Clone, PartialEq, Deserialize)] -#[serde(untagged)] +#[derive(Debug, Clone, PartialEq)] pub enum KeyTrie { Leaf(MappableCommand), Sequence(Vec<MappableCommand>), Node(KeyTrieNode), } +impl<'de> Deserialize<'de> for KeyTrie { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_any(KeyTrieVisitor) + } +} + +struct KeyTrieVisitor; + +impl<'de> serde::de::Visitor<'de> for KeyTrieVisitor { + type Value = KeyTrie; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "a command, list of commands, or sub-keymap") + } + + fn visit_str<E>(self, command: &str) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + command + .parse::<MappableCommand>() + .map(KeyTrie::Leaf) + .map_err(E::custom) + } + + fn visit_seq<S>(self, mut seq: S) -> Result<Self::Value, S::Error> + where + S: serde::de::SeqAccess<'de>, + { + let mut commands = Vec::new(); + while let Some(command) = seq.next_element::<&str>()? { + commands.push( + command + .parse::<MappableCommand>() + .map_err(serde::de::Error::custom)?, + ) + } + Ok(KeyTrie::Sequence(commands)) + } + + fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error> + where + M: serde::de::MapAccess<'de>, + { + let mut mapping = HashMap::new(); + let mut order = Vec::new(); + while let Some((key, value)) = map.next_entry::<KeyEvent, KeyTrie>()? { + mapping.insert(key, value); + order.push(key); + } + Ok(KeyTrie::Node(KeyTrieNode::new("", mapping, order))) + } +} + impl KeyTrie { pub fn node(&self) -> Option<&KeyTrieNode> { match *self { |