Use vscode watches for tsserver (#193848)

This commit is contained in:
Sheetal Nandi 2024-04-09 04:39:34 -07:00 committed by GitHub
parent e36423a09c
commit b2c4302323
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 175 additions and 5 deletions

View file

@ -172,4 +172,5 @@
"css.format.spaceAroundSelectorSeparator": true,
"inlineChat.mode": "live",
"typescript.enablePromptUseWorkspaceTsdk": true,
"typescript.tsserver.experimental.useVsCodeWatcher": true
}

View file

@ -9,6 +9,7 @@
"aiKey": "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255",
"enabledApiProposals": [
"workspaceTrust",
"createFileSystemWatcher",
"multiDocumentHighlightProvider",
"mappedEditsProvider",
"codeActionAI",
@ -1168,6 +1169,14 @@
"experimental"
]
},
"typescript.tsserver.experimental.useVsCodeWatcher": {
"type": "boolean",
"description": "%configuration.tsserver.useVsCodeWatcher%",
"default": false,
"tags": [
"experimental"
]
},
"typescript.tsserver.watchOptions": {
"type": "object",
"description": "%configuration.tsserver.watchOptions%",

View file

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

View file

@ -117,6 +117,7 @@ export interface TypeScriptServiceConfiguration {
readonly enableProjectDiagnostics: boolean;
readonly maxTsServerMemory: number;
readonly enablePromptUseWorkspaceTsdk: boolean;
readonly useVsCodeWatcher: boolean;
readonly watchOptions: Proto.WatchOptions | undefined;
readonly includePackageJsonAutoImports: 'auto' | 'on' | 'off' | undefined;
readonly enableTsServerTracing: boolean;
@ -154,6 +155,7 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu
enableProjectDiagnostics: this.readEnableProjectDiagnostics(configuration),
maxTsServerMemory: this.readMaxTsServerMemory(configuration),
enablePromptUseWorkspaceTsdk: this.readEnablePromptUseWorkspaceTsdk(configuration),
useVsCodeWatcher: this.readUseVsCodeWatcher(configuration),
watchOptions: this.readWatchOptions(configuration),
includePackageJsonAutoImports: this.readIncludePackageJsonAutoImports(configuration),
enableTsServerTracing: this.readEnableTsServerTracing(configuration),
@ -222,7 +224,11 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu
return configuration.get<boolean>('typescript.tsserver.experimental.enableProjectDiagnostics', false);
}
protected readWatchOptions(configuration: vscode.WorkspaceConfiguration): Proto.WatchOptions | undefined {
private readUseVsCodeWatcher(configuration: vscode.WorkspaceConfiguration): boolean {
return configuration.get<boolean>('typescript.tsserver.experimental.useVsCodeWatcher', false);
}
private readWatchOptions(configuration: vscode.WorkspaceConfiguration): Proto.WatchOptions | undefined {
const watchOptions = configuration.get<Proto.WatchOptions>('typescript.tsserver.watchOptions');
// Returned value may be a proxy. Clone it into a normal object
return { ...(watchOptions ?? {}) };

View file

@ -35,6 +35,7 @@ export class API {
public static readonly v500 = API.fromSimpleString('5.0.0');
public static readonly v510 = API.fromSimpleString('5.1.0');
public static readonly v520 = API.fromSimpleString('5.2.0');
public static readonly v544 = API.fromSimpleString('5.4.4');
public static readonly v540 = API.fromSimpleString('5.4.0');
public static fromVersionString(versionString: string): API {

View file

@ -88,6 +88,9 @@ export enum EventName {
surveyReady = 'surveyReady',
projectLoadingStart = 'projectLoadingStart',
projectLoadingFinish = 'projectLoadingFinish',
createFileWatcher = 'createFileWatcher',
createDirectoryWatcher = 'createDirectoryWatcher',
closeFileWatcher = 'closeFileWatcher',
}
export enum OrganizeImportsMode {

View file

@ -271,6 +271,10 @@ export class TypeScriptServerSpawner {
args.push('--noGetErrOnBackgroundUpdate');
if (apiVersion.gte(API.v544) && configuration.useVsCodeWatcher) {
args.push('--canUseWatchEvents');
}
args.push('--validateDefaultNpmLocation');
if (isWebAndHasSharedArrayBuffers()) {

View file

@ -86,6 +86,7 @@ interface NoResponseTsServerRequests {
'compilerOptionsForInferredProjects': [Proto.SetCompilerOptionsForInferredProjectsArgs, null];
'reloadProjects': [null, null];
'configurePlugin': [Proto.ConfigurePluginRequest, Proto.ConfigurePluginResponse];
'watchChange': [Proto.Request, null];
}
interface AsyncTsServerRequests {

View file

@ -21,7 +21,7 @@ import { TypeScriptVersionManager } from './tsServer/versionManager';
import { ITypeScriptVersionProvider, TypeScriptVersion } from './tsServer/versionProvider';
import { ClientCapabilities, ClientCapability, ExecConfig, ITypeScriptServiceClient, ServerResponse, TypeScriptRequests } from './typescriptService';
import { ServiceConfigurationProvider, SyntaxServerConfiguration, TsServerLogLevel, TypeScriptServiceConfiguration, areServiceConfigurationsEqual } from './configuration/configuration';
import { Disposable } from './utils/dispose';
import { Disposable, DisposableStore, disposeAll } from './utils/dispose';
import * as fileSchemes from './configuration/fileSchemes';
import { Logger } from './logging/logger';
import { isWeb, isWebAndHasSharedArrayBuffers } from './utils/platform';
@ -97,6 +97,12 @@ export const emptyAuthority = 'ts-nul-authority';
export const inMemoryResourcePrefix = '^';
interface WatchEvent {
updated?: Set<string>;
created?: Set<string>;
deleted?: Set<string>;
}
export default class TypeScriptServiceClient extends Disposable implements ITypeScriptServiceClient {
@ -128,6 +134,10 @@ export default class TypeScriptServiceClient extends Disposable implements IType
private readonly versionProvider: ITypeScriptVersionProvider;
private readonly processFactory: TsServerProcessFactory;
private readonly watches = new Map<number, Disposable>();
private readonly watchEvents = new Map<number, WatchEvent>();
private watchChangeTimeout: NodeJS.Timeout | undefined;
constructor(
private readonly context: vscode.ExtensionContext,
onCaseInsenitiveFileSystem: boolean,
@ -298,6 +308,8 @@ export default class TypeScriptServiceClient extends Disposable implements IType
}
this.loadingIndicator.reset();
this.resetWatchers();
}
public restartTsServer(fromUserAction = false): void {
@ -401,6 +413,8 @@ export default class TypeScriptServiceClient extends Disposable implements IType
this.info(`Using Node installation from ${nodePath} to run TS Server`);
}
this.resetWatchers();
const apiVersion = version.apiVersion || API.defaultVersion;
const mytoken = ++this.token;
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
return this.serverState;
}
private resetWatchers() {
clearTimeout(this.watchChangeTimeout);
disposeAll(Array.from(this.watches.values()));
}
public async showVersionPicker(): Promise<void> {
this._versionManager.promptUserForVersion();
}
@ -594,6 +613,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType
}
private serviceExited(restart: boolean): void {
this.resetWatchers();
this.loadingIndicator.reset();
const previousState = this.serverState;
@ -973,6 +993,120 @@ export default class TypeScriptServiceClient extends Disposable implements IType
case EventName.projectLoadingFinish:
this.loadingIndicator.finishedLoadingProject((event as Proto.ProjectLoadingFinishEvent).body.projectName);
break;
case EventName.createDirectoryWatcher:
this.createFileSystemWatcher(
(event.body as Proto.CreateDirectoryWatcherEventBody).id,
new vscode.RelativePattern(
vscode.Uri.file((event.body as Proto.CreateDirectoryWatcherEventBody).path),
(event.body as Proto.CreateDirectoryWatcherEventBody).recursive ? '**' : '*'
),
(event.body as Proto.CreateDirectoryWatcherEventBody).ignoreUpdate
);
break;
case EventName.createFileWatcher:
this.createFileSystemWatcher(
(event.body as Proto.CreateFileWatcherEventBody).id,
new vscode.RelativePattern(
vscode.Uri.file((event.body as Proto.CreateFileWatcherEventBody).path),
'*'
)
);
break;
case EventName.closeFileWatcher:
this.closeFileSystemWatcher(event.body.id);
break;
}
}
private scheduleExecuteWatchChangeRequest() {
if (!this.watchChangeTimeout) {
this.watchChangeTimeout = setTimeout(() => {
this.watchChangeTimeout = undefined;
const allEvents = Array.from(this.watchEvents, ([id, event]) => ({
id,
updated: event.updated && Array.from(event.updated),
created: event.created && Array.from(event.created),
deleted: event.deleted && Array.from(event.deleted)
}));
this.watchEvents.clear();
this.executeWithoutWaitingForResponse('watchChange', allEvents);
}, 100); /* aggregate events over 100ms to reduce client<->server IPC overhead */
}
}
private addWatchEvent(id: number, eventType: keyof WatchEvent, path: string) {
let event = this.watchEvents.get(id);
const removeEvent = (typeOfEventToRemove: keyof WatchEvent) => {
if (event?.[typeOfEventToRemove]?.delete(path) && event[typeOfEventToRemove].size === 0) {
event[typeOfEventToRemove] = undefined;
}
};
const aggregateEvent = () => {
if (!event) {
this.watchEvents.set(id, event = {});
}
(event[eventType] ??= new Set()).add(path);
};
switch (eventType) {
case 'created':
removeEvent('deleted');
removeEvent('updated');
aggregateEvent();
break;
case 'deleted':
removeEvent('created');
removeEvent('updated');
aggregateEvent();
break;
case 'updated':
if (event?.created?.has(path)) {
return;
}
removeEvent('deleted');
aggregateEvent();
break;
}
this.scheduleExecuteWatchChangeRequest();
}
private createFileSystemWatcher(
id: number,
pattern: vscode.RelativePattern,
ignoreChangeEvents?: boolean,
) {
const disposable = new DisposableStore();
const watcher = disposable.add(vscode.workspace.createFileSystemWatcher(pattern, { excludes: [] /* TODO:: need to fill in excludes list */, ignoreChangeEvents }));
disposable.add(watcher.onDidChange(changeFile =>
this.addWatchEvent(id, 'updated', changeFile.fsPath)
));
disposable.add(watcher.onDidCreate(createFile =>
this.addWatchEvent(id, 'created', createFile.fsPath)
));
disposable.add(watcher.onDidDelete(deletedFile =>
this.addWatchEvent(id, 'deleted', deletedFile.fsPath)
));
disposable.add({
dispose: () => {
this.watchEvents.delete(id);
this.watches.delete(id);
}
});
if (this.watches.has(id)) {
this.closeFileSystemWatcher(id);
}
this.watches.set(id, disposable);
}
private closeFileSystemWatcher(
id: number,
) {
const existing = this.watches.get(id);
if (existing) {
existing.dispose();
}
}

View file

@ -6,10 +6,10 @@
import * as vscode from 'vscode';
export function disposeAll(disposables: vscode.Disposable[]) {
while (disposables.length) {
const item = disposables.pop();
item?.dispose();
for (const disposable of disposables) {
disposable.dispose();
}
disposables.length = 0;
}
export interface IDisposable {
@ -42,3 +42,12 @@ export abstract class Disposable {
return this._isDisposed;
}
}
export class DisposableStore extends Disposable {
public add<T extends IDisposable>(disposable: T): T {
this._register(disposable);
return disposable;
}
}

View file

@ -11,6 +11,7 @@
"include": [
"src/**/*",
"../../src/vscode-dts/vscode.d.ts",
"../../src/vscode-dts/vscode.proposed.createFileSystemWatcher.d.ts",
"../../src/vscode-dts/vscode.proposed.codeActionAI.d.ts",
"../../src/vscode-dts/vscode.proposed.codeActionRanges.d.ts",
"../../src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts",