sidebar: Better search (#56166)

Cameron Mcloughlin created

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 ...

Change summary

crates/agent_ui/src/threads_archive_view.rs | 36 +++++++++++++---------
crates/sidebar/src/sidebar.rs               | 23 --------------
2 files changed, 22 insertions(+), 37 deletions(-)

Detailed changes

crates/agent_ui/src/threads_archive_view.rs 🔗

@@ -102,24 +102,30 @@ impl TimeBucket {
     }
 }
 
-fn fuzzy_match_positions(query: &str, text: &str) -> Option<Vec<usize>> {
-    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<Vec<usize>> {
+    let query_chars: Vec<char> = 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 {

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<Vec<usize>> {
-    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