From 478bae451ad77a0c3e0d736cd9f2367de338e5c3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 3 Jun 2026 01:08:42 +0000 Subject: [PATCH] fix(tui): find files in deeply nested directories via @ and Ctrl+P (#2488) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ignore`'s `max_depth(Some(6))` excludes files inside a 6-level-deep directory (they sit at component depth 7), so @-mention completion and the Ctrl+P picker silently missed them. Raise the default walk depth to 10 (covers conventionally nested Java/.NET/web trees) and make the Ctrl+P picker honor the configurable `mention_walk_depth` — including `0` for an unlimited walk — so it matches @-mention behavior and the existing "set mention_walk_depth 0 to search deeper" guidance. The walk stays bounded by `.gitignore` and `MAX_CANDIDATES`. Adds a regression test covering depth-6 miss, default reach, and unlimited. https://claude.ai/code/session_01MQrnh6wHfrEYN5BBdMarC1 --- crates/tui/src/settings.rs | 4 +- crates/tui/src/tui/file_picker.rs | 80 ++++++++++++++++++--- crates/tui/src/tui/file_picker_relevance.rs | 13 ++-- crates/tui/src/working_set.rs | 8 ++- 4 files changed, 86 insertions(+), 19 deletions(-) diff --git a/crates/tui/src/settings.rs b/crates/tui/src/settings.rs index 021475ca..42fcfe13 100644 --- a/crates/tui/src/settings.rs +++ b/crates/tui/src/settings.rs @@ -335,7 +335,7 @@ impl Default for Settings { bracketed_paste: true, paste_burst_detection: true, mention_menu_limit: 128, - mention_walk_depth: 6, + mention_walk_depth: 10, mention_menu_behavior: "fuzzy".to_string(), show_thinking: true, show_tool_details: true, @@ -1300,7 +1300,7 @@ mod tests { fn mention_completion_caps_are_configurable() { let mut settings = Settings::default(); assert_eq!(settings.mention_menu_limit, 128); - assert_eq!(settings.mention_walk_depth, 6); + assert_eq!(settings.mention_walk_depth, 10); assert_eq!(settings.mention_menu_behavior, "fuzzy"); settings diff --git a/crates/tui/src/tui/file_picker.rs b/crates/tui/src/tui/file_picker.rs index 76b05d15..dceab769 100644 --- a/crates/tui/src/tui/file_picker.rs +++ b/crates/tui/src/tui/file_picker.rs @@ -1,7 +1,8 @@ //! Fuzzy file-picker modal (Ctrl+P). //! //! Opens an overlay populated with workspace-relative paths discovered by a -//! single-pass `WalkBuilder` walk (depth 6, hidden=true, follow_links=false, +//! single-pass `WalkBuilder` walk (depth from `mention_walk_depth`, default +//! 10, `0` = unlimited; hidden=true, follow_links=false, //! `.gitignore` honored). Subsequent keystrokes filter the cached candidate //! list in memory using a small subsequence + first-letter-bonus scorer — no //! per-keystroke disk traversal. @@ -31,8 +32,10 @@ use crate::workspace_discovery::{DISCOVERY_ALWAYS_DIRS, path_is_excluded_from_di /// equivalent overlay. const MAX_CANDIDATES: usize = 20_000; -/// Walk depth for the initial scan. Mirrors the `Workspace` fuzzy index. -const WALK_DEPTH: usize = 6; +/// Default walk depth for the initial scan when no explicit depth is supplied. +/// Mirrors the `Workspace` fuzzy index default (`DEFAULT_COMPLETIONS_WALK_DEPTH`). +/// `mention_walk_depth = 0` overrides this with an unlimited walk. +const WALK_DEPTH: usize = 10; /// Visible candidate rows in the overlay. const VISIBLE_ROWS: usize = 14; @@ -121,9 +124,26 @@ pub struct FilePickerView { } impl FilePickerView { - /// Build a picker with working-set relevance hints. + /// Build a picker with working-set relevance hints, using the default + /// walk depth ([`WALK_DEPTH`]). pub fn new_with_relevance(workspace_root: &Path, relevance: FilePickerRelevance) -> Self { - let candidates = collect_candidates(workspace_root); + Self::new_with_relevance_and_depth(workspace_root, relevance, WALK_DEPTH) + } + + /// Build a picker with working-set relevance hints and an explicit walk + /// depth. A depth of `0` disables the depth limit so files in deeply + /// nested workspaces (>= 6 levels) remain discoverable (#2488). + pub fn new_with_relevance_and_depth( + workspace_root: &Path, + relevance: FilePickerRelevance, + walk_depth: usize, + ) -> Self { + let max_depth = if walk_depth == 0 { + None + } else { + Some(walk_depth) + }; + let candidates = collect_candidates(workspace_root, max_depth); let mut view = Self { candidates, relevance, @@ -406,13 +426,15 @@ fn truncate_path(path: &str, max: usize) -> String { format!("…{truncated}") } -/// Single-pass walk that collects workspace-relative paths. -fn collect_candidates(root: &Path) -> Vec { +/// Single-pass walk that collects workspace-relative paths. `max_depth` of +/// `None` walks the whole tree (still bounded by `MAX_CANDIDATES` and +/// `.gitignore`); `Some(n)` caps the recursion at `n` levels. +fn collect_candidates(root: &Path, max_depth: Option) -> Vec { let mut builder = WalkBuilder::new(root); builder .hidden(true) .follow_links(false) - .max_depth(Some(WALK_DEPTH)) + .max_depth(max_depth) .git_ignore(true) .git_exclude(true) .git_global(true); @@ -449,7 +471,7 @@ fn collect_candidates(root: &Path) -> Vec { .follow_links(false) .git_ignore(false) .ignore(false) - .max_depth(Some(WALK_DEPTH.saturating_sub(1))); + .max_depth(max_depth.map(|d| d.saturating_sub(1))); for entry in dot_builder.build().flatten() { // Exclude machine-generated bulk (e.g. .deepseek/snapshots/). if path_is_excluded_from_discovery(root, entry.path()) { @@ -735,6 +757,44 @@ mod tests { ); } + #[test] + fn picker_finds_deeply_nested_files_within_walk_depth() { + // #2488: a file inside a 6-level-deep directory sits at component depth + // 7 and was excluded by the old depth-6 cap. The default depth (10) now + // reaches it, and `0` (unlimited) reaches arbitrarily deep files. + let dir = TempDir::new().expect("tempdir"); + let root = dir.path(); + let nested = root.join("a/b/c/d/e/f"); + fs::create_dir_all(&nested).unwrap(); + fs::write(nested.join("deep.rs"), "deep").unwrap(); + let deeper = root.join("a/b/c/d/e/f/g/h/i/j/k"); + fs::create_dir_all(&deeper).unwrap(); + fs::write(deeper.join("very_deep.rs"), "deeper").unwrap(); + + // The old default (6) misses the depth-7 file — the reported bug. + let shallow = collect_candidates(root, Some(6)); + assert!( + !shallow.iter().any(|p| p == "a/b/c/d/e/f/deep.rs"), + "depth-6 cap should miss the depth-7 file: {shallow:?}" + ); + + // The new default reaches files inside a 6-level-deep directory. + let default = collect_candidates(root, Some(WALK_DEPTH)); + assert!( + default.iter().any(|p| p == "a/b/c/d/e/f/deep.rs"), + "default walk depth should reach depth-7 files: {default:?}" + ); + + // Unlimited (mention_walk_depth = 0) reaches arbitrarily deep files. + let unlimited = collect_candidates(root, None); + assert!( + unlimited + .iter() + .any(|p| p == "a/b/c/d/e/f/g/h/i/j/k/very_deep.rs"), + "unlimited walk should reach very deep files: {unlimited:?}" + ); + } + #[test] fn picker_skips_generated_worktree_bulk_inside_unignored_dot_dirs() { let dir = TempDir::new().expect("tempdir"); @@ -760,7 +820,7 @@ mod tests { ) .unwrap(); - let candidates = collect_candidates(root); + let candidates = collect_candidates(root, Some(WALK_DEPTH)); assert!(candidates.iter().any(|path| path == "src/main.rs")); assert!( diff --git a/crates/tui/src/tui/file_picker_relevance.rs b/crates/tui/src/tui/file_picker_relevance.rs index dbb07e22..73e69227 100644 --- a/crates/tui/src/tui/file_picker_relevance.rs +++ b/crates/tui/src/tui/file_picker_relevance.rs @@ -28,10 +28,15 @@ use crate::tui::file_picker::FilePickerView; /// per-session relevance ranks (modified, @-mentioned, tool-touched). pub(super) fn open_file_picker(app: &mut App) { let relevance = build_relevance(app); - app.view_stack.push(FilePickerView::new_with_relevance( - &app.workspace, - relevance, - )); + // Honor the configured `mention_walk_depth` (0 = unlimited) so the picker + // and `@`-mention completion agree, and files in deeply nested trees stay + // discoverable (#2488). + app.view_stack + .push(FilePickerView::new_with_relevance_and_depth( + &app.workspace, + relevance, + app.mention_walk_depth, + )); } pub(super) fn build_relevance(app: &App) -> FilePickerRelevance { diff --git a/crates/tui/src/working_set.rs b/crates/tui/src/working_set.rs index 4e4ce95e..277c74f9 100644 --- a/crates/tui/src/working_set.rs +++ b/crates/tui/src/working_set.rs @@ -362,9 +362,11 @@ fn browser_completion_dir_part(dir_part: &str) -> Option { } /// Default directory depth walked when surfacing file-mention completions. -/// Mirrors the existing `project_tree` cutoff and keeps Tab snappy in deep -/// monorepos unless the user opts into a deeper walk. -pub const DEFAULT_COMPLETIONS_WALK_DEPTH: usize = 6; +/// Set high enough that conventionally nested source trees (Java/.NET/web +/// projects routinely reach 7-9 levels) stay reachable, while a `0` override +/// removes the limit entirely. Keeps Tab snappy in deep monorepos via the +/// `.gitignore`-aware walk and per-keypress candidate caps (#2488). +pub const DEFAULT_COMPLETIONS_WALK_DEPTH: usize = 10; fn normalize_completion_walk_depth(depth: usize) -> Option { if depth == 0 { None } else { Some(depth) }