From 6e9baaa9886324adae87602717e84d6d04dae7dc Mon Sep 17 00:00:00 2001 From: Hunter B Date: Fri, 12 Jun 2026 03:05:56 -0700 Subject: [PATCH] refactor(tui): group completion search state Introduce a small SearchContext for workspace completion walks so the related search state moves together and the helper signatures no longer need too-many-arguments allowances. Also keeps the nearby hotbar test clippy-clean under the all-targets TUI gate. Modified harvest from PR #3128 by @Hmbown. Co-authored-by: Hmbown <101357273+Hmbown@users.noreply.github.com> --- crates/tui/src/tui/ui/tests.rs | 6 +- crates/tui/src/working_set.rs | 157 +++++++++++++-------------------- 2 files changed, 66 insertions(+), 97 deletions(-) diff --git a/crates/tui/src/tui/ui/tests.rs b/crates/tui/src/tui/ui/tests.rs index 68a001a0..ca75e80c 100644 --- a/crates/tui/src/tui/ui/tests.rs +++ b/crates/tui/src/tui/ui/tests.rs @@ -3340,8 +3340,10 @@ fn hotbar_dispatches_bound_slot_and_ignores_empty_slot() { "mode-changing hotbar actions should leave the app ready to redraw" ); - let mut empty_config = Config::default(); - empty_config.hotbar = Some(Vec::new()); + let empty_config = Config { + hotbar: Some(Vec::new()), + ..Config::default() + }; assert_eq!( dispatch_hotbar_slot(&mut app, &empty_config, 1).expect("empty slot is ok"), None diff --git a/crates/tui/src/working_set.rs b/crates/tui/src/working_set.rs index d604553c..a6ecd2cb 100644 --- a/crates/tui/src/working_set.rs +++ b/crates/tui/src/working_set.rs @@ -35,6 +35,33 @@ pub struct Workspace { completion_walk_depth: Option, } +struct SearchContext<'a> { + needle: &'a str, + limit: usize, + prefix_hits: &'a mut Vec, + substring_hits: &'a mut Vec, + seen: &'a mut HashSet, +} + +impl SearchContext<'_> { + fn is_full(&self) -> bool { + self.prefix_hits.len() + self.substring_hits.len() >= self.limit + } + + fn remember(&mut self, path: PathBuf) -> bool { + self.seen.insert(path) + } + + fn push_match(&mut self, candidate: String) { + let lower = candidate.to_lowercase(); + if self.needle.is_empty() || lower.starts_with(self.needle) { + self.prefix_hits.push(candidate); + } else if lower.contains(self.needle) { + self.substring_hits.push(candidate); + } + } +} + impl Workspace { /// Construct a workspace anchored at `root`, capturing the process CWD as /// the secondary resolution pass. Convenience entry point intended for @@ -220,53 +247,32 @@ impl Workspace { // Walk the recorded cwd first when it diverges from the workspace // root, so cwd-relative entries appear ahead of duplicates surfaced by // the workspace walk. - let cwd_diverges = self - .cwd - .as_deref() - .map(|c| c != self.root.as_path()) - .unwrap_or(false); - if cwd_diverges && let Some(cwd) = self.cwd.as_deref() { - walk_for_completions( - cwd, - cwd, - &needle, + { + let mut ctx = SearchContext { + needle: &needle, limit, - &mut prefix_hits, - &mut substring_hits, - &mut seen, - self.completion_walk_depth, - ); + prefix_hits: &mut prefix_hits, + substring_hits: &mut substring_hits, + seen: &mut seen, + }; + + let cwd_diverges = self + .cwd + .as_deref() + .map(|c| c != self.root.as_path()) + .unwrap_or(false); + if cwd_diverges && let Some(cwd) = self.cwd.as_deref() { + walk_for_completions(cwd, cwd, &mut ctx, self.completion_walk_depth); + add_local_reference_completions(cwd, cwd, &mut ctx, self.completion_walk_depth); + } + walk_for_completions(&self.root, &self.root, &mut ctx, self.completion_walk_depth); add_local_reference_completions( - cwd, - cwd, - &needle, - limit, - &mut prefix_hits, - &mut substring_hits, - &mut seen, + &self.root, + &self.root, + &mut ctx, self.completion_walk_depth, ); } - walk_for_completions( - &self.root, - &self.root, - &needle, - limit, - &mut prefix_hits, - &mut substring_hits, - &mut seen, - self.completion_walk_depth, - ); - add_local_reference_completions( - &self.root, - &self.root, - &needle, - limit, - &mut prefix_hits, - &mut substring_hits, - &mut seen, - self.completion_walk_depth, - ); prefix_hits.sort(); substring_hits.sort(); @@ -402,15 +408,10 @@ fn discovery_walk_builder(root: &Path, max_depth: Option) -> WalkBuilder /// Walk the AI-tool dot-directories (`.deepseek/`, `.cursor/`, `.claude/`, /// `.agents/`) with gitignore disabled so their contents are discoverable /// even when the project's `.gitignore` / `.ignore` excludes them. -#[allow(clippy::too_many_arguments)] fn walk_always_discoverable_dirs( walk_root: &Path, display_root: &Path, - needle: &str, - limit: usize, - prefix_hits: &mut Vec, - substring_hits: &mut Vec, - seen: &mut HashSet, + ctx: &mut SearchContext<'_>, max_depth: Option, ) { for dir_name in DISCOVERY_ALWAYS_DIRS { @@ -428,7 +429,7 @@ fn walk_always_discoverable_dirs( builder.max_depth(Some(depth.saturating_sub(1))); } for entry in builder.build().flatten() { - if prefix_hits.len() + substring_hits.len() >= limit { + if ctx.is_full() { break; } let path = entry.path(); @@ -445,7 +446,7 @@ fn walk_always_discoverable_dirs( continue; } let abs = path.to_path_buf(); - if !seen.insert(abs) { + if !ctx.remember(abs) { continue; } let is_dir = entry.file_type().is_some_and(|ft| ft.is_dir()); @@ -454,31 +455,21 @@ fn walk_always_discoverable_dirs( } else { rel_str.clone() }; - let lower = candidate.to_lowercase(); - if needle.is_empty() || lower.starts_with(needle) { - prefix_hits.push(candidate); - } else if lower.contains(needle) { - substring_hits.push(candidate); - } + ctx.push_match(candidate); } } } -#[allow(clippy::too_many_arguments)] fn walk_for_completions( walk_root: &Path, display_root: &Path, - needle: &str, - limit: usize, - prefix_hits: &mut Vec, - substring_hits: &mut Vec, - seen: &mut HashSet, + ctx: &mut SearchContext<'_>, max_depth: Option, ) { let builder = discovery_walk_builder(walk_root, max_depth); for entry in builder.build().flatten() { - if prefix_hits.len() + substring_hits.len() >= limit { + if ctx.is_full() { break; } let path = entry.path(); @@ -492,7 +483,7 @@ fn walk_for_completions( // Dedup across the (cwd, workspace) double-walk by absolute path; we // want the cwd-relative display when both walks see the same file. let abs = path.to_path_buf(); - if !seen.insert(abs) { + if !ctx.remember(abs) { continue; } let is_dir = entry.file_type().is_some_and(|ft| ft.is_dir()); @@ -501,62 +492,38 @@ fn walk_for_completions( } else { rel_str.clone() }; - let lower = candidate.to_lowercase(); - if needle.is_empty() || lower.starts_with(needle) { - prefix_hits.push(candidate); - } else if lower.contains(needle) { - substring_hits.push(candidate); - } + ctx.push_match(candidate); } // Also walk the AI-tool dot-directories with gitignore disabled so // `.deepseek/`, `.cursor/`, etc. are always discoverable. - walk_always_discoverable_dirs( - walk_root, - display_root, - needle, - limit, - prefix_hits, - substring_hits, - seen, - max_depth, - ); + walk_always_discoverable_dirs(walk_root, display_root, ctx, max_depth); } const LOCAL_REFERENCE_SCAN_LIMIT: usize = 4096; -#[allow(clippy::too_many_arguments)] fn add_local_reference_completions( root: &Path, display_root: &Path, - needle: &str, - limit: usize, - prefix_hits: &mut Vec, - substring_hits: &mut Vec, - seen: &mut HashSet, + ctx: &mut SearchContext<'_>, max_depth: Option, ) { - if !should_try_local_reference_completion(needle) { + if !should_try_local_reference_completion(ctx.needle) { return; } for path in local_reference_paths(root, LOCAL_REFERENCE_SCAN_LIMIT, max_depth) { - if prefix_hits.len() + substring_hits.len() >= limit { + if ctx.is_full() { break; } let Ok(rel) = path.strip_prefix(display_root) else { continue; }; let rel_str = rel.to_string_lossy().replace('\\', "/"); - if rel_str.is_empty() || !seen.insert(path.clone()) { + if rel_str.is_empty() || !ctx.remember(path.clone()) { continue; } - let lower = rel_str.to_lowercase(); - if needle.is_empty() || lower.starts_with(needle) { - prefix_hits.push(rel_str); - } else if lower.contains(needle) { - substring_hits.push(rel_str); - } + ctx.push_match(rel_str); } }