Skip to content

Commit cff53f1

Browse files
committed
Watch using events
1 parent 972299a commit cff53f1

File tree

12 files changed

+1371
-29
lines changed

12 files changed

+1371
-29
lines changed

src/server/editorServices.ts

+137-3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
Diagnostic,
2828
directorySeparator,
2929
DirectoryStructureHost,
30+
DirectoryWatcherCallback,
3031
DocumentPosition,
3132
DocumentPositionMapper,
3233
DocumentRegistry,
@@ -37,6 +38,7 @@ import {
3738
FileExtensionInfo,
3839
fileExtensionIs,
3940
FileWatcher,
41+
FileWatcherCallback,
4042
FileWatcherEventKind,
4143
find,
4244
flatMap,
@@ -126,6 +128,7 @@ import {
126128
version,
127129
WatchDirectoryFlags,
128130
WatchFactory,
131+
WatchFactoryHost,
129132
WatchLogLevel,
130133
WatchOptions,
131134
WatchType,
@@ -192,6 +195,9 @@ export const ConfigFileDiagEvent = "configFileDiag";
192195
export const ProjectLanguageServiceStateEvent = "projectLanguageServiceState";
193196
export const ProjectInfoTelemetryEvent = "projectInfo";
194197
export const OpenFileInfoTelemetryEvent = "openFileInfo";
198+
export const CreateFileWatcherEvent: protocol.CreateFileWatcherEventName = "createFileWatcher";
199+
export const CreateDirectoryWatcherEvent: protocol.CreateDirectoryWatcherEventName = "createDirectoryWatcher";
200+
export const CloseFileWatcherEvent: protocol.CloseFileWatcherEventName = "closeFileWatcher";
195201
const ensureProjectForOpenFileSchedule = "*ensureProjectForOpenFiles*";
196202

197203
export interface ProjectsUpdatedInBackgroundEvent {
@@ -319,6 +325,21 @@ export interface OpenFileInfo {
319325
readonly checkJs: boolean;
320326
}
321327

328+
export interface CreateFileWatcherEvent {
329+
readonly eventName: protocol.CreateFileWatcherEventName;
330+
readonly data: protocol.CreateFileWatcherEventBody;
331+
}
332+
333+
export interface CreateDirectoryWatcherEvent {
334+
readonly eventName: protocol.CreateDirectoryWatcherEventName;
335+
readonly data: protocol.CreateDirectoryWatcherEventBody;
336+
}
337+
338+
export interface CloseFileWatcherEvent {
339+
readonly eventName: protocol.CloseFileWatcherEventName;
340+
readonly data: protocol.CloseFileWatcherEventBody;
341+
}
342+
322343
export type ProjectServiceEvent =
323344
LargeFileReferencedEvent
324345
| ProjectsUpdatedInBackgroundEvent
@@ -327,7 +348,10 @@ export type ProjectServiceEvent =
327348
| ConfigFileDiagEvent
328349
| ProjectLanguageServiceStateEvent
329350
| ProjectInfoTelemetryEvent
330-
| OpenFileInfoTelemetryEvent;
351+
| OpenFileInfoTelemetryEvent
352+
| CreateFileWatcherEvent
353+
| CreateDirectoryWatcherEvent
354+
| CloseFileWatcherEvent;
331355

332356
export type ProjectServiceEventHandler = (event: ProjectServiceEvent) => void;
333357

@@ -582,6 +606,7 @@ export interface ProjectServiceOptions {
582606
useInferredProjectPerProjectRoot: boolean;
583607
typingsInstaller?: ITypingsInstaller;
584608
eventHandler?: ProjectServiceEventHandler;
609+
canUseWatchEvents?: boolean;
585610
suppressDiagnosticEvents?: boolean;
586611
throttleWaitMilliseconds?: number;
587612
globalPlugins?: readonly string[];
@@ -852,6 +877,109 @@ function createProjectNameFactoryWithCounter(nameFactory: (counter: number) => s
852877
return () => nameFactory(nextId++);
853878
}
854879

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

857985
/** @internal */
@@ -958,7 +1086,8 @@ export class ProjectService {
9581086
public readonly typingsInstaller: ITypingsInstaller;
9591087
private readonly globalCacheLocationDirectoryPath: Path | undefined;
9601088
public readonly throttleWaitMilliseconds?: number;
961-
private readonly eventHandler?: ProjectServiceEventHandler;
1089+
/** @internal */
1090+
readonly eventHandler?: ProjectServiceEventHandler;
9621091
private readonly suppressDiagnosticEvents?: boolean;
9631092

9641093
public readonly globalPlugins: readonly string[];
@@ -1058,7 +1187,12 @@ export class ProjectService {
10581187
watchFile: returnNoopFileWatcher,
10591188
watchDirectory: returnNoopFileWatcher,
10601189
} :
1061-
getWatchFactory(this.host, watchLogLevel, log, getDetailWatchInfo);
1190+
getWatchFactory(
1191+
createWatchFactoryHostUsingWatchEvents(this, opts.canUseWatchEvents) || this.host,
1192+
watchLogLevel,
1193+
log,
1194+
getDetailWatchInfo,
1195+
);
10621196
opts.incrementalVerifier?.(this);
10631197
}
10641198

src/server/protocol.ts

+50-2
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,8 @@ export const enum CommandTypes {
172172
PrepareCallHierarchy = "prepareCallHierarchy",
173173
ProvideCallHierarchyIncomingCalls = "provideCallHierarchyIncomingCalls",
174174
ProvideCallHierarchyOutgoingCalls = "provideCallHierarchyOutgoingCalls",
175-
ProvideInlayHints = "provideInlayHints"
175+
ProvideInlayHints = "provideInlayHints",
176+
WatchChange = "watchChange",
176177
}
177178

178179
/**
@@ -1959,6 +1960,17 @@ export interface CloseRequest extends FileRequest {
19591960
command: CommandTypes.Close;
19601961
}
19611962

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

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

30333081
/**
30343082
* Arguments for reload request.

src/server/session.ts

+22-18
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,14 @@ import {
140140
WithMetadata,
141141
} from "./_namespaces/ts";
142142
import {
143+
CloseFileWatcherEvent,
143144
ConfigFileDiagEvent,
144145
ConfiguredProject,
145146
convertFormatOptions,
146147
convertScriptKindName,
147148
convertUserPreferences,
149+
CreateDirectoryWatcherEvent,
150+
CreateFileWatcherEvent,
148151
EmitResult,
149152
emptyArray,
150153
Errors,
@@ -935,6 +938,7 @@ export interface SessionOptions {
935938
* If falsy, all events are suppressed.
936939
*/
937940
canUseEvents: boolean;
941+
canUseWatchEvents?: boolean;
938942
eventHandler?: ProjectServiceEventHandler;
939943
/** Has no effect if eventHandler is also specified. */
940944
suppressDiagnosticEvents?: boolean;
@@ -1012,6 +1016,7 @@ export class Session<TMessage = string> implements EventSender {
10121016
typesMapLocation: opts.typesMapLocation,
10131017
serverMode: opts.serverMode,
10141018
session: this,
1019+
canUseWatchEvents: opts.canUseWatchEvents,
10151020
incrementalVerifier: opts.incrementalVerifier,
10161021
};
10171022
this.projectService = new ProjectService(settings);
@@ -1066,38 +1071,37 @@ export class Session<TMessage = string> implements EventSender {
10661071
private defaultEventHandler(event: ProjectServiceEvent) {
10671072
switch (event.eventName) {
10681073
case ProjectsUpdatedInBackgroundEvent:
1069-
const { openFiles } = event.data;
1070-
this.projectsUpdatedInBackgroundEvent(openFiles);
1074+
this.projectsUpdatedInBackgroundEvent(event.data.openFiles);
10711075
break;
10721076
case ProjectLoadingStartEvent:
1073-
const { project, reason } = event.data;
1074-
this.event<protocol.ProjectLoadingStartEventBody>(
1075-
{ projectName: project.getProjectName(), reason },
1076-
ProjectLoadingStartEvent);
1077+
this.event<protocol.ProjectLoadingStartEventBody>({
1078+
projectName: event.data.project.getProjectName(),
1079+
reason: event.data.reason
1080+
}, event.eventName);
10771081
break;
10781082
case ProjectLoadingFinishEvent:
1079-
const { project: finishProject } = event.data;
1080-
this.event<protocol.ProjectLoadingFinishEventBody>({ projectName: finishProject.getProjectName() }, ProjectLoadingFinishEvent);
1083+
this.event<protocol.ProjectLoadingFinishEventBody>({
1084+
projectName: event.data.project.getProjectName()
1085+
}, event.eventName);
10811086
break;
10821087
case LargeFileReferencedEvent:
1083-
const { file, fileSize, maxFileSize } = event.data;
1084-
this.event<protocol.LargeFileReferencedEventBody>({ file, fileSize, maxFileSize }, LargeFileReferencedEvent);
1088+
case CreateFileWatcherEvent:
1089+
case CreateDirectoryWatcherEvent:
1090+
case CloseFileWatcherEvent:
1091+
this.event(event.data, event.eventName);
10851092
break;
10861093
case ConfigFileDiagEvent:
1087-
const { triggerFile, configFileName: configFile, diagnostics } = event.data;
1088-
const bakedDiags = map(diagnostics, diagnostic => formatDiagnosticToProtocol(diagnostic, /*includeFileName*/ true));
10891094
this.event<protocol.ConfigFileDiagnosticEventBody>({
1090-
triggerFile,
1091-
configFile,
1092-
diagnostics: bakedDiags
1093-
}, ConfigFileDiagEvent);
1095+
triggerFile: event.data.triggerFile,
1096+
configFile: event.data.configFileName,
1097+
diagnostics: map(event.data.diagnostics, diagnostic => formatDiagnosticToProtocol(diagnostic, /*includeFileName*/ true))
1098+
}, event.eventName);
10941099
break;
10951100
case ProjectLanguageServiceStateEvent: {
1096-
const eventName: protocol.ProjectLanguageServiceStateEventName = ProjectLanguageServiceStateEvent;
10971101
this.event<protocol.ProjectLanguageServiceStateEventBody>({
10981102
projectName: event.data.project.getProjectName(),
10991103
languageServiceEnabled: event.data.languageServiceEnabled
1100-
}, eventName);
1104+
}, event.eventName);
11011105
break;
11021106
}
11031107
case ProjectInfoTelemetryEvent: {

src/testRunner/tests.ts

+1
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ import "./unittests/tsserver/events/largeFileReferenced";
146146
import "./unittests/tsserver/events/projectLanguageServiceState";
147147
import "./unittests/tsserver/events/projectLoading";
148148
import "./unittests/tsserver/events/projectUpdatedInBackground";
149+
import "./unittests/tsserver/events/watchEvents";
149150
import "./unittests/tsserver/exportMapCache";
150151
import "./unittests/tsserver/externalProjects";
151152
import "./unittests/tsserver/findAllReferences";

0 commit comments

Comments
 (0)