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