@theia/filesystem 1.70.0 → 1.71.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/browser/file-resource.spec.js +1 -1
- package/lib/browser/file-resource.spec.js.map +1 -1
- package/lib/browser/file-service-watcher.spec.d.ts +2 -0
- package/lib/browser/file-service-watcher.spec.d.ts.map +1 -0
- package/lib/browser/file-service-watcher.spec.js +616 -0
- package/lib/browser/file-service-watcher.spec.js.map +1 -0
- package/lib/browser/file-service.d.ts +23 -0
- package/lib/browser/file-service.d.ts.map +1 -1
- package/lib/browser/file-service.js +253 -12
- package/lib/browser/file-service.js.map +1 -1
- package/lib/node/disk-file-system-provider.spec.js +2 -1
- package/lib/node/disk-file-system-provider.spec.js.map +1 -1
- package/lib/node/parcel-watcher/parcel-filesystem-watcher.spec.js +5 -4
- package/lib/node/parcel-watcher/parcel-filesystem-watcher.spec.js.map +1 -1
- package/package.json +3 -3
- package/src/browser/file-resource.spec.ts +1 -1
- package/src/browser/file-service-watcher.spec.ts +807 -0
- package/src/browser/file-service.ts +295 -14
- package/src/node/disk-file-system-provider.spec.ts +2 -1
- package/src/node/parcel-watcher/parcel-filesystem-watcher.spec.ts +5 -4
|
@@ -0,0 +1,807 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 Safi Seid-Ahmad, K2view and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
|
|
18
|
+
let disableJSDOM = enableJSDOM();
|
|
19
|
+
|
|
20
|
+
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
|
21
|
+
FrontendApplicationConfigProvider.set({});
|
|
22
|
+
|
|
23
|
+
import { Disposable, Emitter, URI } from '@theia/core';
|
|
24
|
+
import { expect } from 'chai';
|
|
25
|
+
import * as sinon from 'sinon';
|
|
26
|
+
import { FileChange, FileChangeType, FileSystemProvider, FileSystemProviderCapabilities, WatchOptions } from '../common/files';
|
|
27
|
+
import { FileService } from './file-service';
|
|
28
|
+
|
|
29
|
+
disableJSDOM();
|
|
30
|
+
|
|
31
|
+
interface MockWatcher {
|
|
32
|
+
resource: URI;
|
|
33
|
+
options: WatchOptions;
|
|
34
|
+
disposed: boolean;
|
|
35
|
+
disposable: Disposable;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createMockProvider(caseSensitive: boolean = true): FileSystemProvider & { watchers: MockWatcher[] } {
|
|
39
|
+
const watchers: MockWatcher[] = [];
|
|
40
|
+
return {
|
|
41
|
+
watchers,
|
|
42
|
+
capabilities: caseSensitive ? FileSystemProviderCapabilities.PathCaseSensitive : 0,
|
|
43
|
+
onDidChangeCapabilities: () => Disposable.NULL,
|
|
44
|
+
onDidChangeFile: () => Disposable.NULL,
|
|
45
|
+
onFileWatchError: () => Disposable.NULL,
|
|
46
|
+
watch(resource: URI, options: WatchOptions): Disposable {
|
|
47
|
+
const watcher: MockWatcher = {
|
|
48
|
+
resource,
|
|
49
|
+
options,
|
|
50
|
+
disposed: false,
|
|
51
|
+
disposable: Disposable.create(() => { watcher.disposed = true; }),
|
|
52
|
+
};
|
|
53
|
+
watchers.push(watcher);
|
|
54
|
+
return watcher.disposable;
|
|
55
|
+
},
|
|
56
|
+
stat: () => { throw new Error('not implemented'); },
|
|
57
|
+
readdir: () => { throw new Error('not implemented'); },
|
|
58
|
+
readFile: () => { throw new Error('not implemented'); },
|
|
59
|
+
writeFile: () => { throw new Error('not implemented'); },
|
|
60
|
+
delete: () => { throw new Error('not implemented'); },
|
|
61
|
+
mkdir: () => { throw new Error('not implemented'); },
|
|
62
|
+
rename: () => { throw new Error('not implemented'); },
|
|
63
|
+
} as FileSystemProvider & { watchers: MockWatcher[] };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Return the number of watchers that are still alive (not disposed). */
|
|
67
|
+
function liveWatcherCount(provider: { watchers: MockWatcher[] }): number {
|
|
68
|
+
return provider.watchers.filter(w => !w.disposed).length;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe('FileService watcher deduplication', () => {
|
|
72
|
+
const sandbox = sinon.createSandbox();
|
|
73
|
+
let fileService: FileService;
|
|
74
|
+
let mockProvider: FileSystemProvider & { watchers: MockWatcher[] };
|
|
75
|
+
|
|
76
|
+
before(() => {
|
|
77
|
+
disableJSDOM = enableJSDOM();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
after(() => {
|
|
81
|
+
disableJSDOM();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
beforeEach(() => {
|
|
85
|
+
sandbox.restore();
|
|
86
|
+
fileService = new FileService();
|
|
87
|
+
mockProvider = createMockProvider();
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
89
|
+
sandbox.stub(fileService as any, 'withProvider').resolves(mockProvider);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
afterEach(() => {
|
|
93
|
+
sandbox.restore();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ── (A) Exact-match dedup ──────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
describe('exact-match dedup', () => {
|
|
99
|
+
it('should reuse an existing watcher for the same resource and options', async () => {
|
|
100
|
+
const uri = new URI('file:///project/src');
|
|
101
|
+
const opts: WatchOptions = { recursive: true, excludes: [] };
|
|
102
|
+
|
|
103
|
+
const d1 = await fileService.doWatch(uri, opts);
|
|
104
|
+
const d2 = await fileService.doWatch(uri, opts);
|
|
105
|
+
|
|
106
|
+
// Only one real OS watcher should have been created
|
|
107
|
+
expect(mockProvider.watchers).to.have.lengthOf(1);
|
|
108
|
+
expect(liveWatcherCount(mockProvider)).to.equal(1);
|
|
109
|
+
|
|
110
|
+
// Disposing one handle should NOT dispose the real watcher
|
|
111
|
+
d2.dispose();
|
|
112
|
+
expect(liveWatcherCount(mockProvider)).to.equal(1);
|
|
113
|
+
|
|
114
|
+
// Disposing the last handle should dispose it
|
|
115
|
+
d1.dispose();
|
|
116
|
+
expect(liveWatcherCount(mockProvider)).to.equal(0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should not let a restrictive parent subsume a permissive child at the same URI', async () => {
|
|
120
|
+
const uri = new URI('file:///project/src');
|
|
121
|
+
const opts1: WatchOptions = { recursive: true, excludes: ['**/node_modules'] };
|
|
122
|
+
const opts2: WatchOptions = { recursive: true, excludes: [] };
|
|
123
|
+
|
|
124
|
+
const d1 = await fileService.doWatch(uri, opts1);
|
|
125
|
+
const d2 = await fileService.doWatch(uri, opts2);
|
|
126
|
+
|
|
127
|
+
// The first watcher (with excludes) cannot subsume the second (no excludes),
|
|
128
|
+
// but the second (watching everything) CAN subsume the first via subsumeExistingChildren.
|
|
129
|
+
// Two real watchers were created, but only the permissive one (d2) remains live.
|
|
130
|
+
expect(mockProvider.watchers).to.have.lengthOf(2);
|
|
131
|
+
expect(liveWatcherCount(mockProvider)).to.equal(1);
|
|
132
|
+
|
|
133
|
+
// Disposing d1 (already subsumed, no real watcher) should have no OS-level effect
|
|
134
|
+
d1.dispose();
|
|
135
|
+
expect(liveWatcherCount(mockProvider)).to.equal(1);
|
|
136
|
+
|
|
137
|
+
d2.dispose();
|
|
138
|
+
expect(liveWatcherCount(mockProvider)).to.equal(0);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should subsume a watcher with matching excludes on the same URI', async () => {
|
|
142
|
+
const uri = new URI('file:///project/src');
|
|
143
|
+
const opts1: WatchOptions = { recursive: true, excludes: ['**/node_modules'] };
|
|
144
|
+
const opts2: WatchOptions = { recursive: true, excludes: ['**/node_modules'] };
|
|
145
|
+
|
|
146
|
+
const d1 = await fileService.doWatch(uri, opts1);
|
|
147
|
+
const d2 = await fileService.doWatch(uri, opts2);
|
|
148
|
+
|
|
149
|
+
// Same excludes — exact-match dedup applies
|
|
150
|
+
expect(mockProvider.watchers).to.have.lengthOf(1);
|
|
151
|
+
|
|
152
|
+
d2.dispose();
|
|
153
|
+
expect(liveWatcherCount(mockProvider)).to.equal(1);
|
|
154
|
+
|
|
155
|
+
d1.dispose();
|
|
156
|
+
expect(liveWatcherCount(mockProvider)).to.equal(0);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should subsume a watcher with more excludes (superset) on the same URI', async () => {
|
|
160
|
+
const uri = new URI('file:///project/src');
|
|
161
|
+
const opts1: WatchOptions = { recursive: true, excludes: [] };
|
|
162
|
+
const opts2: WatchOptions = { recursive: true, excludes: ['**/node_modules'] };
|
|
163
|
+
|
|
164
|
+
const d1 = await fileService.doWatch(uri, opts1);
|
|
165
|
+
const d2 = await fileService.doWatch(uri, opts2);
|
|
166
|
+
|
|
167
|
+
// Parent has no excludes, so it watches everything the child needs — subsumption is safe
|
|
168
|
+
expect(mockProvider.watchers).to.have.lengthOf(1);
|
|
169
|
+
|
|
170
|
+
d2.dispose();
|
|
171
|
+
expect(liveWatcherCount(mockProvider)).to.equal(1);
|
|
172
|
+
|
|
173
|
+
d1.dispose();
|
|
174
|
+
expect(liveWatcherCount(mockProvider)).to.equal(0);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should subsume a non-recursive watcher on the same URI as a recursive watcher', async () => {
|
|
178
|
+
const uri = new URI('file:///project/src');
|
|
179
|
+
const opts1: WatchOptions = { recursive: true, excludes: [] };
|
|
180
|
+
const opts2: WatchOptions = { recursive: false, excludes: [] };
|
|
181
|
+
|
|
182
|
+
const d1 = await fileService.doWatch(uri, opts1);
|
|
183
|
+
const d2 = await fileService.doWatch(uri, opts2);
|
|
184
|
+
|
|
185
|
+
// The recursive watcher subsumes the non-recursive one on the same URI
|
|
186
|
+
expect(mockProvider.watchers).to.have.lengthOf(1);
|
|
187
|
+
|
|
188
|
+
d2.dispose();
|
|
189
|
+
expect(liveWatcherCount(mockProvider)).to.equal(1);
|
|
190
|
+
|
|
191
|
+
d1.dispose();
|
|
192
|
+
expect(liveWatcherCount(mockProvider)).to.equal(0);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should support three refs and only dispose when all are gone', async () => {
|
|
196
|
+
const uri = new URI('file:///project/src');
|
|
197
|
+
const opts: WatchOptions = { recursive: false, excludes: [] };
|
|
198
|
+
|
|
199
|
+
const d1 = await fileService.doWatch(uri, opts);
|
|
200
|
+
const d2 = await fileService.doWatch(uri, opts);
|
|
201
|
+
const d3 = await fileService.doWatch(uri, opts);
|
|
202
|
+
|
|
203
|
+
expect(mockProvider.watchers).to.have.lengthOf(1);
|
|
204
|
+
|
|
205
|
+
d1.dispose();
|
|
206
|
+
expect(liveWatcherCount(mockProvider)).to.equal(1);
|
|
207
|
+
|
|
208
|
+
d2.dispose();
|
|
209
|
+
expect(liveWatcherCount(mockProvider)).to.equal(1);
|
|
210
|
+
|
|
211
|
+
d3.dispose();
|
|
212
|
+
expect(liveWatcherCount(mockProvider)).to.equal(0);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// ── (B) Parent subsumption of new child ────────────────────────────
|
|
217
|
+
|
|
218
|
+
describe('parent subsumption', () => {
|
|
219
|
+
it('should not create an OS watcher when a recursive parent already covers the child', async () => {
|
|
220
|
+
const parentUri = new URI('file:///project');
|
|
221
|
+
const childUri = new URI('file:///project/src');
|
|
222
|
+
|
|
223
|
+
await fileService.doWatch(parentUri, { recursive: true, excludes: [] });
|
|
224
|
+
await fileService.doWatch(childUri, { recursive: false, excludes: [] });
|
|
225
|
+
|
|
226
|
+
// Only the parent's OS watcher should exist
|
|
227
|
+
expect(mockProvider.watchers).to.have.lengthOf(1);
|
|
228
|
+
expect(mockProvider.watchers[0].resource.toString()).to.equal(parentUri.toString());
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should not subsume a child that is excluded by the parent', async () => {
|
|
232
|
+
const parentUri = new URI('file:///project');
|
|
233
|
+
const childUri = new URI('file:///project/node_modules/foo');
|
|
234
|
+
|
|
235
|
+
await fileService.doWatch(parentUri, { recursive: true, excludes: ['**/node_modules'] });
|
|
236
|
+
await fileService.doWatch(childUri, { recursive: false, excludes: [] });
|
|
237
|
+
|
|
238
|
+
// The child should get its own real watcher
|
|
239
|
+
expect(mockProvider.watchers).to.have.lengthOf(2);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should not subsume when the parent is non-recursive', async () => {
|
|
243
|
+
const parentUri = new URI('file:///project');
|
|
244
|
+
const childUri = new URI('file:///project/src');
|
|
245
|
+
|
|
246
|
+
await fileService.doWatch(parentUri, { recursive: false, excludes: [] });
|
|
247
|
+
await fileService.doWatch(childUri, { recursive: false, excludes: [] });
|
|
248
|
+
|
|
249
|
+
expect(mockProvider.watchers).to.have.lengthOf(2);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should subsume a deeply nested child', async () => {
|
|
253
|
+
const parentUri = new URI('file:///project');
|
|
254
|
+
const childUri = new URI('file:///project/src/main/java/com/example');
|
|
255
|
+
|
|
256
|
+
await fileService.doWatch(parentUri, { recursive: true, excludes: [] });
|
|
257
|
+
await fileService.doWatch(childUri, { recursive: false, excludes: [] });
|
|
258
|
+
|
|
259
|
+
expect(mockProvider.watchers).to.have.lengthOf(1);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('should not subsume a sibling directory', async () => {
|
|
263
|
+
const parentUri = new URI('file:///project/src');
|
|
264
|
+
const siblingUri = new URI('file:///project/test');
|
|
265
|
+
|
|
266
|
+
await fileService.doWatch(parentUri, { recursive: true, excludes: [] });
|
|
267
|
+
await fileService.doWatch(siblingUri, { recursive: false, excludes: [] });
|
|
268
|
+
|
|
269
|
+
expect(mockProvider.watchers).to.have.lengthOf(2);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('should not subsume a child when it is under an excluded ancestor directory', async () => {
|
|
273
|
+
const parentUri = new URI('file:///project');
|
|
274
|
+
// node_modules is excluded, so anything under it should not be subsumed
|
|
275
|
+
const childUri = new URI('file:///project/node_modules/pkg/lib');
|
|
276
|
+
|
|
277
|
+
await fileService.doWatch(parentUri, { recursive: true, excludes: ['**/node_modules'] });
|
|
278
|
+
await fileService.doWatch(childUri, { recursive: true, excludes: [] });
|
|
279
|
+
|
|
280
|
+
// Child needs its own watcher since it's under an excluded directory
|
|
281
|
+
expect(mockProvider.watchers).to.have.lengthOf(2);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// ── (D) New recursive parent subsumes existing children ────────────
|
|
286
|
+
|
|
287
|
+
describe('child subsumption by new parent', () => {
|
|
288
|
+
it('should subsume existing non-recursive children when a new recursive parent is created', async () => {
|
|
289
|
+
const childUri1 = new URI('file:///project/src');
|
|
290
|
+
const childUri2 = new URI('file:///project/test');
|
|
291
|
+
const parentUri = new URI('file:///project');
|
|
292
|
+
|
|
293
|
+
await fileService.doWatch(childUri1, { recursive: false, excludes: [] });
|
|
294
|
+
await fileService.doWatch(childUri2, { recursive: false, excludes: [] });
|
|
295
|
+
|
|
296
|
+
expect(mockProvider.watchers).to.have.lengthOf(2);
|
|
297
|
+
expect(liveWatcherCount(mockProvider)).to.equal(2);
|
|
298
|
+
|
|
299
|
+
// Adding a recursive parent should subsume both children
|
|
300
|
+
await fileService.doWatch(parentUri, { recursive: true, excludes: [] });
|
|
301
|
+
|
|
302
|
+
// Three watchers created total, but only the parent should remain live
|
|
303
|
+
expect(mockProvider.watchers).to.have.lengthOf(3);
|
|
304
|
+
expect(liveWatcherCount(mockProvider)).to.equal(1);
|
|
305
|
+
expect(mockProvider.watchers[0].disposed).to.be.true; // child1 real watcher disposed
|
|
306
|
+
expect(mockProvider.watchers[1].disposed).to.be.true; // child2 real watcher disposed
|
|
307
|
+
expect(mockProvider.watchers[2].disposed).to.be.false; // parent alive
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('should subsume existing recursive children and re-parent grandchildren', async () => {
|
|
311
|
+
const grandchildUri = new URI('file:///project/src/deep');
|
|
312
|
+
const childUri = new URI('file:///project/src');
|
|
313
|
+
const parentUri = new URI('file:///project');
|
|
314
|
+
|
|
315
|
+
// Create a recursive child first, then a grandchild under it
|
|
316
|
+
await fileService.doWatch(childUri, { recursive: true, excludes: [] });
|
|
317
|
+
await fileService.doWatch(grandchildUri, { recursive: false, excludes: [] });
|
|
318
|
+
|
|
319
|
+
// Grandchild should already be subsumed by the child
|
|
320
|
+
expect(mockProvider.watchers).to.have.lengthOf(1); // only child has a real watcher
|
|
321
|
+
|
|
322
|
+
// Now create a parent that covers everything
|
|
323
|
+
await fileService.doWatch(parentUri, { recursive: true, excludes: [] });
|
|
324
|
+
|
|
325
|
+
// Child's real watcher gets disposed, parent takes over
|
|
326
|
+
expect(liveWatcherCount(mockProvider)).to.equal(1);
|
|
327
|
+
expect(mockProvider.watchers[0].disposed).to.be.true; // child disposed
|
|
328
|
+
expect(mockProvider.watchers[1].disposed).to.be.false; // parent alive
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('should promote and re-index a recursive grandchild excluded by the new parent', async () => {
|
|
332
|
+
const childUri = new URI('file:///project/src');
|
|
333
|
+
const grandchildUri = new URI('file:///project/src/vendor');
|
|
334
|
+
const parentUri = new URI('file:///project');
|
|
335
|
+
|
|
336
|
+
// Child and grandchild share the same excludes — grandchild is compatible with child.
|
|
337
|
+
// The pattern '**/src/vendor' does NOT match the relative path from child to grandchild
|
|
338
|
+
// ('vendor'), so isExcludedByParent(child, grandchild) is false — grandchild is subsumed.
|
|
339
|
+
await fileService.doWatch(childUri, { recursive: true, excludes: ['**/src/vendor'] });
|
|
340
|
+
await fileService.doWatch(grandchildUri, { recursive: true, excludes: ['**/src/vendor'] });
|
|
341
|
+
|
|
342
|
+
expect(mockProvider.watchers).to.have.lengthOf(1); // only child has a real watcher
|
|
343
|
+
expect(liveWatcherCount(mockProvider)).to.equal(1);
|
|
344
|
+
|
|
345
|
+
// Create a parent with the same excludes. The parent subsumes the child
|
|
346
|
+
// (areExcludesCompatible passes). But the grandchild at /project/src/vendor has
|
|
347
|
+
// relative path 'src/vendor' from /project, which DOES match '**/src/vendor'.
|
|
348
|
+
// So isExcludedByParent(parent, grandchild) returns true — grandchild is promoted
|
|
349
|
+
// with its own real watcher and re-indexed.
|
|
350
|
+
await fileService.doWatch(parentUri, { recursive: true, excludes: ['**/src/vendor'] });
|
|
351
|
+
|
|
352
|
+
// Parent is live (subsumes child). Grandchild is promoted with its own real watcher.
|
|
353
|
+
expect(liveWatcherCount(mockProvider)).to.equal(2);
|
|
354
|
+
|
|
355
|
+
// A new watcher under the promoted grandchild should be subsumed by it,
|
|
356
|
+
// which only works if the grandchild was re-indexed after promotion.
|
|
357
|
+
const deepUri = new URI('file:///project/src/vendor/utils');
|
|
358
|
+
await fileService.doWatch(deepUri, { recursive: false, excludes: [] });
|
|
359
|
+
|
|
360
|
+
// No new OS watcher — the promoted grandchild covers it
|
|
361
|
+
expect(liveWatcherCount(mockProvider)).to.equal(2);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('should not delete parent index when subsuming a recursive child at the same URI', async () => {
|
|
365
|
+
const uri = new URI('file:///project');
|
|
366
|
+
const childUri = new URI('file:///project/src');
|
|
367
|
+
|
|
368
|
+
// Create a recursive watcher with excludes
|
|
369
|
+
await fileService.doWatch(uri, { recursive: true, excludes: ['**/node_modules'] });
|
|
370
|
+
expect(liveWatcherCount(mockProvider)).to.equal(1);
|
|
371
|
+
|
|
372
|
+
// Create a new recursive watcher at the SAME URI with no excludes.
|
|
373
|
+
// Different key (different excludes), so it's a new entry. The new watcher
|
|
374
|
+
// subsumes the old one via subsumeExistingChildren. The old watcher is recursive,
|
|
375
|
+
// so removeFromRecursiveIndex is called — but it must NOT delete the new parent's
|
|
376
|
+
// index entry (they share the same URI).
|
|
377
|
+
await fileService.doWatch(uri, { recursive: true, excludes: [] });
|
|
378
|
+
|
|
379
|
+
// Old watcher's OS watcher is disposed; new parent is live
|
|
380
|
+
expect(liveWatcherCount(mockProvider)).to.equal(1);
|
|
381
|
+
|
|
382
|
+
// A child watcher should be subsumed by the new parent. Without the fix,
|
|
383
|
+
// the parent's index entry was deleted, causing unnecessary OS watchers.
|
|
384
|
+
await fileService.doWatch(childUri, { recursive: false, excludes: [] });
|
|
385
|
+
|
|
386
|
+
// No new OS watcher — parent's index entry is intact
|
|
387
|
+
expect(liveWatcherCount(mockProvider)).to.equal(1);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('should not subsume excluded children when creating a new parent', async () => {
|
|
391
|
+
const childUri = new URI('file:///project/node_modules/pkg');
|
|
392
|
+
const parentUri = new URI('file:///project');
|
|
393
|
+
|
|
394
|
+
await fileService.doWatch(childUri, { recursive: false, excludes: [] });
|
|
395
|
+
expect(liveWatcherCount(mockProvider)).to.equal(1);
|
|
396
|
+
|
|
397
|
+
// Parent excludes node_modules
|
|
398
|
+
await fileService.doWatch(parentUri, { recursive: true, excludes: ['**/node_modules'] });
|
|
399
|
+
|
|
400
|
+
// Child should NOT be subsumed — both watchers should remain live
|
|
401
|
+
expect(liveWatcherCount(mockProvider)).to.equal(2);
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// ── Disposal and promotion ─────────────────────────────────────────
|
|
406
|
+
|
|
407
|
+
describe('disposal and promotion', () => {
|
|
408
|
+
it('should promote subsumed children by creating real watchers when the parent is disposed', async () => {
|
|
409
|
+
const parentUri = new URI('file:///project');
|
|
410
|
+
const childUri = new URI('file:///project/src');
|
|
411
|
+
|
|
412
|
+
const parentDisposable = await fileService.doWatch(parentUri, { recursive: true, excludes: [] });
|
|
413
|
+
await fileService.doWatch(childUri, { recursive: false, excludes: [] });
|
|
414
|
+
|
|
415
|
+
// Only parent watcher is real
|
|
416
|
+
expect(mockProvider.watchers).to.have.lengthOf(1);
|
|
417
|
+
|
|
418
|
+
// Dispose the parent
|
|
419
|
+
parentDisposable.dispose();
|
|
420
|
+
|
|
421
|
+
// Child should now have its own real watcher
|
|
422
|
+
expect(liveWatcherCount(mockProvider)).to.equal(1);
|
|
423
|
+
// The new watcher (index 1) should be for the child
|
|
424
|
+
expect(mockProvider.watchers).to.have.lengthOf(2);
|
|
425
|
+
expect(mockProvider.watchers[0].disposed).to.be.true; // parent
|
|
426
|
+
expect(mockProvider.watchers[1].disposed).to.be.false; // child promoted
|
|
427
|
+
expect(mockProvider.watchers[1].resource.toString()).to.equal(childUri.toString());
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('should re-parent children to another existing parent on disposal', async () => {
|
|
431
|
+
const grandparentUri = new URI('file:///project');
|
|
432
|
+
const parentUri = new URI('file:///project/src');
|
|
433
|
+
const childUri = new URI('file:///project/src/components');
|
|
434
|
+
|
|
435
|
+
// Create grandparent, then parent, then child
|
|
436
|
+
await fileService.doWatch(grandparentUri, { recursive: true, excludes: [] });
|
|
437
|
+
const parentDisposable = await fileService.doWatch(parentUri, { recursive: true, excludes: [] });
|
|
438
|
+
await fileService.doWatch(childUri, { recursive: false, excludes: [] });
|
|
439
|
+
|
|
440
|
+
// Grandparent subsumed parent; parent did not get a real watcher.
|
|
441
|
+
// Child was also subsumed (by grandparent after re-parenting).
|
|
442
|
+
expect(liveWatcherCount(mockProvider)).to.equal(1);
|
|
443
|
+
|
|
444
|
+
// Dispose the parent
|
|
445
|
+
parentDisposable.dispose();
|
|
446
|
+
|
|
447
|
+
// Grandparent still covers the child — no new OS watcher needed
|
|
448
|
+
expect(liveWatcherCount(mockProvider)).to.equal(1);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it('should promote multiple children when a parent is disposed', async () => {
|
|
452
|
+
const parentUri = new URI('file:///project');
|
|
453
|
+
const child1Uri = new URI('file:///project/src');
|
|
454
|
+
const child2Uri = new URI('file:///project/test');
|
|
455
|
+
const child3Uri = new URI('file:///project/docs');
|
|
456
|
+
|
|
457
|
+
const parentDisposable = await fileService.doWatch(parentUri, { recursive: true, excludes: [] });
|
|
458
|
+
await fileService.doWatch(child1Uri, { recursive: false, excludes: [] });
|
|
459
|
+
await fileService.doWatch(child2Uri, { recursive: false, excludes: [] });
|
|
460
|
+
await fileService.doWatch(child3Uri, { recursive: false, excludes: [] });
|
|
461
|
+
|
|
462
|
+
expect(mockProvider.watchers).to.have.lengthOf(1);
|
|
463
|
+
|
|
464
|
+
parentDisposable.dispose();
|
|
465
|
+
|
|
466
|
+
// All three children should now have their own real watchers
|
|
467
|
+
expect(liveWatcherCount(mockProvider)).to.equal(3);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('should not corrupt index when disposing a subsumed recursive watcher at the same URI', async () => {
|
|
471
|
+
const uri = new URI('file:///project');
|
|
472
|
+
const childUri = new URI('file:///project/src');
|
|
473
|
+
|
|
474
|
+
// Watcher A: recursive, no excludes — gets indexed
|
|
475
|
+
await fileService.doWatch(uri, { recursive: true, excludes: [] });
|
|
476
|
+
|
|
477
|
+
// Watcher B: recursive, with excludes at the same URI — subsumed by A (not indexed)
|
|
478
|
+
const bDisposable = await fileService.doWatch(uri, { recursive: true, excludes: ['**/node_modules'] });
|
|
479
|
+
|
|
480
|
+
expect(liveWatcherCount(mockProvider)).to.equal(1);
|
|
481
|
+
|
|
482
|
+
// Disposing B must NOT remove A's index entry.
|
|
483
|
+
// Without the fix, removeFromRecursiveIndex would delete /project from
|
|
484
|
+
// the tree, causing subsequent child watchers to miss A as a parent.
|
|
485
|
+
bDisposable.dispose();
|
|
486
|
+
|
|
487
|
+
// A child watcher should still be subsumed by A
|
|
488
|
+
await fileService.doWatch(childUri, { recursive: false, excludes: [] });
|
|
489
|
+
expect(liveWatcherCount(mockProvider)).to.equal(1);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it('should handle disposing a subsumed child cleanly', async () => {
|
|
493
|
+
const parentUri = new URI('file:///project');
|
|
494
|
+
const childUri = new URI('file:///project/src');
|
|
495
|
+
|
|
496
|
+
await fileService.doWatch(parentUri, { recursive: true, excludes: [] });
|
|
497
|
+
const childDisposable = await fileService.doWatch(childUri, { recursive: false, excludes: [] });
|
|
498
|
+
|
|
499
|
+
expect(mockProvider.watchers).to.have.lengthOf(1);
|
|
500
|
+
|
|
501
|
+
// Disposing the subsumed child should not affect the parent's watcher
|
|
502
|
+
childDisposable.dispose();
|
|
503
|
+
expect(liveWatcherCount(mockProvider)).to.equal(1);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it('should handle dispose-then-rewatch correctly', async () => {
|
|
507
|
+
const parentUri = new URI('file:///project');
|
|
508
|
+
const childUri = new URI('file:///project/src');
|
|
509
|
+
|
|
510
|
+
const parentDisposable = await fileService.doWatch(parentUri, { recursive: true, excludes: [] });
|
|
511
|
+
await fileService.doWatch(childUri, { recursive: false, excludes: [] });
|
|
512
|
+
|
|
513
|
+
parentDisposable.dispose();
|
|
514
|
+
|
|
515
|
+
// Child is promoted, now re-create the parent
|
|
516
|
+
const parentDisposable2 = await fileService.doWatch(parentUri, { recursive: true, excludes: [] });
|
|
517
|
+
|
|
518
|
+
// The child should be subsumed again — its promoted watcher should be disposed
|
|
519
|
+
expect(liveWatcherCount(mockProvider)).to.equal(1);
|
|
520
|
+
|
|
521
|
+
parentDisposable2.dispose();
|
|
522
|
+
|
|
523
|
+
// Child promoted again
|
|
524
|
+
expect(liveWatcherCount(mockProvider)).to.equal(1);
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// ── Exclude pattern matching ───────────────────────────────────────
|
|
529
|
+
|
|
530
|
+
describe('exclude pattern matching', () => {
|
|
531
|
+
it('should exclude with glob patterns using **', async () => {
|
|
532
|
+
const parentUri = new URI('file:///project');
|
|
533
|
+
const excludedChild = new URI('file:///project/dist/bundle.js');
|
|
534
|
+
|
|
535
|
+
await fileService.doWatch(parentUri, { recursive: true, excludes: ['**/dist'] });
|
|
536
|
+
await fileService.doWatch(excludedChild, { recursive: false, excludes: [] });
|
|
537
|
+
|
|
538
|
+
// Should not be subsumed because dist is excluded
|
|
539
|
+
expect(mockProvider.watchers).to.have.lengthOf(2);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it('should handle multiple exclude patterns', async () => {
|
|
543
|
+
const parentUri = new URI('file:///project');
|
|
544
|
+
const nmChild = new URI('file:///project/node_modules/pkg');
|
|
545
|
+
const distChild = new URI('file:///project/dist/out');
|
|
546
|
+
const srcChild = new URI('file:///project/src/main');
|
|
547
|
+
|
|
548
|
+
await fileService.doWatch(parentUri, { recursive: true, excludes: ['**/node_modules', '**/dist'] });
|
|
549
|
+
await fileService.doWatch(nmChild, { recursive: false, excludes: [] });
|
|
550
|
+
await fileService.doWatch(distChild, { recursive: false, excludes: [] });
|
|
551
|
+
await fileService.doWatch(srcChild, { recursive: false, excludes: [] });
|
|
552
|
+
|
|
553
|
+
// nm and dist children need their own watchers; src is subsumed
|
|
554
|
+
expect(mockProvider.watchers).to.have.lengthOf(3); // parent + nm + dist
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it('should exclude based on ancestor directory matching', async () => {
|
|
558
|
+
const parentUri = new URI('file:///project');
|
|
559
|
+
// The exclude is for node_modules — anything under it should also be excluded
|
|
560
|
+
const deepChild = new URI('file:///project/node_modules/pkg/lib/index.js');
|
|
561
|
+
|
|
562
|
+
await fileService.doWatch(parentUri, { recursive: true, excludes: ['**/node_modules'] });
|
|
563
|
+
await fileService.doWatch(deepChild, { recursive: false, excludes: [] });
|
|
564
|
+
|
|
565
|
+
expect(mockProvider.watchers).to.have.lengthOf(2);
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it('should not exclude a non-matching path', async () => {
|
|
569
|
+
const parentUri = new URI('file:///project');
|
|
570
|
+
const childUri = new URI('file:///project/src/node_modules_backup/file.ts');
|
|
571
|
+
|
|
572
|
+
await fileService.doWatch(parentUri, { recursive: true, excludes: ['**/node_modules'] });
|
|
573
|
+
await fileService.doWatch(childUri, { recursive: false, excludes: [] });
|
|
574
|
+
|
|
575
|
+
// 'node_modules_backup' does not match '**/node_modules', so child should be subsumed
|
|
576
|
+
expect(mockProvider.watchers).to.have.lengthOf(1);
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
// ── Edge cases ─────────────────────────────────────────────────────
|
|
581
|
+
|
|
582
|
+
describe('edge cases', () => {
|
|
583
|
+
it('should promote a non-recursive watcher when its recursive same-URI subsumer is disposed', async () => {
|
|
584
|
+
const uri = new URI('file:///project/src');
|
|
585
|
+
const recursiveOpts: WatchOptions = { recursive: true, excludes: [] };
|
|
586
|
+
const nonRecursiveOpts: WatchOptions = { recursive: false, excludes: [] };
|
|
587
|
+
|
|
588
|
+
const d1 = await fileService.doWatch(uri, recursiveOpts);
|
|
589
|
+
const d2 = await fileService.doWatch(uri, nonRecursiveOpts);
|
|
590
|
+
|
|
591
|
+
// findSubstr matches exact URIs, so the recursive watcher subsumes the non-recursive one
|
|
592
|
+
expect(mockProvider.watchers).to.have.lengthOf(1);
|
|
593
|
+
|
|
594
|
+
// Disposing the recursive watcher promotes the non-recursive one
|
|
595
|
+
d1.dispose();
|
|
596
|
+
expect(liveWatcherCount(mockProvider)).to.equal(1);
|
|
597
|
+
|
|
598
|
+
d2.dispose();
|
|
599
|
+
expect(liveWatcherCount(mockProvider)).to.equal(0);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it('should handle double dispose gracefully', async () => {
|
|
603
|
+
const uri = new URI('file:///project/src');
|
|
604
|
+
const d1 = await fileService.doWatch(uri, { recursive: false, excludes: [] });
|
|
605
|
+
|
|
606
|
+
d1.dispose();
|
|
607
|
+
// Double dispose should not throw
|
|
608
|
+
d1.dispose();
|
|
609
|
+
expect(liveWatcherCount(mockProvider)).to.equal(0);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it('should handle many watchers under one parent', async () => {
|
|
613
|
+
const parentUri = new URI('file:///project');
|
|
614
|
+
await fileService.doWatch(parentUri, { recursive: true, excludes: [] });
|
|
615
|
+
|
|
616
|
+
const childDisposables: Disposable[] = [];
|
|
617
|
+
for (let i = 0; i < 20; i++) {
|
|
618
|
+
const d = await fileService.doWatch(
|
|
619
|
+
new URI(`file:///project/dir${i}`),
|
|
620
|
+
{ recursive: false, excludes: [] }
|
|
621
|
+
);
|
|
622
|
+
childDisposables.push(d);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Only the parent's real watcher exists
|
|
626
|
+
expect(mockProvider.watchers).to.have.lengthOf(1);
|
|
627
|
+
expect(liveWatcherCount(mockProvider)).to.equal(1);
|
|
628
|
+
|
|
629
|
+
// Dispose all children — parent still alive
|
|
630
|
+
for (const d of childDisposables) {
|
|
631
|
+
d.dispose();
|
|
632
|
+
}
|
|
633
|
+
expect(liveWatcherCount(mockProvider)).to.equal(1);
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Creates a mock provider with a real onDidChangeFile emitter, suitable for
|
|
640
|
+
* registering with FileService.registerProvider and testing event delivery.
|
|
641
|
+
*/
|
|
642
|
+
function createEventMockProvider(caseSensitive: boolean = true): FileSystemProvider & {
|
|
643
|
+
watchers: MockWatcher[];
|
|
644
|
+
fileChangeEmitter: Emitter<readonly FileChange[]>;
|
|
645
|
+
/** Simulate a file change event delivered by active OS watchers. */
|
|
646
|
+
simulateChange(uri: URI, type?: FileChangeType): void;
|
|
647
|
+
} {
|
|
648
|
+
const watchers: MockWatcher[] = [];
|
|
649
|
+
const fileChangeEmitter = new Emitter<readonly FileChange[]>();
|
|
650
|
+
const provider = {
|
|
651
|
+
watchers,
|
|
652
|
+
fileChangeEmitter,
|
|
653
|
+
capabilities: caseSensitive ? FileSystemProviderCapabilities.PathCaseSensitive : 0,
|
|
654
|
+
onDidChangeCapabilities: () => Disposable.NULL,
|
|
655
|
+
onDidChangeFile: fileChangeEmitter.event,
|
|
656
|
+
onFileWatchError: () => Disposable.NULL,
|
|
657
|
+
watch(resource: URI, options: WatchOptions): Disposable {
|
|
658
|
+
const watcher: MockWatcher = {
|
|
659
|
+
resource, options, disposed: false,
|
|
660
|
+
disposable: Disposable.create(() => { watcher.disposed = true; }),
|
|
661
|
+
};
|
|
662
|
+
watchers.push(watcher);
|
|
663
|
+
return watcher.disposable;
|
|
664
|
+
},
|
|
665
|
+
simulateChange(uri: URI, type: FileChangeType = FileChangeType.UPDATED): void {
|
|
666
|
+
fileChangeEmitter.fire([{ resource: uri, type }]);
|
|
667
|
+
},
|
|
668
|
+
stat: () => { throw new Error('not implemented'); },
|
|
669
|
+
readdir: () => { throw new Error('not implemented'); },
|
|
670
|
+
readFile: () => { throw new Error('not implemented'); },
|
|
671
|
+
writeFile: () => { throw new Error('not implemented'); },
|
|
672
|
+
delete: () => { throw new Error('not implemented'); },
|
|
673
|
+
mkdir: () => { throw new Error('not implemented'); },
|
|
674
|
+
rename: () => { throw new Error('not implemented'); },
|
|
675
|
+
};
|
|
676
|
+
return provider as FileSystemProvider & typeof provider;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
describe('FileService watcher event delivery', () => {
|
|
680
|
+
let fileService: FileService;
|
|
681
|
+
let provider: ReturnType<typeof createEventMockProvider>;
|
|
682
|
+
let disJSDOM: () => void;
|
|
683
|
+
|
|
684
|
+
before(() => {
|
|
685
|
+
disJSDOM = enableJSDOM();
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
after(() => {
|
|
689
|
+
disJSDOM();
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
beforeEach(() => {
|
|
693
|
+
fileService = new FileService();
|
|
694
|
+
provider = createEventMockProvider();
|
|
695
|
+
fileService.registerProvider('file', provider);
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
it('should deliver events for a subsumed child through the parent watcher', async () => {
|
|
699
|
+
const received: URI[] = [];
|
|
700
|
+
fileService.onDidFilesChange(e => {
|
|
701
|
+
for (const change of e.changes) {
|
|
702
|
+
received.push(change.resource);
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
const parentUri = new URI('file:///project');
|
|
707
|
+
const childUri = new URI('file:///project/src');
|
|
708
|
+
|
|
709
|
+
await fileService.doWatch(parentUri, { recursive: true, excludes: [] });
|
|
710
|
+
await fileService.doWatch(childUri, { recursive: false, excludes: [] });
|
|
711
|
+
|
|
712
|
+
// Child is subsumed — only parent has a real watcher
|
|
713
|
+
expect(liveWatcherCount(provider)).to.equal(1);
|
|
714
|
+
|
|
715
|
+
// Simulate a change under the child path — should be delivered through the parent
|
|
716
|
+
const changedFile = new URI('file:///project/src/index.ts');
|
|
717
|
+
provider.simulateChange(changedFile);
|
|
718
|
+
|
|
719
|
+
expect(received).to.have.lengthOf(1);
|
|
720
|
+
expect(received[0].toString()).to.equal(changedFile.toString());
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
it('should deliver events after a child is promoted due to parent disposal', async () => {
|
|
724
|
+
const received: URI[] = [];
|
|
725
|
+
fileService.onDidFilesChange(e => {
|
|
726
|
+
for (const change of e.changes) {
|
|
727
|
+
received.push(change.resource);
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
const parentUri = new URI('file:///project');
|
|
732
|
+
const childUri = new URI('file:///project/src');
|
|
733
|
+
|
|
734
|
+
const parentDisposable = await fileService.doWatch(parentUri, { recursive: true, excludes: [] });
|
|
735
|
+
await fileService.doWatch(childUri, { recursive: false, excludes: [] });
|
|
736
|
+
|
|
737
|
+
// Dispose parent — child gets promoted with its own real watcher
|
|
738
|
+
parentDisposable.dispose();
|
|
739
|
+
expect(liveWatcherCount(provider)).to.equal(1);
|
|
740
|
+
|
|
741
|
+
// Simulate a change — should still be delivered
|
|
742
|
+
const changedFile = new URI('file:///project/src/index.ts');
|
|
743
|
+
provider.simulateChange(changedFile);
|
|
744
|
+
|
|
745
|
+
expect(received).to.have.lengthOf(1);
|
|
746
|
+
expect(received[0].toString()).to.equal(changedFile.toString());
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
it('should not let a parent with excludes suppress events for a child that needs them', async () => {
|
|
750
|
+
const received: URI[] = [];
|
|
751
|
+
fileService.onDidFilesChange(e => {
|
|
752
|
+
for (const change of e.changes) {
|
|
753
|
+
received.push(change.resource);
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
const parentUri = new URI('file:///project');
|
|
758
|
+
const childUri = new URI('file:///project/src');
|
|
759
|
+
|
|
760
|
+
await fileService.doWatch(parentUri, { recursive: true, excludes: ['**/node_modules'] });
|
|
761
|
+
await fileService.doWatch(childUri, { recursive: true, excludes: [] });
|
|
762
|
+
|
|
763
|
+
// Child is recursive with no excludes, parent has excludes — child should NOT be subsumed
|
|
764
|
+
// Both should have their own real watchers
|
|
765
|
+
expect(liveWatcherCount(provider)).to.equal(2);
|
|
766
|
+
|
|
767
|
+
// Simulate a change inside node_modules under the child — only the child's watcher would catch it
|
|
768
|
+
const changedFile = new URI('file:///project/src/node_modules/pkg/index.js');
|
|
769
|
+
provider.simulateChange(changedFile);
|
|
770
|
+
|
|
771
|
+
expect(received).to.have.lengthOf(1);
|
|
772
|
+
expect(received[0].toString()).to.equal(changedFile.toString());
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
it('should preserve recursive watcher state through subsumption and promotion', async () => {
|
|
776
|
+
const received: URI[] = [];
|
|
777
|
+
fileService.onDidFilesChange(e => {
|
|
778
|
+
for (const change of e.changes) {
|
|
779
|
+
received.push(change.resource);
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
const grandparentUri = new URI('file:///project');
|
|
784
|
+
const parentUri = new URI('file:///project/src');
|
|
785
|
+
const childUri = new URI('file:///project/src/lib');
|
|
786
|
+
|
|
787
|
+
// Create parent (recursive), then child under it
|
|
788
|
+
await fileService.doWatch(parentUri, { recursive: true, excludes: [] });
|
|
789
|
+
await fileService.doWatch(childUri, { recursive: false, excludes: [] });
|
|
790
|
+
expect(liveWatcherCount(provider)).to.equal(1); // only parent has real watcher
|
|
791
|
+
|
|
792
|
+
// Now create a grandparent that subsumes the parent
|
|
793
|
+
const gpDisposable = await fileService.doWatch(grandparentUri, { recursive: true, excludes: [] });
|
|
794
|
+
expect(liveWatcherCount(provider)).to.equal(1); // only grandparent is live
|
|
795
|
+
|
|
796
|
+
// Dispose the grandparent — parent should be promoted, child should re-parent to parent
|
|
797
|
+
gpDisposable.dispose();
|
|
798
|
+
expect(liveWatcherCount(provider)).to.equal(1); // promoted parent is live
|
|
799
|
+
|
|
800
|
+
// Simulate a change under the child — should still be delivered
|
|
801
|
+
const changedFile = new URI('file:///project/src/lib/utils.ts');
|
|
802
|
+
provider.simulateChange(changedFile);
|
|
803
|
+
|
|
804
|
+
expect(received).to.have.lengthOf(1);
|
|
805
|
+
expect(received[0].toString()).to.equal(changedFile.toString());
|
|
806
|
+
});
|
|
807
|
+
});
|