From 10be45d0db2236c4d78ce9bc0ff8d41711410ec0 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Wed, 11 Jun 2025 21:53:54 +0200 Subject: [PATCH] add search bar Co-Authored-By: Mikayla --- Cargo.lock | 2 + crates/command_palette/src/command_palette.rs | 65 ++++--- crates/settings_ui/Cargo.toml | 2 + crates/settings_ui/src/keybindings.rs | 168 +++++++++++++----- 4 files changed, 163 insertions(+), 74 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bb03cba07c141de700f8a43e9d03effc9853657b..ecd32e22efa9198a4e72c1c5dc785938992d4be8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14566,12 +14566,14 @@ dependencies = [ name = "settings_ui" version = "0.1.0" dependencies = [ + "command_palette", "command_palette_hooks", "component", "db", "editor", "feature_flags", "fs", + "fuzzy", "gpui", "log", "paths", diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 2e411fd139c4d6410bee6512dd3537a9592f2420..abb8978d5a103fb66f862af6c5ee69beee0f6251 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -41,7 +41,7 @@ pub struct CommandPalette { /// 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. -fn normalize_query(input: &str) -> String { +pub fn normalize_action_query(input: &str) -> String { let mut result = String::with_capacity(input.len()); let mut last_char = None; @@ -297,7 +297,7 @@ impl PickerDelegate for CommandPaletteDelegate { let mut commands = self.all_commands.clone(); let hit_counts = self.hit_counts(); let executor = cx.background_executor().clone(); - let query = normalize_query(query.as_str()); + let query = normalize_action_query(query.as_str()); async move { commands.sort_by_key(|action| { ( @@ -311,29 +311,17 @@ impl PickerDelegate for CommandPaletteDelegate { .enumerate() .map(|(ix, command)| StringMatchCandidate::new(ix, &command.name)) .collect::>(); - let matches = if query.is_empty() { - candidates - .into_iter() - .enumerate() - .map(|(index, candidate)| StringMatch { - candidate_id: index, - string: candidate.string, - positions: Vec::new(), - score: 0.0, - }) - .collect() - } else { - fuzzy::match_strings( - &candidates, - &query, - true, - true, - 10000, - &Default::default(), - executor, - ) - .await - }; + + let matches = fuzzy::match_strings( + &candidates, + &query, + true, + true, + 10000, + &Default::default(), + executor, + ) + .await; tx.send((commands, matches)).await.log_err(); } @@ -422,8 +410,8 @@ impl PickerDelegate for CommandPaletteDelegate { window: &mut Window, cx: &mut Context>, ) -> Option { - let r#match = self.matches.get(ix)?; - let command = self.commands.get(r#match.candidate_id)?; + let matching_command = self.matches.get(ix)?; + let command = self.commands.get(matching_command.candidate_id)?; Some( ListItem::new(ix) .inset(true) @@ -436,7 +424,7 @@ impl PickerDelegate for CommandPaletteDelegate { .justify_between() .child(HighlightedLabel::new( command.name.clone(), - r#match.positions.clone(), + matching_command.positions.clone(), )) .children(KeyBinding::for_action_in( &*command.action, @@ -512,19 +500,28 @@ mod tests { #[test] fn test_normalize_query() { - assert_eq!(normalize_query("editor: backspace"), "editor: backspace"); - assert_eq!(normalize_query("editor: backspace"), "editor: backspace"); - assert_eq!(normalize_query("editor: backspace"), "editor: backspace"); assert_eq!( - normalize_query("editor::GoToDefinition"), + normalize_action_query("editor: backspace"), + "editor: backspace" + ); + assert_eq!( + normalize_action_query("editor: backspace"), + "editor: backspace" + ); + assert_eq!( + normalize_action_query("editor: backspace"), + "editor: backspace" + ); + assert_eq!( + normalize_action_query("editor::GoToDefinition"), "editor:GoToDefinition" ); assert_eq!( - normalize_query("editor::::GoToDefinition"), + normalize_action_query("editor::::GoToDefinition"), "editor:GoToDefinition" ); assert_eq!( - normalize_query("editor: :GoToDefinition"), + normalize_action_query("editor: :GoToDefinition"), "editor: :GoToDefinition" ); } diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml index 2af699938d00bbb878b12d6b585aed95f97f3072..4ac0e562f68ac664e74cd3aabc8e5861d9bf8634 100644 --- a/crates/settings_ui/Cargo.toml +++ b/crates/settings_ui/Cargo.toml @@ -12,12 +12,14 @@ workspace = true path = "src/settings_ui.rs" [dependencies] +command_palette.workspace = true command_palette_hooks.workspace = true component.workspace = true db.workspace = true editor.workspace = true feature_flags.workspace = true fs.workspace = true +fuzzy.workspace = true gpui.workspace = true log.workspace = true paths.workspace = true diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index a45e13413a90c19f677364b5c9572dcac98e0c59..c179f95735be013aec814eeb642be1d51c4e303a 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -1,9 +1,11 @@ -use std::{fmt::Write as _, ops::Range}; +use std::{fmt::Write as _, ops::Range, sync::Arc}; use db::anyhow::anyhow; +use editor::{Editor, EditorEvent}; +use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - AppContext as _, Context, Entity, EventEmitter, FocusHandle, Focusable, Global, Subscription, - actions, div, + AppContext as _, Context, Entity, EventEmitter, FocusHandle, Focusable, Global, ScrollStrategy, + Subscription, actions, div, }; use ui::{ @@ -25,7 +27,7 @@ pub fn init(cx: &mut App) { cx.observe_new(|workspace: &mut Workspace, _window, _cx| { workspace.register_action(|workspace, _: &OpenKeymapEditor, window, cx| { - let open_keymap_editor = KeymapEditor::new(window, cx); + let open_keymap_editor = cx.new(|cx| KeymapEditor::new(window, cx)); workspace.add_item_to_center(Box::new(open_keymap_editor), window, cx); }); }) @@ -53,8 +55,12 @@ impl KeymapEventChannel { struct KeymapEditor { focus_handle: FocusHandle, _keymap_subscription: Subscription, - processed_bindings: Vec, + keybindings: Vec, + // corresponds 1 to 1 with keybindings + string_match_candidates: Arc>, + matches: Vec, table_interaction_state: Entity, + filter_editor: Entity, } impl EventEmitter<()> for KeymapEditor {} @@ -66,33 +72,86 @@ impl Focusable for KeymapEditor { } impl KeymapEditor { - fn new(window: &mut Window, cx: &mut App) -> Entity { - let this = cx.new(|cx| { - let focus_handle = cx.focus_handle(); + fn new(window: &mut Window, cx: &mut Context) -> Self { + let focus_handle = cx.focus_handle(); - let _keymap_subscription = cx.observe_global::(|this, cx| { - let key_bindings = Self::process_bindings(cx); - this.processed_bindings = key_bindings; - }); + let _keymap_subscription = + cx.observe_global::(Self::update_keybindings); + let table_interaction_state = TableInteractionState::new(window, cx); - let table_interaction_state = TableInteractionState::new(window, cx); + let filter_editor = cx.new(|cx| { + let mut editor = Editor::single_line(window, cx); + editor.set_placeholder_text("Filter action names...", cx); + editor + }); - Self { - focus_handle: focus_handle.clone(), - _keymap_subscription, - processed_bindings: vec![], - table_interaction_state, + cx.subscribe(&filter_editor, |this, _, e: &EditorEvent, cx| { + if !matches!(e, EditorEvent::BufferEdited) { + return; } - }); + + this.update_matches(cx); + }) + .detach(); + + let mut this = Self { + keybindings: vec![], + string_match_candidates: Arc::new(vec![]), + matches: vec![], + focus_handle: focus_handle.clone(), + _keymap_subscription, + table_interaction_state, + filter_editor, + }; + + this.update_keybindings(cx); + this } - fn process_bindings(cx: &mut Context) -> Vec { + fn update_matches(&mut self, cx: &mut Context) { + let query = dbg!(self.filter_editor.read(cx).text(cx)); + let string_match_candidates = self.string_match_candidates.clone(); + let executor = cx.background_executor().clone(); + let keybind_count = self.keybindings.len(); + let query = command_palette::normalize_action_query(&query); + dbg!(&query); + let fuzzy_match = cx.background_spawn(async move { + fuzzy::match_strings( + &string_match_candidates, + &query, + true, + true, + keybind_count, + &Default::default(), + executor, + ) + .await + }); + + cx.spawn(async move |this, cx| { + let matches = fuzzy_match.await; + dbg!(&matches); + this.update(cx, |this, cx| { + this.table_interaction_state.update(cx, |this, _cx| { + this.scroll_handle.scroll_to_item(0, ScrollStrategy::Top); + }); + this.matches = matches; + cx.notify(); + }) + }) + .detach(); + } + + fn process_bindings( + cx: &mut Context, + ) -> (Vec, Vec) { let key_bindings_ptr = cx.key_bindings(); let lock = key_bindings_ptr.borrow(); let key_bindings = lock.bindings(); let mut processed_bindings = Vec::new(); + let mut string_match_candidates = Vec::new(); for key_binding in key_bindings { let mut keystroke_text = String::new(); @@ -110,17 +169,43 @@ impl KeymapEditor { .meta() .map(|meta| settings::KeybindSource::from_meta(meta).name().into()); + let action_name = key_binding.action().name(); + + let index = processed_bindings.len(); + let string_match_candidate = StringMatchCandidate::new(index, &action_name); processed_bindings.push(ProcessedKeybinding { keystroke_text: keystroke_text.into(), - action: key_binding.action().name().into(), + action: action_name.into(), context: context.into(), source, - }) + }); + string_match_candidates.push(string_match_candidate); } - processed_bindings + (processed_bindings, string_match_candidates) + } + + fn update_keybindings(self: &mut KeymapEditor, cx: &mut Context) { + let (key_bindings, string_match_candidates) = Self::process_bindings(cx); + self.keybindings = key_bindings; + self.string_match_candidates = Arc::new(string_match_candidates); + self.matches = self + .string_match_candidates + .iter() + .enumerate() + .map(|(ix, candidate)| StringMatch { + candidate_id: ix, + score: 0.0, + positions: vec![], + string: candidate.string.clone(), + }) + .collect(); + + self.update_matches(cx); + cx.notify(); } } +#[derive(Clone)] struct ProcessedKeybinding { keystroke_text: SharedString, action: SharedString, @@ -138,38 +223,41 @@ impl Item for KeymapEditor { impl Render for KeymapEditor { fn render(&mut self, _window: &mut Window, cx: &mut ui::Context) -> impl ui::IntoElement { - if self.processed_bindings.is_empty() { - self.processed_bindings = Self::process_bindings(cx); - } - - let row_count = self.processed_bindings.len(); - + let row_count = self.matches.len(); let theme = cx.theme(); + dbg!(&self.matches); + div() .size_full() .bg(theme.colors().background) .id("keymap-editor") .track_focus(&self.focus_handle) + .child(self.filter_editor.clone()) .child( Table::new() .interactable(&self.table_interaction_state) + .striped() + .column_widths([rems(24.), rems(16.), rems(32.), rems(8.)]) .header(["Command", "Keystrokes", "Context", "Source"]) - .column_widths([rems(16.), rems(24.), rems(32.), rems(8.)]) .uniform_list( "keymap-editor-table", row_count, cx.processor(move |this, range: Range, _window, _cx| { range - .map(|index| { - let binding = &this.processed_bindings[index]; - [ - binding.action.clone(), - binding.keystroke_text.clone(), - binding.context.clone(), - binding.source.clone().unwrap_or_default(), - ] - .map(IntoElement::into_any_element) + .filter_map(|index| { + dbg!(index); + let candidate_id = this.matches.get(index)?.candidate_id; + let binding = &this.keybindings[candidate_id]; + Some( + [ + binding.action.clone(), + binding.keystroke_text.clone(), + binding.context.clone(), + binding.source.clone().unwrap_or_default(), + ] + .map(IntoElement::into_any_element), + ) }) .collect() }), @@ -211,7 +299,7 @@ impl SerializableItem for KeymapEditor { .get_keybinding_editor(item_id, workspace_id)? .is_some() { - cx.update(KeymapEditor::new) + cx.update(|window, cx| cx.new(|cx| KeymapEditor::new(window, cx))) } else { Err(anyhow!("No keybinding editor to deserialize")) }