Compare commits

...
Sign in to create a new pull request.

2 commits

Author SHA1 Message Date
Conrad Irwin
66af5030a7 dbg! 2025-08-25 21:57:33 -06:00
Conrad Irwin
18c5cbacd6 Add SearchHistory to command palette
This makes vim mode significantly nicer to use
2025-08-14 10:28:53 -06:00
5 changed files with 135 additions and 2 deletions

View file

@ -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

View file

@ -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<Picker<CommandPaletteDelegate>>,
}
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<CommandPalette>,
all_commands: Vec<Command>,
commands: Vec<Command>,
@ -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,47 @@ impl PickerDelegate for CommandPaletteDelegate {
.log_err();
}
fn handle_history(
&mut self,
direction: Direction,
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<String> {
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<Picker<Self>>) {
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!(

View file

@ -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<Picker<Self>>,
) -> Option<String> {
None
}
/// Override if you want to have <enter> update the query instead of confirming.
fn confirm_update_query(
&mut self,
@ -436,6 +446,10 @@ impl<D: PickerDelegate> Picker<D> {
window: &mut Window,
cx: &mut Context<Self>,
) {
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<D: PickerDelegate> Picker<D> {
window: &mut Window,
cx: &mut Context<Self>,
) {
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();

View file

@ -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
"});
}
}

View file

@ -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"}}