13
//! startup (`mcp::init`), before any session exists, so a second root's
14
//! `.sigit/mcp.toml` has nobody to tell.
15
16
-use std::path::{Path, PathBuf};
16
+use std::path::{Component, Path, PathBuf};
17
use std::sync::RwLock;
18
19
/// The session's roots beyond `cwd`. Empty for an ordinary single-root project.
25
/// dropped — an editor is free to send any of those, and every consumer here
26
/// would either scan nothing or do the same work twice.
27
pub fn set_additional_roots(cwd: &Path, roots: &[PathBuf]) -> Vec<PathBuf> {
28
- let mut seen: Vec<PathBuf> = vec![canonical(cwd)];
28
+ let mut seen: Vec<PathBuf> = vec![canonical_key(cwd)];
29
let mut kept: Vec<PathBuf> = Vec::new();
30
31
for root in roots {
36
);
37
continue;
38
}
39
- let key = canonical(root);
39
+ let key = canonical_key(root);
40
if seen.contains(&key) {
41
continue;
42
}
70
dirs
71
}
72
73
-fn canonical(path: &Path) -> PathBuf {
74
- path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
73
+/// A comparison key for a path: its canonical form when the filesystem can
74
+/// give one, and otherwise the path made absolute and lexically normalized.
75
+///
76
+/// The fallback matters. `canonicalize` fails on a path the process cannot
77
+/// resolve (an unreadable parent, a root that vanished between the client
78
+/// sending it and us reading it), and returning the path untouched there would
79
+/// let `.` and `..` segments or a relative spelling of a root that is already
80
+/// in the list read as a new one.
81
+pub fn canonical_key(path: &Path) -> PathBuf {
82
+ path.canonicalize().unwrap_or_else(|_| normalize(path))
83
+}
84
+
85
+/// Absolute + lexically normalized: `.` dropped, `..` folded into the previous
86
+/// segment. Purely textual, so it never touches the filesystem.
87
+fn normalize(path: &Path) -> PathBuf {
88
+ let absolute = if path.is_absolute() {
89
+ path.to_path_buf()
90
+ } else {
91
+ std::env::current_dir()
92
+ .map(|cwd| cwd.join(path))
93
+ .unwrap_or_else(|_| path.to_path_buf())
94
+ };
95
+
96
+ let mut out = PathBuf::new();
97
+ for component in absolute.components() {
98
+ match component {
99
+ Component::CurDir => {}
100
+ Component::ParentDir => {
101
+ if !out.pop() {
102
+ out.push(component);
103
+ }
104
+ }
105
+ other => out.push(other),
106
+ }
107
+ }
108
+ out
109
}
110
111
#[cfg(test)]
143
assert!(additional_roots().is_empty());
144
std::fs::remove_dir_all(&temp).ok();
145
}
146
+
147
+ #[test]
148
+ fn keys_a_path_the_filesystem_cannot_resolve() {
149
+ // Nothing here exists, so `canonicalize` fails on every one of them and
150
+ // the lexical fallback is what has to make the three agree.
151
+ let base = std::env::temp_dir().join("sigit-ws-missing");
152
+ let direct = canonical_key(&base.join("root"));
153
+ let dotted = canonical_key(&base.join(".").join("root"));
154
+ let backtracked = canonical_key(&base.join("other").join("..").join("root"));
155
+
156
+ assert_eq!(direct, dotted);
157
+ assert_eq!(direct, backtracked);
158
+ assert!(direct.is_absolute());
159
+ }
160
}