Skip to content

Commit 521d3fb

Browse files
TypeScript Botsheetalkamat
TypeScript Bot
andauthored
🤖 Pick PR #57950 (Watch events enhancements) into release-5.4 (#57967)
Co-authored-by: Sheetal Nandi <[email protected]>
1 parent 6ea273c commit 521d3fb

File tree

6 files changed

+1190
-133
lines changed

6 files changed

+1190
-133
lines changed

src/server/editorServices.ts

+30-26
Original file line numberDiff line numberDiff line change
@@ -933,7 +933,16 @@ function createWatchFactoryHostUsingWatchEvents(service: ProjectService, canUseW
933933
recursive ? watchedDirectoriesRecursive : watchedDirectories,
934934
path,
935935
callback,
936-
id => ({ eventName: CreateDirectoryWatcherEvent, data: { id, path, recursive: !!recursive } }),
936+
id => ({
937+
eventName: CreateDirectoryWatcherEvent,
938+
data: {
939+
id,
940+
path,
941+
recursive: !!recursive,
942+
// Special case node_modules as we watch it for changes to closed script infos as well
943+
ignoreUpdate: !path.endsWith("/node_modules") ? true : undefined,
944+
},
945+
}),
937946
);
938947
}
939948
function getOrCreateFileWatcher<T>(
@@ -963,37 +972,32 @@ function createWatchFactoryHostUsingWatchEvents(service: ProjectService, canUseW
963972
},
964973
};
965974
}
966-
function onWatchChange({ id, path, eventType }: protocol.WatchChangeRequestArgs) {
967-
// console.log(`typescript-vscode-watcher:: Invoke:: ${id}:: ${path}:: ${eventType}`);
968-
onFileWatcherCallback(id, path, eventType);
969-
onDirectoryWatcherCallback(watchedDirectories, id, path, eventType);
970-
onDirectoryWatcherCallback(watchedDirectoriesRecursive, id, path, eventType);
975+
function onWatchChange(args: protocol.WatchChangeRequestArgs | readonly protocol.WatchChangeRequestArgs[]) {
976+
if (isArray(args)) args.forEach(onWatchChangeRequestArgs);
977+
else onWatchChangeRequestArgs(args);
971978
}
972979

973-
function onFileWatcherCallback(
974-
id: number,
975-
eventPath: string,
976-
eventType: "create" | "delete" | "update",
977-
) {
978-
watchedFiles.idToCallbacks.get(id)?.forEach(callback => {
979-
const eventKind = eventType === "create" ?
980-
FileWatcherEventKind.Created :
981-
eventType === "delete" ?
982-
FileWatcherEventKind.Deleted :
983-
FileWatcherEventKind.Changed;
984-
callback(eventPath, eventKind);
985-
});
980+
function onWatchChangeRequestArgs({ id, created, deleted, updated }: protocol.WatchChangeRequestArgs) {
981+
onWatchEventType(id, created, FileWatcherEventKind.Created);
982+
onWatchEventType(id, deleted, FileWatcherEventKind.Deleted);
983+
onWatchEventType(id, updated, FileWatcherEventKind.Changed);
984+
}
985+
986+
function onWatchEventType(id: number, paths: readonly string[] | undefined, eventKind: FileWatcherEventKind) {
987+
if (!paths?.length) return;
988+
forEachCallback(watchedFiles, id, paths, (callback, eventPath) => callback(eventPath, eventKind));
989+
forEachCallback(watchedDirectories, id, paths, (callback, eventPath) => callback(eventPath));
990+
forEachCallback(watchedDirectoriesRecursive, id, paths, (callback, eventPath) => callback(eventPath));
986991
}
987992

988-
function onDirectoryWatcherCallback(
989-
{ idToCallbacks }: HostWatcherMap<DirectoryWatcherCallback>,
993+
function forEachCallback<T>(
994+
hostWatcherMap: HostWatcherMap<T>,
990995
id: number,
991-
eventPath: string,
992-
eventType: "create" | "delete" | "update",
996+
eventPaths: readonly string[],
997+
cb: (callback: T, eventPath: string) => void,
993998
) {
994-
if (eventType === "update") return;
995-
idToCallbacks.get(id)?.forEach(callback => {
996-
callback(eventPath);
999+
hostWatcherMap.idToCallbacks.get(id)?.forEach(callback => {
1000+
eventPaths.forEach(eventPath => cb(callback, eventPath));
9971001
});
9981002
}
9991003
}

src/server/protocol.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -1960,13 +1960,13 @@ export interface CloseRequest extends FileRequest {
19601960

19611961
export interface WatchChangeRequest extends Request {
19621962
command: CommandTypes.WatchChange;
1963-
arguments: WatchChangeRequestArgs;
1963+
arguments: WatchChangeRequestArgs | readonly WatchChangeRequestArgs[];
19641964
}
1965-
19661965
export interface WatchChangeRequestArgs {
19671966
id: number;
1968-
path: string;
1969-
eventType: "create" | "delete" | "update";
1967+
created?: string[];
1968+
deleted?: string[];
1969+
updated?: string[];
19701970
}
19711971

19721972
/**
@@ -3052,6 +3052,7 @@ export interface CreateDirectoryWatcherEventBody {
30523052
readonly id: number;
30533053
readonly path: string;
30543054
readonly recursive: boolean;
3055+
readonly ignoreUpdate?: boolean;
30553056
}
30563057

30573058
export type CloseFileWatcherEventName = "closeFileWatcher";

src/testRunner/unittests/tsserver/events/watchEvents.ts

+148-24
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
} from "../../../../harness/tsserverLogger";
55
import {
66
createWatchUtils,
7+
Watches,
78
WatchUtils,
89
} from "../../../../harness/watchUtils";
910
import * as ts from "../../../_namespaces/ts";
@@ -60,11 +61,11 @@ describe("unittests:: tsserver:: events:: watchEvents", () => {
6061
}
6162

6263
function watchDirectory(data: ts.server.protocol.CreateDirectoryWatcherEventBody) {
63-
logger.log(`Custom watchDirectory: ${data.id}: ${data.path} ${data.recursive}`);
64+
logger.log(`Custom watchDirectory: ${data.id}: ${data.path} ${data.recursive} ${data.ignoreUpdate}`);
6465
ts.Debug.assert(!idToClose.has(data.id));
6566
const result = host.factoryData.watchUtils.fsWatch(data.path, data.recursive, data);
6667
idToClose.set(data.id, () => {
67-
logger.log(`Custom watchDirectory:: Close:: ${data.id}: ${data.path} ${data.recursive}`);
68+
logger.log(`Custom watchDirectory:: Close:: ${data.id}: ${data.path} ${data.recursive} ${data.ignoreUpdate}`);
6869
result.close();
6970
});
7071
}
@@ -89,37 +90,132 @@ describe("unittests:: tsserver:: events:: watchEvents", () => {
8990
}
9091
}
9192

92-
function updateFileOnHost(session: TestSession, file: string, log: string) {
93+
function updateFileOnHost(session: TestSession, file: string, log: string, content?: string) {
9394
// Change b.ts
94-
session.logger.log(log);
95-
session.host.writeFile(file, session.host.readFile("/user/username/projects/myproject/a.ts")!);
95+
session.logger.log(`${log}: ${file}`);
96+
if (content) session.host.appendFile(file, content);
97+
else session.host.writeFile(file, session.host.readFile("/user/username/projects/myproject/a.ts")!);
9698
session.host.runQueuedTimeoutCallbacks();
9799
}
98100

101+
function collectWatchChanges<T extends ts.server.protocol.CreateFileWatcherEventBody | ts.server.protocol.CreateDirectoryWatcherEventBody>(
102+
session: TestSession,
103+
watches: Watches<T>,
104+
path: string,
105+
eventPath: string,
106+
eventType: "created" | "deleted" | "updated",
107+
ignoreUpdate?: (data: T) => boolean,
108+
) {
109+
session.logger.log(`Custom watch:: ${path} ${eventPath} ${eventType}`);
110+
let result: ts.server.protocol.WatchChangeRequestArgs[] | undefined;
111+
watches.forEach(
112+
path,
113+
data => {
114+
if (ignoreUpdate?.(data)) return;
115+
switch (eventType) {
116+
case "created":
117+
(result ??= []).push({ id: data.id, created: [eventPath] });
118+
break;
119+
case "deleted":
120+
(result ??= []).push({ id: data.id, deleted: [eventPath] });
121+
break;
122+
case "updated":
123+
(result ??= []).push({ id: data.id, updated: [eventPath] });
124+
break;
125+
default:
126+
ts.Debug.assertNever(eventType);
127+
}
128+
},
129+
);
130+
return result;
131+
}
132+
133+
function collectDirectoryWatcherChanges(
134+
session: TestSession,
135+
dir: string,
136+
eventPath: string,
137+
eventType: "created" | "deleted" | "updated",
138+
) {
139+
return collectWatchChanges(
140+
session,
141+
(session.logger.host as TestServerHostWithCustomWatch).factoryData.watchUtils.fsWatchesRecursive,
142+
dir,
143+
eventPath,
144+
eventType,
145+
data => !!data.ignoreUpdate && eventType === "updated",
146+
);
147+
}
148+
149+
function collectFileWatcherChanges(
150+
session: TestSession,
151+
file: string,
152+
eventType: "created" | "deleted" | "updated",
153+
) {
154+
return collectWatchChanges(
155+
session,
156+
(session.logger.host as TestServerHostWithCustomWatch).factoryData.watchUtils.pollingWatches,
157+
file,
158+
file,
159+
eventType,
160+
);
161+
}
162+
163+
function invokeWatchChange(
164+
session: TestSession,
165+
...args: (ts.server.protocol.WatchChangeRequestArgs[] | undefined)[]
166+
) {
167+
let result: Map<number, ts.server.protocol.WatchChangeRequestArgs> | undefined;
168+
args.forEach(arg =>
169+
arg?.forEach(value => {
170+
result ??= new Map();
171+
const valueInResult = result.get(value.id);
172+
if (!valueInResult) result.set(value.id, value);
173+
else {
174+
valueInResult.created = ts.combine(valueInResult.created, value.created);
175+
valueInResult.deleted = ts.combine(valueInResult.deleted, value.deleted);
176+
valueInResult.updated = ts.combine(valueInResult.updated, value.updated);
177+
}
178+
})
179+
);
180+
if (result) {
181+
session.executeCommandSeq<ts.server.protocol.WatchChangeRequest>({
182+
command: ts.server.protocol.CommandTypes.WatchChange,
183+
arguments: ts.singleOrMany(ts.arrayFrom(result.values())),
184+
});
185+
}
186+
}
187+
99188
function addFile(session: TestSession, path: string) {
100189
updateFileOnHost(session, path, "Add file");
101-
session.logger.log("Custom watch");
102-
(session.logger.host as TestServerHostWithCustomWatch).factoryData.watchUtils.fsWatchesRecursive.forEach(
103-
"/user/username/projects/myproject",
104-
data =>
105-
session.executeCommandSeq<ts.server.protocol.WatchChangeRequest>({
106-
command: ts.server.protocol.CommandTypes.WatchChange,
107-
arguments: { id: data.id, path, eventType: "create" },
108-
}),
190+
invokeWatchChange(
191+
session,
192+
collectDirectoryWatcherChanges(session, "/user/username/projects/myproject", path, "created"),
109193
);
110194
session.host.runQueuedTimeoutCallbacks();
111195
}
112196

113-
function changeFile(session: TestSession, path: string) {
114-
updateFileOnHost(session, path, "Change File");
115-
session.logger.log("Custom watch");
116-
(session.logger.host as TestServerHostWithCustomWatch).factoryData.watchUtils.pollingWatches.forEach(
117-
path,
118-
data =>
119-
session.executeCommandSeq<ts.server.protocol.WatchChangeRequest>({
120-
command: ts.server.protocol.CommandTypes.WatchChange,
121-
arguments: { id: data.id, path, eventType: "update" },
122-
}),
197+
function changeFile(session: TestSession, path: string, content?: string) {
198+
updateFileOnHost(session, path, "Change File", content);
199+
invokeWatchChange(
200+
session,
201+
collectFileWatcherChanges(session, path, "updated"),
202+
collectDirectoryWatcherChanges(session, ts.getDirectoryPath(path), path, "updated"),
203+
);
204+
session.host.runQueuedTimeoutCallbacks();
205+
}
206+
207+
function npmInstall(session: TestSession) {
208+
session.logger.log("update with npm install");
209+
session.host.appendFile("/user/username/projects/myproject/node_modules/something/index.d.ts", `export const y = 20;`);
210+
session.host.runQueuedTimeoutCallbacks();
211+
invokeWatchChange(
212+
session,
213+
collectDirectoryWatcherChanges(
214+
session,
215+
"/user/username/projects/myproject/node_modules",
216+
"/user/username/projects/myproject/node_modules/something/index.d.ts",
217+
"updated",
218+
),
123219
);
124220
session.host.runQueuedTimeoutCallbacks();
125221
}
@@ -129,6 +225,8 @@ describe("unittests:: tsserver:: events:: watchEvents", () => {
129225
"/user/username/projects/myproject/tsconfig.json": "{}",
130226
"/user/username/projects/myproject/a.ts": `export class a { prop = "hello"; foo() { return this.prop; } }`,
131227
"/user/username/projects/myproject/b.ts": `export class b { prop = "hello"; foo() { return this.prop; } }`,
228+
"/user/username/projects/myproject/m.ts": `import { x } from "something"`,
229+
"/user/username/projects/myproject/node_modules/something/index.d.ts": `export const x = 10;`,
132230
[libFile.path]: libFile.content,
133231
});
134232
const logger = createLoggerWithInMemoryLogs(inputHost);
@@ -153,6 +251,26 @@ describe("unittests:: tsserver:: events:: watchEvents", () => {
153251
// Re watch
154252
closeFilesForSession(["/user/username/projects/myproject/b.ts"], session);
155253

254+
// Update c.ts
255+
changeFile(session, "/user/username/projects/myproject/c.ts", `export const y = 20;`);
256+
257+
// Update with npm install
258+
npmInstall(session);
259+
host.runQueuedTimeoutCallbacks();
260+
261+
// Add and change multiple files - combine and send multiple requests together
262+
updateFileOnHost(session, "/user/username/projects/myproject/d.ts", "Add file");
263+
updateFileOnHost(session, "/user/username/projects/myproject/c.ts", "Change File", `export const z = 30;`);
264+
updateFileOnHost(session, "/user/username/projects/myproject/e.ts", "Add File");
265+
invokeWatchChange(
266+
session,
267+
collectDirectoryWatcherChanges(session, "/user/username/projects/myproject", "/user/username/projects/myproject/d.ts", "created"),
268+
collectFileWatcherChanges(session, "/user/username/projects/myproject/c.ts", "updated"),
269+
collectDirectoryWatcherChanges(session, "/user/username/projects/myproject", "/user/username/projects/myproject/c.ts", "updated"),
270+
collectDirectoryWatcherChanges(session, "/user/username/projects/myproject", "/user/username/projects/myproject/e.ts", "created"),
271+
);
272+
session.host.runQueuedTimeoutCallbacks();
273+
156274
baselineTsserverLogs("events/watchEvents", `canUseWatchEvents`, session);
157275
function handleWatchEvents(event: ts.server.ProjectServiceEvent) {
158276
switch (event.eventName) {
@@ -192,9 +310,15 @@ describe("unittests:: tsserver:: events:: watchEvents", () => {
192310
logger.msg = (s, type) => logger.info(`${type}:: ${s}`);
193311
session.executeCommandSeq<ts.server.protocol.WatchChangeRequest>({
194312
command: ts.server.protocol.CommandTypes.WatchChange,
195-
arguments: { id: 1, path: "/user/username/projects/myproject/b.ts", eventType: "update" },
313+
arguments: { id: 1, updated: ["/user/username/projects/myproject/b.ts"] },
196314
});
197315

316+
// Update c.ts
317+
changeFile(session, "/user/username/projects/myproject/c.ts", `export const y = 20;`);
318+
319+
// Update with npm install
320+
npmInstall(session);
321+
198322
baselineTsserverLogs("events/watchEvents", `canUseWatchEvents without canUseEvents`, session);
199323
});
200324
});

tests/baselines/reference/api/typescript.d.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -1510,12 +1510,13 @@ declare namespace ts {
15101510
}
15111511
interface WatchChangeRequest extends Request {
15121512
command: CommandTypes.WatchChange;
1513-
arguments: WatchChangeRequestArgs;
1513+
arguments: WatchChangeRequestArgs | readonly WatchChangeRequestArgs[];
15141514
}
15151515
interface WatchChangeRequestArgs {
15161516
id: number;
1517-
path: string;
1518-
eventType: "create" | "delete" | "update";
1517+
created?: string[];
1518+
deleted?: string[];
1519+
updated?: string[];
15191520
}
15201521
/**
15211522
* Request to obtain the list of files that should be regenerated if target file is recompiled.
@@ -2452,6 +2453,7 @@ declare namespace ts {
24522453
readonly id: number;
24532454
readonly path: string;
24542455
readonly recursive: boolean;
2456+
readonly ignoreUpdate?: boolean;
24552457
}
24562458
type CloseFileWatcherEventName = "closeFileWatcher";
24572459
interface CloseFileWatcherEvent extends Event {

0 commit comments

Comments
 (0)