Skip to content

Commit 69f7975

Browse files
jdesrosiersclaude
andcommitted
Debounce revalidating dependents while editing an open schema
Every edit to an open schema notified schema change handlers, which cleared the compiled schemas of its dependents and revalidated them. When typing quickly, those revalidations overlapped and each started a new compile, so all but the last were thrown away. Notifications caused by edits to an open document now wait until typing pauses for 200ms and are sent together. The registry is still updated on every edit, so the edited document's own diagnostics aren't delayed. Changes from the file system, opening, and closing are still notified immediately. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent ef24d26 commit 69f7975

2 files changed

Lines changed: 72 additions & 4 deletions

File tree

‎language-server/src/features/diagnostics/SchemaValidationDiagnosticsProvider.test.ts‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,41 @@ describe("Schema Validation", () => {
943943
await expect(secondValidation).resolves.toHaveLength(0);
944944
});
945945

946+
test("editing an open schema revalidates dependents once when typing pauses", async () => {
947+
const schemaWithFooType = (type: string) => `{
948+
"$schema": "https://json-schema.org/draft/2020-12/schema",
949+
"type": "object",
950+
"properties": {
951+
"foo": { "type": "${type}" }
952+
}
953+
}`;
954+
955+
fixtureSchemaUri = await client.writeDocument("schema.json", schemaWithFooType("string"));
956+
await client.openDocument("schema.json");
957+
958+
await client.writeDocument("instance.json", `{
959+
"$schema": "${fixtureSchemaUri}",
960+
"foo": 42
961+
}`);
962+
const initialValidation = client.getDiagnostics("instance.json");
963+
const instanceUri = await client.openDocument("instance.json");
964+
await expect(initialValidation).resolves.toHaveLength(1);
965+
966+
const instanceValidations: number[] = [];
967+
client.onNotification(PublishDiagnosticsNotification.type, (params) => {
968+
if (params.uri === instanceUri) {
969+
instanceValidations.push(params.diagnostics.length);
970+
}
971+
});
972+
973+
await client.changeDocument("schema.json", schemaWithFooType("boolean"));
974+
await client.changeDocument("schema.json", schemaWithFooType("null"));
975+
await client.changeDocument("schema.json", schemaWithFooType("number"));
976+
await new Promise((resolve) => setTimeout(resolve, 500));
977+
978+
expect(instanceValidations).toEqual([0]);
979+
});
980+
946981
test("editing an open referenced schema revalidates dependents", async () => {
947982
const referencedSchema = await client.writeDocument("B.schema.json", `{
948983
"$schema": "https://json-schema.org/draft/2020-12/schema",

‎language-server/src/services/JsonSchemaRegistry.ts‎

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ type DidChangeSchemaParams = {
3333

3434
const readChunkSize = 100;
3535

36+
// How long typing in an open document has to pause before documents that depend on it are revalidated
37+
const editDebounceMs = 200;
38+
3639
export class JsonSchemaRegistry {
3740
private server: Server;
3841
private workspace: Workspace;
@@ -43,6 +46,8 @@ export class JsonSchemaRegistry {
4346
private failedSchemas: Map<string, { id: string; message: string }> = new Map();
4447
private didChangeSchemaHandlers: Set<DidChangeSchemaHandler> = new Set();
4548
private pending: Promise<void>;
49+
private pendingEditChanges: Map<string, DidChangeSchemaParams> = new Map();
50+
private editDebounceTimer: ReturnType<typeof setTimeout> | undefined;
4651

4752
constructor(server: Server, workspace: Workspace, jsonDocuments: JsonDocuments) {
4853
this.server = server;
@@ -90,6 +95,7 @@ export class JsonSchemaRegistry {
9095
});
9196

9297
server.onShutdown(() => {
98+
clearTimeout(this.editDebounceTimer);
9399
for (const schemaUri of this.workspaceSchemas.values()) {
94100
unregisterSchema(schemaUri);
95101
}
@@ -192,12 +198,39 @@ export class JsonSchemaRegistry {
192198
const applied = this.pending.then(() => this.applyChanges(fileUris));
193199
this.pending = applied.then(() => undefined, () => undefined);
194200

195-
// Handlers aren't part of the queue because they can wait on `ready`
201+
const changes: DidChangeSchemaParams[] = [];
196202
for (const [fileUri, schemaUris] of await applied) {
197203
for (const schemaUri of schemaUris) {
198-
for (const handler of this.didChangeSchemaHandlers) {
199-
await handler({ schemaUri, fileUri, editedDocumentUri });
200-
}
204+
changes.push({ schemaUri, fileUri, editedDocumentUri });
205+
}
206+
}
207+
208+
if (editedDocumentUri) {
209+
// Typing in a schema changes it on every keystroke, and revalidating its dependents means
210+
// recompiling them. The registry is already up to date, so only the notification waits.
211+
for (const change of changes) {
212+
this.pendingEditChanges.set(`${change.fileUri} ${change.schemaUri}`, change);
213+
}
214+
215+
clearTimeout(this.editDebounceTimer);
216+
this.editDebounceTimer = setTimeout(() => {
217+
const pendingChanges = [...this.pendingEditChanges.values()];
218+
this.pendingEditChanges.clear();
219+
this.notify(pendingChanges).catch((error: unknown) => {
220+
const message = error instanceof Error ? error.message : String(error);
221+
this.server.console.error(`Failed to process schema changes: ${message}`);
222+
});
223+
}, editDebounceMs);
224+
} else {
225+
await this.notify(changes);
226+
}
227+
}
228+
229+
// Handlers aren't part of the queue because they can wait on `ready`
230+
private async notify(changes: DidChangeSchemaParams[]) {
231+
for (const change of changes) {
232+
for (const handler of this.didChangeSchemaHandlers) {
233+
await handler(change);
201234
}
202235
}
203236
}

0 commit comments

Comments
 (0)