From e8069b0d00aea0571adfc71a92e856bc192b6e5a Mon Sep 17 00:00:00 2001 From: shubhook Date: Thu, 15 Oct 2026 18:33:54 +0530 Subject: [PATCH] editor: implement command registry and keymap engine with modes Fill in the T-106 stubs: keystroke parsing and normal form, the command registry with the ten editor commands, keymaps with modes and bindings indexed by first keystroke, and the dispatcher with chords, precedence, typing, pending operators and generation-based keymap switching. Remove the #[ignore] lines from the acceptance tests and add the keymap/resolve_10k_keys_300_bindings benchmark. Refs: T-106, ED-4, ED-6 --- crates/helm-bench/Cargo.toml | 4 + crates/helm-bench/benches/keymap.rs | 143 ++++++++++++++ crates/helm-editor/src/command.rs | 103 +++++++--- crates/helm-editor/src/dispatch.rs | 155 +++++++++++++-- crates/helm-editor/src/editor_commands.rs | 67 ++++++- crates/helm-editor/src/key.rs | 181 +++++++++++++++--- crates/helm-editor/src/keymap.rs | 120 +++++++++--- crates/helm-editor/tests/commands.rs | 11 -- crates/helm-editor/tests/dispatch.rs | 11 -- crates/helm-editor/tests/keymap_properties.rs | 4 - crates/helm-editor/tests/keymaps.rs | 25 --- crates/helm-editor/tests/keys.rs | 12 -- 12 files changed, 672 insertions(+), 164 deletions(-) create mode 100644 crates/helm-bench/benches/keymap.rs diff --git a/crates/helm-bench/Cargo.toml b/crates/helm-bench/Cargo.toml index 63947f0..aaaec85 100644 --- a/crates/helm-bench/Cargo.toml +++ b/crates/helm-bench/Cargo.toml @@ -48,3 +48,7 @@ harness = false name = "editor" harness = false + +[[bench]] +name = "keymap" +harness = false diff --git a/crates/helm-bench/benches/keymap.rs b/crates/helm-bench/benches/keymap.rs new file mode 100644 index 0000000..89e2e47 --- /dev/null +++ b/crates/helm-bench/benches/keymap.rs @@ -0,0 +1,143 @@ +//! Key resolution benchmark for `helm-editor`, gated by the `bench` CI job +//! against `budgets.toml`. NF-1 gives a keypress 5 ms from input to pixel, +//! and resolving the key to a command is the first step. + +use std::hint::black_box; + +use criterion::{Criterion, criterion_group, criterion_main}; +use helm_editor::{ + Binding, CommandRegistry, Dispatcher, KeyEvent, Keymap, Keystroke, Mode, Resolution, Typing, +}; + +/// The chars bound with modifiers, as chord tails, and in normal mode. +const CHARS: &str = "abcdefghijklmnopqrstuvwxyz0123456789"; + +/// The named keys bound in insert mode with the `editor` context. +const NAMED: [&str; 12] = [ + "enter", + "tab", + "backspace", + "delete", + "escape", + "left", + "right", + "up", + "down", + "home", + "end", + "pageup", +]; + +/// How many times the 100-event list repeats per iteration. +const REPEATS: usize = 100; + +/// The two-mode keymap with the 300 bindings the task file lists, each +/// running a distinct command id. +fn build_keymap() -> Keymap { + let mut keymap = Keymap::new( + "bench", + vec![ + Mode::new("insert", Typing::Insert), + Mode::new("normal", Typing::Ignore), + ], + ) + .expect("distinct modes"); + let mut bindings = Vec::new(); + for prefix in ["ctrl-", "alt-", "ctrl-alt-", "ctrl-cmd-"] { + for c in CHARS.chars() { + bindings.push(format!("{prefix}{c}")); + } + } + for chord in ["cmd-k", "cmd-j"] { + for c in CHARS.chars() { + bindings.push(format!("{chord} {c}")); + } + } + let mut count = 0; + let mut add = |keymap: &mut Keymap, binding: Binding| { + keymap.bind(binding).expect("modes exist"); + count += 1; + }; + for (i, keys) in bindings.iter().enumerate() { + add( + &mut keymap, + Binding::run(keys, format!("bench.c{i}")).expect("valid keys"), + ); + } + for (i, c) in CHARS.chars().enumerate() { + let binding = Binding::run(&c.to_string(), format!("bench.normal{i}")).expect("valid keys"); + add(&mut keymap, binding.in_mode("normal")); + } + for (i, name) in NAMED.iter().enumerate() { + for (j, prefix) in ["", "shift-", "alt-", "cmd-"].iter().enumerate() { + let binding = Binding::run(&format!("{prefix}{name}"), format!("bench.named{i}_{j}")) + .expect("valid keys"); + add(&mut keymap, binding.in_mode("insert").when("editor")); + } + } + assert_eq!(count, 300); + keymap +} + +/// The 100 events the task file lists: 70 typed chars, 10 bound single +/// keystrokes, 5 two-key chords and 10 bound named keys, interleaved. +fn build_events() -> Vec { + let parse = |text: &str| KeyEvent::new(Keystroke::parse(text).expect("valid keystroke")); + let typed: Vec = CHARS.chars().chain([' ']).collect(); + let mut events = Vec::with_capacity(100); + for i in 0..10 { + for j in 0..7 { + let c = typed[(i * 7 + j) % typed.len()]; + events.push(if c == ' ' { + parse("space") + } else { + parse(&c.to_string()) + }); + } + let c = CHARS.as_bytes()[i * 3] as char; + let prefix = ["ctrl-", "alt-", "ctrl-alt-", "ctrl-cmd-"][i % 4]; + events.push(parse(&format!("{prefix}{c}"))); + let prefix = ["", "shift-", "alt-", "cmd-"][i % 4]; + events.push(parse(&format!("{prefix}{}", NAMED[i]))); + if i % 2 == 0 { + events.push(parse(["cmd-k", "cmd-j"][i / 2 % 2])); + events.push(parse(&(CHARS.as_bytes()[i + 10] as char).to_string())); + } + } + assert_eq!(events.len(), 100); + events +} + +fn keymap(c: &mut Criterion) { + let dispatcher = Dispatcher::new(CommandRegistry::new(), build_keymap()); + let mut state = dispatcher.key_state(); + let events = build_events(); + let contexts = ["editor"]; + + // Check once, outside the timed code, that every event resolves as the + // task file says: nothing left pending, and no key unbound. + for event in &events { + let resolution = dispatcher.resolve(&mut state, event, &contexts); + assert!( + matches!(resolution, Resolution::Run(_) | Resolution::Pending), + "{:?} resolved to {resolution:?}", + event.keystroke + ); + } + assert!(state.pending().is_empty()); + + let mut group = c.benchmark_group("keymap"); + group.bench_function("resolve_10k_keys_300_bindings", |b| { + b.iter(|| { + for _ in 0..REPEATS { + for event in &events { + black_box(dispatcher.resolve(&mut state, black_box(event), &contexts)); + } + } + }) + }); + group.finish(); +} + +criterion_group!(benches, keymap); +criterion_main!(benches); diff --git a/crates/helm-editor/src/command.rs b/crates/helm-editor/src/command.rs index 4d31e91..50dadd3 100644 --- a/crates/helm-editor/src/command.rs +++ b/crates/helm-editor/src/command.rs @@ -1,19 +1,14 @@ //! The command registry: every command has a stable string id, and runs //! against one [`Editor`] and the [`BufferSet`] that owns its buffer. -#![expect( - unused_variables, - dead_code, - reason = "T-106 interface stubs; the implementation deletes this" -)] - use std::collections::BTreeMap; +use std::collections::btree_map::Entry; use std::fmt; use std::sync::Arc; use helm_text::BufferSet; -use crate::{Editor, EditorError}; +use crate::{Editor, EditorError, editor_commands}; /// A command's id, such as `"editor.delete_backward"`. Keymaps, the /// keybindings file and the command palette name commands by id, so an id @@ -31,26 +26,26 @@ pub struct CommandId(Arc); impl CommandId { /// The id as text. pub fn as_str(&self) -> &str { - todo!() + &self.0 } } impl From<&str> for CommandId { fn from(id: &str) -> Self { - todo!() + Self(Arc::from(id)) } } impl From for CommandId { fn from(id: String) -> Self { - todo!() + Self(Arc::from(id)) } } impl fmt::Display for CommandId { /// Writes the id as it is. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - todo!() + f.write_str(&self.0) } } @@ -82,14 +77,23 @@ pub struct Invocation { impl Invocation { /// `command` with no arguments. pub fn new(command: impl Into) -> Self { - todo!() + Self { + command: command.into(), + args: CommandArgs::default(), + } } /// The insert command, /// [`editor_commands::INSERT`](crate::editor_commands::INSERT), typing /// `text`. pub fn insert(text: impl Into) -> Self { - todo!() + Self { + command: CommandId::from(editor_commands::INSERT), + args: CommandArgs { + text: Some(text.into()), + motion: None, + }, + } } } @@ -126,13 +130,15 @@ pub struct Command { /// `helm-ui`, which runs the ids it owns itself and hands the rest here. #[derive(Clone, Debug, Default)] pub struct CommandRegistry { - commands: BTreeMap, + /// Keyed by the id's text, so [`get`](Self::get) looks up a `&str`. + /// `Arc` sorts as `CommandId` does. + commands: BTreeMap, Command>, } impl CommandRegistry { /// An empty registry. pub fn new() -> Self { - todo!() + Self::default() } /// A registry holding one command for each [`Editor`] command, under @@ -142,18 +148,29 @@ impl CommandRegistry { /// [`CommandArgs::text`] and errors with /// [`CommandError::MissingText`] without it. pub fn with_editor_commands() -> Self { - todo!() + let mut registry = Self::new(); + editor_commands::register_all(&mut registry); + registry } /// Adds `command`. Errors, and adds nothing, if its id isn't in the /// form [`CommandId`] describes or is already registered. pub fn register(&mut self, command: Command) -> Result<(), RegisterError> { - todo!() + if !is_valid_id(command.id.as_str()) { + return Err(RegisterError::InvalidId { id: command.id }); + } + match self.commands.entry(Arc::clone(&command.id.0)) { + Entry::Occupied(_) => Err(RegisterError::Duplicate { id: command.id }), + Entry::Vacant(entry) => { + entry.insert(command); + Ok(()) + } + } } /// The command with id `id`, if registered. pub fn get(&self, id: &str) -> Option<&Command> { - todo!() + self.commands.get(id) } /// Every registered command, sorted by id. @@ -172,13 +189,27 @@ impl CommandRegistry { editor: &mut Editor, set: &mut BufferSet, ) -> Result<(), CommandError> { - todo!() + let command = + self.get(invocation.command.as_str()) + .ok_or_else(|| CommandError::UnknownCommand { + id: invocation.command.clone(), + })?; + (command.run)(CommandContext { + editor, + set, + args: &invocation.args, + }) } } /// Whether `id` is in the form [`CommandId`] describes. fn is_valid_id(id: &str) -> bool { - todo!() + id.split('.').all(|segment| { + !segment.is_empty() + && segment + .bytes() + .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_') + }) } /// Why [`CommandRegistry::register`] refused a command. @@ -198,7 +229,16 @@ pub enum RegisterError { impl fmt::Display for RegisterError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - todo!() + match self { + Self::InvalidId { id } => write!( + f, + "command id {:?} is not dot-separated segments of a-z, 0-9 and _", + id.as_str() + ), + Self::Duplicate { id } => { + write!(f, "command {:?} is already registered", id.as_str()) + } + } } } @@ -224,14 +264,29 @@ pub enum CommandError { impl From for CommandError { fn from(error: EditorError) -> Self { - todo!() + Self::Editor(error) } } impl fmt::Display for CommandError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - todo!() + match self { + Self::UnknownCommand { id } => { + write!(f, "no command {:?} is registered", id.as_str()) + } + Self::MissingText { id } => { + write!(f, "command {:?} needs text and got none", id.as_str()) + } + Self::Editor(error) => write!(f, "editor command failed: {error}"), + } } } -impl std::error::Error for CommandError {} +impl std::error::Error for CommandError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Editor(error) => Some(error), + _ => None, + } + } +} diff --git a/crates/helm-editor/src/dispatch.rs b/crates/helm-editor/src/dispatch.rs index 89c1416..e12e8ab 100644 --- a/crates/helm-editor/src/dispatch.rs +++ b/crates/helm-editor/src/dispatch.rs @@ -1,17 +1,11 @@ //! Turning key events into commands: the active keymap, each editor's key //! state, and the rule that picks a binding. -#![expect( - unused_variables, - dead_code, - reason = "T-106 interface stubs; the implementation deletes this" -)] - use helm_text::BufferSet; use crate::{ - CommandError, CommandId, CommandRegistry, Editor, Invocation, KeyEvent, Keymap, Keystroke, - ModeId, + Action, Binding, CommandArgs, CommandError, CommandId, CommandRegistry, Editor, Invocation, + KeyEvent, Keymap, Keystroke, ModeId, Typing, }; /// One editor's input state: its mode, the keys of a chord typed so far, @@ -35,18 +29,18 @@ pub struct KeyState { impl KeyState { /// The current mode. pub fn mode(&self) -> &ModeId { - todo!() + &self.mode } /// The keys of the chord typed so far, or an empty slice if no chord is /// pending. `helm-ui` shows these while it waits for the next key. pub fn pending(&self) -> &[Keystroke] { - todo!() + &self.pending } /// The pending operator, if one is held. pub fn operator(&self) -> Option<&CommandId> { - todo!() + self.operator.as_ref().map(|(command, _)| command) } /// Drops the pending chord and the pending operator, and returns to the @@ -54,7 +48,15 @@ impl KeyState { /// anything was pending. `helm-ui` calls this when the editor loses /// focus or the user clicks in it. pub fn cancel(&mut self) -> bool { - todo!() + let chord = !self.pending.is_empty(); + self.pending.clear(); + match self.operator.take() { + Some((_, mode)) => { + self.mode = mode; + true + } + None => chord, + } } } @@ -111,29 +113,39 @@ pub struct Dispatcher { impl Dispatcher { /// A dispatcher running commands from `registry`, with `keymap` active. pub fn new(registry: CommandRegistry, keymap: Keymap) -> Self { - todo!() + Self { + registry, + keymap, + generation: 0, + } } /// The command registry. pub fn registry(&self) -> &CommandRegistry { - todo!() + &self.registry } /// The active keymap. pub fn keymap(&self) -> &Keymap { - todo!() + &self.keymap } /// Makes `keymap` the active keymap and returns the old one. See the /// type docs for what happens to each editor's state. pub fn set_keymap(&mut self, keymap: Keymap) -> Keymap { - todo!() + self.generation += 1; + std::mem::replace(&mut self.keymap, keymap) } /// A new state for one editor: in the active keymap's initial mode, /// with nothing pending. pub fn key_state(&self) -> KeyState { - todo!() + KeyState { + mode: self.keymap.initial_mode().clone(), + pending: Vec::new(), + operator: None, + generation: self.generation, + } } /// Brings `state` up to date with the active keymap, as the type docs @@ -141,7 +153,14 @@ impl Dispatcher { /// calls this on every open editor after a switch, so a status bar /// shows the right mode before the next key. pub fn sync(&self, state: &mut KeyState) { - todo!() + if state.generation == self.generation { + return; + } + state.cancel(); + if self.keymap.mode(state.mode.as_str()).is_none() { + state.mode = self.keymap.initial_mode().clone(); + } + state.generation = self.generation; } /// Resolves one key event against the active keymap, updates `state`, @@ -193,7 +212,90 @@ impl Dispatcher { /// Mode changes happen here, before the caller runs the returned /// command, and stand even if the command then fails. pub fn resolve(&self, state: &mut KeyState, event: &KeyEvent, contexts: &[&str]) -> Resolution { - todo!() + self.sync(state); + let keystroke = event.keystroke; + let depth = state.pending.len(); + let first = state.pending.first().copied().unwrap_or(keystroke); + + // Rules 1 and 2: look for a longer binding, and rank the exact ones. + let mut prefix = false; + let mut winner: Option<((bool, bool), &Binding)> = None; + for binding in self.keymap.bindings_starting_with(first) { + let keys = binding.keys.as_slice(); + if keys.len() <= depth + || keys[depth] != keystroke + || keys[..depth] != state.pending[..] + || !applies(binding, &state.mode, contexts) + { + continue; + } + if keys.len() > depth + 1 { + prefix = true; + break; + } + // Later bindings come later, so `>=` lets one of equal rank win. + let rank = (binding.mode.is_some(), binding.context.is_some()); + if winner.is_none_or(|(best, _)| rank >= best) { + winner = Some((rank, binding)); + } + } + + if prefix { + state.pending.push(keystroke); + return Resolution::Pending; + } + if let Some((_, binding)) = winner { + state.pending.clear(); + let resolution = match &binding.action { + Some(Action::Run(command)) => match state.operator.take() { + None => Resolution::Run(Invocation::new(command.clone())), + Some((operator, mode)) => { + state.mode = mode; + Resolution::Run(Invocation { + command: operator, + args: CommandArgs { + text: None, + motion: Some(command.clone()), + }, + }) + } + }, + Some(Action::Operator(_)) if state.operator.is_some() => { + state.cancel(); + return Resolution::Cancelled; + } + Some(Action::Operator(command)) => { + state.operator = Some((command.clone(), state.mode.clone())); + Resolution::Pending + } + None => { + state.cancel(); + Resolution::Done + } + }; + if let Some(mode) = &binding.then_mode { + state.mode = mode.clone(); + } + return resolution; + } + + // Rule 3. + if state.cancel() { + return Resolution::Cancelled; + } + + // Rules 4 and 5. + let modifiers = keystroke.modifiers(); + let types = self + .keymap + .mode(state.mode.as_str()) + .is_some_and(|mode| mode.typing == Typing::Insert); + match &event.text { + Some(text) if types && !text.is_empty() && !modifiers.ctrl && !modifiers.cmd => { + Resolution::Run(Invocation::insert(text.clone())) + } + _ => Resolution::Unbound, + } } /// [`resolve`](Self::resolve), then, for [`Resolution::Run`], runs the @@ -207,6 +309,19 @@ impl Dispatcher { editor: &mut Editor, set: &mut BufferSet, ) -> Result { - todo!() + let resolution = self.resolve(state, event, contexts); + if let Resolution::Run(invocation) = &resolution { + self.registry.run(invocation, editor, set)?; + } + Ok(resolution) } } + +/// Whether `binding` applies in mode `mode` with `contexts` holding. +fn applies(binding: &Binding, mode: &ModeId, contexts: &[&str]) -> bool { + binding.mode.as_ref().is_none_or(|m| m == mode) + && binding + .context + .as_deref() + .is_none_or(|context| contexts.contains(&context)) +} diff --git a/crates/helm-editor/src/editor_commands.rs b/crates/helm-editor/src/editor_commands.rs index f7a109a..46d9109 100644 --- a/crates/helm-editor/src/editor_commands.rs +++ b/crates/helm-editor/src/editor_commands.rs @@ -5,13 +5,7 @@ //! ([`ADD_NEXT_MATCH`] and the three after it) don't edit, so they read //! the set through the context's `&mut BufferSet` without changing it. -#![expect( - unused_variables, - dead_code, - reason = "T-106 interface stubs; the implementation deletes this" -)] - -use crate::{CommandContext, CommandError, CommandRegistry}; +use crate::{Command, CommandContext, CommandError, CommandFn, CommandId, CommandRegistry}; /// [`Editor::insert`](crate::Editor::insert) with /// [`CommandArgs::text`](crate::CommandArgs::text). Keys no binding matches @@ -52,11 +46,66 @@ pub const ALL: [&str; 10] = [ /// Registers one command for each id in [`ALL`]. pub(crate) fn register_all(registry: &mut CommandRegistry) { - todo!() + let commands: [(&str, &'static str, CommandFn); 10] = [ + (INSERT, "Insert text", insert), + (NEWLINE, "Insert line break", |cx| { + cx.editor.newline(cx.set)?; + Ok(()) + }), + (DELETE_BACKWARD, "Delete backward", |cx| { + cx.editor.delete_backward(cx.set)?; + Ok(()) + }), + (DELETE_FORWARD, "Delete forward", |cx| { + cx.editor.delete_forward(cx.set)?; + Ok(()) + }), + (ADD_NEXT_MATCH, "Add selection to next match", |cx| { + cx.editor.add_next_match(cx.set)?; + Ok(()) + }), + (SELECT_ALL_MATCHES, "Select all matches", |cx| { + cx.editor.select_all_matches(cx.set)?; + Ok(()) + }), + (ADD_CURSOR_ABOVE, "Add cursor above", |cx| { + cx.editor.add_cursor_above(cx.set)?; + Ok(()) + }), + (ADD_CURSOR_BELOW, "Add cursor below", |cx| { + cx.editor.add_cursor_below(cx.set)?; + Ok(()) + }), + (UNDO, "Undo", |cx| { + cx.editor.undo(cx.set)?; + Ok(()) + }), + (REDO, "Redo", |cx| { + cx.editor.redo(cx.set)?; + Ok(()) + }), + ]; + for (id, title, run) in commands { + registry + .register(Command { + id: CommandId::from(id), + title, + run, + }) + .expect("editor command ids are valid and distinct"); + } } /// [`INSERT`]: errors with [`CommandError::MissingText`] when the /// invocation has no text. fn insert(cx: CommandContext<'_>) -> Result<(), CommandError> { - todo!() + let text = cx + .args + .text + .as_deref() + .ok_or_else(|| CommandError::MissingText { + id: CommandId::from(INSERT), + })?; + cx.editor.insert(cx.set, text)?; + Ok(()) } diff --git a/crates/helm-editor/src/key.rs b/crates/helm-editor/src/key.rs index e47109d..50728cf 100644 --- a/crates/helm-editor/src/key.rs +++ b/crates/helm-editor/src/key.rs @@ -2,12 +2,6 @@ //! it types. `helm-ui` turns each platform key event into a [`KeyEvent`] //! (Phase 2), so nothing below it sees a GPUI type. -#![expect( - unused_variables, - dead_code, - reason = "T-106 interface stubs; the implementation deletes this" -)] - use std::fmt; /// A key, apart from the modifiers held with it. @@ -140,31 +134,109 @@ pub struct Keystroke { impl Keystroke { /// `key` with `modifiers`, in normal form: Shift is folded into a /// [`Key::Char`] as the type docs describe. - pub fn new(key: Key, modifiers: Modifiers) -> Self { - todo!() + pub fn new(key: Key, mut modifiers: Modifiers) -> Self { + let key = match key { + Key::Char(c) if modifiers.shift => { + modifiers.shift = false; + Key::Char(c.to_ascii_uppercase()) + } + key => key, + }; + Self { key, modifiers } } /// The key. pub fn key(&self) -> Key { - todo!() + self.key } /// The modifiers. `shift` is never set when the key is a /// [`Key::Char`]. pub fn modifiers(&self) -> Modifiers { - todo!() + self.modifiers } /// Reads one keystroke in the notation the type docs describe, such as /// `"cmd-s"`, `"ctrl-shift-tab"` or `"A"`. pub fn parse(text: &str) -> Result { - todo!() + if text.trim().is_empty() { + return Err(KeyParseError::Empty); + } + let keystroke = || text.to_owned(); + let mut modifiers = Modifiers::NONE; + let mut rest = text; + loop { + let (held, after) = if let Some(after) = rest.strip_prefix("ctrl-") { + (&mut modifiers.ctrl, after) + } else if let Some(after) = rest.strip_prefix("alt-") { + (&mut modifiers.alt, after) + } else if let Some(after) = rest.strip_prefix("shift-") { + (&mut modifiers.shift, after) + } else if let Some(after) = rest.strip_prefix("cmd-") { + (&mut modifiers.cmd, after) + } else { + break; + }; + if *held { + return Err(KeyParseError::RepeatedModifier { + keystroke: keystroke(), + }); + } + if after.is_empty() { + return Err(KeyParseError::MissingKey { + keystroke: keystroke(), + }); + } + *held = true; + rest = after; + } + let key = parse_key(rest).ok_or_else(|| KeyParseError::UnknownKey { + keystroke: keystroke(), + })?; + if let Key::Char(c) = key + && modifiers.shift + && !c.is_ascii_alphabetic() + { + return Err(KeyParseError::ShiftedChar { + keystroke: keystroke(), + }); + } + Ok(Self::new(key, modifiers)) } } impl fmt::Display for Keystroke { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - todo!() + let m = self.modifiers; + for (held, prefix) in [ + (m.ctrl, "ctrl-"), + (m.alt, "alt-"), + (m.shift, "shift-"), + (m.cmd, "cmd-"), + ] { + if held { + f.write_str(prefix)?; + } + } + let name = match self.key { + Key::Char(' ') => "space", + Key::Char(c) => return write!(f, "{c}"), + Key::F(n) => return write!(f, "f{n}"), + Key::Enter => "enter", + Key::Tab => "tab", + Key::Backspace => "backspace", + Key::Delete => "delete", + Key::Escape => "escape", + Key::Left => "left", + Key::Right => "right", + Key::Up => "up", + Key::Down => "down", + Key::Home => "home", + Key::End => "end", + Key::PageUp => "pageup", + Key::PageDown => "pagedown", + }; + f.write_str(name) } } @@ -172,7 +244,37 @@ impl fmt::Display for Keystroke { /// stands for in the notation: one char other than a control char, or a /// key name. `None` for anything else. fn parse_key(text: &str) -> Option { - todo!() + let key = match text { + "enter" => Key::Enter, + "tab" => Key::Tab, + "backspace" => Key::Backspace, + "delete" => Key::Delete, + "escape" => Key::Escape, + "space" => Key::Char(' '), + "left" => Key::Left, + "right" => Key::Right, + "up" => Key::Up, + "down" => Key::Down, + "home" => Key::Home, + "end" => Key::End, + "pageup" => Key::PageUp, + "pagedown" => Key::PageDown, + _ => { + let mut chars = text.chars(); + if let (Some(c), None) = (chars.next(), chars.next()) { + return (!c.is_control()).then_some(Key::Char(c)); + } + // `f1` to `f20`, digits only and no leading zero, so the name + // reads back as `Display` writes it. + let digits = text.strip_prefix('f')?; + if digits.starts_with('0') || !digits.bytes().all(|b| b.is_ascii_digit()) { + return None; + } + let n: u8 = digits.parse().ok()?; + return (1..=20).contains(&n).then_some(Key::F(n)); + } + }; + Some(key) } /// One or more keystrokes pressed one after another. A sequence of two or @@ -186,33 +288,45 @@ pub struct KeySequence { impl KeySequence { /// The sequence `keys`, or `None` if `keys` is empty. pub fn new(keys: Vec) -> Option { - todo!() + (!keys.is_empty()).then_some(Self { keys }) } /// Reads keystrokes separated by whitespace, each in /// [`Keystroke`]'s notation: `"cmd-k cmd-s"`, `"g g"`. Errors with /// [`KeyParseError::Empty`] if there is none. pub fn parse(text: &str) -> Result { - todo!() + let keys = text + .split_whitespace() + .map(Keystroke::parse) + .collect::, _>>()?; + Self::new(keys).ok_or(KeyParseError::Empty) } /// The keystrokes, in the order they are pressed. pub fn as_slice(&self) -> &[Keystroke] { - todo!() + &self.keys } } impl From for KeySequence { /// A sequence of one keystroke. fn from(keystroke: Keystroke) -> Self { - todo!() + Self { + keys: vec![keystroke], + } } } impl fmt::Display for KeySequence { /// The keystrokes in [`Keystroke`]'s notation, joined by one space. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - todo!() + for (i, keystroke) in self.keys.iter().enumerate() { + if i > 0 { + f.write_str(" ")?; + } + write!(f, "{keystroke}")?; + } + Ok(()) } } @@ -240,19 +354,27 @@ impl KeyEvent { /// else, `None`. So `a` types `"a"`, `A` types `"A"`, `space` types /// `" "`, and `ctrl-a`, `alt-a` and `enter` type nothing. pub fn new(keystroke: Keystroke) -> Self { - todo!() + let m = keystroke.modifiers; + let text = match keystroke.key { + Key::Char(c) if !(m.alt || m.ctrl || m.cmd) => Some(c.to_string()), + _ => None, + }; + Self { keystroke, text } } /// A key event that types `text`, whatever the keystroke. `helm-ui` /// uses this when the platform reports text the keystroke alone /// doesn't give, such as `"å"` for `alt-a`. pub fn with_text(keystroke: Keystroke, text: impl Into) -> Self { - todo!() + Self { + keystroke, + text: Some(text.into()), + } } /// [`Keystroke::parse`], then [`new`](Self::new). pub fn parse(text: &str) -> Result { - todo!() + Keystroke::parse(text).map(Self::new) } } @@ -288,7 +410,22 @@ pub enum KeyParseError { impl fmt::Display for KeyParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - todo!() + match self { + Self::Empty => f.write_str("no keystroke given"), + Self::MissingKey { keystroke } => { + write!(f, "keystroke {keystroke:?} has modifiers but no key") + } + Self::RepeatedModifier { keystroke } => { + write!(f, "keystroke {keystroke:?} repeats a modifier") + } + Self::UnknownKey { keystroke } => { + write!(f, "keystroke {keystroke:?} names no known key") + } + Self::ShiftedChar { keystroke } => write!( + f, + "keystroke {keystroke:?} puts shift before a char that isn't an ASCII letter; write the shifted char instead" + ), + } } } diff --git a/crates/helm-editor/src/keymap.rs b/crates/helm-editor/src/keymap.rs index 0d38d9f..5d9fb01 100644 --- a/crates/helm-editor/src/keymap.rs +++ b/crates/helm-editor/src/keymap.rs @@ -1,16 +1,11 @@ //! Keymaps as data: the modes a keymap has, and bindings from key sequences //! to commands, each scoped by mode and by an optional context. -#![expect( - unused_variables, - dead_code, - reason = "T-106 interface stubs; the implementation deletes this" -)] - +use std::collections::HashMap; use std::fmt; use std::sync::Arc; -use crate::{CommandId, KeyParseError, KeySequence}; +use crate::{CommandId, KeyParseError, KeySequence, Keystroke}; /// A mode's id, such as `"normal"` or `"insert"`. Unique within a keymap. /// @@ -21,26 +16,26 @@ pub struct ModeId(Arc); impl ModeId { /// The id as text. pub fn as_str(&self) -> &str { - todo!() + &self.0 } } impl From<&str> for ModeId { fn from(id: &str) -> Self { - todo!() + Self(Arc::from(id)) } } impl From for ModeId { fn from(id: String) -> Self { - todo!() + Self(Arc::from(id)) } } impl fmt::Display for ModeId { /// Writes the id as it is. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - todo!() + f.write_str(&self.0) } } @@ -68,7 +63,10 @@ pub struct Mode { impl Mode { /// A mode with id `id`. pub fn new(id: impl Into, typing: Typing) -> Self { - todo!() + Self { + id: id.into(), + typing, + } } } @@ -110,34 +108,55 @@ impl Binding { /// Keys `keys`, in [`KeySequence`]'s notation, that run `command`, in /// every mode and context. pub fn run(keys: &str, command: impl Into) -> Result { - todo!() + Self::with_action(keys, Some(Action::Run(command.into()))) } /// Keys `keys` that hold `command` as the pending operator, in every /// mode and context. pub fn operator(keys: &str, command: impl Into) -> Result { - todo!() + Self::with_action(keys, Some(Action::Operator(command.into()))) } /// Keys `keys` that only switch to mode `mode`, in every mode and /// context. pub fn switch_mode(keys: &str, mode: impl Into) -> Result { - todo!() + Ok(Self::with_action(keys, None)?.then_mode(mode)) } /// This binding, applying only in mode `mode`. pub fn in_mode(self, mode: impl Into) -> Self { - todo!() + Self { + mode: Some(mode.into()), + ..self + } } /// This binding, applying only while context `context` holds. pub fn when(self, context: impl Into) -> Self { - todo!() + Self { + context: Some(context.into()), + ..self + } } /// This binding, also switching to mode `mode` when it matches. pub fn then_mode(self, mode: impl Into) -> Self { - todo!() + Self { + then_mode: Some(mode.into()), + ..self + } + } + + /// Keys `keys` doing `action`, in every mode and context, with no mode + /// switch. + fn with_action(keys: &str, action: Option) -> Result { + Ok(Self { + keys: KeySequence::parse(keys)?, + mode: None, + context: None, + action, + then_mode: None, + }) } } @@ -161,6 +180,10 @@ pub struct Keymap { modes: Vec, /// In the order they were added. bindings: Vec, + /// The index into `bindings` of every binding whose keys start with + /// the keystroke, in the order they were added. Resolving a key only + /// looks at one of these lists, not every binding. + by_first_key: HashMap>, } impl Keymap { @@ -170,27 +193,42 @@ impl Keymap { /// Errors with [`KeymapError::NoModes`] if `modes` is empty, and with /// [`KeymapError::DuplicateMode`] if two modes share an id. pub fn new(name: impl Into, modes: Vec) -> Result { - todo!() + if modes.is_empty() { + return Err(KeymapError::NoModes); + } + for (i, mode) in modes.iter().enumerate() { + if modes[..i].iter().any(|earlier| earlier.id == mode.id) { + return Err(KeymapError::DuplicateMode { + mode: mode.id.clone(), + }); + } + } + Ok(Self { + name: name.into(), + modes, + bindings: Vec::new(), + by_first_key: HashMap::new(), + }) } /// The keymap's name, such as `"vscode"` or `"vim"`. pub fn name(&self) -> &str { - todo!() + &self.name } /// The modes, the initial mode first. pub fn modes(&self) -> &[Mode] { - todo!() + &self.modes } /// The mode with id `id`, if the keymap has it. pub fn mode(&self, id: &str) -> Option<&Mode> { - todo!() + self.modes.iter().find(|mode| mode.id.as_str() == id) } /// The id of the first mode. pub fn initial_mode(&self) -> &ModeId { - todo!() + &self.modes[0].id } /// Adds `binding` after every binding added before it. @@ -199,12 +237,36 @@ impl Keymap { /// `binding.mode` or `binding.then_mode` names a mode the keymap /// doesn't have. pub fn bind(&mut self, binding: Binding) -> Result<(), KeymapError> { - todo!() + for mode in [&binding.mode, &binding.then_mode].into_iter().flatten() { + if self.mode(mode.as_str()).is_none() { + return Err(KeymapError::UnknownMode { mode: mode.clone() }); + } + } + let first = binding.keys.as_slice()[0]; + self.by_first_key + .entry(first) + .or_default() + .push(self.bindings.len()); + self.bindings.push(binding); + Ok(()) } /// The bindings, in the order they were added. pub fn bindings(&self) -> &[Binding] { - todo!() + &self.bindings + } + + /// Every binding whose keys start with `first`, in the order they were + /// added. + pub(crate) fn bindings_starting_with( + &self, + first: Keystroke, + ) -> impl Iterator { + self.by_first_key + .get(&first) + .into_iter() + .flatten() + .map(|&i| &self.bindings[i]) } } @@ -227,7 +289,13 @@ pub enum KeymapError { impl fmt::Display for KeymapError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - todo!() + match self { + Self::NoModes => f.write_str("a keymap needs at least one mode"), + Self::DuplicateMode { mode } => write!(f, "two modes have the id {:?}", mode.as_str()), + Self::UnknownMode { mode } => { + write!(f, "the keymap has no mode {:?}", mode.as_str()) + } + } } } diff --git a/crates/helm-editor/tests/commands.rs b/crates/helm-editor/tests/commands.rs index 2691713..13a158f 100644 --- a/crates/helm-editor/tests/commands.rs +++ b/crates/helm-editor/tests/commands.rs @@ -50,7 +50,6 @@ fn type_motion(cx: CommandContext<'_>) -> Result<(), CommandError> { } #[test] -#[ignore = "T-106: not implemented yet"] fn command_ids_compare_as_text() { let id = CommandId::from("editor.undo"); assert_eq!(id.as_str(), "editor.undo"); @@ -61,7 +60,6 @@ fn command_ids_compare_as_text() { } #[test] -#[ignore = "T-106: not implemented yet"] fn editor_command_ids_are_the_documented_ones() { assert_eq!( editor_commands::ALL, @@ -83,7 +81,6 @@ fn editor_command_ids_are_the_documented_ones() { } #[test] -#[ignore = "T-106: not implemented yet"] fn registers_every_editor_command_sorted_with_a_title() { let registry = CommandRegistry::with_editor_commands(); let ids: Vec<&str> = registry.commands().map(|c| c.id.as_str()).collect(); @@ -97,7 +94,6 @@ fn registers_every_editor_command_sorted_with_a_title() { } #[test] -#[ignore = "T-106: not implemented yet"] fn register_checks_the_id() { let mut registry = CommandRegistry::new(); for id in ["a", "editor.x", "vim.delete_line", "a1.b_2.c3", "_"] { @@ -133,7 +129,6 @@ fn register_checks_the_id() { } #[test] -#[ignore = "T-106: not implemented yet"] fn editor_commands_cannot_be_registered_twice() { let mut registry = CommandRegistry::with_editor_commands(); assert_eq!( @@ -145,7 +140,6 @@ fn editor_commands_cannot_be_registered_twice() { } #[test] -#[ignore = "T-106: not implemented yet"] fn each_editing_command_runs_its_editor_method() { let registry = CommandRegistry::with_editor_commands(); let (mut set, mut editor) = setup("ab\r\ncd", &[Selection::cursor(1)]); @@ -173,7 +167,6 @@ fn each_editing_command_runs_its_editor_method() { } #[test] -#[ignore = "T-106: not implemented yet"] fn each_selection_command_runs_its_editor_method() { let registry = CommandRegistry::with_editor_commands(); let run = |id: &str, editor: &mut Editor, set: &mut BufferSet| { @@ -210,7 +203,6 @@ fn each_selection_command_runs_its_editor_method() { } #[test] -#[ignore = "T-106: not implemented yet"] fn insert_without_text_is_an_error() { let registry = CommandRegistry::with_editor_commands(); let (mut set, mut editor) = setup("ab", &[Selection::cursor(1)]); @@ -228,7 +220,6 @@ fn insert_without_text_is_an_error() { } #[test] -#[ignore = "T-106: not implemented yet"] fn unknown_commands_and_editor_errors_are_reported() { let registry = CommandRegistry::with_editor_commands(); let (mut set, mut editor) = setup("ab", &[Selection::cursor(1)]); @@ -260,7 +251,6 @@ fn unknown_commands_and_editor_errors_are_reported() { } #[test] -#[ignore = "T-106: not implemented yet"] fn run_passes_the_invocation_args() { let mut registry = CommandRegistry::new(); registry @@ -283,7 +273,6 @@ fn run_passes_the_invocation_args() { } #[test] -#[ignore = "T-106: not implemented yet"] fn invocation_constructors() { assert_eq!( Invocation::new("editor.undo"), diff --git a/crates/helm-editor/tests/dispatch.rs b/crates/helm-editor/tests/dispatch.rs index 8e9cb54..0d87b88 100644 --- a/crates/helm-editor/tests/dispatch.rs +++ b/crates/helm-editor/tests/dispatch.rs @@ -133,7 +133,6 @@ impl Session { } #[test] -#[ignore = "T-106: not implemented yet"] fn typing_goes_into_the_buffer() { let mut s = Session::new(vscode_like(), "", &[Selection::cursor(0)]); s.type_text("hello world"); @@ -145,7 +144,6 @@ fn typing_goes_into_the_buffer() { } #[test] -#[ignore = "T-106: not implemented yet"] fn bound_keys_edit_and_undo() { let mut s = Session::new(vscode_like(), "ab\r\ncd", &[Selection::cursor(2)]); s.keys("enter"); @@ -163,7 +161,6 @@ fn bound_keys_edit_and_undo() { } #[test] -#[ignore = "T-106: not implemented yet"] fn typing_at_every_cursor() { let mut s = Session::new(vscode_like(), "let a = a + a;", &[Selection::cursor(4)]); // cmd-d selects the word, then shift-cmd-l every match of it. @@ -177,7 +174,6 @@ fn typing_at_every_cursor() { } #[test] -#[ignore = "T-106: not implemented yet"] fn adding_cursors_with_keys() { let mut s = Session::new(vscode_like(), "ab\nab\nab", &[Selection::cursor(4)]); s.keys("alt-cmd-up alt-cmd-down"); @@ -186,7 +182,6 @@ fn adding_cursors_with_keys() { } #[test] -#[ignore = "T-106: not implemented yet"] fn a_broken_chord_types_nothing() { let mut s = Session::new(vscode_like(), "", &[Selection::cursor(0)]); s.keys("cmd-k x"); @@ -196,7 +191,6 @@ fn a_broken_chord_types_nothing() { } #[test] -#[ignore = "T-106: not implemented yet"] fn option_text_is_typed() { let mut s = Session::new(vscode_like(), "", &[Selection::cursor(0)]); let option_a = KeyEvent::with_text(Keystroke::parse("alt-a").unwrap(), "å"); @@ -205,7 +199,6 @@ fn option_text_is_typed() { } #[test] -#[ignore = "T-106: not implemented yet"] fn vim_like_modes_edit_the_buffer() { let mut s = Session::new(vim_like(), "abc", &[Selection::cursor(0)]); // Normal mode: `x` deletes, other letters do nothing. @@ -234,7 +227,6 @@ fn vim_like_modes_edit_the_buffer() { } #[test] -#[ignore = "T-106: not implemented yet"] fn switching_keymaps_mid_session() { let mut s = Session::new(vscode_like(), "", &[Selection::cursor(0)]); s.type_text("abc"); @@ -261,7 +253,6 @@ fn switching_keymaps_mid_session() { } #[test] -#[ignore = "T-106: not implemented yet"] fn two_editors_share_one_dispatcher() { let mut s = Session::new(vim_like(), "one", &[Selection::cursor(0)]); let id = s.set.add(Buffer::from("two")); @@ -289,7 +280,6 @@ fn two_editors_share_one_dispatcher() { } #[test] -#[ignore = "T-106: not implemented yet"] fn an_unknown_command_is_an_error_and_edits_nothing() { let mut keymap = vscode_like(); keymap.bind(run("cmd-s", "workspace.save")).unwrap(); @@ -311,7 +301,6 @@ fn an_unknown_command_is_an_error_and_edits_nothing() { } #[test] -#[ignore = "T-106: not implemented yet"] fn dispatch_returns_the_resolution() { let mut s = Session::new(vscode_like(), "", &[Selection::cursor(0)]); let parse = |text: &str| KeyEvent::parse(text).unwrap(); diff --git a/crates/helm-editor/tests/keymap_properties.rs b/crates/helm-editor/tests/keymap_properties.rs index 20f033e..0861f12 100644 --- a/crates/helm-editor/tests/keymap_properties.rs +++ b/crates/helm-editor/tests/keymap_properties.rs @@ -137,7 +137,6 @@ proptest! { /// each key but its last and then runs its command, leaving nothing /// pending. #[test] - #[ignore = "T-106: not implemented yet"] fn chords_resolve_to_their_command( sequences in prop::collection::vec(sequence(), 1..40), order in prop::collection::vec(any::(), 1..60), @@ -163,7 +162,6 @@ proptest! { /// binding with a mode over one without, then a context over none, /// then the later one. #[test] - #[ignore = "T-106: not implemented yet"] fn precedence_matches_the_documented_order( bindings in prop::collection::vec( (prop::option::of(0..2usize), prop::option::of(0..3usize)), @@ -221,7 +219,6 @@ proptest! { /// state that had any chord, operator or mode resolves keys exactly as /// a new state does. #[test] - #[ignore = "T-106: not implemented yet"] fn after_a_switch_a_state_acts_like_a_new_one( old in prop::collection::vec(spec(3), 0..30), new in prop::collection::vec(spec(3), 0..30), @@ -258,7 +255,6 @@ proptest! { /// With no bindings on plain chars, typing any text one key per char /// through the dispatcher puts exactly that text at the cursor. #[test] - #[ignore = "T-106: not implemented yet"] fn typed_keys_insert_their_text( initial in "[a-c\n]{0,10}", at in any::(), diff --git a/crates/helm-editor/tests/keymaps.rs b/crates/helm-editor/tests/keymaps.rs index f6eb094..bec570d 100644 --- a/crates/helm-editor/tests/keymaps.rs +++ b/crates/helm-editor/tests/keymaps.rs @@ -117,7 +117,6 @@ fn press_in( } #[test] -#[ignore = "T-106: not implemented yet"] fn keymap_new_checks_modes() { assert_eq!( Keymap::new("empty", Vec::new()).unwrap_err(), @@ -151,7 +150,6 @@ fn keymap_new_checks_modes() { } #[test] -#[ignore = "T-106: not implemented yet"] fn bind_checks_modes_and_keeps_order() { let mut keymap = keymap( "k", @@ -188,7 +186,6 @@ fn bind_checks_modes_and_keeps_order() { } #[test] -#[ignore = "T-106: not implemented yet"] fn binding_builders_fill_the_fields() { let binding = run("cmd-k cmd-s", "workspace.save_all") .in_mode("default") @@ -233,7 +230,6 @@ fn binding_builders_fill_the_fields() { } #[test] -#[ignore = "T-106: not implemented yet"] fn a_new_state_starts_idle_in_the_initial_mode() { let (_, state) = dispatcher(vim_like()); assert_eq!(state.mode().as_str(), "normal"); @@ -242,7 +238,6 @@ fn a_new_state_starts_idle_in_the_initial_mode() { } #[test] -#[ignore = "T-106: not implemented yet"] fn single_keys_run_their_command() { let (d, mut state) = dispatcher(one_mode(vec![ run("cmd-z", "editor.undo"), @@ -261,7 +256,6 @@ fn single_keys_run_their_command() { } #[test] -#[ignore = "T-106: not implemented yet"] fn a_chord_waits_for_its_last_key() { let (d, mut state) = dispatcher(one_mode(vec![ run("cmd-k cmd-s", "workspace.save_all"), @@ -286,7 +280,6 @@ fn a_chord_waits_for_its_last_key() { } #[test] -#[ignore = "T-106: not implemented yet"] fn a_broken_chord_drops_every_key() { let (d, mut state) = dispatcher(one_mode(vec![ run("cmd-k cmd-s", "workspace.save_all"), @@ -313,7 +306,6 @@ fn a_broken_chord_drops_every_key() { } #[test] -#[ignore = "T-106: not implemented yet"] fn a_chord_prefix_beats_an_exact_match() { // `cmd-k` alone is bound, but it also starts a chord, so it waits. let (d, mut state) = dispatcher(one_mode(vec![ @@ -325,7 +317,6 @@ fn a_chord_prefix_beats_an_exact_match() { } #[test] -#[ignore = "T-106: not implemented yet"] fn precedence_table() { // Every binding is on `cmd-d`, which types nothing. Each case lists the // bindings as (mode, context, command) in the order they are added, the @@ -461,7 +452,6 @@ fn precedence_table() { } #[test] -#[ignore = "T-106: not implemented yet"] fn contexts_scope_chords_too() { let (d, mut state) = dispatcher(one_mode(vec![ run("cmd-k cmd-s", "a.save").when("editor_focus"), @@ -476,7 +466,6 @@ fn contexts_scope_chords_too() { } #[test] -#[ignore = "T-106: not implemented yet"] fn unbound_keys_type_their_text() { let (d, mut state) = dispatcher(one_mode(vec![run("cmd-d", "a.d")])); assert_eq!( @@ -514,7 +503,6 @@ fn unbound_keys_type_their_text() { } #[test] -#[ignore = "T-106: not implemented yet"] fn ignoring_modes_drop_unbound_keys() { let (d, mut state) = dispatcher(vim_like()); assert_eq!( @@ -530,7 +518,6 @@ fn ignoring_modes_drop_unbound_keys() { } #[test] -#[ignore = "T-106: not implemented yet"] fn bindings_switch_modes() { let (d, mut state) = dispatcher(vim_like()); assert_eq!(press(&d, &mut state, "i"), [Resolution::Done]); @@ -544,7 +531,6 @@ fn bindings_switch_modes() { } #[test] -#[ignore = "T-106: not implemented yet"] fn an_operator_waits_for_its_motion() { let (d, mut state) = dispatcher(vim_like()); assert_eq!(press(&d, &mut state, "d"), [Resolution::Pending]); @@ -577,7 +563,6 @@ fn an_operator_waits_for_its_motion() { } #[test] -#[ignore = "T-106: not implemented yet"] fn a_pending_operator_is_cancelled_by_any_other_key() { let (d, mut state) = dispatcher(vim_like()); // An unbound key, escape, and a broken chord motion. @@ -598,7 +583,6 @@ fn a_pending_operator_is_cancelled_by_any_other_key() { } #[test] -#[ignore = "T-106: not implemented yet"] fn an_operator_returns_to_the_mode_it_was_set_in() { // `c` is bound in every mode, so it can start in insert mode too. let (d, mut state) = dispatcher(vim_like()); @@ -619,7 +603,6 @@ fn an_operator_returns_to_the_mode_it_was_set_in() { } #[test] -#[ignore = "T-106: not implemented yet"] fn an_empty_binding_hides_an_earlier_one() { let mut empty = run("cmd-d", "unused.command"); empty.action = None; @@ -628,7 +611,6 @@ fn an_empty_binding_hides_an_earlier_one() { } #[test] -#[ignore = "T-106: not implemented yet"] fn an_empty_binding_drops_a_pending_operator() { let mut keymap = vim_like(); let mut empty = run("q", "unused.command").in_mode("operator"); @@ -644,7 +626,6 @@ fn an_empty_binding_drops_a_pending_operator() { } #[test] -#[ignore = "T-106: not implemented yet"] fn cancel_drops_what_is_pending() { let (d, mut state) = dispatcher(vim_like()); assert!(!state.cancel()); @@ -669,7 +650,6 @@ fn cancel_drops_what_is_pending() { } #[test] -#[ignore = "T-106: not implemented yet"] fn switching_keymaps_drops_a_pending_chord() { let (mut d, mut state) = dispatcher(one_mode(vec![run("cmd-k cmd-s", "a.save_all")])); assert_eq!(press(&d, &mut state, "cmd-k"), [Resolution::Pending]); @@ -691,7 +671,6 @@ fn switching_keymaps_drops_a_pending_chord() { } #[test] -#[ignore = "T-106: not implemented yet"] fn switching_keymaps_reaches_every_editor() { let (mut d, mut first) = dispatcher(vim_like()); let mut second = d.key_state(); @@ -709,7 +688,6 @@ fn switching_keymaps_reaches_every_editor() { } #[test] -#[ignore = "T-106: not implemented yet"] fn switching_keeps_a_mode_the_new_keymap_has() { let (mut d, mut state) = dispatcher(vim_like()); press(&d, &mut state, "i"); @@ -732,7 +710,6 @@ fn switching_keeps_a_mode_the_new_keymap_has() { } #[test] -#[ignore = "T-106: not implemented yet"] fn switching_drops_a_pending_operator_and_its_mode() { let (mut d, mut state) = dispatcher(vim_like()); press(&d, &mut state, "d g"); @@ -749,7 +726,6 @@ fn switching_drops_a_pending_operator_and_its_mode() { } #[test] -#[ignore = "T-106: not implemented yet"] fn sync_without_a_switch_changes_nothing() { let (d, mut state) = dispatcher(vim_like()); press(&d, &mut state, "d g"); @@ -763,7 +739,6 @@ fn sync_without_a_switch_changes_nothing() { } #[test] -#[ignore = "T-106: not implemented yet"] fn a_state_made_after_a_switch_starts_in_the_new_initial_mode() { let (mut d, _) = dispatcher(one_mode(Vec::new())); d.set_keymap(vim_like()); diff --git a/crates/helm-editor/tests/keys.rs b/crates/helm-editor/tests/keys.rs index c6ce0a6..1577368 100644 --- a/crates/helm-editor/tests/keys.rs +++ b/crates/helm-editor/tests/keys.rs @@ -19,7 +19,6 @@ fn mods(ctrl: bool, alt: bool, shift: bool, cmd: bool) -> Modifiers { } #[test] -#[ignore = "T-106: not implemented yet"] fn parses_modifiers_in_any_order() { let expected = Keystroke::new(Key::Char('k'), mods(true, true, false, true)); for text in ["ctrl-alt-cmd-k", "cmd-alt-ctrl-k", "alt-cmd-ctrl-k"] { @@ -31,7 +30,6 @@ fn parses_modifiers_in_any_order() { } #[test] -#[ignore = "T-106: not implemented yet"] fn parses_every_key_name() { let names = [ ("enter", Key::Enter), @@ -58,7 +56,6 @@ fn parses_every_key_name() { } #[test] -#[ignore = "T-106: not implemented yet"] fn single_chars_are_taken_as_they_are() { assert_eq!(ks("$").key(), Key::Char('$')); assert_eq!(ks("-").key(), Key::Char('-')); @@ -73,7 +70,6 @@ fn single_chars_are_taken_as_they_are() { } #[test] -#[ignore = "T-106: not implemented yet"] fn shift_folds_into_letters() { let upper = Keystroke::new(Key::Char('A'), Modifiers::NONE); assert_eq!(Keystroke::new(Key::Char('a'), Modifiers::SHIFT), upper); @@ -97,7 +93,6 @@ fn shift_folds_into_letters() { } #[test] -#[ignore = "T-106: not implemented yet"] fn shift_is_dropped_from_other_chars_and_kept_on_named_keys() { // helm-ui has already shifted the char. assert_eq!( @@ -122,7 +117,6 @@ fn shift_is_dropped_from_other_chars_and_kept_on_named_keys() { } #[test] -#[ignore = "T-106: not implemented yet"] fn rejects_bad_keystrokes() { let cases = [ ("cmd-", "missing"), @@ -155,7 +149,6 @@ fn rejects_bad_keystrokes() { } #[test] -#[ignore = "T-106: not implemented yet"] fn display_writes_canonical_notation() { let cases = [ ("cmd-alt-ctrl-k", "ctrl-alt-cmd-k"), @@ -175,7 +168,6 @@ fn display_writes_canonical_notation() { } #[test] -#[ignore = "T-106: not implemented yet"] fn display_round_trips_every_keystroke() { let keys = [ Key::Char('a'), @@ -211,7 +203,6 @@ fn display_round_trips_every_keystroke() { } #[test] -#[ignore = "T-106: not implemented yet"] fn sequences_split_on_whitespace() { let chord = KeySequence::parse("cmd-k cmd-s").unwrap(); assert_eq!(chord.as_slice(), &[ks("cmd-k"), ks("cmd-s")]); @@ -230,7 +221,6 @@ fn sequences_split_on_whitespace() { } #[test] -#[ignore = "T-106: not implemented yet"] fn sequences_reject_empty_text_and_bad_keystrokes() { assert_eq!(KeySequence::parse(""), Err(KeyParseError::Empty)); assert_eq!(KeySequence::parse(" \t "), Err(KeyParseError::Empty)); @@ -243,7 +233,6 @@ fn sequences_reject_empty_text_and_bad_keystrokes() { } #[test] -#[ignore = "T-106: not implemented yet"] fn plain_keys_type_their_char() { let typed = |text: &str| KeyEvent::parse(text).unwrap().text; assert_eq!(typed("a"), Some("a".to_owned())); @@ -271,7 +260,6 @@ fn plain_keys_type_their_char() { } #[test] -#[ignore = "T-106: not implemented yet"] fn with_text_overrides_the_text() { // Option+A on macOS: the keystroke alt-a, typing å. let event = KeyEvent::with_text(ks("alt-a"), "å");