From fd28c5d5807d609cf5ec795bdb03cbeec2af3f98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Sp=C3=B6nemann?= Date: Thu, 9 Nov 2023 23:30:20 +0000 Subject: [PATCH 1/4] Introduce DocumentUpdateHandler service --- examples/arithmetics/src/extension.ts | 11 +- examples/domainmodel/src/extension.ts | 11 +- examples/requirements/src/extension.ts | 11 +- examples/statemachine/src/extension.ts | 11 +- .../templates/vscode/src/extension/main.ts | 11 +- packages/langium-vscode/src/extension.ts | 9 +- packages/langium/src/default-module.ts | 2 + .../src/lsp/document-update-handler.ts | 135 ++++++++++++++++++ packages/langium/src/lsp/language-server.ts | 54 +++---- packages/langium/src/services.ts | 6 +- 10 files changed, 181 insertions(+), 80 deletions(-) create mode 100644 packages/langium/src/lsp/document-update-handler.ts diff --git a/examples/arithmetics/src/extension.ts b/examples/arithmetics/src/extension.ts index 176d9378f..830675815 100644 --- a/examples/arithmetics/src/extension.ts +++ b/examples/arithmetics/src/extension.ts @@ -5,7 +5,7 @@ ******************************************************************************/ import type { LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node.js'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import * as path from 'node:path'; import { LanguageClient, TransportKind } from 'vscode-languageclient/node.js'; @@ -37,16 +37,9 @@ function startLanguageClient(context: vscode.ExtensionContext): LanguageClient { debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions } }; - const fileSystemWatcher = vscode.workspace.createFileSystemWatcher('**/*.calc'); - context.subscriptions.push(fileSystemWatcher); - // Options to control the language client const clientOptions: LanguageClientOptions = { - documentSelector: [{ scheme: 'file', language: 'arithmetics' }], - synchronize: { - // Notify the server about file changes to files contained in the workspace - fileEvents: fileSystemWatcher - } + documentSelector: [{ scheme: 'file', language: 'arithmetics' }] }; // Create the language client and start the client. diff --git a/examples/domainmodel/src/extension.ts b/examples/domainmodel/src/extension.ts index 3917b9bf7..b187d7498 100644 --- a/examples/domainmodel/src/extension.ts +++ b/examples/domainmodel/src/extension.ts @@ -5,7 +5,7 @@ ******************************************************************************/ import type { LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node.js'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import * as path from 'node:path'; import { LanguageClient, TransportKind } from 'vscode-languageclient/node.js'; @@ -37,16 +37,9 @@ function startLanguageClient(context: vscode.ExtensionContext): LanguageClient { debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions } }; - const fileSystemWatcher = vscode.workspace.createFileSystemWatcher('**/*.dmodel'); - context.subscriptions.push(fileSystemWatcher); - // Options to control the language client const clientOptions: LanguageClientOptions = { - documentSelector: [{ scheme: 'file', language: 'domain-model' }], - synchronize: { - // Notify the server about file changes to files contained in the workspace - fileEvents: fileSystemWatcher - } + documentSelector: [{ scheme: 'file', language: 'domain-model' }] }; // Create the language client and start the client. diff --git a/examples/requirements/src/extension.ts b/examples/requirements/src/extension.ts index 3727c577f..6af550b67 100644 --- a/examples/requirements/src/extension.ts +++ b/examples/requirements/src/extension.ts @@ -6,7 +6,7 @@ import type { LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node.js'; import { LanguageClient, TransportKind } from 'vscode-languageclient/node.js'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import * as path from 'node:path'; let client: LanguageClient; @@ -38,18 +38,11 @@ function startLanguageClient(context: vscode.ExtensionContext): LanguageClient { debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions } }; - const fileSystemWatcher = vscode.workspace.createFileSystemWatcher('**/*.(req|tst)'); - context.subscriptions.push(fileSystemWatcher); - // Options to control the language client const clientOptions: LanguageClientOptions = { documentSelector: [ { scheme: 'file', language: 'tests-lang' }, - { scheme: 'file', language: 'requirements-lang' }], - synchronize: { - // Notify the server about file changes to files contained in the workspace - fileEvents: fileSystemWatcher - } + { scheme: 'file', language: 'requirements-lang' }] }; // Create the language client and start the client. diff --git a/examples/statemachine/src/extension.ts b/examples/statemachine/src/extension.ts index aa65f8a30..311b891e7 100644 --- a/examples/statemachine/src/extension.ts +++ b/examples/statemachine/src/extension.ts @@ -5,7 +5,7 @@ ******************************************************************************/ import type { LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node.js'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import * as path from 'node:path'; import { LanguageClient, TransportKind } from 'vscode-languageclient/node.js'; @@ -37,16 +37,9 @@ function startLanguageClient(context: vscode.ExtensionContext): LanguageClient { debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions } }; - const fileSystemWatcher = vscode.workspace.createFileSystemWatcher('**/*.statemachine'); - context.subscriptions.push(fileSystemWatcher); - // Options to control the language client const clientOptions: LanguageClientOptions = { - documentSelector: [{ scheme: 'file', language: 'statemachine' }], - synchronize: { - // Notify the server about file changes to files contained in the workspace - fileEvents: fileSystemWatcher - } + documentSelector: [{ scheme: 'file', language: 'statemachine' }] }; // Create the language client and start the client. diff --git a/packages/generator-langium/templates/vscode/src/extension/main.ts b/packages/generator-langium/templates/vscode/src/extension/main.ts index 5c246b56f..195b8f979 100644 --- a/packages/generator-langium/templates/vscode/src/extension/main.ts +++ b/packages/generator-langium/templates/vscode/src/extension/main.ts @@ -1,5 +1,5 @@ import type { LanguageClientOptions, ServerOptions} from 'vscode-languageclient/node.js'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import * as path from 'node:path'; import { LanguageClient, TransportKind } from 'vscode-languageclient/node.js'; @@ -32,16 +32,9 @@ function startLanguageClient(context: vscode.ExtensionContext): LanguageClient { debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions } }; - const fileSystemWatcher = vscode.workspace.createFileSystemWatcher('**/*.<%= file-glob-extension %>'); - context.subscriptions.push(fileSystemWatcher); - // Options to control the language client const clientOptions: LanguageClientOptions = { - documentSelector: [{ scheme: 'file', language: '<%= language-id %>' }], - synchronize: { - // Notify the server about file changes to files contained in the workspace - fileEvents: fileSystemWatcher - } + documentSelector: [{ scheme: 'file', language: '<%= language-id %>' }] }; // Create the language client and start the client. diff --git a/packages/langium-vscode/src/extension.ts b/packages/langium-vscode/src/extension.ts index c312c1c96..8d09775e0 100644 --- a/packages/langium-vscode/src/extension.ts +++ b/packages/langium-vscode/src/extension.ts @@ -45,17 +45,10 @@ async function startLanguageClient(context: vscode.ExtensionContext): Promise context.connection, LanguageServer: (services) => new DefaultLanguageServer(services), + DocumentUpdateHandler: (services) => new DefaultDocumentUpdateHandler(services), WorkspaceSymbolProvider: (services) => new DefaultWorkspaceSymbolProvider(services), NodeKindProvider: () => new DefaultNodeKindProvider(), FuzzyMatcher: () => new DefaultFuzzyMatcher() diff --git a/packages/langium/src/lsp/document-update-handler.ts b/packages/langium/src/lsp/document-update-handler.ts new file mode 100644 index 000000000..3de42979a --- /dev/null +++ b/packages/langium/src/lsp/document-update-handler.ts @@ -0,0 +1,135 @@ +/****************************************************************************** + * Copyright 2023 TypeFox GmbH + * This program and the accompanying materials are made available under the + * terms of the MIT License, which is available in the project root. + ******************************************************************************/ + +import type { CreateFilesParams, DeleteFilesParams, DidChangeWatchedFilesParams, DidChangeWatchedFilesRegistrationOptions, FileOperationOptions, RenameFilesParams, TextDocumentChangeEvent, WorkspaceEdit} from 'vscode-languageserver'; +import type { TextDocument } from 'vscode-languageserver-textdocument'; +import type { LangiumSharedServices } from '../services.js'; +import type { MaybePromise, MutexLock } from '../utils/promise-util.js'; +import type { DocumentBuilder } from '../workspace/document-builder.js'; +import { DidChangeWatchedFilesNotification, FileChangeType } from 'vscode-languageserver'; +import { URI } from 'vscode-uri'; +import { stream } from '../utils/stream.js'; + +/** + * Shared service for handling document changes such as content changes, file creation, file deletion, etc. + * The interface methods are optional, so they are only registered if they are implemented. + */ +export interface DocumentUpdateHandler { + + /** + * These options are reported to the client as part of the ServerCapabilities. + */ + readonly fileOperationOptions?: FileOperationOptions; + + /** + * A content change event was triggered by the `TextDocuments` service. + */ + didChangeContent?(change: TextDocumentChangeEvent): void; + + /** + * The client detected changes to files and folders watched by the language client. + */ + didChangeWatchedFiles?(params: DidChangeWatchedFilesParams): void; + + /** + * Files were created from within the client. + * This notification must be registered with the {@link fileOperationOptions}. + */ + didCreateFiles?(params: CreateFilesParams): void; + + /** + * Files were renamed from within the client. + * This notification must be registered with the {@link fileOperationOptions}. + */ + didRenameFiles?(params: RenameFilesParams): void; + + /** + * Files were deleted from within the client. + * This notification must be registered with the {@link fileOperationOptions}. + */ + didDeleteFiles?(params: DeleteFilesParams): void; + + /** + * Called before files are actually created as long as the creation is triggered from within + * the client either by a user action or by applying a workspace edit. + * This request must be registered with the {@link fileOperationOptions}. + * @returns a WorkspaceEdit which will be applied to workspace before the files are created. + */ + willCreateFiles?(params: CreateFilesParams): MaybePromise; + + /** + * Called before files are actually renamed as long as the rename is triggered from within + * the client either by a user action or by applying a workspace edit. + * This request must be registered with the {@link fileOperationOptions}. + * @returns a WorkspaceEdit which will be applied to workspace before the files are renamed. + */ + willRenameFiles?(params: RenameFilesParams): MaybePromise; + + /** + * Called before files are actually deleted as long as the deletion is triggered from within + * the client either by a user action or by applying a workspace edit. + * This request must be registered with the {@link fileOperationOptions}. + * @returns a WorkspaceEdit which will be applied to workspace before the files are deleted. + */ + willDeleteFiles?(params: DeleteFilesParams): MaybePromise; + +} + +export class DefaultDocumentUpdateHandler implements DocumentUpdateHandler { + + protected readonly documentBuilder: DocumentBuilder; + protected readonly mutexLock: MutexLock; + + constructor(services: LangiumSharedServices) { + this.documentBuilder = services.workspace.DocumentBuilder; + this.mutexLock = services.workspace.MutexLock; + + let canRegisterFileWatcher = false; + services.lsp.LanguageServer.onInitialize(params => { + canRegisterFileWatcher = Boolean(params.capabilities.workspace?.didChangeWatchedFiles?.dynamicRegistration); + }); + + services.lsp.LanguageServer.onInitialized(_params => { + if (canRegisterFileWatcher) { + this.registerFileWatcher(services); + } + }); + } + + protected registerFileWatcher(services: LangiumSharedServices): void { + const fileExtensions = stream(services.ServiceRegistry.all) + .flatMap(language => language.LanguageMetaData.fileExtensions) + .map(ext => ext.startsWith('.') ? ext.substring(1) : ext) + .distinct() + .toArray(); + if (fileExtensions.length > 0) { + const connection = services.lsp.Connection; + const options: DidChangeWatchedFilesRegistrationOptions = { + watchers: [{ + globPattern: fileExtensions.length === 1 + ? `**/*.${fileExtensions[0]}` + : `**/*.{${fileExtensions.join(',')}}` + }] + }; + connection?.client.register(DidChangeWatchedFilesNotification.type, options); + } + } + + protected fireDocumentUpdate(changed: URI[], deleted: URI[]): void { + this.mutexLock.lock(token => this.documentBuilder.update(changed, deleted, token)); + } + + didChangeContent(change: TextDocumentChangeEvent): void { + this.fireDocumentUpdate([URI.parse(change.document.uri)], []); + } + + didChangeWatchedFiles(params: DidChangeWatchedFilesParams): void { + const changedUris = params.changes.filter(c => c.type !== FileChangeType.Deleted).map(c => URI.parse(c.uri)); + const deletedUris = params.changes.filter(c => c.type === FileChangeType.Deleted).map(c => URI.parse(c.uri)); + this.fireDocumentUpdate(changedUris, deletedUris); + } + +} diff --git a/packages/langium/src/lsp/language-server.ts b/packages/langium/src/lsp/language-server.ts index 15f319839..c4a57975d 100644 --- a/packages/langium/src/lsp/language-server.ts +++ b/packages/langium/src/lsp/language-server.ts @@ -30,7 +30,7 @@ import type { } from 'vscode-languageserver'; import type { LangiumServices, LangiumSharedServices } from '../services.js'; import type { LangiumDocument } from '../workspace/documents.js'; -import { Emitter, FileChangeType, LSPErrorCodes, ResponseError, TextDocumentSyncKind } from 'vscode-languageserver'; +import { Emitter, LSPErrorCodes, ResponseError, TextDocumentSyncKind } from 'vscode-languageserver'; import { eagerLoad } from '../dependency-injection.js'; import { isOperationCancelled } from '../utils/promise-util.js'; import { DocumentState } from '../workspace/documents.js'; @@ -87,6 +87,7 @@ export class DefaultLanguageServer implements LanguageServer { protected buildInitializeResult(_params: InitializeParams): InitializeResult { const languages = this.services.ServiceRegistry.all; + const fileOperationOptions = this.services.lsp.DocumentUpdateHandler.fileOperationOptions; const hasFormattingService = this.hasService(e => e.lsp.Formatter); const formattingOnTypeOptions = languages.map(e => e.lsp.Formatter?.formatOnTypeOptions).find(e => Boolean(e)); const hasCodeActionProvider = this.hasService(e => e.lsp.CodeActionProvider); @@ -117,7 +118,8 @@ export class DefaultLanguageServer implements LanguageServer { workspace: { workspaceFolders: { supported: true - } + }, + fileOperations: fileOperationOptions }, executeCommandProvider: commandNames && { commands: commandNames @@ -222,30 +224,32 @@ export function startLanguageServer(services: LangiumSharedServices): void { } export function addDocumentsHandler(connection: Connection, services: LangiumSharedServices): void { - const documentBuilder = services.workspace.DocumentBuilder; - const mutex = services.workspace.MutexLock; - - function onDidChange(changed: URI[], deleted: URI[]): void { - mutex.lock(token => documentBuilder.update(changed, deleted, token)); - } - + const handler = services.lsp.DocumentUpdateHandler; const documents = services.workspace.TextDocuments; - documents.onDidChangeContent(change => { - onDidChange([URI.parse(change.document.uri)], []); - }); - connection.onDidChangeWatchedFiles(params => { - const changedUris: URI[] = []; - const deletedUris: URI[] = []; - for (const change of params.changes) { - const uri = URI.parse(change.uri); - if (change.type === FileChangeType.Deleted) { - deletedUris.push(uri); - } else { - changedUris.push(uri); - } - } - onDidChange(changedUris, deletedUris); - }); + if (handler.didChangeContent) { + documents.onDidChangeContent(change => handler.didChangeContent!(change)); + } + if (handler.didChangeWatchedFiles) { + connection.onDidChangeWatchedFiles(params => handler.didChangeWatchedFiles!(params)); + } + if (handler.didCreateFiles) { + connection.workspace.onDidCreateFiles(params => handler.didCreateFiles!(params)); + } + if (handler.didRenameFiles) { + connection.workspace.onDidRenameFiles(params => handler.didRenameFiles!(params)); + } + if (handler.didDeleteFiles) { + connection.workspace.onDidDeleteFiles(params => handler.didDeleteFiles!(params)); + } + if (handler.willCreateFiles) { + connection.workspace.onWillCreateFiles(params => handler.willCreateFiles!(params)); + } + if (handler.willRenameFiles) { + connection.workspace.onWillRenameFiles(params => handler.willRenameFiles!(params)); + } + if (handler.willDeleteFiles) { + connection.workspace.onWillDeleteFiles(params => handler.willDeleteFiles!(params)); + } } export function addDiagnosticsHandler(connection: Connection, services: LangiumSharedServices): void { diff --git a/packages/langium/src/services.ts b/packages/langium/src/services.ts index 8aa9fc42e..bc380ee1f 100644 --- a/packages/langium/src/services.ts +++ b/packages/langium/src/services.ts @@ -51,9 +51,10 @@ import type { SignatureHelpProvider } from './lsp/signature-help-provider.js'; import type { TypeDefinitionProvider } from './lsp/type-provider.js'; import type { ImplementationProvider } from './lsp/implementation-provider.js'; import type { CallHierarchyProvider } from './lsp/call-hierarchy-provider.js'; -import type { DocumentLinkProvider } from './lsp/document-link-provider.js'; import type { CodeLensProvider } from './lsp/code-lens-provider.js'; import type { DeclarationProvider } from './lsp/declaration-provider.js'; +import type { DocumentLinkProvider } from './lsp/document-link-provider.js'; +import type { DocumentUpdateHandler } from './lsp/document-update-handler.js'; import type { DocumentationProvider } from './documentation/documentation-provider.js'; import type { InlayHintProvider } from './lsp/inlay-hint-provider.js'; import type { CommentProvider } from './documentation/comment-provider.js'; @@ -162,11 +163,12 @@ export type LangiumDefaultSharedServices = { ServiceRegistry: ServiceRegistry lsp: { Connection?: Connection + LanguageServer: LanguageServer + DocumentUpdateHandler: DocumentUpdateHandler ExecuteCommandHandler?: ExecuteCommandHandler WorkspaceSymbolProvider?: WorkspaceSymbolProvider NodeKindProvider: NodeKindProvider FuzzyMatcher: FuzzyMatcher - LanguageServer: LanguageServer } workspace: { DocumentBuilder: DocumentBuilder From 19e63cb7112ebf5940cfd2969034d9f5f0954ec6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Sp=C3=B6nemann?= Date: Fri, 10 Nov 2023 08:34:31 +0000 Subject: [PATCH 2/4] Added FileOperationHandler --- .../src/lsp/document-update-handler.ts | 58 ++--------------- .../langium/src/lsp/file-operation-handler.ts | 63 +++++++++++++++++++ packages/langium/src/lsp/language-server.ts | 10 ++- packages/langium/src/services.ts | 2 + 4 files changed, 79 insertions(+), 54 deletions(-) create mode 100644 packages/langium/src/lsp/file-operation-handler.ts diff --git a/packages/langium/src/lsp/document-update-handler.ts b/packages/langium/src/lsp/document-update-handler.ts index 3de42979a..892aba19b 100644 --- a/packages/langium/src/lsp/document-update-handler.ts +++ b/packages/langium/src/lsp/document-update-handler.ts @@ -4,77 +4,29 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import type { CreateFilesParams, DeleteFilesParams, DidChangeWatchedFilesParams, DidChangeWatchedFilesRegistrationOptions, FileOperationOptions, RenameFilesParams, TextDocumentChangeEvent, WorkspaceEdit} from 'vscode-languageserver'; +import type { DidChangeWatchedFilesParams, DidChangeWatchedFilesRegistrationOptions, TextDocumentChangeEvent } from 'vscode-languageserver'; import type { TextDocument } from 'vscode-languageserver-textdocument'; import type { LangiumSharedServices } from '../services.js'; -import type { MaybePromise, MutexLock } from '../utils/promise-util.js'; +import type { MutexLock } from '../utils/promise-util.js'; import type { DocumentBuilder } from '../workspace/document-builder.js'; import { DidChangeWatchedFilesNotification, FileChangeType } from 'vscode-languageserver'; import { URI } from 'vscode-uri'; import { stream } from '../utils/stream.js'; /** - * Shared service for handling document changes such as content changes, file creation, file deletion, etc. - * The interface methods are optional, so they are only registered if they are implemented. + * Shared service for handling text document changes and watching relevant files. */ export interface DocumentUpdateHandler { - /** - * These options are reported to the client as part of the ServerCapabilities. - */ - readonly fileOperationOptions?: FileOperationOptions; - /** * A content change event was triggered by the `TextDocuments` service. */ - didChangeContent?(change: TextDocumentChangeEvent): void; + didChangeContent(change: TextDocumentChangeEvent): void; /** * The client detected changes to files and folders watched by the language client. */ - didChangeWatchedFiles?(params: DidChangeWatchedFilesParams): void; - - /** - * Files were created from within the client. - * This notification must be registered with the {@link fileOperationOptions}. - */ - didCreateFiles?(params: CreateFilesParams): void; - - /** - * Files were renamed from within the client. - * This notification must be registered with the {@link fileOperationOptions}. - */ - didRenameFiles?(params: RenameFilesParams): void; - - /** - * Files were deleted from within the client. - * This notification must be registered with the {@link fileOperationOptions}. - */ - didDeleteFiles?(params: DeleteFilesParams): void; - - /** - * Called before files are actually created as long as the creation is triggered from within - * the client either by a user action or by applying a workspace edit. - * This request must be registered with the {@link fileOperationOptions}. - * @returns a WorkspaceEdit which will be applied to workspace before the files are created. - */ - willCreateFiles?(params: CreateFilesParams): MaybePromise; - - /** - * Called before files are actually renamed as long as the rename is triggered from within - * the client either by a user action or by applying a workspace edit. - * This request must be registered with the {@link fileOperationOptions}. - * @returns a WorkspaceEdit which will be applied to workspace before the files are renamed. - */ - willRenameFiles?(params: RenameFilesParams): MaybePromise; - - /** - * Called before files are actually deleted as long as the deletion is triggered from within - * the client either by a user action or by applying a workspace edit. - * This request must be registered with the {@link fileOperationOptions}. - * @returns a WorkspaceEdit which will be applied to workspace before the files are deleted. - */ - willDeleteFiles?(params: DeleteFilesParams): MaybePromise; + didChangeWatchedFiles(params: DidChangeWatchedFilesParams): void; } diff --git a/packages/langium/src/lsp/file-operation-handler.ts b/packages/langium/src/lsp/file-operation-handler.ts new file mode 100644 index 000000000..2b53b93eb --- /dev/null +++ b/packages/langium/src/lsp/file-operation-handler.ts @@ -0,0 +1,63 @@ +/****************************************************************************** + * Copyright 2023 TypeFox GmbH + * This program and the accompanying materials are made available under the + * terms of the MIT License, which is available in the project root. + ******************************************************************************/ + +import type { CreateFilesParams, DeleteFilesParams, FileOperationOptions, RenameFilesParams, WorkspaceEdit } from 'vscode-languageserver'; +import type { MaybePromise } from '../utils/promise-util.js'; + +/** + * Shared service for handling file changes such as file creation, deletion and renaming. + * The interface methods are optional, so they are only registered if they are implemented. + */ +export interface FileOperationHandler { + + /** + * These options are reported to the client as part of the ServerCapabilities. + */ + readonly fileOperationOptions: FileOperationOptions; + + /** + * Files were created from within the client. + * This notification must be registered with the {@link fileOperationOptions}. + */ + didCreateFiles?(params: CreateFilesParams): void; + + /** + * Files were renamed from within the client. + * This notification must be registered with the {@link fileOperationOptions}. + */ + didRenameFiles?(params: RenameFilesParams): void; + + /** + * Files were deleted from within the client. + * This notification must be registered with the {@link fileOperationOptions}. + */ + didDeleteFiles?(params: DeleteFilesParams): void; + + /** + * Called before files are actually created as long as the creation is triggered from within + * the client either by a user action or by applying a workspace edit. + * This request must be registered with the {@link fileOperationOptions}. + * @returns a WorkspaceEdit which will be applied to workspace before the files are created. + */ + willCreateFiles?(params: CreateFilesParams): MaybePromise; + + /** + * Called before files are actually renamed as long as the rename is triggered from within + * the client either by a user action or by applying a workspace edit. + * This request must be registered with the {@link fileOperationOptions}. + * @returns a WorkspaceEdit which will be applied to workspace before the files are renamed. + */ + willRenameFiles?(params: RenameFilesParams): MaybePromise; + + /** + * Called before files are actually deleted as long as the deletion is triggered from within + * the client either by a user action or by applying a workspace edit. + * This request must be registered with the {@link fileOperationOptions}. + * @returns a WorkspaceEdit which will be applied to workspace before the files are deleted. + */ + willDeleteFiles?(params: DeleteFilesParams): MaybePromise; + +} diff --git a/packages/langium/src/lsp/language-server.ts b/packages/langium/src/lsp/language-server.ts index c4a57975d..b8fff429b 100644 --- a/packages/langium/src/lsp/language-server.ts +++ b/packages/langium/src/lsp/language-server.ts @@ -87,7 +87,7 @@ export class DefaultLanguageServer implements LanguageServer { protected buildInitializeResult(_params: InitializeParams): InitializeResult { const languages = this.services.ServiceRegistry.all; - const fileOperationOptions = this.services.lsp.DocumentUpdateHandler.fileOperationOptions; + const fileOperationOptions = this.services.lsp.FileOperationHandler?.fileOperationOptions; const hasFormattingService = this.hasService(e => e.lsp.Formatter); const formattingOnTypeOptions = languages.map(e => e.lsp.Formatter?.formatOnTypeOptions).find(e => Boolean(e)); const hasCodeActionProvider = this.hasService(e => e.lsp.CodeActionProvider); @@ -183,6 +183,7 @@ export function startLanguageServer(services: LangiumSharedServices): void { } addDocumentsHandler(connection, services); + addFileOperationHandler(connection, services); addDiagnosticsHandler(connection, services); addCompletionHandler(connection, services); addFindReferencesHandler(connection, services); @@ -232,6 +233,13 @@ export function addDocumentsHandler(connection: Connection, services: LangiumSha if (handler.didChangeWatchedFiles) { connection.onDidChangeWatchedFiles(params => handler.didChangeWatchedFiles!(params)); } +} + +export function addFileOperationHandler(connection: Connection, services: LangiumSharedServices): void { + const handler = services.lsp.FileOperationHandler; + if (!handler) { + return; + } if (handler.didCreateFiles) { connection.workspace.onDidCreateFiles(params => handler.didCreateFiles!(params)); } diff --git a/packages/langium/src/services.ts b/packages/langium/src/services.ts index bc380ee1f..effacfd8a 100644 --- a/packages/langium/src/services.ts +++ b/packages/langium/src/services.ts @@ -56,6 +56,7 @@ import type { DeclarationProvider } from './lsp/declaration-provider.js'; import type { DocumentLinkProvider } from './lsp/document-link-provider.js'; import type { DocumentUpdateHandler } from './lsp/document-update-handler.js'; import type { DocumentationProvider } from './documentation/documentation-provider.js'; +import type { FileOperationHandler } from './lsp/file-operation-handler.js'; import type { InlayHintProvider } from './lsp/inlay-hint-provider.js'; import type { CommentProvider } from './documentation/comment-provider.js'; import type { WorkspaceSymbolProvider } from './lsp/workspace-symbol-provider.js'; @@ -166,6 +167,7 @@ export type LangiumDefaultSharedServices = { LanguageServer: LanguageServer DocumentUpdateHandler: DocumentUpdateHandler ExecuteCommandHandler?: ExecuteCommandHandler + FileOperationHandler?: FileOperationHandler WorkspaceSymbolProvider?: WorkspaceSymbolProvider NodeKindProvider: NodeKindProvider FuzzyMatcher: FuzzyMatcher From d2cfea2effb0f24f7a5b9383d6935f68016f37ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Sp=C3=B6nemann?= Date: Thu, 16 Nov 2023 14:15:44 +0000 Subject: [PATCH 3/4] Addressed review comments --- packages/langium/src/lsp/language-server.ts | 12 +++----- packages/langium/src/services.ts | 34 ++++++++++----------- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/packages/langium/src/lsp/language-server.ts b/packages/langium/src/lsp/language-server.ts index b8fff429b..1542ad1a2 100644 --- a/packages/langium/src/lsp/language-server.ts +++ b/packages/langium/src/lsp/language-server.ts @@ -182,7 +182,7 @@ export function startLanguageServer(services: LangiumSharedServices): void { throw new Error('Starting a language server requires the languageServer.Connection service to be set.'); } - addDocumentsHandler(connection, services); + addDocumentUpdateHandler(connection, services); addFileOperationHandler(connection, services); addDiagnosticsHandler(connection, services); addCompletionHandler(connection, services); @@ -224,15 +224,11 @@ export function startLanguageServer(services: LangiumSharedServices): void { connection.listen(); } -export function addDocumentsHandler(connection: Connection, services: LangiumSharedServices): void { +export function addDocumentUpdateHandler(connection: Connection, services: LangiumSharedServices): void { const handler = services.lsp.DocumentUpdateHandler; const documents = services.workspace.TextDocuments; - if (handler.didChangeContent) { - documents.onDidChangeContent(change => handler.didChangeContent!(change)); - } - if (handler.didChangeWatchedFiles) { - connection.onDidChangeWatchedFiles(params => handler.didChangeWatchedFiles!(params)); - } + documents.onDidChangeContent(change => handler.didChangeContent(change)); + connection.onDidChangeWatchedFiles(params => handler.didChangeWatchedFiles(params)); } export function addFileOperationHandler(connection: Connection, services: LangiumSharedServices): void { diff --git a/packages/langium/src/services.ts b/packages/langium/src/services.ts index effacfd8a..80ad06d6e 100644 --- a/packages/langium/src/services.ts +++ b/packages/langium/src/services.ts @@ -81,26 +81,26 @@ export type LangiumGeneratedServices = { * Services related to the Language Server Protocol (LSP). */ export type LangiumLspServices = { + CallHierarchyProvider?: CallHierarchyProvider + CodeActionProvider?: CodeActionProvider + CodeLensProvider?: CodeLensProvider CompletionProvider?: CompletionProvider + DeclarationProvider?: DeclarationProvider + DefinitionProvider?: DefinitionProvider DocumentHighlightProvider?: DocumentHighlightProvider + DocumentLinkProvider?: DocumentLinkProvider DocumentSymbolProvider?: DocumentSymbolProvider - HoverProvider?: HoverProvider FoldingRangeProvider?: FoldingRangeProvider - DefinitionProvider?: DefinitionProvider - TypeProvider?: TypeDefinitionProvider + Formatter?: Formatter + HoverProvider?: HoverProvider ImplementationProvider?: ImplementationProvider + InlayHintProvider?: InlayHintProvider ReferencesProvider?: ReferencesProvider - CodeActionProvider?: CodeActionProvider - SemanticTokenProvider?: SemanticTokenProvider RenameProvider?: RenameProvider - Formatter?: Formatter + SemanticTokenProvider?: SemanticTokenProvider SignatureHelp?: SignatureHelpProvider - CallHierarchyProvider?: CallHierarchyProvider TypeHierarchyProvider?: TypeHierarchyProvider; - DeclarationProvider?: DeclarationProvider - InlayHintProvider?: InlayHintProvider - CodeLensProvider?: CodeLensProvider - DocumentLinkProvider?: DocumentLinkProvider + TypeProvider?: TypeDefinitionProvider } /** @@ -164,24 +164,24 @@ export type LangiumDefaultSharedServices = { ServiceRegistry: ServiceRegistry lsp: { Connection?: Connection - LanguageServer: LanguageServer DocumentUpdateHandler: DocumentUpdateHandler ExecuteCommandHandler?: ExecuteCommandHandler FileOperationHandler?: FileOperationHandler - WorkspaceSymbolProvider?: WorkspaceSymbolProvider - NodeKindProvider: NodeKindProvider FuzzyMatcher: FuzzyMatcher + LanguageServer: LanguageServer + NodeKindProvider: NodeKindProvider + WorkspaceSymbolProvider?: WorkspaceSymbolProvider } workspace: { + ConfigurationProvider: ConfigurationProvider DocumentBuilder: DocumentBuilder + FileSystemProvider: FileSystemProvider IndexManager: IndexManager LangiumDocuments: LangiumDocuments LangiumDocumentFactory: LangiumDocumentFactory + MutexLock: MutexLock TextDocuments: TextDocuments WorkspaceManager: WorkspaceManager - FileSystemProvider: FileSystemProvider - MutexLock: MutexLock - ConfigurationProvider: ConfigurationProvider } } From a8ae60dabca3d755aff7c4bd7bd8eacbc60ae300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Sp=C3=B6nemann?= Date: Mon, 11 Dec 2023 08:43:47 +0000 Subject: [PATCH 4/4] Added deduplication to file watcher event URIs --- packages/langium/src/lsp/document-update-handler.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/langium/src/lsp/document-update-handler.ts b/packages/langium/src/lsp/document-update-handler.ts index 892aba19b..3ff3a8c7b 100644 --- a/packages/langium/src/lsp/document-update-handler.ts +++ b/packages/langium/src/lsp/document-update-handler.ts @@ -79,8 +79,16 @@ export class DefaultDocumentUpdateHandler implements DocumentUpdateHandler { } didChangeWatchedFiles(params: DidChangeWatchedFilesParams): void { - const changedUris = params.changes.filter(c => c.type !== FileChangeType.Deleted).map(c => URI.parse(c.uri)); - const deletedUris = params.changes.filter(c => c.type === FileChangeType.Deleted).map(c => URI.parse(c.uri)); + const changedUris = stream(params.changes) + .filter(c => c.type !== FileChangeType.Deleted) + .distinct(c => c.uri) + .map(c => URI.parse(c.uri)) + .toArray(); + const deletedUris = stream(params.changes) + .filter(c => c.type === FileChangeType.Deleted) + .distinct(c => c.uri) + .map(c => URI.parse(c.uri)) + .toArray(); this.fireDocumentUpdate(changedUris, deletedUris); }