From 0c3a08778ed24df357be14ba9899ee94958b3246 Mon Sep 17 00:00:00 2001 From: Jason Desrosiers Date: Tue, 16 Jun 2026 14:06:22 -0700 Subject: [PATCH] Add workspace watch capability --- language-server/src/services/JsonDocuments.ts | 19 +++++++++++++-- language-server/src/services/server.ts | 24 ++++++++++++++++--- language-server/src/test/test-client.ts | 22 +++++++++++++++-- 3 files changed, 58 insertions(+), 7 deletions(-) diff --git a/language-server/src/services/JsonDocuments.ts b/language-server/src/services/JsonDocuments.ts index aef9d4a..ce5da40 100644 --- a/language-server/src/services/JsonDocuments.ts +++ b/language-server/src/services/JsonDocuments.ts @@ -1,4 +1,4 @@ -import { TextDocuments, TextDocumentSyncKind } from "vscode-languageserver"; +import { DidChangeWatchedFilesNotification, TextDocuments, TextDocumentSyncKind } from "vscode-languageserver"; import { TextDocument } from "vscode-languageserver-textdocument"; import { JsonDocument } from "../models/JsonDocument.ts"; import { Server } from "./server.ts"; @@ -6,6 +6,9 @@ import { Server } from "./server.ts"; import type { DocumentUri, ServerCapabilities, TextDocumentContentChangeEvent } from "vscode-languageserver"; export class JsonDocuments extends TextDocuments { + private server: Server; + private hasWorkspaceWatchCapability: boolean = false; + constructor(server: Server) { super({ create(uri: DocumentUri, languageId: string, version: number, content: string) { @@ -18,7 +21,11 @@ export class JsonDocuments extends TextDocuments { } }); - server.onInitialize(() => { + this.server = server; + + server.onInitialize(({ capabilities }) => { + this.hasWorkspaceWatchCapability = !!capabilities.workspace?.didChangeWatchedFiles?.dynamicRegistration; + const serverCapabilities: ServerCapabilities = { textDocumentSync: TextDocumentSyncKind.Incremental }; @@ -27,5 +34,13 @@ export class JsonDocuments extends TextDocuments { capabilities: serverCapabilities }; }); + + server.onInitialized(async () => { + if (this.hasWorkspaceWatchCapability) { + await this.server.client.register(DidChangeWatchedFilesNotification.type, { + watchers: [{ globPattern: "**/*" }] + }); + } + }); } } diff --git a/language-server/src/services/server.ts b/language-server/src/services/server.ts index 48720eb..ab4967f 100644 --- a/language-server/src/services/server.ts +++ b/language-server/src/services/server.ts @@ -2,6 +2,7 @@ import { merge } from "merge-anything"; import type { Connection, + DidChangeWatchedFilesParams, Disposable, InitializedParams, InitializeError, @@ -19,6 +20,7 @@ export class Server implements Connection { private initializedHandlers: Set>; private shutdownHandlers: Set>; private exitHandlers: Set; + private didChangeWatchedFilesHandlers: Set>; declare listen: Connection["listen"]; declare onRequest: Connection["onRequest"]; @@ -28,7 +30,6 @@ export class Server implements Connection { declare onProgress: Connection["onProgress"]; declare sendProgress: Connection["sendProgress"]; declare onDidChangeConfiguration: Connection["onDidChangeConfiguration"]; - declare onDidChangeWatchedFiles: Connection["onDidChangeWatchedFiles"]; declare onDidOpenTextDocument: Connection["onDidOpenTextDocument"]; declare onDidChangeTextDocument: Connection["onDidChangeTextDocument"]; declare onDidCloseTextDocument: Connection["onDidCloseTextDocument"]; @@ -72,7 +73,7 @@ export class Server implements Connection { this.initializeHandlers = new Set(); this.connection.onInitialize((params, token, workDoneProgress) => { - connection.console.log("Initializing JSON service ..."); + connection.console.log("Initializing"); let initializeResult: InitializeResult = { capabilities: {} @@ -90,6 +91,8 @@ export class Server implements Connection { for (const handler of this.initializedHandlers) { await handler(params); } + + connection.console.log("Ready"); }); this.shutdownHandlers = new Set(); @@ -106,6 +109,13 @@ export class Server implements Connection { } }); + this.didChangeWatchedFilesHandlers = new Set(); + this.connection.onDidChangeWatchedFiles(async (params) => { + for (const handler of this.didChangeWatchedFilesHandlers) { + await handler(params); + } + }); + this.listen = this.connection.listen.bind(this.connection); this.onRequest = this.connection.onRequest.bind(this.connection); this.sendRequest = this.connection.sendRequest.bind(this.connection); @@ -114,7 +124,6 @@ export class Server implements Connection { this.onProgress = this.connection.onProgress.bind(this.connection); this.sendProgress = this.connection.sendProgress.bind(this.connection); this.onDidChangeConfiguration = this.connection.onDidChangeConfiguration.bind(this.connection); - this.onDidChangeWatchedFiles = this.connection.onDidChangeWatchedFiles.bind(this.connection); this.onDidOpenTextDocument = this.connection.onDidOpenTextDocument.bind(this.connection); this.onDidChangeTextDocument = this.connection.onDidChangeTextDocument.bind(this.connection); this.onDidCloseTextDocument = this.connection.onDidCloseTextDocument.bind(this.connection); @@ -190,6 +199,15 @@ export class Server implements Connection { }; } + onDidChangeWatchedFiles(handler: NotificationHandler): Disposable { + this.didChangeWatchedFilesHandlers.add(handler); + return { + dispose: () => { + this.didChangeWatchedFilesHandlers.delete(handler); + } + }; + } + get console() { return this.connection.console; } diff --git a/language-server/src/test/test-client.ts b/language-server/src/test/test-client.ts index d3d5474..6505bb8 100644 --- a/language-server/src/test/test-client.ts +++ b/language-server/src/test/test-client.ts @@ -1,4 +1,4 @@ -import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { Duplex } from "node:stream"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -7,9 +7,11 @@ import { ConfigurationRequest, DidChangeConfigurationNotification, DidChangeTextDocumentNotification, + DidChangeWatchedFilesNotification, DidCloseTextDocumentNotification, DidOpenTextDocumentNotification, ExitNotification, + FileChangeType, InitializedNotification, InitializeRequest, RegistrationRequest, @@ -30,6 +32,7 @@ import type { export class TestClient { private client: Connection; private serverName: string; + private watchEnabled: boolean; private _serverCapabilities: ServerCapabilities | undefined; private languageServerSettings: Partial | undefined; private configurationChangeNotificationOptions: DidChangeConfigurationRegistrationOptions | null | undefined; @@ -45,6 +48,7 @@ export class TestClient { constructor(serverName = "jsonLanguageServer") { this.serverName = serverName; + this.watchEnabled = false; this.openDocuments = new Set(); this.workspaceFolder = mkdtemp(join(tmpdir(), "test-workspace-")) .then((path) => URI.file(path).toString() + "/"); @@ -71,6 +75,8 @@ export class TestClient { this.configurationChangeNotificationOptions = registration.registerOptions === undefined ? null : registration.registerOptions as DidChangeConfigurationRegistrationOptions; + } else if (registration.method === DidChangeWatchedFilesNotification.method) { + this.watchEnabled = true; } else { throw Error(`Unsupported Registration: '${registration.method}'`); } @@ -211,8 +217,21 @@ export class TestClient { async writeDocument(uri: string, text: string) { const fullUri = Utils.resolvePath(URI.parse(await this.workspaceFolder), uri); + const exists = await access(fullUri.fsPath) + .then(() => true) + .catch(() => false); + await writeFile(fullUri.fsPath, text, "utf-8"); + if (this.watchEnabled) { + await this.client.sendNotification(DidChangeWatchedFilesNotification.type, { + changes: [{ + type: exists ? FileChangeType.Changed : FileChangeType.Created, + uri: fullUri.toString() + }] + }); + } + return fullUri.toString(); } @@ -242,7 +261,6 @@ export class TestClient { async changeDocument(uri: string, text: string) { const documentUri = Utils.resolvePath(URI.parse(await this.workspaceFolder), uri); - await writeFile(documentUri.fsPath, text, "utf-8"); await this.client.sendNotification(DidChangeTextDocumentNotification.type, { textDocument: {