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"), "å");