use std::path::{Path, PathBuf};
fn find_root_upward(start_dir: &Path, markers: &[String]) -> PathBuf {
let abs = start_dir
.canonicalize()
.unwrap_or_else(|_| start_dir.to_path_buf());
if markers.is_empty() {
return abs;
}
let mut cur: &Path = &abs;
loop {
if markers.iter().any(|m| cur.join(m).exists()) {
return cur.to_path_buf();
}
match cur.parent() {
Some(p) => cur = p,
None => return abs.clone(),
}
}
}
pub fn discover_root(cwd: &Path, file: Option<&Path>, markers: &[String]) -> PathBuf {
let cwd_abs = cwd.canonicalize().unwrap_or_else(|_| cwd.to_path_buf());
if markers.is_empty() {
return cwd_abs;
}
if markers.iter().any(|m| cwd_abs.join(m).exists()) {
return cwd_abs;
}
if let Some(found) = bfs_for_marker(&cwd_abs, markers) {
return found;
}
if let Some(file) = file {
let file_abs = file.canonicalize().unwrap_or_else(|_| file.to_path_buf());
let outside_cwd = !file_abs.starts_with(&cwd_abs);
if outside_cwd && let Some(parent) = file_abs.parent() {
return find_root_upward(parent, markers);
}
}
cwd_abs
}
const DESCEND_MAX_DEPTH: usize = 6;
const SKIP_DIRS: &[&str] = &[
".git",
".hg",
".svn",
"target",
"node_modules",
".venv",
"venv",
"__pycache__",
"dist",
"build",
".direnv",
".cache",
".idea",
".vscode",
];
fn bfs_for_marker(root: &Path, markers: &[String]) -> Option<PathBuf> {
use std::collections::VecDeque;
let mut queue: VecDeque<(PathBuf, usize)> = VecDeque::new();
queue.push_back((root.to_path_buf(), 0));
while let Some((dir, depth)) = queue.pop_front() {
if depth > 0 && markers.iter().any(|m| dir.join(m).exists()) {
return Some(dir);
}
if depth >= DESCEND_MAX_DEPTH {
continue;
}
let entries = match std::fs::read_dir(&dir) {
Ok(e) => e,
Err(_) => continue,
};
for entry in entries.flatten() {
let name = entry.file_name();
let name_s = name.to_string_lossy();
if name_s.starts_with('.') {
continue;
}
if SKIP_DIRS.iter().any(|d| *d == name_s) {
continue;
}
let path = entry.path();
if path.is_dir() {
queue.push_back((path, depth + 1));
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn find_root_upward_walks_to_marker() {
let tmp = std::env::temp_dir().join(format!("vorto-lsp-{}", std::process::id()));
let inner = tmp.join("a/b/c");
std::fs::create_dir_all(&inner).unwrap();
std::fs::write(tmp.join("Cargo.toml"), "").unwrap();
let root = find_root_upward(&inner, &["Cargo.toml".to_string()]);
assert_eq!(root, tmp.canonicalize().unwrap());
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn find_root_upward_handles_relative_path() {
let tmp = std::env::temp_dir().join(format!("vorto-lsp-rel-{}", std::process::id()));
let inner = tmp.join("nested");
std::fs::create_dir_all(&inner).unwrap();
std::fs::write(tmp.join("Cargo.toml"), "").unwrap();
let root = find_root_upward(&inner, &["Cargo.toml".to_string()]);
assert_eq!(root, tmp.canonicalize().unwrap());
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn discover_root_picks_cwd_when_marker_at_cwd() {
let tmp = std::env::temp_dir().join(format!("vorto-disc1-{}", std::process::id()));
std::fs::create_dir_all(&tmp).unwrap();
std::fs::write(tmp.join("Cargo.toml"), "").unwrap();
let root = discover_root(&tmp, None, &["Cargo.toml".to_string()]);
assert_eq!(root, tmp.canonicalize().unwrap());
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn discover_root_descends_into_subdir() {
let tmp = std::env::temp_dir().join(format!("vorto-disc2-{}", std::process::id()));
let nested = tmp.join("apps/foo");
std::fs::create_dir_all(&nested).unwrap();
std::fs::write(nested.join("Cargo.toml"), "").unwrap();
let root = discover_root(&tmp, None, &["Cargo.toml".to_string()]);
assert_eq!(root, nested.canonicalize().unwrap());
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn discover_root_falls_back_to_cwd_when_no_marker() {
let tmp = std::env::temp_dir().join(format!("vorto-disc3-{}", std::process::id()));
std::fs::create_dir_all(&tmp).unwrap();
let root = discover_root(&tmp, None, &["Cargo.toml".to_string()]);
assert_eq!(root, tmp.canonicalize().unwrap());
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn discover_root_walks_up_for_outside_file() {
let tmp = std::env::temp_dir().join(format!("vorto-disc4-{}", std::process::id()));
let other = std::env::temp_dir().join(format!("vorto-disc4other-{}", std::process::id()));
let nested = other.join("src");
std::fs::create_dir_all(&tmp).unwrap();
std::fs::create_dir_all(&nested).unwrap();
std::fs::write(other.join("Cargo.toml"), "").unwrap();
let file = nested.join("main.rs");
std::fs::write(&file, "").unwrap();
let root = discover_root(&tmp, Some(&file), &["Cargo.toml".to_string()]);
assert_eq!(root, other.canonicalize().unwrap());
let _ = std::fs::remove_dir_all(&tmp);
let _ = std::fs::remove_dir_all(&other);
}
#[test]
fn discover_root_skips_target_dir() {
let tmp = std::env::temp_dir().join(format!("vorto-disc5-{}", std::process::id()));
let bogus = tmp.join("target/debug/some_crate");
std::fs::create_dir_all(&bogus).unwrap();
std::fs::write(bogus.join("Cargo.toml"), "").unwrap();
let root = discover_root(&tmp, None, &["Cargo.toml".to_string()]);
assert_eq!(root, tmp.canonicalize().unwrap());
let _ = std::fs::remove_dir_all(&tmp);
}
}