1use super::path_utils::canonicalize_path;
19use super::types::{ProjectError, ProjectRootMode};
20use std::path::{Path, PathBuf};
21
22#[must_use]
29pub fn find_git_root(file_path: &Path) -> Option<PathBuf> {
30 let mut current = file_path;
31
32 loop {
34 let git_dir = current.join(".git");
35
36 if git_dir.exists() {
38 return Some(current.to_path_buf());
40 }
41
42 match current.parent() {
44 Some(parent) => current = parent,
45 None => break, }
47 }
48
49 None
50}
51
52pub fn resolve_index_root(
70 file_path: &Path,
71 mode: ProjectRootMode,
72 workspace_folders: &[PathBuf],
73) -> Result<PathBuf, ProjectError> {
74 match mode {
75 ProjectRootMode::GitRoot => resolve_git_root_mode(file_path, workspace_folders),
76 ProjectRootMode::WorkspaceFolder => {
77 resolve_workspace_folder_mode(file_path, workspace_folders)
78 }
79 ProjectRootMode::WorkspaceRoot => resolve_workspace_root_mode(file_path, workspace_folders),
80 }
81}
82
83fn resolve_git_root_mode(
91 file_path: &Path,
92 workspace_folders: &[PathBuf],
93) -> Result<PathBuf, ProjectError> {
94 if let Some(git_root) = find_git_root(file_path) {
96 log::debug!(
97 "Found git root for '{}': '{}'",
98 file_path.display(),
99 git_root.display()
100 );
101 return Ok(git_root);
102 }
103
104 if !workspace_folders.is_empty() {
106 if let Some(folder) = find_containing_workspace_folder(file_path, workspace_folders) {
108 log::info!(
109 "No git root for '{}', using workspace folder '{}'",
110 file_path.display(),
111 folder.display()
112 );
113 return Ok(folder);
114 }
115
116 let first_folder = &workspace_folders[0];
118 log::warn!(
119 "File '{}' outside all workspace folders, using first folder '{}' as root",
120 file_path.display(),
121 first_folder.display()
122 );
123 return Ok(first_folder.clone());
124 }
125
126 if let Some(parent) = file_path.parent() {
128 if parent.as_os_str().is_empty() {
129 log::info!(
131 "No workspace folders, using current directory as root for '{}'",
132 file_path.display()
133 );
134 return Ok(PathBuf::from("."));
135 }
136 log::info!(
137 "No workspace folders, using parent directory '{}' as root",
138 parent.display()
139 );
140 return Ok(parent.to_path_buf());
141 }
142
143 Err(ProjectError::no_git_root(file_path))
145}
146
147fn resolve_workspace_folder_mode(
151 file_path: &Path,
152 workspace_folders: &[PathBuf],
153) -> Result<PathBuf, ProjectError> {
154 if workspace_folders.is_empty() {
155 return file_path
157 .parent()
158 .map(Path::to_path_buf)
159 .ok_or_else(|| ProjectError::no_git_root(file_path));
160 }
161
162 if let Some(folder) = find_containing_workspace_folder(file_path, workspace_folders) {
164 return Ok(folder);
165 }
166
167 let first_folder = &workspace_folders[0];
169 log::warn!(
170 "File '{}' outside all workspace folders, using first folder '{}' as root",
171 file_path.display(),
172 first_folder.display()
173 );
174 Ok(first_folder.clone())
175}
176
177fn resolve_workspace_root_mode(
182 file_path: &Path,
183 workspace_folders: &[PathBuf],
184) -> Result<PathBuf, ProjectError> {
185 if workspace_folders.is_empty() {
186 return file_path
188 .parent()
189 .map(Path::to_path_buf)
190 .ok_or_else(|| ProjectError::no_git_root(file_path));
191 }
192
193 Ok(workspace_folders[0].clone())
195}
196
197fn find_containing_workspace_folder(
202 file_path: &Path,
203 workspace_folders: &[PathBuf],
204) -> Option<PathBuf> {
205 for folder in workspace_folders {
206 if file_path.starts_with(folder) {
207 return Some(folder.clone());
208 }
209 }
210 None
211}
212
213pub fn canonicalize_and_resolve(
231 file_path: &Path,
232 mode: ProjectRootMode,
233 workspace_folders: &[PathBuf],
234) -> Result<PathBuf, ProjectError> {
235 let canonical = canonicalize_path(file_path)
237 .map_err(|e| ProjectError::canonicalization_failed(file_path, e))?;
238
239 resolve_index_root(&canonical, mode, workspace_folders)
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246 use tempfile::TempDir;
247
248 fn tempdir_outside_git_repo() -> TempDir {
249 #[cfg(unix)]
250 fn is_in_git_repo(path: &Path) -> bool {
251 path.ancestors()
252 .any(|ancestor| ancestor.join(".git").is_dir())
253 }
254
255 #[cfg(unix)]
256 {
257 for base in [Path::new("/var/tmp"), Path::new("/dev/shm")] {
258 if base.is_dir()
259 && !is_in_git_repo(base)
260 && let Ok(tmp) = TempDir::new_in(base)
261 {
262 return tmp;
263 }
264 }
265 }
266
267 TempDir::new().expect("create temp dir")
268 }
269
270 fn setup_git_repo(temp: &TempDir) -> PathBuf {
271 let git_dir = temp.path().join(".git");
272 std::fs::create_dir(&git_dir).unwrap();
273 temp.path().to_path_buf()
274 }
275
276 #[test]
277 fn test_find_git_root_exists() {
278 let temp = TempDir::new().unwrap();
279 let repo_root = setup_git_repo(&temp);
280
281 let subdir = repo_root.join("src");
283 std::fs::create_dir(&subdir).unwrap();
284 let file = subdir.join("main.rs");
285 std::fs::write(&file, "fn main() {}").unwrap();
286
287 let git_root = find_git_root(&file);
289 assert!(git_root.is_some());
290 assert_eq!(git_root.unwrap(), repo_root);
291 }
292
293 #[test]
294 fn test_find_git_root_not_exists() {
295 let temp = tempdir_outside_git_repo();
296 let file = temp.path().join("loose_file.rs");
297 std::fs::write(&file, "fn main() {}").unwrap();
298
299 let git_root = find_git_root(&file);
301 assert!(git_root.is_none());
302 }
303
304 #[test]
305 fn test_find_git_root_nested_repos() {
306 let temp = TempDir::new().unwrap();
307
308 let outer_git = temp.path().join(".git");
310 std::fs::create_dir(&outer_git).unwrap();
311
312 let inner = temp.path().join("inner");
314 std::fs::create_dir(&inner).unwrap();
315 let inner_git = inner.join(".git");
316 std::fs::create_dir(&inner_git).unwrap();
317
318 let file = inner.join("lib.rs");
320 std::fs::write(&file, "pub fn foo() {}").unwrap();
321
322 let git_root = find_git_root(&file);
324 assert!(git_root.is_some());
325 assert_eq!(git_root.unwrap(), inner);
326 }
327
328 #[test]
329 fn test_resolve_git_root_mode_with_git() {
330 let temp = TempDir::new().unwrap();
331 let repo_root = setup_git_repo(&temp);
332
333 let file = repo_root.join("file.rs");
334 std::fs::write(&file, "").unwrap();
335
336 let result = resolve_git_root_mode(&file, &[]).unwrap();
337 assert_eq!(result, repo_root);
338 }
339
340 #[test]
341 fn test_resolve_git_root_mode_no_git_with_workspace() {
342 let temp = tempdir_outside_git_repo();
343 let file = temp.path().join("file.rs");
344 std::fs::write(&file, "").unwrap();
345
346 let workspace_folders = vec![temp.path().to_path_buf()];
347 let result = resolve_git_root_mode(&file, &workspace_folders).unwrap();
348 assert_eq!(result, temp.path());
349 }
350
351 #[test]
352 fn test_resolve_git_root_mode_file_outside_workspace() {
353 let temp1 = tempdir_outside_git_repo();
354 let temp2 = tempdir_outside_git_repo();
355
356 let file = temp1.path().join("file.rs");
357 std::fs::write(&file, "").unwrap();
358
359 let workspace_folders = vec![temp2.path().to_path_buf()];
361
362 let result = resolve_git_root_mode(&file, &workspace_folders).unwrap();
364 assert_eq!(result, temp2.path());
365 }
366
367 #[test]
368 fn test_resolve_git_root_mode_single_file_mode() {
369 let temp = tempdir_outside_git_repo();
370 let file = temp.path().join("file.rs");
371 std::fs::write(&file, "").unwrap();
372
373 let result = resolve_git_root_mode(&file, &[]).unwrap();
375 assert_eq!(result, temp.path());
376 }
377
378 #[test]
379 fn test_resolve_workspace_folder_mode() {
380 let temp = TempDir::new().unwrap();
381 let folder1 = temp.path().join("proj1");
382 let folder2 = temp.path().join("proj2");
383 std::fs::create_dir(&folder1).unwrap();
384 std::fs::create_dir(&folder2).unwrap();
385
386 let file = folder1.join("src").join("main.rs");
387 std::fs::create_dir_all(file.parent().unwrap()).unwrap();
388 std::fs::write(&file, "").unwrap();
389
390 let workspace_folders = vec![folder1.clone(), folder2];
391 let result = resolve_workspace_folder_mode(&file, &workspace_folders).unwrap();
392 assert_eq!(result, folder1);
393 }
394
395 #[test]
396 fn test_resolve_workspace_root_mode() {
397 let temp = TempDir::new().unwrap();
398 let folder1 = temp.path().join("proj1");
399 let folder2 = temp.path().join("proj2");
400 std::fs::create_dir(&folder1).unwrap();
401 std::fs::create_dir(&folder2).unwrap();
402
403 let file = folder2.join("file.rs");
404 std::fs::write(&file, "").unwrap();
405
406 let workspace_folders = vec![folder1.clone(), folder2];
408 let result = resolve_workspace_root_mode(&file, &workspace_folders).unwrap();
409 assert_eq!(result, folder1);
410 }
411
412 #[test]
413 fn test_resolve_index_root_delegates_correctly() {
414 let temp = TempDir::new().unwrap();
415 let repo_root = setup_git_repo(&temp);
416 let file = repo_root.join("file.rs");
417 std::fs::write(&file, "").unwrap();
418
419 let result = resolve_index_root(&file, ProjectRootMode::GitRoot, &[]).unwrap();
421 assert_eq!(result, repo_root);
422
423 let result = resolve_index_root(&file, ProjectRootMode::WorkspaceFolder, &[]).unwrap();
425 assert_eq!(result, repo_root);
426
427 let result = resolve_index_root(&file, ProjectRootMode::WorkspaceRoot, &[]).unwrap();
429 assert_eq!(result, repo_root);
430 }
431
432 #[test]
433 fn test_find_containing_workspace_folder() {
434 let temp = TempDir::new().unwrap();
435 let folder1 = temp.path().join("a");
436 let folder2 = temp.path().join("b");
437 std::fs::create_dir(&folder1).unwrap();
438 std::fs::create_dir(&folder2).unwrap();
439
440 let file_in_a = folder1.join("file.rs");
441 let file_in_b = folder2.join("file.rs");
442 let file_outside = temp.path().join("file.rs");
443
444 let workspace_folders = vec![folder1.clone(), folder2.clone()];
445
446 assert_eq!(
447 find_containing_workspace_folder(&file_in_a, &workspace_folders),
448 Some(folder1)
449 );
450 assert_eq!(
451 find_containing_workspace_folder(&file_in_b, &workspace_folders),
452 Some(folder2)
453 );
454 assert_eq!(
455 find_containing_workspace_folder(&file_outside, &workspace_folders),
456 None
457 );
458 }
459
460 #[test]
461 fn test_workspace_folder_order_preserved() {
462 let temp = TempDir::new().unwrap();
463 let folder_z = temp.path().join("z_folder");
464 let folder_a = temp.path().join("a_folder");
465 std::fs::create_dir(&folder_z).unwrap();
466 std::fs::create_dir(&folder_a).unwrap();
467
468 let file_outside = temp.path().join("file.rs");
469 std::fs::write(&file_outside, "").unwrap();
470
471 let folders = vec![folder_z.clone(), folder_a];
473
474 let result = resolve_workspace_folder_mode(&file_outside, &folders).unwrap();
476 assert_eq!(result, folder_z);
477 }
478
479 #[test]
480 fn test_canonicalize_and_resolve() {
481 let temp = TempDir::new().unwrap();
482 let repo_root = setup_git_repo(&temp);
483 let file = repo_root.join("file.rs");
484 std::fs::write(&file, "").unwrap();
485
486 let non_canonical = repo_root.join(".").join("file.rs");
488
489 let result = canonicalize_and_resolve(&non_canonical, ProjectRootMode::GitRoot, &[]);
490 assert!(result.is_ok());
491 let resolved = result.unwrap();
493 assert_eq!(
495 canonicalize_path(&resolved).unwrap(),
496 canonicalize_path(&repo_root).unwrap()
497 );
498 }
499}