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>
This commit is contained in:
Hunter B
2026-06-12 03:05:56 -07:00
parent c95122fb43
commit 6e9baaa988
2 changed files with 66 additions and 97 deletions
+4 -2
View File
@@ -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
+62 -95
View File
@@ -35,6 +35,33 @@ pub struct Workspace {
completion_walk_depth: Option<usize>,
}
struct SearchContext<'a> {
needle: &'a str,
limit: usize,
prefix_hits: &'a mut Vec<String>,
substring_hits: &'a mut Vec<String>,
seen: &'a mut HashSet<PathBuf>,
}
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<usize>) -> 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<String>,
substring_hits: &mut Vec<String>,
seen: &mut HashSet<PathBuf>,
ctx: &mut SearchContext<'_>,
max_depth: Option<usize>,
) {
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<String>,
substring_hits: &mut Vec<String>,
seen: &mut HashSet<PathBuf>,
ctx: &mut SearchContext<'_>,
max_depth: Option<usize>,
) {
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<String>,
substring_hits: &mut Vec<String>,
seen: &mut HashSet<PathBuf>,
ctx: &mut SearchContext<'_>,
max_depth: Option<usize>,
) {
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);
}
}