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