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
11 changed files with 175 additions and 5 deletions

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;
}
}