From 2a12f951651b9709f4dc011d9f939e7ab2f27b9c Mon Sep 17 00:00:00 2001 From: Julien Calixte Date: Tue, 14 Jul 2026 10:33:16 +0200 Subject: [PATCH] feat(editor): open the Cmd-P palette from every mode Insert/Visual/View/Command now accept Cmd-P by first bailing out as Esc would (end insert session, drop selection, abandon the : line), then opening the palette; closing always lands in Normal. Opening mid-change aborts the `.` recording so a palette round-trip is never replayed. --- editor/src/lib.rs | 134 ++++++++++++++++++++++++++++++++++++++++++---- keymap/src/lib.rs | 5 +- 2 files changed, 127 insertions(+), 12 deletions(-) diff --git a/editor/src/lib.rs b/editor/src/lib.rs index 5409b3d..2d40676 100644 --- a/editor/src/lib.rs +++ b/editor/src/lib.rs @@ -89,10 +89,11 @@ pub enum Mode { /// Enter runs it, Esc cancels. Handles `:fmt` (in-core) plus `:w`/`:gp` /// (which ask the host to persist/publish via an [`Effect`]). Command, - /// File palette (`Cmd-P`) — a modal transient panel over the writing column. - /// Typing fuzzy-filters the file list ([`Editor::set_file_list`]); `Ctrl-n`/ - /// `Ctrl-p` move the selection, Enter opens it, Esc (or `Cmd-P` again) - /// cancels. See [`Editor::palette_key`]. + /// File palette (`Cmd-P`, reachable from every mode) — a modal transient + /// panel over the writing column. Typing fuzzy-filters the file list + /// ([`Editor::set_file_list`]); `Ctrl-n`/`Ctrl-p` move the selection, Enter + /// opens it, Esc (or `Cmd-P` again) cancels back to Normal. See + /// [`Editor::palette_key`]. Palette, } @@ -1110,6 +1111,13 @@ impl Editor { self.snippet_stops.clear(); } + // Cmd-p mid-change (e.g. during an insert session) aborts the `.` + // recording — a palette round-trip (file switch, `>` command) is not a + // repeatable edit, and replaying it from `.` would reopen the palette. + if key == Key::Palette { + self.dot_recording = None; + } + if !self.replaying { self.record_dot(key, before_mode, before_pending); } @@ -1191,10 +1199,19 @@ impl Editor { Key::DeleteLine => self.delete_to_line_start(), // Half-page scroll and the Ctrl-n/Ctrl-p line motions are navigation // gestures — Normal/View only. In Insert they're a no-op rather than - // yanking the caret off the text you're typing. Redo (Ctrl-r) and the - // palette (Cmd-p) are likewise ignored here. - Key::HalfPageDown | Key::HalfPageUp | Key::Redo | Key::Palette | Key::Down - | Key::Up => {} + // yanking the caret off the text you're typing. Redo (Ctrl-r) is + // likewise ignored here. + Key::HalfPageDown | Key::HalfPageUp | Key::Redo | Key::Down | Key::Up => {} + // Cmd-p works from every mode: acts like Esc (ending the insert + // session, caret onto the last inserted char), then opens the + // palette. Closing it lands in Normal, as Esc would have. + Key::Palette => { + self.mode = Mode::Normal; + if self.caret > self.line_start(self.caret) { + self.caret = self.prev_char(self.caret); + } + self.open_palette(); + } Key::Escape => { self.mode = Mode::Normal; // vim drops the caret onto the last inserted char. @@ -1512,6 +1529,12 @@ impl Editor { self.cmdline.clear(); self.mode = Mode::Normal; } + // Cmd-p works from every mode: abandon the half-typed `:`/`/` line + // (as Esc would) and open the palette. + Key::Palette => { + self.cmdline.clear(); + self.open_palette(); + } Key::DeleteWord => { // Readline Ctrl-W: drop trailing spaces, then the word before the // caret — editing the `:` command line while typing it. Unlike @@ -2388,6 +2411,13 @@ impl Editor { self.exit_visual(); return; } + // Cmd-p works from every mode: drop the selection (as Esc would) + // and open the palette. + Key::Palette => { + self.exit_visual(); + self.open_palette(); + return; + } // Enter/Backspace/etc. carry no Visual meaning; drop any count. _ => { self.count = 0; @@ -2569,6 +2599,12 @@ impl Editor { self.mode = Mode::Normal; self.pending_g = false; } + // Cmd-p works from every mode; leaving View for the palette (and + // then Normal on close), exactly as Esc-then-Cmd-p would. + Key::Palette => { + self.pending_g = false; + self.open_palette(); + } _ => {} } } @@ -5496,10 +5532,88 @@ mod tests { } #[test] - fn cmd_p_is_ignored_in_insert_mode() { + fn cmd_p_opens_the_palette_from_insert_ending_the_session_like_esc() { let mut e = typed("hi"); e.handle(Key::Palette); - assert_eq!(e.mode(), Mode::Insert); // a Normal gesture only; no-op here + assert_eq!(e.mode(), Mode::Palette); + assert_eq!(e.caret, 1); // caret dropped onto the last inserted char, as Esc does + e.handle(Key::Escape); + assert_eq!(e.mode(), Mode::Normal); // closing lands in Normal, not back in Insert + assert_eq!(e.text, "hi"); + } + + #[test] + fn cmd_p_opens_the_palette_from_visual_dropping_the_selection() { + let mut e = typed("hello"); + e.handle(Key::Escape); + e.handle(Key::Char('v')); // charwise Visual, anchor set + assert_eq!(e.mode(), Mode::Visual); + e.handle(Key::Palette); + assert_eq!(e.mode(), Mode::Palette); + assert_eq!(e.visual_anchor, None); // selection gone, as Esc would leave it + e.handle(Key::Escape); + assert_eq!(e.mode(), Mode::Normal); + } + + #[test] + fn cmd_p_opens_the_palette_from_visual_line() { + let mut e = typed("hello"); + e.handle(Key::Escape); + e.handle(Key::Char('V')); + assert_eq!(e.mode(), Mode::VisualLine); + e.handle(Key::Palette); + assert_eq!(e.mode(), Mode::Palette); + assert_eq!(e.visual_anchor, None); + } + + #[test] + fn cmd_p_opens_the_palette_from_view_mode() { + let mut e = typed("hello"); + e.handle(Key::Escape); + e.handle(Key::Char('g')); + e.handle(Key::Char('r')); // gr — go-read (View) + assert_eq!(e.mode(), Mode::View); + e.handle(Key::Palette); + assert_eq!(e.mode(), Mode::Palette); + e.handle(Key::Escape); + assert_eq!(e.mode(), Mode::Normal); + } + + #[test] + fn cmd_p_opens_the_palette_from_command_abandoning_the_line() { + let mut e = palette_editor(&["/sd/repo/notes.md"]); + e.handle(Key::Char(':')); + for c in "del".chars() { + e.handle(Key::Char(c)); // half-typed command + } + e.handle(Key::Palette); + assert_eq!(e.mode(), Mode::Palette); + assert_eq!(e.cmdline, ""); // the abandoned `:del` is gone + assert_eq!(e.palette_query, ""); // and didn't leak into the palette query + e.handle(Key::Escape); + assert_eq!(e.mode(), Mode::Normal); + assert!(e.take_effects().is_empty()); // `:del` never ran + } + + #[test] + fn cmd_p_mid_insert_aborts_the_dot_recording() { + let mut e = typed("ab"); // `i a b` — a dot recording in progress + e.handle(Key::Palette); // palette trip aborts it + e.handle(Key::Escape); // back to Normal — would normally complete a recording + e.handle(Key::Char('.')); + assert_eq!(e.text, "ab"); // nothing to repeat: no re-insert, no reopened palette + assert_eq!(e.mode(), Mode::Normal); + } + + #[test] + fn a_completed_dot_survives_a_palette_round_trip() { + let mut e = typed("abc"); + e.handle(Key::Escape); // caret on 'c' + e.handle(Key::Char('x')); // dot = [x], text "ab" + e.handle(Key::Palette); + e.handle(Key::Escape); // palette round trip + e.handle(Key::Char('.')); // still repeats the x + assert_eq!(e.text, "a"); } #[test] diff --git a/keymap/src/lib.rs b/keymap/src/lib.rs index 5141b06..586ad45 100644 --- a/keymap/src/lib.rs +++ b/keymap/src/lib.rs @@ -38,8 +38,9 @@ pub enum Key { /// ignored elsewhere. Redo, /// Cmd+P — open the file palette (fuzzy open, v0.5), VS Code "Go to File" - /// style. A Normal-mode gesture; **inside** the palette the same chord closes - /// it (toggle). Esc also closes. Ignored in Insert. + /// style. Available from **every** mode (Insert/Visual/View/Command included, + /// each first bailing out as Esc would); **inside** the palette the same + /// chord closes it (toggle). Esc also closes. Palette, /// Ctrl+N — move down: one line in Normal/View (vim `CTRL-N` ≡ `j`), or one /// row in the file palette. Ignored in Insert.