Compare commits
2 commits
main
...
command-pa
Author | SHA1 | Date | |
---|---|---|---|
![]() |
66af5030a7 | ||
![]() |
18c5cbacd6 |
5 changed files with 135 additions and 2 deletions
|
@ -23,6 +23,7 @@ gpui.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
picker.workspace = true
|
picker.workspace = true
|
||||||
postage.workspace = true
|
postage.workspace = true
|
||||||
|
project.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
settings.workspace = true
|
settings.workspace = true
|
||||||
theme.workspace = true
|
theme.workspace = true
|
||||||
|
|
|
@ -14,12 +14,13 @@ use command_palette_hooks::{
|
||||||
|
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use gpui::{
|
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,
|
ParentElement, Render, Styled, Task, WeakEntity, Window,
|
||||||
};
|
};
|
||||||
use persistence::COMMAND_PALETTE_HISTORY;
|
use persistence::COMMAND_PALETTE_HISTORY;
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Direction, Picker, PickerDelegate};
|
||||||
use postage::{sink::Sink, stream::Stream};
|
use postage::{sink::Sink, stream::Stream};
|
||||||
|
use project::search_history::{QueryInsertionBehavior, SearchHistory, SearchHistoryCursor};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, h_flex, prelude::*, v_flex};
|
use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, h_flex, prelude::*, v_flex};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
@ -38,6 +39,21 @@ pub struct CommandPalette {
|
||||||
picker: Entity<Picker<CommandPaletteDelegate>>,
|
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.
|
/// 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.
|
/// 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 {
|
pub struct CommandPaletteDelegate {
|
||||||
latest_query: String,
|
latest_query: String,
|
||||||
|
history_cursor: SearchHistoryCursor,
|
||||||
command_palette: WeakEntity<CommandPalette>,
|
command_palette: WeakEntity<CommandPalette>,
|
||||||
all_commands: Vec<Command>,
|
all_commands: Vec<Command>,
|
||||||
commands: Vec<Command>,
|
commands: Vec<Command>,
|
||||||
|
@ -182,6 +199,7 @@ impl CommandPaletteDelegate {
|
||||||
all_commands: commands.clone(),
|
all_commands: commands.clone(),
|
||||||
matches: vec![],
|
matches: vec![],
|
||||||
commands,
|
commands,
|
||||||
|
history_cursor: SearchHistoryCursor::default(),
|
||||||
selected_ix: 0,
|
selected_ix: 0,
|
||||||
previous_focus_handle,
|
previous_focus_handle,
|
||||||
latest_query: String::new(),
|
latest_query: String::new(),
|
||||||
|
@ -378,11 +396,47 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||||
.log_err();
|
.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>>) {
|
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||||
if self.matches.is_empty() {
|
if self.matches.is_empty() {
|
||||||
self.dismissed(window, cx);
|
self.dismissed(window, cx);
|
||||||
return;
|
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 action_ix = self.matches[self.selected_ix].candidate_id;
|
||||||
let command = self.commands.swap_remove(action_ix);
|
let command = self.commands.swap_remove(action_ix);
|
||||||
telemetry::event!(
|
telemetry::event!(
|
||||||
|
|
|
@ -144,6 +144,16 @@ pub trait PickerDelegate: Sized + 'static {
|
||||||
false
|
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.
|
/// Override if you want to have <enter> update the query instead of confirming.
|
||||||
fn confirm_update_query(
|
fn confirm_update_query(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
@ -436,6 +446,10 @@ impl<D: PickerDelegate> Picker<D> {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
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();
|
let count = self.delegate.match_count();
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
let index = self.delegate.selected_index();
|
let index = self.delegate.selected_index();
|
||||||
|
@ -455,6 +469,10 @@ impl<D: PickerDelegate> Picker<D> {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
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();
|
let count = self.delegate.match_count();
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
let index = self.delegate.selected_index();
|
let index = self.delegate.selected_index();
|
||||||
|
|
|
@ -2584,4 +2584,46 @@ mod test {
|
||||||
assert_active_item(workspace, path!("/root/dir/file_3.rs"), "", cx);
|
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
|
||||||
|
"});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
18
crates/vim/test_data/test_command_history.json
Normal file
18
crates/vim/test_data/test_command_history.json
Normal 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"}}
|
Loading…
Add table
Add a link
Reference in a new issue