1use std::path::{Path, PathBuf};
2
3use crate::lsp::registry::ServerKind;
4
5pub fn find_workspace_root<S>(file_path: &Path, markers: &[S]) -> Option<PathBuf>
6where
7 S: AsRef<str>,
8{
9 let resolved_path = crate::inspect::job::canonicalize_normalized(file_path);
23
24 let start_dir = if resolved_path.is_dir() {
25 resolved_path
26 } else {
27 resolved_path.parent()?.to_path_buf()
28 };
29
30 let mut current = Some(start_dir.as_path());
31 while let Some(dir) = current {
32 if markers
33 .iter()
34 .any(|marker| dir.join(marker.as_ref()).exists())
35 {
36 return Some(dir.to_path_buf());
37 }
38
39 current = dir.parent();
40 }
41
42 None
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Hash)]
48pub struct ServerKey {
49 pub kind: ServerKind,
50 pub root: PathBuf,
51}
52
53#[cfg(test)]
54mod tests {
55 use std::fs;
56 use std::path::PathBuf;
57
58 use tempfile::tempdir;
59
60 use super::{find_workspace_root, ServerKey};
61 use crate::inspect::job::canonicalize_normalized;
62 use crate::lsp::registry::ServerKind;
63
64 #[test]
65 fn test_find_root_with_cargo_toml() {
66 let temp_dir = tempdir().unwrap();
67 let root = temp_dir.path().join("workspace");
68 let src_dir = root.join("src");
69 let file = src_dir.join("lib.rs");
70
71 fs::create_dir_all(&src_dir).unwrap();
72 fs::write(root.join("Cargo.toml"), "[package]\nname = \"demo\"\n").unwrap();
73 fs::write(&file, "fn main() {}\n").unwrap();
74
75 let expected_root = crate::inspect::job::canonicalize_normalized(&root);
79 assert_eq!(
80 find_workspace_root(&file, &["Cargo.toml"]),
81 Some(expected_root)
82 );
83 }
84
85 #[test]
86 fn test_find_root_nested() {
87 let temp_dir = tempdir().unwrap();
88 let repo_root = temp_dir.path().join("repo");
89 let crate_root = repo_root.join("crates").join("foo");
90 let src_dir = crate_root.join("src");
91 let file = src_dir.join("lib.rs");
92
93 fs::create_dir_all(&src_dir).unwrap();
94 fs::write(repo_root.join("Cargo.toml"), "[workspace]\n").unwrap();
95 fs::write(crate_root.join("Cargo.toml"), "[package]\nname = \"foo\"\n").unwrap();
96 fs::write(&file, "fn main() {}\n").unwrap();
97
98 let expected_root = crate::inspect::job::canonicalize_normalized(&crate_root);
99 assert_eq!(
100 find_workspace_root(&file, &["Cargo.toml"]),
101 Some(expected_root)
102 );
103 }
104
105 #[test]
106 fn test_find_root_none() {
107 let temp_dir = tempdir().unwrap();
108 let src_dir = temp_dir.path().join("src");
109 let file = src_dir.join("main.rs");
110
111 fs::create_dir_all(&src_dir).unwrap();
112 fs::write(&file, "fn main() {}\n").unwrap();
113
114 assert_eq!(find_workspace_root(&file, &["Cargo.toml"]), None);
115 }
116
117 #[test]
118 fn test_find_root_multiple_markers() {
119 let temp_dir = tempdir().unwrap();
120 let root = temp_dir.path().join("web");
121 let src_dir = root.join("src");
122 let file = src_dir.join("index.ts");
123
124 fs::create_dir_all(&src_dir).unwrap();
125 fs::write(root.join("tsconfig.json"), "{}\n").unwrap();
126 fs::create_dir(root.join("package.json")).unwrap();
127 fs::write(&file, "export {};\n").unwrap();
128
129 let expected_root = crate::inspect::job::canonicalize_normalized(&root);
130 assert_eq!(
131 find_workspace_root(&file, &["tsconfig.json", "package.json"]),
132 Some(expected_root)
133 );
134 }
135
136 #[test]
137 fn test_server_key_equality() {
138 let root = PathBuf::from("/tmp/workspace");
139 let same = ServerKey {
140 kind: ServerKind::Rust,
141 root: root.clone(),
142 };
143 let equal = ServerKey {
144 kind: ServerKind::Rust,
145 root,
146 };
147 let different = ServerKey {
148 kind: ServerKind::Rust,
149 root: PathBuf::from("/tmp/other"),
150 };
151
152 assert_eq!(same, equal);
153 assert_ne!(same, different);
154 }
155
156 #[test]
173 fn test_find_root_strips_windows_verbatim_prefix() {
174 let temp_dir = tempdir().unwrap();
175 let root = temp_dir.path().join("workspace");
176 let src_dir = root.join("src");
177 let nested = src_dir.join("deep").join("lib.rs");
178
179 fs::create_dir_all(nested.parent().unwrap()).unwrap();
180 fs::write(root.join("Cargo.toml"), "[package]\nname = \"demo\"\n").unwrap();
181 fs::write(&nested, "fn main() {}\n").unwrap();
182
183 let found = find_workspace_root(&nested, &["Cargo.toml"]).expect("root found");
184
185 let display = found.to_string_lossy();
187 assert!(
188 !display.starts_with("\\\\?\\"),
189 "workspace root must not carry a Windows verbatim prefix: {display}"
190 );
191
192 let expected = canonicalize_normalized(&root);
199 assert_eq!(found, expected);
200 }
201}