Skip to main content

aft/lsp/
roots.rs

1use std::path::{Path, PathBuf};
2
3use crate::lsp::registry::ServerKind;
4
5/// Find the workspace root for a file given root marker filenames.
6/// Walks up from the file's parent directory looking for any of the markers.
7/// Returns the deepest directory containing a marker (closest to the file).
8/// If no marker is found, returns None.
9pub fn find_workspace_root(file_path: &Path, markers: &[&str]) -> Option<PathBuf> {
10    let resolved_path = match std::fs::canonicalize(file_path) {
11        Ok(path) => path,
12        Err(_) => file_path.to_path_buf(),
13    };
14
15    let start_dir = if resolved_path.is_dir() {
16        resolved_path
17    } else {
18        resolved_path.parent()?.to_path_buf()
19    };
20
21    let mut current = Some(start_dir.as_path());
22    while let Some(dir) = current {
23        if markers.iter().any(|marker| dir.join(marker).exists()) {
24            return Some(dir.to_path_buf());
25        }
26
27        current = dir.parent();
28    }
29
30    None
31}
32
33/// Composite key for caching server instances.
34/// Each unique (ServerKind, workspace_root) pair gets its own server process.
35#[derive(Debug, Clone, PartialEq, Eq, Hash)]
36pub struct ServerKey {
37    pub kind: ServerKind,
38    pub root: PathBuf,
39}
40
41#[cfg(test)]
42mod tests {
43    use std::fs;
44    use std::path::PathBuf;
45
46    use tempfile::tempdir;
47
48    use super::{find_workspace_root, ServerKey};
49    use crate::lsp::registry::ServerKind;
50
51    #[test]
52    fn test_find_root_with_cargo_toml() {
53        let temp_dir = tempdir().unwrap();
54        let root = temp_dir.path().join("workspace");
55        let src_dir = root.join("src");
56        let file = src_dir.join("lib.rs");
57
58        fs::create_dir_all(&src_dir).unwrap();
59        fs::write(root.join("Cargo.toml"), "[package]\nname = \"demo\"\n").unwrap();
60        fs::write(&file, "fn main() {}\n").unwrap();
61
62        let expected_root = fs::canonicalize(&root).unwrap();
63        assert_eq!(
64            find_workspace_root(&file, &["Cargo.toml"]),
65            Some(expected_root)
66        );
67    }
68
69    #[test]
70    fn test_find_root_nested() {
71        let temp_dir = tempdir().unwrap();
72        let repo_root = temp_dir.path().join("repo");
73        let crate_root = repo_root.join("crates").join("foo");
74        let src_dir = crate_root.join("src");
75        let file = src_dir.join("lib.rs");
76
77        fs::create_dir_all(&src_dir).unwrap();
78        fs::write(repo_root.join("Cargo.toml"), "[workspace]\n").unwrap();
79        fs::write(crate_root.join("Cargo.toml"), "[package]\nname = \"foo\"\n").unwrap();
80        fs::write(&file, "fn main() {}\n").unwrap();
81
82        let expected_root = fs::canonicalize(&crate_root).unwrap();
83        assert_eq!(
84            find_workspace_root(&file, &["Cargo.toml"]),
85            Some(expected_root)
86        );
87    }
88
89    #[test]
90    fn test_find_root_none() {
91        let temp_dir = tempdir().unwrap();
92        let src_dir = temp_dir.path().join("src");
93        let file = src_dir.join("main.rs");
94
95        fs::create_dir_all(&src_dir).unwrap();
96        fs::write(&file, "fn main() {}\n").unwrap();
97
98        assert_eq!(find_workspace_root(&file, &["Cargo.toml"]), None);
99    }
100
101    #[test]
102    fn test_find_root_multiple_markers() {
103        let temp_dir = tempdir().unwrap();
104        let root = temp_dir.path().join("web");
105        let src_dir = root.join("src");
106        let file = src_dir.join("index.ts");
107
108        fs::create_dir_all(&src_dir).unwrap();
109        fs::write(root.join("tsconfig.json"), "{}\n").unwrap();
110        fs::create_dir(root.join("package.json")).unwrap();
111        fs::write(&file, "export {};\n").unwrap();
112
113        let expected_root = fs::canonicalize(&root).unwrap();
114        assert_eq!(
115            find_workspace_root(&file, &["tsconfig.json", "package.json"]),
116            Some(expected_root)
117        );
118    }
119
120    #[test]
121    fn test_server_key_equality() {
122        let root = PathBuf::from("/tmp/workspace");
123        let same = ServerKey {
124            kind: ServerKind::Rust,
125            root: root.clone(),
126        };
127        let equal = ServerKey {
128            kind: ServerKind::Rust,
129            root,
130        };
131        let different = ServerKey {
132            kind: ServerKind::Rust,
133            root: PathBuf::from("/tmp/other"),
134        };
135
136        assert_eq!(same, equal);
137        assert_ne!(same, different);
138    }
139}