Skip to content

Commit ef603d5

Browse files
sheetalkamatsnovader
authored andcommitted
Support using client watch in tsserver using events (microsoft#54662)
1 parent cd3bb39 commit ef603d5

File tree

12 files changed

+1372
-28
lines changed

12 files changed

+1372
-28
lines changed

src/server/editorServices.ts

Lines changed: 137 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
Diagnostic,
2929
directorySeparator,
3030
DirectoryStructureHost,
31+
DirectoryWatcherCallback,
3132
DocumentPosition,
3233
DocumentPositionMapper,
3334
DocumentRegistry,
@@ -38,6 +39,7 @@ import {
3839
FileExtensionInfo,
3940
fileExtensionIs,
4041
FileWatcher,
42+
FileWatcherCallback,
4143
FileWatcherEventKind,
4244
find,
4345
flatMap,
@@ -128,6 +130,7 @@ import {
128130
version,
129131
WatchDirectoryFlags,
130132
WatchFactory,
133+
WatchFactoryHost,
131134
WatchLogLevel,
132135
WatchOptions,
133136
WatchType,
@@ -194,6 +197,9 @@ export const ConfigFileDiagEvent = "configFileDiag";
194197
export const ProjectLanguageServiceStateEvent = "projectLanguageServiceState";
195198
export const ProjectInfoTelemetryEvent = "projectInfo";
196199
export const OpenFileInfoTelemetryEvent = "openFileInfo";
200+
export const CreateFileWatcherEvent: protocol.CreateFileWatcherEventName = "createFileWatcher";
201+
export const CreateDirectoryWatcherEvent: protocol.CreateDirectoryWatcherEventName = "createDirectoryWatcher";
202+
export const CloseFileWatcherEvent: protocol.CloseFileWatcherEventName = "closeFileWatcher";
197203
const ensureProjectForOpenFileSchedule = "*ensureProjectForOpenFiles*";
198204

199205
export interface ProjectsUpdatedInBackgroundEvent {
@@ -321,6 +327,21 @@ export interface OpenFileInfo {
321327
readonly checkJs: boolean;
322328
}
323329

330+
export interface CreateFileWatcherEvent {
331+
readonly eventName: protocol.CreateFileWatcherEventName;
332+
readonly data: protocol.CreateFileWatcherEventBody;
333+
}
334+
335+
export interface CreateDirectoryWatcherEvent {
336+
readonly eventName: protocol.CreateDirectoryWatcherEventName;
337+
readonly data: protocol.CreateDirectoryWatcherEventBody;
338+
}
339+
340+
export interface CloseFileWatcherEvent {
341+
readonly eventName: protocol.CloseFileWatcherEventName;
342+
readonly data: protocol.CloseFileWatcherEventBody;
343+
}
344+
324345
export type ProjectServiceEvent =
325346
| LargeFileReferencedEvent
326347
| ProjectsUpdatedInBackgroundEvent
@@ -329,7 +350,10 @@ export type ProjectServiceEvent =
329350
| ConfigFileDiagEvent
330351
| ProjectLanguageServiceStateEvent
331352
| ProjectInfoTelemetryEvent
332-
| OpenFileInfoTelemetryEvent;
353+
| OpenFileInfoTelemetryEvent
354+
| CreateFileWatcherEvent
355+
| CreateDirectoryWatcherEvent
356+
| CloseFileWatcherEvent;
333357

334358
export type ProjectServiceEventHandler = (event: ProjectServiceEvent) => void;
335359

@@ -584,6 +608,7 @@ export interface ProjectServiceOptions {
584608
useInferredProjectPerProjectRoot: boolean;
585609
typingsInstaller?: ITypingsInstaller;
586610
eventHandler?: ProjectServiceEventHandler;
611+
canUseWatchEvents?: boolean;
587612
suppressDiagnosticEvents?: boolean;
588613
throttleWaitMilliseconds?: number;
589614
globalPlugins?: readonly string[];
@@ -858,6 +883,109 @@ function createProjectNameFactoryWithCounter(nameFactory: (counter: number) => s
858883
return () => nameFactory(nextId++);
859884
}
860885

886+
interface HostWatcherMap<T> {
887+
idToCallbacks: Map<number, Set<T>>;
888+
pathToId: Map<Path, number>;
889+
}
890+
891+
function getHostWatcherMap<T>(): HostWatcherMap<T> {
892+
return { idToCallbacks: new Map(), pathToId: new Map() };
893+
}
894+
895+
function createWatchFactoryHostUsingWatchEvents(service: ProjectService, canUseWatchEvents: boolean | undefined): WatchFactoryHost | undefined {
896+
if (!canUseWatchEvents || !service.eventHandler || !service.session) return undefined;
897+
const watchedFiles = getHostWatcherMap<FileWatcherCallback>();
898+
const watchedDirectories = getHostWatcherMap<DirectoryWatcherCallback>();
899+
const watchedDirectoriesRecursive = getHostWatcherMap<DirectoryWatcherCallback>();
900+
let ids = 1;
901+
service.session.addProtocolHandler(protocol.CommandTypes.WatchChange, req => {
902+
onWatchChange((req as protocol.WatchChangeRequest).arguments);
903+
return { responseRequired: false };
904+
});
905+
return {
906+
watchFile,
907+
watchDirectory,
908+
getCurrentDirectory: () => service.host.getCurrentDirectory(),
909+
useCaseSensitiveFileNames: service.host.useCaseSensitiveFileNames,
910+
};
911+
function watchFile(path: string, callback: FileWatcherCallback): FileWatcher {
912+
return getOrCreateFileWatcher(
913+
watchedFiles,
914+
path,
915+
callback,
916+
id => ({ eventName: CreateFileWatcherEvent, data: { id, path } }),
917+
);
918+
}
919+
function watchDirectory(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher {
920+
return getOrCreateFileWatcher(
921+
recursive ? watchedDirectoriesRecursive : watchedDirectories,
922+
path,
923+
callback,
924+
id => ({ eventName: CreateDirectoryWatcherEvent, data: { id, path, recursive: !!recursive } }),
925+
);
926+
}
927+
function getOrCreateFileWatcher<T>(
928+
{ pathToId, idToCallbacks }: HostWatcherMap<T>,
929+
path: string,
930+
callback: T,
931+
event: (id: number) => CreateFileWatcherEvent | CreateDirectoryWatcherEvent,
932+
) {
933+
const key = service.toPath(path);
934+
let id = pathToId.get(key);
935+
if (!id) pathToId.set(key, id = ids++);
936+
let callbacks = idToCallbacks.get(id);
937+
if (!callbacks) {
938+
idToCallbacks.set(id, callbacks = new Set());
939+
// Add watcher
940+
service.eventHandler!(event(id));
941+
}
942+
callbacks.add(callback);
943+
return {
944+
close() {
945+
const callbacks = idToCallbacks.get(id!);
946+
if (!callbacks?.delete(callback)) return;
947+
if (callbacks.size) return;
948+
idToCallbacks.delete(id!);
949+
pathToId.delete(key);
950+
service.eventHandler!({ eventName: CloseFileWatcherEvent, data: { id: id! } });
951+
},
952+
};
953+
}
954+
function onWatchChange({ id, path, eventType }: protocol.WatchChangeRequestArgs) {
955+
// console.log(`typescript-vscode-watcher:: Invoke:: ${id}:: ${path}:: ${eventType}`);
956+
onFileWatcherCallback(id, path, eventType);
957+
onDirectoryWatcherCallback(watchedDirectories, id, path, eventType);
958+
onDirectoryWatcherCallback(watchedDirectoriesRecursive, id, path, eventType);
959+
}
960+
961+
function onFileWatcherCallback(
962+
id: number,
963+
eventPath: string,
964+
eventType: "create" | "delete" | "update",
965+
) {
966+
watchedFiles.idToCallbacks.get(id)?.forEach(callback => {
967+
const eventKind = eventType === "create" ?
968+
FileWatcherEventKind.Created :
969+
eventType === "delete" ?
970+
FileWatcherEventKind.Deleted :
971+
FileWatcherEventKind.Changed;
972+
callback(eventPath, eventKind);
973+
});
974+
}
975+
976+
function onDirectoryWatcherCallback(
977+
{ idToCallbacks }: HostWatcherMap<DirectoryWatcherCallback>,
978+
id: number,
979+
eventPath: string,
980+
eventType: "create" | "delete" | "update",
981+
) {
982+
if (eventType === "update") return;
983+
idToCallbacks.get(id)?.forEach(callback => {
984+
callback(eventPath);
985+
});
986+
}
987+
}
988+
861989
export class ProjectService {
862990
/** @internal */
863991
readonly typingsCache: TypingsCache;
@@ -962,7 +1090,8 @@ export class ProjectService {
9621090
public readonly typingsInstaller: ITypingsInstaller;
9631091
private readonly globalCacheLocationDirectoryPath: Path | undefined;
9641092
public readonly throttleWaitMilliseconds?: number;
965-
private readonly eventHandler?: ProjectServiceEventHandler;
1093+
/** @internal */
1094+
readonly eventHandler?: ProjectServiceEventHandler;
9661095
private readonly suppressDiagnosticEvents?: boolean;
9671096

9681097
public readonly globalPlugins: readonly string[];
@@ -1068,7 +1197,12 @@ export class ProjectService {
10681197
watchFile: returnNoopFileWatcher,
10691198
watchDirectory: returnNoopFileWatcher,
10701199
} :
1071-
getWatchFactory(this.host, watchLogLevel, log, getDetailWatchInfo);
1200+
getWatchFactory(
1201+
createWatchFactoryHostUsingWatchEvents(this, opts.canUseWatchEvents) || this.host,
1202+
watchLogLevel,
1203+
log,
1204+
getDetailWatchInfo,
1205+
);
10721206

10731207
this.pnpWatcher = this.watchPnpFile();
10741208
opts.incrementalVerifier?.(this);

src/server/protocol.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ export const enum CommandTypes {
173173
ProvideCallHierarchyIncomingCalls = "provideCallHierarchyIncomingCalls",
174174
ProvideCallHierarchyOutgoingCalls = "provideCallHierarchyOutgoingCalls",
175175
ProvideInlayHints = "provideInlayHints",
176+
WatchChange = "watchChange",
176177
}
177178

178179
/**
@@ -1956,6 +1957,17 @@ export interface CloseRequest extends FileRequest {
19561957
command: CommandTypes.Close;
19571958
}
19581959

1960+
export interface WatchChangeRequest extends Request {
1961+
command: CommandTypes.WatchChange;
1962+
arguments: WatchChangeRequestArgs;
1963+
}
1964+
1965+
export interface WatchChangeRequestArgs {
1966+
id: number;
1967+
path: string;
1968+
eventType: "create" | "delete" | "update";
1969+
}
1970+
19591971
/**
19601972
* Request to obtain the list of files that should be regenerated if target file is recompiled.
19611973
* NOTE: this us query-only operation and does not generate any output on disk.
@@ -3018,6 +3030,39 @@ export interface LargeFileReferencedEventBody {
30183030
maxFileSize: number;
30193031
}
30203032

3033+
export type CreateFileWatcherEventName = "createFileWatcher";
3034+
export interface CreateFileWatcherEvent extends Event {
3035+
readonly event: CreateFileWatcherEventName;
3036+
readonly body: CreateFileWatcherEventBody;
3037+
}
3038+
3039+
export interface CreateFileWatcherEventBody {
3040+
readonly id: number;
3041+
readonly path: string;
3042+
}
3043+
3044+
export type CreateDirectoryWatcherEventName = "createDirectoryWatcher";
3045+
export interface CreateDirectoryWatcherEvent extends Event {
3046+
readonly event: CreateDirectoryWatcherEventName;
3047+
readonly body: CreateDirectoryWatcherEventBody;
3048+
}
3049+
3050+
export interface CreateDirectoryWatcherEventBody {
3051+
readonly id: number;
3052+
readonly path: string;
3053+
readonly recursive: boolean;
3054+
}
3055+
3056+
export type CloseFileWatcherEventName = "closeFileWatcher";
3057+
export interface CloseFileWatcherEvent extends Event {
3058+
readonly event: CloseFileWatcherEventName;
3059+
readonly body: CloseFileWatcherEventBody;
3060+
}
3061+
3062+
export interface CloseFileWatcherEventBody {
3063+
readonly id: number;
3064+
}
3065+
30213066
/** @internal */
30223067
export type AnyEvent =
30233068
| RequestCompletedEvent
@@ -3029,7 +3074,10 @@ export type AnyEvent =
30293074
| ProjectLoadingStartEvent
30303075
| ProjectLoadingFinishEvent
30313076
| SurveyReadyEvent
3032-
| LargeFileReferencedEvent;
3077+
| LargeFileReferencedEvent
3078+
| CreateFileWatcherEvent
3079+
| CreateDirectoryWatcherEvent
3080+
| CloseFileWatcherEvent;
30333081

30343082
/**
30353083
* Arguments for reload request.

src/server/session.ts

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -139,11 +139,14 @@ import {
139139
WithMetadata,
140140
} from "./_namespaces/ts";
141141
import {
142+
CloseFileWatcherEvent,
142143
ConfigFileDiagEvent,
143144
ConfiguredProject,
144145
convertFormatOptions,
145146
convertScriptKindName,
146147
convertUserPreferences,
148+
CreateDirectoryWatcherEvent,
149+
CreateFileWatcherEvent,
147150
EmitResult,
148151
emptyArray,
149152
Errors,
@@ -949,6 +952,7 @@ export interface SessionOptions {
949952
* If falsy, all events are suppressed.
950953
*/
951954
canUseEvents: boolean;
955+
canUseWatchEvents?: boolean;
952956
eventHandler?: ProjectServiceEventHandler;
953957
/** Has no effect if eventHandler is also specified. */
954958
suppressDiagnosticEvents?: boolean;
@@ -1026,6 +1030,7 @@ export class Session<TMessage = string> implements EventSender {
10261030
typesMapLocation: opts.typesMapLocation,
10271031
serverMode: opts.serverMode,
10281032
session: this,
1033+
canUseWatchEvents: opts.canUseWatchEvents,
10291034
incrementalVerifier: opts.incrementalVerifier,
10301035
};
10311036
this.projectService = new ProjectService(settings);
@@ -1080,39 +1085,37 @@ export class Session<TMessage = string> implements EventSender {
10801085
private defaultEventHandler(event: ProjectServiceEvent) {
10811086
switch (event.eventName) {
10821087
case ProjectsUpdatedInBackgroundEvent:
1083-
const { openFiles } = event.data;
1084-
this.projectsUpdatedInBackgroundEvent(openFiles);
1088+
this.projectsUpdatedInBackgroundEvent(event.data.openFiles);
10851089
break;
10861090
case ProjectLoadingStartEvent:
1087-
const { project, reason } = event.data;
1088-
this.event<protocol.ProjectLoadingStartEventBody>(
1089-
{ projectName: project.getProjectName(), reason },
1090-
ProjectLoadingStartEvent,
1091-
);
1091+
this.event<protocol.ProjectLoadingStartEventBody>({
1092+
projectName: event.data.project.getProjectName(),
1093+
reason: event.data.reason,
1094+
}, event.eventName);
10921095
break;
10931096
case ProjectLoadingFinishEvent:
1094-
const { project: finishProject } = event.data;
1095-
this.event<protocol.ProjectLoadingFinishEventBody>({ projectName: finishProject.getProjectName() }, ProjectLoadingFinishEvent);
1097+
this.event<protocol.ProjectLoadingFinishEventBody>({
1098+
projectName: event.data.project.getProjectName(),
1099+
}, event.eventName);
10961100
break;
10971101
case LargeFileReferencedEvent:
1098-
const { file, fileSize, maxFileSize } = event.data;
1099-
this.event<protocol.LargeFileReferencedEventBody>({ file, fileSize, maxFileSize }, LargeFileReferencedEvent);
1102+
case CreateFileWatcherEvent:
1103+
case CreateDirectoryWatcherEvent:
1104+
case CloseFileWatcherEvent:
1105+
this.event(event.data, event.eventName);
11001106
break;
11011107
case ConfigFileDiagEvent:
1102-
const { triggerFile, configFileName: configFile, diagnostics } = event.data;
1103-
const bakedDiags = map(diagnostics, diagnostic => formatDiagnosticToProtocol(diagnostic, /*includeFileName*/ true));
11041108
this.event<protocol.ConfigFileDiagnosticEventBody>({
1105-
triggerFile,
1106-
configFile,
1107-
diagnostics: bakedDiags,
1108-
}, ConfigFileDiagEvent);
1109+
triggerFile: event.data.triggerFile,
1110+
configFile: event.data.configFileName,
1111+
diagnostics: map(event.data.diagnostics, diagnostic => formatDiagnosticToProtocol(diagnostic, /*includeFileName*/ true)),
1112+
}, event.eventName);
11091113
break;
11101114
case ProjectLanguageServiceStateEvent: {
1111-
const eventName: protocol.ProjectLanguageServiceStateEventName = ProjectLanguageServiceStateEvent;
11121115
this.event<protocol.ProjectLanguageServiceStateEventBody>({
11131116
projectName: event.data.project.getProjectName(),
11141117
languageServiceEnabled: event.data.languageServiceEnabled,
1115-
}, eventName);
1118+
}, event.eventName);
11161119
break;
11171120
}
11181121
case ProjectInfoTelemetryEvent: {

src/testRunner/tests.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ import "./unittests/tsserver/events/largeFileReferenced";
153153
import "./unittests/tsserver/events/projectLanguageServiceState";
154154
import "./unittests/tsserver/events/projectLoading";
155155
import "./unittests/tsserver/events/projectUpdatedInBackground";
156+
import "./unittests/tsserver/events/watchEvents";
156157
import "./unittests/tsserver/exportMapCache";
157158
import "./unittests/tsserver/extends";
158159
import "./unittests/tsserver/externalProjects";

0 commit comments

Comments
 (0)