-
Notifications
You must be signed in to change notification settings - Fork 28.7k
/
workspaceWatcher.ts
185 lines (155 loc) · 6.6 KB
/
workspaceWatcher.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { IDisposable, Disposable, dispose, DisposableStore } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
import { IFilesConfiguration } from 'vs/platform/files/common/files';
import { IWorkspaceContextService, IWorkspaceFolder, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace';
import { ResourceMap } from 'vs/base/common/map';
import { INotificationService, Severity, NeverShowAgainScope } from 'vs/platform/notification/common/notification';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { isAbsolute } from 'vs/base/common/path';
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { IWorkbenchFileService } from 'vs/workbench/services/files/common/files';
export class WorkspaceWatcher extends Disposable {
private readonly watchedWorkspaces = new ResourceMap<IDisposable>(resource => this.uriIdentityService.extUri.getComparisonKey(resource));
constructor(
@IWorkbenchFileService private readonly fileService: IWorkbenchFileService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@INotificationService private readonly notificationService: INotificationService,
@IOpenerService private readonly openerService: IOpenerService,
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
@IHostService private readonly hostService: IHostService
) {
super();
this.registerListeners();
this.refresh();
}
private registerListeners(): void {
this._register(this.contextService.onDidChangeWorkspaceFolders(e => this.onDidChangeWorkspaceFolders(e)));
this._register(this.contextService.onDidChangeWorkbenchState(() => this.onDidChangeWorkbenchState()));
this._register(this.configurationService.onDidChangeConfiguration(e => this.onDidChangeConfiguration(e)));
this._register(this.fileService.onDidWatchError(error => this.onDidWatchError(error)));
}
private onDidChangeWorkspaceFolders(e: IWorkspaceFoldersChangeEvent): void {
// Removed workspace: Unwatch
for (const removed of e.removed) {
this.unwatchWorkspace(removed);
}
// Added workspace: Watch
for (const added of e.added) {
this.watchWorkspace(added);
}
}
private onDidChangeWorkbenchState(): void {
this.refresh();
}
private onDidChangeConfiguration(e: IConfigurationChangeEvent): void {
if (e.affectsConfiguration('files.watcherExclude') || e.affectsConfiguration('files.watcherInclude')) {
this.refresh();
}
}
private onDidWatchError(error: Error): void {
const msg = error.toString();
// Detect if we run into ENOSPC issues
if (msg.indexOf('ENOSPC') >= 0) {
this.notificationService.prompt(
Severity.Warning,
localize('enospcError', "Unable to watch for file changes in this large workspace folder. Please follow the instructions link to resolve this issue."),
[{
label: localize('learnMore', "Instructions"),
run: () => this.openerService.open(URI.parse('https://go.microsoft.com/fwlink/?linkid=867693'))
}],
{
sticky: true,
neverShowAgain: { id: 'ignoreEnospcError', isSecondary: true, scope: NeverShowAgainScope.WORKSPACE }
}
);
}
// Detect when the watcher throws an error unexpectedly
else if (msg.indexOf('EUNKNOWN') >= 0) {
this.notificationService.prompt(
Severity.Warning,
localize('eshutdownError', "File changes watcher stopped unexpectedly. A reload of the window may enable the watcher again unless the workspace cannot be watched for file changes."),
[{
label: localize('reload', "Reload"),
run: () => this.hostService.reload()
}],
{
sticky: true,
silent: true // reduce potential spam since we don't really know how often this fires
}
);
}
}
private watchWorkspace(workspace: IWorkspaceFolder): void {
// Compute the watcher exclude rules from configuration
const excludes: string[] = [];
const config = this.configurationService.getValue<IFilesConfiguration>({ resource: workspace.uri });
if (config.files?.watcherExclude) {
for (const key in config.files.watcherExclude) {
if (config.files.watcherExclude[key] === true) {
excludes.push(key);
}
}
}
const pathsToWatch = new ResourceMap<URI>(uri => this.uriIdentityService.extUri.getComparisonKey(uri));
// Add the workspace as path to watch
pathsToWatch.set(workspace.uri, workspace.uri);
// Compute additional includes from configuration
if (config.files?.watcherInclude) {
for (const includePath of config.files.watcherInclude) {
if (!includePath) {
continue;
}
// Absolute: verify a child of the workspace
if (isAbsolute(includePath)) {
const candidate = URI.file(includePath).with({ scheme: workspace.uri.scheme });
if (this.uriIdentityService.extUri.isEqualOrParent(candidate, workspace.uri)) {
pathsToWatch.set(candidate, candidate);
}
}
// Relative: join against workspace folder
else {
const candidate = workspace.toResource(includePath);
pathsToWatch.set(candidate, candidate);
}
}
}
// Watch all paths as instructed
const disposables = new DisposableStore();
for (const [, pathToWatch] of pathsToWatch) {
disposables.add(this.fileService.watch(pathToWatch, { recursive: true, excludes }));
}
this.watchedWorkspaces.set(workspace.uri, disposables);
}
private unwatchWorkspace(workspace: IWorkspaceFolder): void {
if (this.watchedWorkspaces.has(workspace.uri)) {
dispose(this.watchedWorkspaces.get(workspace.uri));
this.watchedWorkspaces.delete(workspace.uri);
}
}
private refresh(): void {
// Unwatch all first
this.unwatchWorkspaces();
// Watch each workspace folder
for (const folder of this.contextService.getWorkspace().folders) {
this.watchWorkspace(folder);
}
}
private unwatchWorkspaces(): void {
for (const [, disposable] of this.watchedWorkspaces) {
disposable.dispose();
}
this.watchedWorkspaces.clear();
}
override dispose(): void {
super.dispose();
this.unwatchWorkspaces();
}
}