fix(tui): find files in deeply nested directories via @ and Ctrl+P (#2488)
`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
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<String> {
|
||||
/// 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<usize>) -> Vec<String> {
|
||||
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<String> {
|
||||
.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!(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -362,9 +362,11 @@ fn browser_completion_dir_part(dir_part: &str) -> Option<PathBuf> {
|
||||
}
|
||||
|
||||
/// 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<usize> {
|
||||
if depth == 0 { None } else { Some(depth) }
|
||||
|
||||
Reference in New Issue
Block a user