Skip to main content

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}