fix(tui): honor deepseekignore in mention fallback

This commit is contained in:
samhandsome
2026-05-09 14:03:15 +08:00
committed by Hunter Bown
parent cdcb907780
commit 0866fd6edc
+160
View File
@@ -370,6 +370,96 @@ fn walk_for_completions(
);
}
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>,
) {
if !should_try_local_reference_completion(needle) {
return;
}
for path in local_reference_paths(root, LOCAL_REFERENCE_SCAN_LIMIT) {
if prefix_hits.len() + substring_hits.len() >= limit {
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()) {
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);
}
}
}
fn should_try_local_reference_completion(needle: &str) -> bool {
!needle.is_empty() && (needle.starts_with('.') || needle.contains('/') || needle.contains('\\'))
}
fn local_reference_paths(root: &Path, limit: usize) -> Vec<PathBuf> {
let mut out = Vec::new();
let mut builder = WalkBuilder::new(root);
builder
.hidden(false)
.follow_links(false)
.max_depth(Some(COMPLETIONS_WALK_DEPTH))
.git_ignore(false)
.git_global(false)
.git_exclude(false);
let _ = builder.add_custom_ignore_filename(".deepseekignore");
builder.filter_entry(|entry| !should_skip_local_reference_dir(entry.path()));
for entry in builder.build().flatten() {
if out.len() >= limit {
break;
}
let path = entry.path();
if path == root {
continue;
}
if entry
.file_type()
.is_some_and(|ft| ft.is_file() || ft.is_dir())
{
out.push(path.to_path_buf());
}
}
out
}
fn should_skip_local_reference_dir(path: &Path) -> bool {
let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
return false;
};
matches!(
name,
".git"
| "target"
| "node_modules"
| ".venv"
| "venv"
| "env"
| "dist"
| "build"
| "__pycache__"
| ".ruff_cache"
)
}
impl Clone for Workspace {
fn clone(&self) -> Self {
// Don't carry the cached file_index — clones get a fresh OnceLock so
@@ -1318,6 +1408,76 @@ mod tests {
);
}
#[test]
fn workspace_completions_surface_explicit_hidden_and_ignored_paths() {
let tmp = TempDir::new().unwrap();
std::fs::write(tmp.path().join(".gitignore"), ".deepseek/\n.generated/\n").unwrap();
std::fs::write(
tmp.path().join(".deepseekignore"),
".generated/specs/secrets.env\n",
)
.unwrap();
let deepseek_commands = tmp.path().join(".deepseek").join("commands");
let generated_specs = tmp.path().join(".generated").join("specs");
std::fs::create_dir_all(&deepseek_commands).unwrap();
std::fs::create_dir_all(&generated_specs).unwrap();
std::fs::write(deepseek_commands.join("start-task.md"), "start").unwrap();
std::fs::write(generated_specs.join("device-layout.md"), "layout").unwrap();
std::fs::write(generated_specs.join("secrets.env"), "secret").unwrap();
let ws = Workspace::with_cwd(tmp.path().to_path_buf(), Some(tmp.path().to_path_buf()));
let start_entries = ws.completions(".deepseek/commands", 16);
assert!(
start_entries
.iter()
.any(|e| e == ".deepseek/commands/start-task.md"),
"expected explicitly addressed hidden command file in completions: {start_entries:?}",
);
let generated_entries = ws.completions(".generated/specs", 16);
assert!(
generated_entries
.iter()
.any(|e| e == ".generated/specs/device-layout.md"),
"expected explicitly addressed ignored user folder in completions: {generated_entries:?}",
);
assert!(
!generated_entries
.iter()
.any(|e| e == ".generated/specs/secrets.env"),
".deepseekignore entries must not be reintroduced by local fallback: {generated_entries:?}",
);
}
#[test]
fn fuzzy_index_resolves_hidden_and_ignored_files_except_deepseekignored() {
let tmp = TempDir::new().unwrap();
std::fs::write(tmp.path().join(".gitignore"), ".generated/\n").unwrap();
std::fs::write(
tmp.path().join(".deepseekignore"),
".generated/specs/secrets.env\n",
)
.unwrap();
let generated_specs = tmp.path().join(".generated").join("specs");
std::fs::create_dir_all(&generated_specs).unwrap();
std::fs::write(generated_specs.join("device-layout.md"), "layout").unwrap();
std::fs::write(generated_specs.join("secrets.env"), "secret").unwrap();
let ws = Workspace::with_cwd(tmp.path().to_path_buf(), None);
let resolved = ws.resolve("device-layout.md").unwrap();
assert!(resolved.ends_with(".generated/specs/device-layout.md"));
assert!(
ws.resolve("secrets.env").is_err(),
"basename fuzzy resolution must honor .deepseekignore"
);
assert!(
ws.resolve(".generated/specs/secrets.env").is_ok(),
"exact user-specified paths should still resolve"
);
}
#[test]
fn fuzzy_index_finds_files_and_directories() {
let tmp = TempDir::new().unwrap();