Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use vscode watches for tsserver #193848

Merged
merged 28 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
dc7891d
Use vscode watches for tsserver
sheetalkamat Jun 15, 2023
3a9bcf6
Merge branch 'main' into canUseWatchEvents
sheetalkamat Oct 11, 2023
e1ea8a5
Merge branch 'main' into canUseWatchEvents
bpasero Oct 12, 2023
a936df4
towards using new proposed watch API
bpasero Oct 12, 2023
bf56b37
Add setting and make sure to clear watchers
mjbvz Oct 18, 2023
90373b6
Merge branch 'main' into canUseWatchEvents
sheetalkamat Oct 18, 2023
9dea1ae
Merge branch 'main' into canUseWatchEvents
bpasero Feb 27, 2024
77d693d
fix bad merge
bpasero Feb 27, 2024
6ffc534
fix bad merge
bpasero Feb 27, 2024
5b070e4
more disposables work
bpasero Feb 27, 2024
c2d1bbd
use relative pattern everywhere
bpasero Feb 27, 2024
a3b6e59
Merge branch 'main' into canUseWatchEvents
bpasero Mar 10, 2024
ed23de6
Merge branch 'main' into canUseWatchEvents
bpasero Mar 13, 2024
5042fc0
Merge branch 'main' into canUseWatchEvents
bpasero Mar 13, 2024
dc712dc
Merge branch 'main' into canUseWatchEvents
bpasero Mar 14, 2024
18aca9d
Merge branch 'main' into canUseWatchEvents
bpasero Mar 23, 2024
a37cfa4
Add ignoreUpdate
sheetalkamat Mar 26, 2024
7273ebb
Merge branch 'main' into canUseWatchEvents
bpasero Mar 27, 2024
24679a7
Protocol changes for tsserver to batch the notifications
sheetalkamat Mar 27, 2024
342f77c
Merge branch 'main' into canUseWatchEvents
bpasero Mar 28, 2024
04bb6f6
add event aggregation
bpasero Mar 28, 2024
5ffe264
Update version numbers
mjbvz Mar 28, 2024
56b4e35
Aggregate events over multiple watchers
sheetalkamat Apr 2, 2024
f6ba53d
Merge branch 'main' into canUseWatchEvents
bpasero Apr 7, 2024
a16c6aa
Merge branch 'main' into canUseWatchEvents
bpasero Apr 9, 2024
1cd941e
:lipstick:
bpasero Apr 9, 2024
0a0d36e
fix todo
bpasero Apr 9, 2024
0dbdd58
enable in workspace
bpasero Apr 9, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,4 +172,5 @@
"css.format.spaceAroundSelectorSeparator": true,
"inlineChat.mode": "live",
"typescript.enablePromptUseWorkspaceTsdk": true,
"typescript.tsserver.experimental.useVsCodeWatcher": true
}
9 changes: 9 additions & 0 deletions extensions/typescript-language-features/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"aiKey": "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255",
"enabledApiProposals": [
"workspaceTrust",
"createFileSystemWatcher",
"multiDocumentHighlightProvider",
"mappedEditsProvider",
"codeActionAI",
Expand Down Expand Up @@ -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%",
Expand Down
1 change: 1 addition & 0 deletions extensions/typescript-language-features/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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 ?? {}) };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ export enum EventName {
surveyReady = 'surveyReady',
projectLoadingStart = 'projectLoadingStart',
projectLoadingFinish = 'projectLoadingFinish',
createFileWatcher = 'createFileWatcher',
createDirectoryWatcher = 'createDirectoryWatcher',
closeFileWatcher = 'closeFileWatcher',
}

export enum OrganizeImportsMode {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ interface NoResponseTsServerRequests {
'compilerOptionsForInferredProjects': [Proto.SetCompilerOptionsForInferredProjectsArgs, null];
'reloadProjects': [null, null];
'configurePlugin': [Proto.ConfigurePluginRequest, Proto.ConfigurePluginResponse];
'watchChange': [Proto.Request, null];
}

interface AsyncTsServerRequests {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 {


Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -298,6 +308,8 @@ export default class TypeScriptServiceClient extends Disposable implements IType
}

this.loadingIndicator.reset();

this.resetWatchers();
}

public restartTsServer(fromUserAction = false): void {
Expand Down Expand Up @@ -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, {
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
};
bpasero marked this conversation as resolved.
Show resolved Hide resolved
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();
}
}

Expand Down
15 changes: 12 additions & 3 deletions extensions/typescript-language-features/src/utils/dispose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}
}
1 change: 1 addition & 0 deletions extensions/typescript-language-features/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading