From 7940ded92a16bb7355f55f227465583f672a813e Mon Sep 17 00:00:00 2001 From: Cameron Mcloughlin Date: Fri, 8 May 2026 14:13:14 +0100 Subject: [PATCH] sidebar: Better search (#56166) Makes the sidebar search case insensitive, and also require contiguous matches. Also removes the duplicate logic for the sidebar and thread history view Release Notes: - N/A or Added/Fixed/Improved ... --- crates/agent_ui/src/threads_archive_view.rs | 36 ++++++++++++--------- crates/sidebar/src/sidebar.rs | 23 +------------ 2 files changed, 22 insertions(+), 37 deletions(-) diff --git a/crates/agent_ui/src/threads_archive_view.rs b/crates/agent_ui/src/threads_archive_view.rs index 5da5526b3df3687fae840a963cabea1a1fa172e9..ebd8c3d94f01d4bea1db6395047ad15bfb34c120 100644 --- a/crates/agent_ui/src/threads_archive_view.rs +++ b/crates/agent_ui/src/threads_archive_view.rs @@ -102,24 +102,30 @@ impl TimeBucket { } } -fn fuzzy_match_positions(query: &str, text: &str) -> Option> { - let mut positions = Vec::new(); - let mut query_chars = query.chars().peekable(); - for (byte_idx, candidate_char) in text.char_indices() { - if let Some(&query_char) = query_chars.peek() { - if candidate_char.eq_ignore_ascii_case(&query_char) { - positions.push(byte_idx); - query_chars.next(); +pub fn fuzzy_match_positions(query: &str, candidate: &str) -> Option> { + let query_chars: Vec = query.chars().collect(); + if query_chars.is_empty() { + return Some(Vec::new()); + } + + let candidate_chars: Vec<(usize, char)> = candidate.char_indices().collect(); + let window_count = candidate_chars.len().checked_sub(query_chars.len() - 1)?; + + 'outer: for window_start in 0..window_count { + for (qi, &query_char) in query_chars.iter().enumerate() { + let (_, cand_char) = candidate_chars[window_start + qi]; + if !cand_char.eq_ignore_ascii_case(&query_char) { + continue 'outer; } - } else { - break; } + return Some( + (0..query_chars.len()) + .map(|qi| candidate_chars[window_start + qi].0) + .collect(), + ); } - if query_chars.peek().is_none() { - Some(positions) - } else { - None - } + + None } pub enum ThreadsArchiveViewEvent { diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs index a25fbac1513ae2d546af066ef0288660c205b1db..597bb21811d1a38badcfbf6b951a1758d8a5d2db 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/sidebar/src/sidebar.rs @@ -10,6 +10,7 @@ use agent_ui::thread_metadata_store::{ use agent_ui::thread_worktree_archive; use agent_ui::threads_archive_view::{ ThreadsArchiveView, ThreadsArchiveViewEvent, format_history_entry_timestamp, + fuzzy_match_positions, }; use agent_ui::{ AcpThreadImportOnboarding, Agent, AgentPanel, AgentPanelEvent, AgentPanelTerminalInfo, @@ -377,28 +378,6 @@ impl SidebarContents { } } -fn fuzzy_match_positions(query: &str, candidate: &str) -> Option> { - let mut positions = Vec::new(); - let mut query_chars = query.chars().peekable(); - - for (byte_idx, candidate_char) in candidate.char_indices() { - if let Some(&query_char) = query_chars.peek() { - if candidate_char.eq_ignore_ascii_case(&query_char) { - positions.push(byte_idx); - query_chars.next(); - } - } else { - break; - } - } - - if query_chars.peek().is_none() { - Some(positions) - } else { - None - } -} - // TODO: The mapping from workspace root paths to git repositories needs a // unified approach across the codebase: this function, `AgentPanel::classify_worktrees`, // thread persistence (which PathList is saved to the database), and thread