From 18c5cbacd60eab93e359ec5887e5b4000c03c3b7 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 14 Aug 2025 10:28:53 -0600 Subject: [PATCH 1/2] Add SearchHistory to command palette This makes vim mode significantly nicer to use --- crates/command_palette/Cargo.toml | 1 + crates/command_palette/src/command_palette.rs | 59 ++++++++++++++++++- crates/picker/src/picker.rs | 18 ++++++ crates/vim/src/command.rs | 42 +++++++++++++ .../vim/test_data/test_command_history.json | 18 ++++++ 5 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 crates/vim/test_data/test_command_history.json diff --git a/crates/command_palette/Cargo.toml b/crates/command_palette/Cargo.toml index c97d142152..a08586a194 100644 --- a/crates/command_palette/Cargo.toml +++ b/crates/command_palette/Cargo.toml @@ -23,6 +23,7 @@ gpui.workspace = true log.workspace = true picker.workspace = true postage.workspace = true +project.workspace = true serde.workspace = true settings.workspace = true theme.workspace = true diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index b8800ff912..315419643f 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -14,12 +14,13 @@ use command_palette_hooks::{ use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - Action, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, + Action, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Global, ParentElement, Render, Styled, Task, WeakEntity, Window, }; use persistence::COMMAND_PALETTE_HISTORY; -use picker::{Picker, PickerDelegate}; +use picker::{Direction, Picker, PickerDelegate}; use postage::{sink::Sink, stream::Stream}; +use project::search_history::{QueryInsertionBehavior, SearchHistory, SearchHistoryCursor}; use settings::Settings; use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, h_flex, prelude::*, v_flex}; use util::ResultExt; @@ -38,6 +39,21 @@ pub struct CommandPalette { picker: Entity>, } +struct CommandPaletteSearchHistory { + history: SearchHistory, +} +impl Default for CommandPaletteSearchHistory { + fn default() -> Self { + Self { + history: SearchHistory::new( + Some(500), + QueryInsertionBehavior::ReplacePreviousIfContains, + ), + } + } +} +impl Global for CommandPaletteSearchHistory {} + /// Removes subsequent whitespace characters and double colons from the query. /// /// This improves the likelihood of a match by either humanized name or keymap-style name. @@ -145,6 +161,7 @@ impl Render for CommandPalette { pub struct CommandPaletteDelegate { latest_query: String, + history_cursor: SearchHistoryCursor, command_palette: WeakEntity, all_commands: Vec, commands: Vec, @@ -182,6 +199,7 @@ impl CommandPaletteDelegate { all_commands: commands.clone(), matches: vec![], commands, + history_cursor: SearchHistoryCursor::default(), selected_ix: 0, previous_focus_handle, latest_query: String::new(), @@ -378,11 +396,48 @@ impl PickerDelegate for CommandPaletteDelegate { .log_err(); } + fn handle_history( + &mut self, + direction: Direction, + _window: &mut Window, + cx: &mut Context>, + ) -> Option { + dbg!(self.selected_ix); + if self.selected_ix != 0 { + return None; + } + match direction { + Direction::Up => { + cx.update_default_global(|history: &mut CommandPaletteSearchHistory, _| { + history + .history + .previous(&mut self.history_cursor) + .map(|s| s.to_owned()) + .or(Some("".to_owned())) + }) + } + Direction::Down => { + cx.update_default_global(|history: &mut CommandPaletteSearchHistory, _| { + history + .history + .previous(&mut self.history_cursor) + .map(|s| s.to_owned()) + }) + } + } + } + fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { if self.matches.is_empty() { self.dismissed(window, cx); return; } + + cx.update_default_global(|history: &mut CommandPaletteSearchHistory, _| { + history + .history + .add(&mut self.history_cursor, self.latest_query.clone()) + }); let action_ix = self.matches[self.selected_ix].candidate_id; let command = self.commands.swap_remove(action_ix); telemetry::event!( diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 34af5fed02..172716ca66 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -144,6 +144,16 @@ pub trait PickerDelegate: Sized + 'static { false } + // Allow intercepting up and down for history navigation in the command palette. + fn handle_history( + &mut self, + _direction: Direction, + _window: &mut Window, + _cx: &mut Context>, + ) -> Option { + None + } + /// Override if you want to have update the query instead of confirming. fn confirm_update_query( &mut self, @@ -436,6 +446,10 @@ impl Picker { window: &mut Window, cx: &mut Context, ) { + if let Some(query) = self.delegate.handle_history(Direction::Down, window, cx) { + self.set_query(query, window, cx); + return; + } let count = self.delegate.match_count(); if count > 0 { let index = self.delegate.selected_index(); @@ -455,6 +469,10 @@ impl Picker { window: &mut Window, cx: &mut Context, ) { + if let Some(query) = self.delegate.handle_history(Direction::Down, window, cx) { + self.set_query(query, window, cx); + return; + } let count = self.delegate.match_count(); if count > 0 { let index = self.delegate.selected_index(); diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 264fa4bf2f..10e2da6712 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -2584,4 +2584,46 @@ mod test { assert_active_item(workspace, path!("/root/dir/file_3.rs"), "", cx); }); } + + #[gpui::test] + async fn test_command_history(cx: &mut TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! {" + The quick + brown fox + ˇjumps over + the lazy dog + "}) + .await; + + cx.simulate_shared_keystrokes(": s / o / a enter").await; + + cx.shared_state().await.assert_eq(indoc! {" + The quick + brown fox + ˇjumps aver + the lazy dog + "}); + + // n.b ^ fixes a selection mismatch after u. should be removable eventually + cx.simulate_shared_keystrokes("u ^").await; + + cx.shared_state().await.assert_eq(indoc! {" + The quick + brown fox + ˇjumps over + the lazy dog + "}); + + cx.simulate_shared_keystrokes(": up backspace e enter") + .await; + + cx.shared_state().await.assert_eq(indoc! {" + The quick + brown fox + ˇjumps ever + the lazy dog + "}); + } } diff --git a/crates/vim/test_data/test_command_history.json b/crates/vim/test_data/test_command_history.json new file mode 100644 index 0000000000..72080aa42c --- /dev/null +++ b/crates/vim/test_data/test_command_history.json @@ -0,0 +1,18 @@ +{"Put":{"state":"The quick\nbrown fox\nˇjumps over\nthe lazy dog\n"}} +{"Key":":"} +{"Key":"s"} +{"Key":"/"} +{"Key":"o"} +{"Key":"/"} +{"Key":"a"} +{"Key":"enter"} +{"Get":{"state":"The quick\nbrown fox\nˇjumps aver\nthe lazy dog\n","mode":"Normal"}} +{"Key":"u"} +{"Key":"^"} +{"Get":{"state":"The quick\nbrown fox\nˇjumps over\nthe lazy dog\n","mode":"Normal"}} +{"Key":":"} +{"Key":"up"} +{"Key":"backspace"} +{"Key":"e"} +{"Key":"enter"} +{"Get":{"state":"The quick\nbrown fox\nˇjumps ever\nthe lazy dog\n","mode":"Normal"}} From 66af5030a7ea8dd4ee6140b7b2bbacba71046b36 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 25 Aug 2025 21:57:33 -0600 Subject: [PATCH 2/2] dbg! --- crates/command_palette/src/command_palette.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 315419643f..abb381a67e 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -402,7 +402,6 @@ impl PickerDelegate for CommandPaletteDelegate { _window: &mut Window, cx: &mut Context>, ) -> Option { - dbg!(self.selected_ix); if self.selected_ix != 0 { return None; }