Skip to content

Commit

Permalink
Support using client watch in tsserver using events (#54662)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheetalkamat committed Sep 22, 2023
1 parent 4c96543 commit 934216f
Show file tree
Hide file tree
Showing 12 changed files with 1,372 additions and 28 deletions.
140 changes: 137 additions & 3 deletions src/server/editorServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
Diagnostic,
directorySeparator,
DirectoryStructureHost,
DirectoryWatcherCallback,
DocumentPosition,
DocumentPositionMapper,
DocumentRegistry,
Expand All @@ -37,6 +38,7 @@ import {
FileExtensionInfo,
fileExtensionIs,
FileWatcher,
FileWatcherCallback,
FileWatcherEventKind,
find,
flatMap,
Expand Down Expand Up @@ -127,6 +129,7 @@ import {
version,
WatchDirectoryFlags,
WatchFactory,
WatchFactoryHost,
WatchLogLevel,
WatchOptions,
WatchType,
Expand Down Expand Up @@ -193,6 +196,9 @@ export const ConfigFileDiagEvent = "configFileDiag";
export const ProjectLanguageServiceStateEvent = "projectLanguageServiceState";
export const ProjectInfoTelemetryEvent = "projectInfo";
export const OpenFileInfoTelemetryEvent = "openFileInfo";
export const CreateFileWatcherEvent: protocol.CreateFileWatcherEventName = "createFileWatcher";
export const CreateDirectoryWatcherEvent: protocol.CreateDirectoryWatcherEventName = "createDirectoryWatcher";
export const CloseFileWatcherEvent: protocol.CloseFileWatcherEventName = "closeFileWatcher";
const ensureProjectForOpenFileSchedule = "*ensureProjectForOpenFiles*";

export interface ProjectsUpdatedInBackgroundEvent {
Expand Down Expand Up @@ -320,6 +326,21 @@ export interface OpenFileInfo {
readonly checkJs: boolean;
}

export interface CreateFileWatcherEvent {
readonly eventName: protocol.CreateFileWatcherEventName;
readonly data: protocol.CreateFileWatcherEventBody;
}

export interface CreateDirectoryWatcherEvent {
readonly eventName: protocol.CreateDirectoryWatcherEventName;
readonly data: protocol.CreateDirectoryWatcherEventBody;
}

export interface CloseFileWatcherEvent {
readonly eventName: protocol.CloseFileWatcherEventName;
readonly data: protocol.CloseFileWatcherEventBody;
}

export type ProjectServiceEvent =
| LargeFileReferencedEvent
| ProjectsUpdatedInBackgroundEvent
Expand All @@ -328,7 +349,10 @@ export type ProjectServiceEvent =
| ConfigFileDiagEvent
| ProjectLanguageServiceStateEvent
| ProjectInfoTelemetryEvent
| OpenFileInfoTelemetryEvent;
| OpenFileInfoTelemetryEvent
| CreateFileWatcherEvent
| CreateDirectoryWatcherEvent
| CloseFileWatcherEvent;

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

Expand Down Expand Up @@ -583,6 +607,7 @@ export interface ProjectServiceOptions {
useInferredProjectPerProjectRoot: boolean;
typingsInstaller?: ITypingsInstaller;
eventHandler?: ProjectServiceEventHandler;
canUseWatchEvents?: boolean;
suppressDiagnosticEvents?: boolean;
throttleWaitMilliseconds?: number;
globalPlugins?: readonly string[];
Expand Down Expand Up @@ -857,6 +882,109 @@ function createProjectNameFactoryWithCounter(nameFactory: (counter: number) => s
return () => nameFactory(nextId++);
}

interface HostWatcherMap<T> {
idToCallbacks: Map<number, Set<T>>;
pathToId: Map<Path, number>;
}

function getHostWatcherMap<T>(): HostWatcherMap<T> {
return { idToCallbacks: new Map(), pathToId: new Map() };
}

function createWatchFactoryHostUsingWatchEvents(service: ProjectService, canUseWatchEvents: boolean | undefined): WatchFactoryHost | undefined {
if (!canUseWatchEvents || !service.eventHandler || !service.session) return undefined;
const watchedFiles = getHostWatcherMap<FileWatcherCallback>();
const watchedDirectories = getHostWatcherMap<DirectoryWatcherCallback>();
const watchedDirectoriesRecursive = getHostWatcherMap<DirectoryWatcherCallback>();
let ids = 1;
service.session.addProtocolHandler(protocol.CommandTypes.WatchChange, req => {
onWatchChange((req as protocol.WatchChangeRequest).arguments);
return { responseRequired: false };
});
return {
watchFile,
watchDirectory,
getCurrentDirectory: () => service.host.getCurrentDirectory(),
useCaseSensitiveFileNames: service.host.useCaseSensitiveFileNames,
};
function watchFile(path: string, callback: FileWatcherCallback): FileWatcher {
return getOrCreateFileWatcher(
watchedFiles,
path,
callback,
id => ({ eventName: CreateFileWatcherEvent, data: { id, path } }),
);
}
function watchDirectory(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher {
return getOrCreateFileWatcher(
recursive ? watchedDirectoriesRecursive : watchedDirectories,
path,
callback,
id => ({ eventName: CreateDirectoryWatcherEvent, data: { id, path, recursive: !!recursive } }),
);
}
function getOrCreateFileWatcher<T>(
{ pathToId, idToCallbacks }: HostWatcherMap<T>,
path: string,
callback: T,
event: (id: number) => CreateFileWatcherEvent | CreateDirectoryWatcherEvent,
) {
const key = service.toPath(path);
let id = pathToId.get(key);
if (!id) pathToId.set(key, id = ids++);
let callbacks = idToCallbacks.get(id);
if (!callbacks) {
idToCallbacks.set(id, callbacks = new Set());
// Add watcher
service.eventHandler!(event(id));
}
callbacks.add(callback);
return {
close() {
const callbacks = idToCallbacks.get(id!);
if (!callbacks?.delete(callback)) return;
if (callbacks.size) return;
idToCallbacks.delete(id!);
pathToId.delete(key);
service.eventHandler!({ eventName: CloseFileWatcherEvent, data: { id: id! } });
},
};
}
function onWatchChange({ id, path, eventType }: protocol.WatchChangeRequestArgs) {
// console.log(`typescript-vscode-watcher:: Invoke:: ${id}:: ${path}:: ${eventType}`);
onFileWatcherCallback(id, path, eventType);
onDirectoryWatcherCallback(watchedDirectories, id, path, eventType);
onDirectoryWatcherCallback(watchedDirectoriesRecursive, id, path, eventType);
}

function onFileWatcherCallback(
id: number,
eventPath: string,
eventType: "create" | "delete" | "update",
) {
watchedFiles.idToCallbacks.get(id)?.forEach(callback => {
const eventKind = eventType === "create" ?
FileWatcherEventKind.Created :
eventType === "delete" ?
FileWatcherEventKind.Deleted :
FileWatcherEventKind.Changed;
callback(eventPath, eventKind);
});
}

function onDirectoryWatcherCallback(
{ idToCallbacks }: HostWatcherMap<DirectoryWatcherCallback>,
id: number,
eventPath: string,
eventType: "create" | "delete" | "update",
) {
if (eventType === "update") return;
idToCallbacks.get(id)?.forEach(callback => {
callback(eventPath);
});
}
}

export class ProjectService {
/** @internal */
readonly typingsCache: TypingsCache;
Expand Down Expand Up @@ -961,7 +1089,8 @@ export class ProjectService {
public readonly typingsInstaller: ITypingsInstaller;
private readonly globalCacheLocationDirectoryPath: Path | undefined;
public readonly throttleWaitMilliseconds?: number;
private readonly eventHandler?: ProjectServiceEventHandler;
/** @internal */
readonly eventHandler?: ProjectServiceEventHandler;
private readonly suppressDiagnosticEvents?: boolean;

public readonly globalPlugins: readonly string[];
Expand Down Expand Up @@ -1065,7 +1194,12 @@ export class ProjectService {
watchFile: returnNoopFileWatcher,
watchDirectory: returnNoopFileWatcher,
} :
getWatchFactory(this.host, watchLogLevel, log, getDetailWatchInfo);
getWatchFactory(
createWatchFactoryHostUsingWatchEvents(this, opts.canUseWatchEvents) || this.host,
watchLogLevel,
log,
getDetailWatchInfo,
);
opts.incrementalVerifier?.(this);
}

Expand Down
50 changes: 49 additions & 1 deletion src/server/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ export const enum CommandTypes {
ProvideCallHierarchyIncomingCalls = "provideCallHierarchyIncomingCalls",
ProvideCallHierarchyOutgoingCalls = "provideCallHierarchyOutgoingCalls",
ProvideInlayHints = "provideInlayHints",
WatchChange = "watchChange",
}

/**
Expand Down Expand Up @@ -1956,6 +1957,17 @@ export interface CloseRequest extends FileRequest {
command: CommandTypes.Close;
}

export interface WatchChangeRequest extends Request {
command: CommandTypes.WatchChange;
arguments: WatchChangeRequestArgs;
}

export interface WatchChangeRequestArgs {
id: number;
path: string;
eventType: "create" | "delete" | "update";
}

/**
* Request to obtain the list of files that should be regenerated if target file is recompiled.
* NOTE: this us query-only operation and does not generate any output on disk.
Expand Down Expand Up @@ -3018,6 +3030,39 @@ export interface LargeFileReferencedEventBody {
maxFileSize: number;
}

export type CreateFileWatcherEventName = "createFileWatcher";
export interface CreateFileWatcherEvent extends Event {
readonly event: CreateFileWatcherEventName;
readonly body: CreateFileWatcherEventBody;
}

export interface CreateFileWatcherEventBody {
readonly id: number;
readonly path: string;
}

export type CreateDirectoryWatcherEventName = "createDirectoryWatcher";
export interface CreateDirectoryWatcherEvent extends Event {
readonly event: CreateDirectoryWatcherEventName;
readonly body: CreateDirectoryWatcherEventBody;
}

export interface CreateDirectoryWatcherEventBody {
readonly id: number;
readonly path: string;
readonly recursive: boolean;
}

export type CloseFileWatcherEventName = "closeFileWatcher";
export interface CloseFileWatcherEvent extends Event {
readonly event: CloseFileWatcherEventName;
readonly body: CloseFileWatcherEventBody;
}

export interface CloseFileWatcherEventBody {
readonly id: number;
}

/** @internal */
export type AnyEvent =
| RequestCompletedEvent
Expand All @@ -3029,7 +3074,10 @@ export type AnyEvent =
| ProjectLoadingStartEvent
| ProjectLoadingFinishEvent
| SurveyReadyEvent
| LargeFileReferencedEvent;
| LargeFileReferencedEvent
| CreateFileWatcherEvent
| CreateDirectoryWatcherEvent
| CloseFileWatcherEvent;

/**
* Arguments for reload request.
Expand Down
41 changes: 22 additions & 19 deletions src/server/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,14 @@ import {
WithMetadata,
} from "./_namespaces/ts";
import {
CloseFileWatcherEvent,
ConfigFileDiagEvent,
ConfiguredProject,
convertFormatOptions,
convertScriptKindName,
convertUserPreferences,
CreateDirectoryWatcherEvent,
CreateFileWatcherEvent,
EmitResult,
emptyArray,
Errors,
Expand Down Expand Up @@ -949,6 +952,7 @@ export interface SessionOptions {
* If falsy, all events are suppressed.
*/
canUseEvents: boolean;
canUseWatchEvents?: boolean;
eventHandler?: ProjectServiceEventHandler;
/** Has no effect if eventHandler is also specified. */
suppressDiagnosticEvents?: boolean;
Expand Down Expand Up @@ -1026,6 +1030,7 @@ export class Session<TMessage = string> implements EventSender {
typesMapLocation: opts.typesMapLocation,
serverMode: opts.serverMode,
session: this,
canUseWatchEvents: opts.canUseWatchEvents,
incrementalVerifier: opts.incrementalVerifier,
};
this.projectService = new ProjectService(settings);
Expand Down Expand Up @@ -1080,39 +1085,37 @@ export class Session<TMessage = string> implements EventSender {
private defaultEventHandler(event: ProjectServiceEvent) {
switch (event.eventName) {
case ProjectsUpdatedInBackgroundEvent:
const { openFiles } = event.data;
this.projectsUpdatedInBackgroundEvent(openFiles);
this.projectsUpdatedInBackgroundEvent(event.data.openFiles);
break;
case ProjectLoadingStartEvent:
const { project, reason } = event.data;
this.event<protocol.ProjectLoadingStartEventBody>(
{ projectName: project.getProjectName(), reason },
ProjectLoadingStartEvent,
);
this.event<protocol.ProjectLoadingStartEventBody>({
projectName: event.data.project.getProjectName(),
reason: event.data.reason,
}, event.eventName);
break;
case ProjectLoadingFinishEvent:
const { project: finishProject } = event.data;
this.event<protocol.ProjectLoadingFinishEventBody>({ projectName: finishProject.getProjectName() }, ProjectLoadingFinishEvent);
this.event<protocol.ProjectLoadingFinishEventBody>({
projectName: event.data.project.getProjectName(),
}, event.eventName);
break;
case LargeFileReferencedEvent:
const { file, fileSize, maxFileSize } = event.data;
this.event<protocol.LargeFileReferencedEventBody>({ file, fileSize, maxFileSize }, LargeFileReferencedEvent);
case CreateFileWatcherEvent:
case CreateDirectoryWatcherEvent:
case CloseFileWatcherEvent:
this.event(event.data, event.eventName);
break;
case ConfigFileDiagEvent:
const { triggerFile, configFileName: configFile, diagnostics } = event.data;
const bakedDiags = map(diagnostics, diagnostic => formatDiagnosticToProtocol(diagnostic, /*includeFileName*/ true));
this.event<protocol.ConfigFileDiagnosticEventBody>({
triggerFile,
configFile,
diagnostics: bakedDiags,
}, ConfigFileDiagEvent);
triggerFile: event.data.triggerFile,
configFile: event.data.configFileName,
diagnostics: map(event.data.diagnostics, diagnostic => formatDiagnosticToProtocol(diagnostic, /*includeFileName*/ true)),
}, event.eventName);
break;
case ProjectLanguageServiceStateEvent: {
const eventName: protocol.ProjectLanguageServiceStateEventName = ProjectLanguageServiceStateEvent;
this.event<protocol.ProjectLanguageServiceStateEventBody>({
projectName: event.data.project.getProjectName(),
languageServiceEnabled: event.data.languageServiceEnabled,
}, eventName);
}, event.eventName);
break;
}
case ProjectInfoTelemetryEvent: {
Expand Down
1 change: 1 addition & 0 deletions src/testRunner/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ import "./unittests/tsserver/events/largeFileReferenced";
import "./unittests/tsserver/events/projectLanguageServiceState";
import "./unittests/tsserver/events/projectLoading";
import "./unittests/tsserver/events/projectUpdatedInBackground";
import "./unittests/tsserver/events/watchEvents";
import "./unittests/tsserver/exportMapCache";
import "./unittests/tsserver/extends";
import "./unittests/tsserver/externalProjects";
Expand Down
Loading

0 comments on commit 934216f

Please sign in to comment.