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..3ff3a8c7b --- /dev/null +++ b/packages/langium/src/lsp/document-update-handler.ts @@ -0,0 +1,95 @@ +/****************************************************************************** + * 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 { DidChangeWatchedFilesParams, DidChangeWatchedFilesRegistrationOptions, TextDocumentChangeEvent } from 'vscode-languageserver'; +import type { TextDocument } from 'vscode-languageserver-textdocument'; +import type { LangiumSharedServices } from '../services.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 text document changes and watching relevant files. + */ +export interface DocumentUpdateHandler { + + /** + * 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; + +} + +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 = 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); + } + +} 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 15f319839..1542ad1a2 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.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); @@ -117,7 +118,8 @@ export class DefaultLanguageServer implements LanguageServer { workspace: { workspaceFolders: { supported: true - } + }, + fileOperations: fileOperationOptions }, executeCommandProvider: commandNames && { commands: commandNames @@ -180,7 +182,8 @@ 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); addFindReferencesHandler(connection, services); @@ -221,31 +224,36 @@ export function startLanguageServer(services: LangiumSharedServices): void { connection.listen(); } -export function addDocumentsHandler(connection: Connection, services: LangiumSharedServices): void { - const documentBuilder = services.workspace.DocumentBuilder; - const mutex = services.workspace.MutexLock; +export function addDocumentUpdateHandler(connection: Connection, services: LangiumSharedServices): void { + const handler = services.lsp.DocumentUpdateHandler; + const documents = services.workspace.TextDocuments; + documents.onDidChangeContent(change => handler.didChangeContent(change)); + connection.onDidChangeWatchedFiles(params => handler.didChangeWatchedFiles(params)); +} - function onDidChange(changed: URI[], deleted: URI[]): void { - mutex.lock(token => documentBuilder.update(changed, deleted, token)); +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)); + } + 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)); } - - 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); - }); } export function addDiagnosticsHandler(connection: Connection, services: LangiumSharedServices): void { diff --git a/packages/langium/src/services.ts b/packages/langium/src/services.ts index 8aa9fc42e..80ad06d6e 100644 --- a/packages/langium/src/services.ts +++ b/packages/langium/src/services.ts @@ -51,10 +51,12 @@ 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 { 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'; @@ -79,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 } /** @@ -162,22 +164,24 @@ export type LangiumDefaultSharedServices = { ServiceRegistry: ServiceRegistry lsp: { Connection?: Connection + DocumentUpdateHandler: DocumentUpdateHandler ExecuteCommandHandler?: ExecuteCommandHandler - WorkspaceSymbolProvider?: WorkspaceSymbolProvider - NodeKindProvider: NodeKindProvider + FileOperationHandler?: FileOperationHandler 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 } }