From b928e595c2061a871ce83e05a71a5d893353d225 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 15 Apr 2026 11:59:49 +0300 Subject: [PATCH] Select and scroll to the best search match in settings (#53916) When adding code lens to the long list of search items, I've failed to find it when searching, as we always select first item in the matches list, which is not necessarily the best one. Left is before, right is after: https://github.com/user-attachments/assets/9033dfd8-abde-4a45-a214-ae859105d6ad Release Notes: - Improved settings search ergonomics --- crates/settings_ui/src/settings_ui.rs | 88 +++++++++++++++++++++------ 1 file changed, 71 insertions(+), 17 deletions(-) diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index c5df1910a4d6a2d0b660c73cb31b936f67a9d76b..63813b2783b6bfebed3599fece426a8f3ee23141 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -587,9 +587,9 @@ pub fn open_settings_editor( settings_window.opening_link = true; settings_window.search_bar.update(cx, |editor, cx| { - editor.set_text(query, window, cx); + editor.set_text(query.clone(), window, cx); }); - settings_window.apply_match_indices(indices.iter().copied()); + settings_window.apply_match_indices(indices.iter().copied(), &query); if indices.len() == 1 && let Some(search_index) = settings_window.search_index.as_ref() @@ -1873,7 +1873,7 @@ impl SettingsWindow { indices } - fn apply_match_indices(&mut self, match_indices: impl Iterator) { + fn apply_match_indices(&mut self, match_indices: impl Iterator, query: &str) { let Some(search_index) = self.search_index.as_ref() else { return; }; @@ -1895,8 +1895,11 @@ impl SettingsWindow { } self.has_query = true; self.filter_matches_to_file(); - self.open_first_nav_page(); + let query_lower = query.to_lowercase(); + let query_words: Vec<&str> = query_lower.split_whitespace().collect(); + self.open_best_matching_nav_page(&query_words); self.reset_list_state(); + self.scroll_content_to_best_match(&query_words); } fn update_matches(&mut self, cx: &mut Context) { @@ -1917,7 +1920,7 @@ impl SettingsWindow { if is_json_link_query { let indices = self.filter_by_json_path(&query); if !indices.is_empty() { - self.apply_match_indices(indices.into_iter()); + self.apply_match_indices(indices.into_iter(), &query); cx.notify(); return; } @@ -1960,19 +1963,18 @@ impl SettingsWindow { let fuzzy_matches = fuzzy_search_task.await; let exact_matches = exact_match_task.await; - _ = this - .update(cx, |this, cx| { - let exact_indices = exact_matches.into_iter(); - let fuzzy_indices = fuzzy_matches - .into_iter() - .take_while(|fuzzy_match| fuzzy_match.score >= 0.5) - .map(|fuzzy_match| fuzzy_match.candidate_id); - let merged_indices = exact_indices.chain(fuzzy_indices); + this.update(cx, |this, cx| { + let exact_indices = exact_matches.into_iter(); + let fuzzy_indices = fuzzy_matches + .into_iter() + .take_while(|fuzzy_match| fuzzy_match.score >= 0.5) + .map(|fuzzy_match| fuzzy_match.candidate_id); + let merged_indices = exact_indices.chain(fuzzy_indices); - this.apply_match_indices(merged_indices); - cx.notify(); - }) - .ok(); + this.apply_match_indices(merged_indices, &query); + cx.notify(); + }) + .ok(); cx.background_executor().timer(Duration::from_secs(1)).await; telemetry::event!("Settings Searched", query = query) @@ -2249,6 +2251,58 @@ impl SettingsWindow { self.sub_page_stack.clear(); } + fn open_best_matching_nav_page(&mut self, query_words: &[&str]) { + let mut entries = self.visible_navbar_entries().peekable(); + let first_entry = entries.peek().map(|(index, _)| (0, *index)); + let best_match = entries + .enumerate() + .filter(|(_, (_, entry))| !entry.is_root) + .map(|(logical_index, (index, entry))| { + let title_lower = entry.title.to_lowercase(); + let matching_words = query_words + .iter() + .filter(|query_word| { + title_lower + .split_whitespace() + .any(|title_word| title_word.starts_with(*query_word)) + }) + .count(); + (logical_index, index, matching_words) + }) + .filter(|(_, _, count)| *count > 0) + .max_by_key(|(_, _, count)| *count) + .map(|(logical_index, index, _)| (logical_index, index)); + if let Some((logical_index, navbar_entry_index)) = best_match.or(first_entry) { + self.open_navbar_entry_page(navbar_entry_index); + self.navbar_scroll_handle + .scroll_to_item(logical_index + 1, gpui::ScrollStrategy::Top); + } + } + + fn scroll_content_to_best_match(&self, query_words: &[&str]) { + let position = self + .visible_page_items() + .enumerate() + .find(|(_, (_, item))| match item { + SettingsPageItem::SectionHeader(title) => { + let title_lower = title.to_lowercase(); + query_words.iter().all(|query_word| { + title_lower + .split_whitespace() + .any(|title_word| title_word.starts_with(query_word)) + }) + } + _ => false, + }) + .map(|(position, _)| position); + if let Some(position) = position { + self.list_state.scroll_to(gpui::ListOffset { + item_ix: position + 1, + offset_in_item: px(0.), + }); + } + } + fn open_first_nav_page(&mut self) { let Some(first_navbar_entry_index) = self.visible_navbar_entries().next().map(|e| e.0) else {