use std::fmt::Write as _;
use std::path::{Path, PathBuf};
use ignore::WalkBuilder;
const MAX_WALK_DEPTH: usize = 12;
const MAX_FILES_SCANNED: usize = 5_000;
const MAX_HITS: usize = 5;
const MAX_FILE_SIZE: u64 = 512_000;
const MIN_NEEDLE_LEN: usize = 12;
pub(crate) fn moved_or_deleted_hint(target: &Path) -> String {
let Some(name) = target.file_name().and_then(|n| n.to_str()) else {
return String::new();
};
let Some(root) = search_root(target) else {
return format!(
"\nNo file at `{}` — it may have been deleted, or the path/name is wrong \
(use create=true to create it).",
target.display()
);
};
let mut hits: Vec<PathBuf> = Vec::new();
let mut scanned = 0usize;
for entry in walk(&root) {
if entry.file_type().is_none_or(|ft| !ft.is_file()) {
continue;
}
scanned += 1;
if scanned > MAX_FILES_SCANNED {
break;
}
if entry.file_name().to_str() == Some(name) {
hits.push(entry.into_path());
if hits.len() >= MAX_HITS {
break;
}
}
}
if hits.is_empty() {
format!(
"\nNo file named `{name}` exists under {}. It may have been deleted, \
or the path/name is wrong (use create=true to create it).",
root.display()
)
} else {
let mut out = String::from(
"\nThe path does not exist, but a same-named file was found — did you mean:",
);
for p in &hits {
let _ = write!(out, "\n - {}", p.display());
}
out
}
}
pub(crate) fn cross_file_hint(target: &Path, old_str: &str) -> String {
let Some(needle) = distinctive_line(old_str) else {
return String::new();
};
let Some(root) = search_root(target) else {
return String::new();
};
let target_canon = std::fs::canonicalize(target).ok();
let mut hits: Vec<PathBuf> = Vec::new();
let mut scanned = 0usize;
for entry in walk(&root) {
if entry.file_type().is_none_or(|ft| !ft.is_file()) {
continue;
}
let path = entry.path();
if target_canon.is_some()
&& target.file_name() == Some(entry.file_name())
&& std::fs::canonicalize(path).ok() == target_canon
{
continue;
}
scanned += 1;
if scanned > MAX_FILES_SCANNED {
break;
}
let too_big = std::fs::metadata(path).is_ok_and(|m| m.len() > MAX_FILE_SIZE);
if too_big {
continue;
}
if let Ok(content) = std::fs::read_to_string(path) {
if content.contains(needle) {
hits.push(path.to_path_buf());
if hits.len() >= MAX_HITS {
break;
}
}
}
}
if hits.is_empty() {
return String::new();
}
let mut out = String::from("\nold_string was not found here, but a matching line exists in:");
for p in &hits {
let _ = write!(out, "\n - {}", p.display());
}
out.push_str("\nIf you meant one of these, retry the edit against that file.");
out
}
fn walk(root: &Path) -> impl Iterator<Item = ignore::DirEntry> {
WalkBuilder::new(root)
.hidden(true)
.git_ignore(true)
.git_global(true)
.git_exclude(true)
.max_depth(Some(MAX_WALK_DEPTH))
.build()
.filter_map(Result::ok)
}
fn search_root(target: &Path) -> Option<PathBuf> {
let abs = if target.is_absolute() {
target.to_path_buf()
} else {
std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join(target)
};
let mut base = abs;
while !base.exists() {
base = base.parent()?.to_path_buf();
}
if base.is_file() {
base = base.parent()?.to_path_buf();
}
let mut probe: &Path = base.as_path();
for _ in 0..40 {
if probe.join(".git").exists() {
return Some(probe.to_path_buf());
}
probe = probe.parent()?;
}
None
}
fn distinctive_line(s: &str) -> Option<&str> {
s.lines()
.map(str::trim)
.filter(|l| l.len() >= MIN_NEEDLE_LEN)
.max_by_key(|l| l.len())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
fn repo(dir: &Path) {
fs::create_dir_all(dir.join(".git")).unwrap();
}
#[test]
fn moved_hint_points_to_relocated_file() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
repo(root);
fs::create_dir_all(root.join("src/new")).unwrap();
fs::write(root.join("src/new/widget.rs"), "fn widget() {}\n").unwrap();
let hint = moved_or_deleted_hint(&root.join("src/old/widget.rs"));
assert!(hint.contains("same-named file was found"), "got: {hint}");
assert!(hint.contains("widget.rs"), "got: {hint}");
}
#[test]
fn moved_hint_reports_truly_missing() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
repo(root);
fs::write(root.join("present.rs"), "fn present() {}\n").unwrap();
let hint = moved_or_deleted_hint(&root.join("totally_unique_zzz.rs"));
assert!(hint.contains("No file named"), "got: {hint}");
assert!(hint.contains("totally_unique_zzz.rs"), "got: {hint}");
}
#[test]
fn cross_file_hint_finds_symbol_in_other_file() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
repo(root);
let target = root.join("a.rs");
fs::write(&target, "fn unrelated_in_a() {}\n").unwrap();
fs::write(root.join("b.rs"), "pub fn the_distinctive_function() {}\n").unwrap();
let hint = cross_file_hint(&target, "pub fn the_distinctive_function() {}");
assert!(hint.contains("matching line exists in"), "got: {hint}");
assert!(hint.contains("b.rs"), "got: {hint}");
assert!(
!hint.contains("a.rs"),
"must not point back at target: {hint}"
);
}
#[test]
fn cross_file_hint_empty_when_nowhere() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
repo(root);
let target = root.join("a.rs");
fs::write(&target, "fn only_here() {}\n").unwrap();
let hint = cross_file_hint(&target, "fn nonexistent_symbol_xyzzy() {}");
assert!(hint.is_empty(), "got: {hint}");
}
#[test]
fn distinctive_line_skips_trivial_lines() {
assert_eq!(distinctive_line("}\n{\n )"), None);
assert_eq!(
distinctive_line("}\nfn meaningful_name() {\n}"),
Some("fn meaningful_name() {")
);
}
#[test]
fn outside_a_repo_does_not_scan() {
let f = tempfile::NamedTempFile::new().unwrap();
assert!(
cross_file_hint(f.path(), "fn a_distinctive_needle_line() {}").is_empty(),
"no repo => no cross-file scan"
);
let missing = f.path().with_file_name("definitely_missing_zzz_q9.rs");
let hint = moved_or_deleted_hint(&missing);
assert!(hint.contains("No file at"), "got: {hint}");
}
#[cfg(unix)]
#[test]
fn cross_file_hint_skips_blocking_fifo() {
use std::ffi::CString;
use std::sync::mpsc;
use std::time::Duration;
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().to_path_buf();
repo(&root);
let target = root.join("a.rs");
fs::write(&target, "fn unrelated_a() {}\n").unwrap();
fs::write(root.join("b.rs"), "pub fn the_real_target_symbol() {}\n").unwrap();
let fifo = root.join("blocking.pipe");
let c = CString::new(fifo.to_str().unwrap()).unwrap();
assert_eq!(
unsafe { libc::mkfifo(c.as_ptr(), 0o644) },
0,
"mkfifo failed"
);
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
let _ = tx.send(cross_file_hint(
&target,
"pub fn the_real_target_symbol() {}",
));
});
let hint = rx.recv_timeout(Duration::from_secs(10)).expect(
"cross_file_hint hung on a FIFO — non-regular files must be skipped (#331 regression)",
);
assert!(hint.contains("b.rs"), "got: {hint}");
assert!(
!hint.contains("blocking.pipe"),
"must skip the FIFO: {hint}"
);
}
}