DevelopmentNodeEnvironment_MicrosoftVSCodeDependency_22NodeVersion_Bundle_Clean_Debug_ElectronProfile_EsbuildCompiler_Mountain/Environment/FileWatcherIgnore.rs
1//! Server-side ignore filter for file-watcher events.
2//!
3//! Events that match the ignore list never cross the
4//! Mountain→Cocoon gRPC notification boundary. Stops the cargo /
5//! pnpm / git-object churn from drowning the editor in dead
6//! `$fileWatcher:event` traffic.
7//!
8//! Why server-side: the watcher root + glob pattern coming from
9//! extensions (`**/*.md`, `**/package.json`, `**/*.ts`) does not
10//! exclude build directories. Even with the per-event glob filter
11//! a single `cargo check` produces thousands of `.rcgu.o` create /
12//! delete events that all match `**/*.md`'s sibling traversal -
13//! every one of them triggers a notification, which Cocoon then
14//! tries to stat back through Mountain (returning a 404 because
15//! cargo has already deleted the file). One side-effect per call.
16//!
17//! The list is deliberately conservative: only paths whose
18//! contents are never meaningful to user-facing editor state are
19//! excluded. The git work tree (`.git/index`, `.git/HEAD`,
20//! `.git/refs/`) is NOT excluded because the Git extension relies
21//! on those events to refresh branch / staged-files state.
22//!
23//! Override via `WatchIgnore` env var (colon-separated path
24//! segments). Empty value disables the filter entirely. Useful
25//! when debugging an extension that legitimately probes inside a
26//! `Target/` tree.
27
28use std::sync::OnceLock;
29
30/// Default ignore segments. A match anywhere in the path's
31/// component list (case-sensitive) suppresses the event. Tuned
32/// against the segments most likely to host high-frequency build
33/// churn without containing user-edited source.
34const DEFAULT_IGNORE_SEGMENTS:&[&str] = &[
35 // Rust
36 "target",
37 // Node
38 "node_modules",
39 // Git object database. Refs/HEAD/index changes still fire
40 // because they're addressed via separate parent segments.
41 ".git/objects",
42 ".git/lfs",
43 // macOS metadata
44 ".DS_Store",
45 // Build outputs that mirror source one-to-one - watching the
46 // output adds no signal the source watcher doesn't already
47 // give us.
48 "dist",
49 ".next",
50 ".turbo",
51 ".astro",
52 ".parcel-cache",
53 ".vite",
54 ".cache",
55 // Test snapshots / coverage dumps - regenerated by CI, never
56 // hand-edited.
57 "coverage",
58 "__snapshots__",
59];
60
61/// Lazily-resolved active ignore list. `WatchIgnore` overrides
62/// the default; an empty string disables filtering entirely.
63fn IgnoreSegments() -> &'static Vec<String> {
64 static CACHE:OnceLock<Vec<String>> = OnceLock::new();
65
66 CACHE.get_or_init(|| {
67 match std::env::var("WatchIgnore") {
68 Ok(Raw) if Raw.is_empty() => Vec::new(),
69 Ok(Raw) => Raw.split(':').map(|S| S.trim().to_string()).filter(|S| !S.is_empty()).collect(),
70 Err(_) => DEFAULT_IGNORE_SEGMENTS.iter().map(|S| (*S).to_string()).collect(),
71 }
72 })
73}
74
75/// `true` when the path should be silently dropped before any
76/// IPC traffic is emitted. Implementation is a single linear
77/// scan over the string - tested against `git/.git/objects/...`,
78/// `Target/debug/build/.../foo.rcgu.o`, and
79/// `node_modules/.bin/...`. Worst case is on every event so we
80/// keep this allocation-free.
81pub fn Fn(Path:&str) -> bool {
82 let Segments = IgnoreSegments();
83
84 if Segments.is_empty() {
85 return false;
86 }
87
88 for Needle in Segments {
89 if Path_ContainsSegment(Path, Needle) {
90 return true;
91 }
92 }
93
94 false
95}
96
97/// Match a `/seg1/seg2` substring as a complete path segment so
98/// `target` matches `/target/...` but not `/get-target-info/...`.
99/// Slashes are platform-agnostic - matches both `/` and `\`. A
100/// needle that itself contains a slash (`".git/objects"`) is
101/// matched as a literal substring with leading-slash gating.
102fn Path_ContainsSegment(Path:&str, Needle:&str) -> bool {
103 if Needle.contains('/') || Needle.contains('\\') {
104 // Composite needle - look for it as a substring with at
105 // least one path-separator immediately before it (or at
106 // the start of the path).
107 let Bytes = Path.as_bytes();
108
109 let NeedleBytes = Needle.as_bytes();
110
111 let mut Start = 0;
112
113 while let Some(Hit) = Path[Start..].find(Needle) {
114 let Index = Start + Hit;
115
116 let PreviousIsSep = Index == 0 || matches!(Bytes[Index - 1], b'/' | b'\\');
117
118 let NextIsSepOrEnd = match Bytes.get(Index + NeedleBytes.len()) {
119 None => true,
120
121 Some(b) => matches!(*b, b'/' | b'\\'),
122 };
123
124 if PreviousIsSep && NextIsSepOrEnd {
125 return true;
126 }
127
128 Start = Index + 1;
129 }
130
131 return false;
132 }
133
134 Path.split(|C| C == '/' || C == '\\').any(|Segment| Segment == Needle)
135}
136
137#[cfg(test)]
138mod tests {
139
140 use super::*;
141
142 #[test]
143 fn TargetSegmentMatchesCargoBuildPath() {
144 assert!(ShouldIgnore(
145 "/Volumes/CORSAIR/Land/Target/debug/build/foo-abc/build_script.rcgu.o"
146 ));
147 }
148
149 #[test]
150 fn TargetSegmentDoesNotMatchUnrelatedSubstring() {
151 // `Target` only excluded at top level (case-sensitive on
152 // the default list); a directory called `target-info`
153 // should not be swept up.
154 assert!(!ShouldIgnore("/Volumes/CORSAIR/Land/target-info/source.ts"));
155 }
156
157 #[test]
158 fn NodeModulesMatches() {
159 assert!(ShouldIgnore("/repo/node_modules/.bin/eslint"));
160 }
161
162 #[test]
163 fn GitObjectsCompositeMatches() {
164 assert!(ShouldIgnore("/repo/.git/objects/ab/cdef1234"));
165 }
166
167 #[test]
168 fn GitIndexNotIgnored() {
169 // The Git extension needs index / HEAD events; the ignore
170 // list must not swallow those.
171 assert!(!ShouldIgnore("/repo/.git/index"));
172
173 assert!(!ShouldIgnore("/repo/.git/HEAD"));
174 }
175
176 #[test]
177 fn UserSourceFileNotIgnored() {
178 assert!(!ShouldIgnore("/repo/Source/Application/Foo.ts"));
179 }
180}