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>
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#[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}