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
Conrad Irwin created
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 ++++++++++++++
crates/vim/test_data/test_command_history.json | 18 ++++++
5 files changed, 136 insertions(+), 2 deletions(-)
@@ -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
@@ -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,48 @@ impl PickerDelegate for CommandPaletteDelegate {
.log_err();
}
+ fn handle_history(
+ &mut self,
+ direction: Direction,
+ _window: &mut Window,
+ cx: &mut Context<Picker<Self>>,
+ ) -> Option<String> {
+ 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<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!(
@@ -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();
@@ -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
+ "});
+ }
}
@@ -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"}}