Skip to content

Commit b2c4302

Browse files
authored
Use vscode watches for tsserver (microsoft#193848)
1 parent e36423a commit b2c4302

File tree

11 files changed

+175
-5
lines changed

11 files changed

+175
-5
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,4 +172,5 @@
172172
"css.format.spaceAroundSelectorSeparator": true,
173173
"inlineChat.mode": "live",
174174
"typescript.enablePromptUseWorkspaceTsdk": true,
175+
"typescript.tsserver.experimental.useVsCodeWatcher": true
175176
}

extensions/typescript-language-features/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"aiKey": "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255",
1010
"enabledApiProposals": [
1111
"workspaceTrust",
12+
"createFileSystemWatcher",
1213
"multiDocumentHighlightProvider",
1314
"mappedEditsProvider",
1415
"codeActionAI",
@@ -1168,6 +1169,14 @@
11681169
"experimental"
11691170
]
11701171
},
1172+
"typescript.tsserver.experimental.useVsCodeWatcher": {
1173+
"type": "boolean",
1174+
"description": "%configuration.tsserver.useVsCodeWatcher%",
1175+
"default": false,
1176+
"tags": [
1177+
"experimental"
1178+
]
1179+
},
11711180
"typescript.tsserver.watchOptions": {
11721181
"type": "object",
11731182
"description": "%configuration.tsserver.watchOptions%",

extensions/typescript-language-features/package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@
164164
"typescript.suggest.enabled": "Enabled/disable autocomplete suggestions.",
165165
"configuration.surveys.enabled": "Enabled/disable occasional surveys that help us improve VS Code's JavaScript and TypeScript support.",
166166
"configuration.suggest.completeJSDocs": "Enable/disable suggestion to complete JSDoc comments.",
167+
"configuration.tsserver.useVsCodeWatcher": "Use VS Code's file watchers instead of TypeScript's. Requires using TypeScript 5.4+ in the workspace.",
167168
"configuration.tsserver.watchOptions": "Configure which watching strategies should be used to keep track of files and directories.",
168169
"configuration.tsserver.watchOptions.watchFile": "Strategy for how individual files are watched.",
169170
"configuration.tsserver.watchOptions.watchFile.fixedChunkSizePolling": "Polls files in chunks at regular interval.",

extensions/typescript-language-features/src/configuration/configuration.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export interface TypeScriptServiceConfiguration {
117117
readonly enableProjectDiagnostics: boolean;
118118
readonly maxTsServerMemory: number;
119119
readonly enablePromptUseWorkspaceTsdk: boolean;
120+
readonly useVsCodeWatcher: boolean;
120121
readonly watchOptions: Proto.WatchOptions | undefined;
121122
readonly includePackageJsonAutoImports: 'auto' | 'on' | 'off' | undefined;
122123
readonly enableTsServerTracing: boolean;
@@ -154,6 +155,7 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu
154155
enableProjectDiagnostics: this.readEnableProjectDiagnostics(configuration),
155156
maxTsServerMemory: this.readMaxTsServerMemory(configuration),
156157
enablePromptUseWorkspaceTsdk: this.readEnablePromptUseWorkspaceTsdk(configuration),
158+
useVsCodeWatcher: this.readUseVsCodeWatcher(configuration),
157159
watchOptions: this.readWatchOptions(configuration),
158160
includePackageJsonAutoImports: this.readIncludePackageJsonAutoImports(configuration),
159161
enableTsServerTracing: this.readEnableTsServerTracing(configuration),
@@ -222,7 +224,11 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu
222224
return configuration.get<boolean>('typescript.tsserver.experimental.enableProjectDiagnostics', false);
223225
}
224226

225-
protected readWatchOptions(configuration: vscode.WorkspaceConfiguration): Proto.WatchOptions | undefined {
227+
private readUseVsCodeWatcher(configuration: vscode.WorkspaceConfiguration): boolean {
228+
return configuration.get<boolean>('typescript.tsserver.experimental.useVsCodeWatcher', false);
229+
}
230+
231+
private readWatchOptions(configuration: vscode.WorkspaceConfiguration): Proto.WatchOptions | undefined {
226232
const watchOptions = configuration.get<Proto.WatchOptions>('typescript.tsserver.watchOptions');
227233
// Returned value may be a proxy. Clone it into a normal object
228234
return { ...(watchOptions ?? {}) };

extensions/typescript-language-features/src/tsServer/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export class API {
3535
public static readonly v500 = API.fromSimpleString('5.0.0');
3636
public static readonly v510 = API.fromSimpleString('5.1.0');
3737
public static readonly v520 = API.fromSimpleString('5.2.0');
38+
public static readonly v544 = API.fromSimpleString('5.4.4');
3839
public static readonly v540 = API.fromSimpleString('5.4.0');
3940

4041
public static fromVersionString(versionString: string): API {

extensions/typescript-language-features/src/tsServer/protocol/protocol.const.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ export enum EventName {
8888
surveyReady = 'surveyReady',
8989
projectLoadingStart = 'projectLoadingStart',
9090
projectLoadingFinish = 'projectLoadingFinish',
91+
createFileWatcher = 'createFileWatcher',
92+
createDirectoryWatcher = 'createDirectoryWatcher',
93+
closeFileWatcher = 'closeFileWatcher',
9194
}
9295

9396
export enum OrganizeImportsMode {

extensions/typescript-language-features/src/tsServer/spawner.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,10 @@ export class TypeScriptServerSpawner {
271271

272272
args.push('--noGetErrOnBackgroundUpdate');
273273

274+
if (apiVersion.gte(API.v544) && configuration.useVsCodeWatcher) {
275+
args.push('--canUseWatchEvents');
276+
}
277+
274278
args.push('--validateDefaultNpmLocation');
275279

276280
if (isWebAndHasSharedArrayBuffers()) {

extensions/typescript-language-features/src/typescriptService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ interface NoResponseTsServerRequests {
8686
'compilerOptionsForInferredProjects': [Proto.SetCompilerOptionsForInferredProjectsArgs, null];
8787
'reloadProjects': [null, null];
8888
'configurePlugin': [Proto.ConfigurePluginRequest, Proto.ConfigurePluginResponse];
89+
'watchChange': [Proto.Request, null];
8990
}
9091

9192
interface AsyncTsServerRequests {

extensions/typescript-language-features/src/typescriptServiceClient.ts

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { TypeScriptVersionManager } from './tsServer/versionManager';
2121
import { ITypeScriptVersionProvider, TypeScriptVersion } from './tsServer/versionProvider';
2222
import { ClientCapabilities, ClientCapability, ExecConfig, ITypeScriptServiceClient, ServerResponse, TypeScriptRequests } from './typescriptService';
2323
import { ServiceConfigurationProvider, SyntaxServerConfiguration, TsServerLogLevel, TypeScriptServiceConfiguration, areServiceConfigurationsEqual } from './configuration/configuration';
24-
import { Disposable } from './utils/dispose';
24+
import { Disposable, DisposableStore, disposeAll } from './utils/dispose';
2525
import * as fileSchemes from './configuration/fileSchemes';
2626
import { Logger } from './logging/logger';
2727
import { isWeb, isWebAndHasSharedArrayBuffers } from './utils/platform';
@@ -97,6 +97,12 @@ export const emptyAuthority = 'ts-nul-authority';
9797

9898
export const inMemoryResourcePrefix = '^';
9999

100+
interface WatchEvent {
101+
updated?: Set<string>;
102+
created?: Set<string>;
103+
deleted?: Set<string>;
104+
}
105+
100106
export default class TypeScriptServiceClient extends Disposable implements ITypeScriptServiceClient {
101107

102108

@@ -128,6 +134,10 @@ export default class TypeScriptServiceClient extends Disposable implements IType
128134
private readonly versionProvider: ITypeScriptVersionProvider;
129135
private readonly processFactory: TsServerProcessFactory;
130136

137+
private readonly watches = new Map<number, Disposable>();
138+
private readonly watchEvents = new Map<number, WatchEvent>();
139+
private watchChangeTimeout: NodeJS.Timeout | undefined;
140+
131141
constructor(
132142
private readonly context: vscode.ExtensionContext,
133143
onCaseInsenitiveFileSystem: boolean,
@@ -298,6 +308,8 @@ export default class TypeScriptServiceClient extends Disposable implements IType
298308
}
299309

300310
this.loadingIndicator.reset();
311+
312+
this.resetWatchers();
301313
}
302314

303315
public restartTsServer(fromUserAction = false): void {
@@ -401,6 +413,8 @@ export default class TypeScriptServiceClient extends Disposable implements IType
401413
this.info(`Using Node installation from ${nodePath} to run TS Server`);
402414
}
403415

416+
this.resetWatchers();
417+
404418
const apiVersion = version.apiVersion || API.defaultVersion;
405419
const mytoken = ++this.token;
406420
const handle = this.typescriptServerSpawner.spawn(version, this.capabilities, this.configuration, this.pluginManager, this.cancellerFactory, {
@@ -493,6 +507,11 @@ export default class TypeScriptServiceClient extends Disposable implements IType
493507
return this.serverState;
494508
}
495509

510+
private resetWatchers() {
511+
clearTimeout(this.watchChangeTimeout);
512+
disposeAll(Array.from(this.watches.values()));
513+
}
514+
496515
public async showVersionPicker(): Promise<void> {
497516
this._versionManager.promptUserForVersion();
498517
}
@@ -594,6 +613,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType
594613
}
595614

596615
private serviceExited(restart: boolean): void {
616+
this.resetWatchers();
597617
this.loadingIndicator.reset();
598618

599619
const previousState = this.serverState;
@@ -973,6 +993,120 @@ export default class TypeScriptServiceClient extends Disposable implements IType
973993
case EventName.projectLoadingFinish:
974994
this.loadingIndicator.finishedLoadingProject((event as Proto.ProjectLoadingFinishEvent).body.projectName);
975995
break;
996+
997+
case EventName.createDirectoryWatcher:
998+
this.createFileSystemWatcher(
999+
(event.body as Proto.CreateDirectoryWatcherEventBody).id,
1000+
new vscode.RelativePattern(
1001+
vscode.Uri.file((event.body as Proto.CreateDirectoryWatcherEventBody).path),
1002+
(event.body as Proto.CreateDirectoryWatcherEventBody).recursive ? '**' : '*'
1003+
),
1004+
(event.body as Proto.CreateDirectoryWatcherEventBody).ignoreUpdate
1005+
);
1006+
break;
1007+
1008+
case EventName.createFileWatcher:
1009+
this.createFileSystemWatcher(
1010+
(event.body as Proto.CreateFileWatcherEventBody).id,
1011+
new vscode.RelativePattern(
1012+
vscode.Uri.file((event.body as Proto.CreateFileWatcherEventBody).path),
1013+
'*'
1014+
)
1015+
);
1016+
break;
1017+
1018+
case EventName.closeFileWatcher:
1019+
this.closeFileSystemWatcher(event.body.id);
1020+
break;
1021+
}
1022+
}
1023+
1024+
private scheduleExecuteWatchChangeRequest() {
1025+
if (!this.watchChangeTimeout) {
1026+
this.watchChangeTimeout = setTimeout(() => {
1027+
this.watchChangeTimeout = undefined;
1028+
const allEvents = Array.from(this.watchEvents, ([id, event]) => ({
1029+
id,
1030+
updated: event.updated && Array.from(event.updated),
1031+
created: event.created && Array.from(event.created),
1032+
deleted: event.deleted && Array.from(event.deleted)
1033+
}));
1034+
this.watchEvents.clear();
1035+
this.executeWithoutWaitingForResponse('watchChange', allEvents);
1036+
}, 100); /* aggregate events over 100ms to reduce client<->server IPC overhead */
1037+
}
1038+
}
1039+
1040+
private addWatchEvent(id: number, eventType: keyof WatchEvent, path: string) {
1041+
let event = this.watchEvents.get(id);
1042+
const removeEvent = (typeOfEventToRemove: keyof WatchEvent) => {
1043+
if (event?.[typeOfEventToRemove]?.delete(path) && event[typeOfEventToRemove].size === 0) {
1044+
event[typeOfEventToRemove] = undefined;
1045+
}
1046+
};
1047+
const aggregateEvent = () => {
1048+
if (!event) {
1049+
this.watchEvents.set(id, event = {});
1050+
}
1051+
(event[eventType] ??= new Set()).add(path);
1052+
};
1053+
switch (eventType) {
1054+
case 'created':
1055+
removeEvent('deleted');
1056+
removeEvent('updated');
1057+
aggregateEvent();
1058+
break;
1059+
case 'deleted':
1060+
removeEvent('created');
1061+
removeEvent('updated');
1062+
aggregateEvent();
1063+
break;
1064+
case 'updated':
1065+
if (event?.created?.has(path)) {
1066+
return;
1067+
}
1068+
removeEvent('deleted');
1069+
aggregateEvent();
1070+
break;
1071+
}
1072+
this.scheduleExecuteWatchChangeRequest();
1073+
}
1074+
1075+
private createFileSystemWatcher(
1076+
id: number,
1077+
pattern: vscode.RelativePattern,
1078+
ignoreChangeEvents?: boolean,
1079+
) {
1080+
const disposable = new DisposableStore();
1081+
const watcher = disposable.add(vscode.workspace.createFileSystemWatcher(pattern, { excludes: [] /* TODO:: need to fill in excludes list */, ignoreChangeEvents }));
1082+
disposable.add(watcher.onDidChange(changeFile =>
1083+
this.addWatchEvent(id, 'updated', changeFile.fsPath)
1084+
));
1085+
disposable.add(watcher.onDidCreate(createFile =>
1086+
this.addWatchEvent(id, 'created', createFile.fsPath)
1087+
));
1088+
disposable.add(watcher.onDidDelete(deletedFile =>
1089+
this.addWatchEvent(id, 'deleted', deletedFile.fsPath)
1090+
));
1091+
disposable.add({
1092+
dispose: () => {
1093+
this.watchEvents.delete(id);
1094+
this.watches.delete(id);
1095+
}
1096+
});
1097+
1098+
if (this.watches.has(id)) {
1099+
this.closeFileSystemWatcher(id);
1100+
}
1101+
this.watches.set(id, disposable);
1102+
}
1103+
1104+
private closeFileSystemWatcher(
1105+
id: number,
1106+
) {
1107+
const existing = this.watches.get(id);
1108+
if (existing) {
1109+
existing.dispose();
9761110
}
9771111
}
9781112

extensions/typescript-language-features/src/utils/dispose.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
import * as vscode from 'vscode';
77

88
export function disposeAll(disposables: vscode.Disposable[]) {
9-
while (disposables.length) {
10-
const item = disposables.pop();
11-
item?.dispose();
9+
for (const disposable of disposables) {
10+
disposable.dispose();
1211
}
12+
disposables.length = 0;
1313
}
1414

1515
export interface IDisposable {
@@ -42,3 +42,12 @@ export abstract class Disposable {
4242
return this._isDisposed;
4343
}
4444
}
45+
46+
export class DisposableStore extends Disposable {
47+
48+
public add<T extends IDisposable>(disposable: T): T {
49+
this._register(disposable);
50+
51+
return disposable;
52+
}
53+
}

0 commit comments

Comments
 (0)