Skip to main content

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}