From a9c8df8b9521786561b03074f1520c1a341ca9cd Mon Sep 17 00:00:00 2001 From: Christian Schneider Date: Fri, 29 Aug 2025 16:54:25 +0200 Subject: [PATCH] langium/lsp: made LSP service minimum document state settings configurable in `startLanguageServer(...)` * added opportunity to distinguish required documents states and workspace states * relaxed some doc state requirements to increase the LS' responsiveness from 'IndexedReferences' to 'Linked', as it is sufficient that the current document is (ideally) linked and all required documents are in state 'ComputedScopes': completion, declaration, definition, typeDefinition, hover, semanticToken * switched requirements of some services to workspace state 'IndexedReferences': findReferences, getImplementation, documentHighlight, callHierarchy, rename --- packages/langium/src/lsp/language-server.ts | 224 +++++++++++++------- 1 file changed, 142 insertions(+), 82 deletions(-) diff --git a/packages/langium/src/lsp/language-server.ts b/packages/langium/src/lsp/language-server.ts index 7953bdc75..cfec1f998 100644 --- a/packages/langium/src/lsp/language-server.ts +++ b/packages/langium/src/lsp/language-server.ts @@ -220,7 +220,50 @@ export class DefaultLanguageServer implements LanguageServer { } } -export function startLanguageServer(services: LangiumSharedServices): void { +export interface ServiceRequirements { + readonly CallHierarchyProvider: ServiceRequirement + readonly CodeActionProvider: ServiceRequirement + readonly CodeLensProvider: ServiceRequirement + readonly CompletionProvider: ServiceRequirement + readonly DeclarationProvider: ServiceRequirement + readonly DefinitionProvider: ServiceRequirement + readonly DocumentHighlightProvider: ServiceRequirement + readonly DocumentLinkProvider: ServiceRequirement + readonly DocumentSymbolProvider: ServiceRequirement + readonly FoldingRangeProvider: ServiceRequirement + readonly Formatter: ServiceRequirement + readonly HoverProvider: ServiceRequirement + readonly ImplementationProvider: ServiceRequirement + readonly InlayHintProvider: ServiceRequirement + readonly ReferencesProvider: ServiceRequirement + readonly RenameProvider: ServiceRequirement + readonly SemanticTokenProvider: ServiceRequirement + readonly SignatureHelp: ServiceRequirement + readonly TypeHierarchyProvider: ServiceRequirement + readonly TypeProvider: ServiceRequirement + readonly WorkspaceSymbolProvider: ServiceRequirement +} + +export type ServiceRequirement = DocumentState | { + // Either wait for the specific document or for the whole workspace to arrive at the given state + readonly type: 'document' | 'workspace'; + readonly state: DocumentState; +} + +function isDocumentState(obj: ServiceRequirement): obj is DocumentState { + return typeof obj === 'number'; +} + +export namespace WorkspaceState { + export const Parsed: ServiceRequirement = Object.freeze({ type: 'workspace', state: DocumentState.Parsed }); + export const IndexedContent: ServiceRequirement = Object.freeze({ type: 'workspace', state: DocumentState.IndexedContent }); + export const ComputedScopes: ServiceRequirement = Object.freeze({ type: 'workspace', state: DocumentState.ComputedScopes }); + export const Linked: ServiceRequirement = Object.freeze({ type: 'workspace', state: DocumentState.Linked }); + export const IndexedReferences: ServiceRequirement = Object.freeze({ type: 'workspace', state: DocumentState.IndexedReferences }); + export const Validated: ServiceRequirement = Object.freeze({ type: 'workspace', state: DocumentState.Validated }); +} + +export function startLanguageServer(services: LangiumSharedServices, serviceRequirements: Partial = {}): void { const connection = services.lsp.Connection; if (!connection) { throw new Error('Starting a language server requires the languageServer.Connection service to be set.'); @@ -229,29 +272,29 @@ export function startLanguageServer(services: LangiumSharedServices): void { addDocumentUpdateHandler(connection, services); addFileOperationHandler(connection, services); addDiagnosticsHandler(connection, services); - addCompletionHandler(connection, services); - addFindReferencesHandler(connection, services); - addDocumentSymbolHandler(connection, services); - addGotoDefinitionHandler(connection, services); - addGoToTypeDefinitionHandler(connection, services); - addGoToImplementationHandler(connection, services); - addDocumentHighlightsHandler(connection, services); - addFoldingRangeHandler(connection, services); - addFormattingHandler(connection, services); - addCodeActionHandler(connection, services); - addRenameHandler(connection, services); - addHoverHandler(connection, services); - addInlayHintHandler(connection, services); - addSemanticTokenHandler(connection, services); + addCompletionHandler(connection, services, serviceRequirements.CompletionProvider); + addFindReferencesHandler(connection, services, serviceRequirements.ReferencesProvider); + addDocumentSymbolHandler(connection, services, serviceRequirements.DocumentSymbolProvider); + addGotoDefinitionHandler(connection, services, serviceRequirements.DefinitionProvider); + addGoToTypeDefinitionHandler(connection, services, serviceRequirements.TypeProvider); + addGoToImplementationHandler(connection, services, serviceRequirements.ImplementationProvider); + addDocumentHighlightHandler(connection, services, serviceRequirements.DocumentHighlightProvider); + addFoldingRangeHandler(connection, services, serviceRequirements.FoldingRangeProvider); + addFormattingHandler(connection, services, serviceRequirements.Formatter); + addCodeActionHandler(connection, services, serviceRequirements.CodeActionProvider); + addRenameHandler(connection, services, serviceRequirements.RenameProvider); + addHoverHandler(connection, services, serviceRequirements.HoverProvider); + addInlayHintHandler(connection, services, serviceRequirements.InlayHintProvider); + addSemanticTokenHandler(connection, services, serviceRequirements.SemanticTokenProvider); addExecuteCommandHandler(connection, services); - addSignatureHelpHandler(connection, services); - addCallHierarchyHandler(connection, services); - addTypeHierarchyHandler(connection, services); - addCodeLensHandler(connection, services); - addDocumentLinkHandler(connection, services); + addSignatureHelpHandler(connection, services, serviceRequirements.SignatureHelp); + addCallHierarchyHandler(connection, services, serviceRequirements.CallHierarchyProvider); + addTypeHierarchyHandler(connection, services, serviceRequirements.TypeHierarchyProvider); + addCodeLensHandler(connection, services, serviceRequirements.CodeLensProvider); + addDocumentLinkHandler(connection, services, serviceRequirements.DocumentLinkProvider); addConfigurationChangeHandler(connection, services); - addGoToDeclarationHandler(connection, services); - addWorkspaceSymbolHandler(connection, services); + addGoToDeclarationHandler(connection, services, serviceRequirements.DeclarationProvider); + addWorkspaceSymbolHandler(connection, services, serviceRequirements.WorkspaceSymbolProvider); connection.onInitialize(params => { return services.lsp.LanguageServer.initialize(params); @@ -343,136 +386,137 @@ export function addDiagnosticsHandler(connection: Connection, services: LangiumS }); } -export function addCompletionHandler(connection: Connection, services: LangiumSharedServices): void { +export function addCompletionHandler(connection: Connection, services: LangiumSharedServices, requiredState: ServiceRequirement = DocumentState.Linked): void { connection.onCompletion(createRequestHandler( (services, document, params, cancelToken) => { return services.lsp?.CompletionProvider?.getCompletion(document, params, cancelToken); }, services, - DocumentState.IndexedReferences + requiredState )); } -export function addFindReferencesHandler(connection: Connection, services: LangiumSharedServices): void { +export function addFindReferencesHandler(connection: Connection, services: LangiumSharedServices, requiredState: ServiceRequirement = WorkspaceState.IndexedReferences): void { connection.onReferences(createRequestHandler( (services, document, params, cancelToken) => services.lsp?.ReferencesProvider?.findReferences(document, params, cancelToken), services, - DocumentState.IndexedReferences + requiredState )); } -export function addCodeActionHandler(connection: Connection, services: LangiumSharedServices): void { +export function addCodeActionHandler(connection: Connection, services: LangiumSharedServices, requiredState: ServiceRequirement = DocumentState.Validated): void { connection.onCodeAction(createRequestHandler( (services, document, params, cancelToken) => services.lsp?.CodeActionProvider?.getCodeActions(document, params, cancelToken), services, - DocumentState.Validated + requiredState )); } -export function addDocumentSymbolHandler(connection: Connection, services: LangiumSharedServices): void { +export function addDocumentSymbolHandler(connection: Connection, services: LangiumSharedServices, requiredState: ServiceRequirement = DocumentState.Parsed): void { connection.onDocumentSymbol(createRequestHandler( (services, document, params, cancelToken) => services.lsp?.DocumentSymbolProvider?.getSymbols(document, params, cancelToken), services, - DocumentState.Parsed + requiredState )); } -export function addGotoDefinitionHandler(connection: Connection, services: LangiumSharedServices): void { +export function addGotoDefinitionHandler(connection: Connection, services: LangiumSharedServices, requiredState: ServiceRequirement = DocumentState.Linked): void { connection.onDefinition(createRequestHandler( (services, document, params, cancelToken) => services.lsp?.DefinitionProvider?.getDefinition(document, params, cancelToken), services, - DocumentState.IndexedReferences + requiredState )); } -export function addGoToTypeDefinitionHandler(connection: Connection, services: LangiumSharedServices): void { +export function addGoToTypeDefinitionHandler(connection: Connection, services: LangiumSharedServices, requiredState: ServiceRequirement = DocumentState.Linked): void { connection.onTypeDefinition(createRequestHandler( (services, document, params, cancelToken) => services.lsp?.TypeProvider?.getTypeDefinition(document, params, cancelToken), services, - DocumentState.IndexedReferences + requiredState )); } -export function addGoToImplementationHandler(connection: Connection, services: LangiumSharedServices) { +export function addGoToImplementationHandler(connection: Connection, services: LangiumSharedServices, requiredState: ServiceRequirement = WorkspaceState.IndexedReferences): void { connection.onImplementation(createRequestHandler( (services, document, params, cancelToken) => services.lsp?.ImplementationProvider?.getImplementation(document, params, cancelToken), services, - DocumentState.IndexedReferences + requiredState )); } -export function addGoToDeclarationHandler(connection: Connection, services: LangiumSharedServices) { +export function addGoToDeclarationHandler(connection: Connection, services: LangiumSharedServices, requiredState: ServiceRequirement = DocumentState.Linked): void { connection.onDeclaration(createRequestHandler( (services, document, params, cancelToken) => services.lsp?.DeclarationProvider?.getDeclaration(document, params, cancelToken), services, - DocumentState.IndexedReferences + requiredState )); } -export function addDocumentHighlightsHandler(connection: Connection, services: LangiumSharedServices): void { +export function addDocumentHighlightHandler(connection: Connection, services: LangiumSharedServices, requiredState: ServiceRequirement = WorkspaceState.IndexedReferences): void { connection.onDocumentHighlight(createRequestHandler( (services, document, params, cancelToken) => services.lsp?.DocumentHighlightProvider?.getDocumentHighlight(document, params, cancelToken), services, - DocumentState.IndexedReferences + requiredState )); } -export function addHoverHandler(connection: Connection, services: LangiumSharedServices): void { +export function addHoverHandler(connection: Connection, services: LangiumSharedServices, requiredState: ServiceRequirement = DocumentState.Linked): void { connection.onHover(createRequestHandler( (services, document, params, cancelToken) => services.lsp?.HoverProvider?.getHoverContent(document, params, cancelToken), services, - DocumentState.IndexedReferences + requiredState )); } -export function addFoldingRangeHandler(connection: Connection, services: LangiumSharedServices): void { +export function addFoldingRangeHandler(connection: Connection, services: LangiumSharedServices, requiredState: ServiceRequirement = DocumentState.Parsed): void { connection.onFoldingRanges(createRequestHandler( (services, document, params, cancelToken) => services.lsp?.FoldingRangeProvider?.getFoldingRanges(document, params, cancelToken), services, - DocumentState.Parsed + requiredState )); } -export function addFormattingHandler(connection: Connection, services: LangiumSharedServices): void { +export function addFormattingHandler(connection: Connection, services: LangiumSharedServices, requiredState: ServiceRequirement = DocumentState.Parsed): void { connection.onDocumentFormatting(createRequestHandler( (services, document, params, cancelToken) => services.lsp?.Formatter?.formatDocument(document, params, cancelToken), services, - DocumentState.Parsed + requiredState )); connection.onDocumentRangeFormatting(createRequestHandler( (services, document, params, cancelToken) => services.lsp?.Formatter?.formatDocumentRange(document, params, cancelToken), services, - DocumentState.Parsed + requiredState )); connection.onDocumentOnTypeFormatting(createRequestHandler( (services, document, params, cancelToken) => services.lsp?.Formatter?.formatDocumentOnType(document, params, cancelToken), services, - DocumentState.Parsed + requiredState )); } -export function addRenameHandler(connection: Connection, services: LangiumSharedServices): void { +export function addRenameHandler(connection: Connection, services: LangiumSharedServices, requiredState: ServiceRequirement = WorkspaceState.IndexedReferences): void { + connection.onRenameRequest(createRequestHandler( (services, document, params, cancelToken) => services.lsp?.RenameProvider?.rename(document, params, cancelToken), services, - DocumentState.IndexedReferences + requiredState )); connection.onPrepareRename(createRequestHandler( (services, document, params, cancelToken) => services.lsp?.RenameProvider?.prepareRename(document, params, cancelToken), services, - DocumentState.IndexedReferences + requiredState )); } -export function addInlayHintHandler(connection: Connection, services: LangiumSharedServices): void { +export function addInlayHintHandler(connection: Connection, services: LangiumSharedServices, requiredState: ServiceRequirement = DocumentState.IndexedReferences): void { connection.languages.inlayHint.on(createServerRequestHandler( (services, document, params, cancelToken) => services.lsp?.InlayHintProvider?.getInlayHints(document, params, cancelToken), services, - DocumentState.IndexedReferences + requiredState )); } -export function addSemanticTokenHandler(connection: Connection, services: LangiumSharedServices): void { +export function addSemanticTokenHandler(connection: Connection, services: LangiumSharedServices, requiredState: ServiceRequirement = DocumentState.Linked): void { // If no semantic token provider is registered that's fine. Just return an empty result const emptyResult: SemanticTokens = { data: [] }; connection.languages.semanticTokens.on(createServerRequestHandler( @@ -483,7 +527,7 @@ export function addSemanticTokenHandler(connection: Connection, services: Langiu return emptyResult; }, services, - DocumentState.IndexedReferences + requiredState )); connection.languages.semanticTokens.onDelta(createServerRequestHandler( (services, document, params, cancelToken) => { @@ -493,7 +537,7 @@ export function addSemanticTokenHandler(connection: Connection, services: Langiu return emptyResult; }, services, - DocumentState.IndexedReferences + requiredState )); connection.languages.semanticTokens.onRange(createServerRequestHandler( (services, document, params, cancelToken) => { @@ -503,7 +547,7 @@ export function addSemanticTokenHandler(connection: Connection, services: Langiu return emptyResult; }, services, - DocumentState.IndexedReferences + requiredState )); } export function addConfigurationChangeHandler(connection: Connection, services: LangiumSharedServices): void { @@ -525,37 +569,42 @@ export function addExecuteCommandHandler(connection: Connection, services: Langi } } -export function addDocumentLinkHandler(connection: Connection, services: LangiumSharedServices): void { +export function addDocumentLinkHandler(connection: Connection, services: LangiumSharedServices, requiredState: ServiceRequirement = DocumentState.Parsed): void { connection.onDocumentLinks(createServerRequestHandler( (services, document, params, cancelToken) => services.lsp?.DocumentLinkProvider?.getDocumentLinks(document, params, cancelToken), services, - DocumentState.Parsed + requiredState )); } -export function addSignatureHelpHandler(connection: Connection, services: LangiumSharedServices): void { +export function addSignatureHelpHandler(connection: Connection, services: LangiumSharedServices, requiredState: ServiceRequirement = DocumentState.IndexedReferences): void { connection.onSignatureHelp(createServerRequestHandler( (services, document, params, cancelToken) => services.lsp?.SignatureHelp?.provideSignatureHelp(document, params, cancelToken), services, - DocumentState.IndexedReferences + requiredState )); } -export function addCodeLensHandler(connection: Connection, services: LangiumSharedServices): void { +export function addCodeLensHandler(connection: Connection, services: LangiumSharedServices, requiredState: ServiceRequirement = DocumentState.IndexedReferences): void { connection.onCodeLens(createServerRequestHandler( (services, document, params, cancelToken) => services.lsp?.CodeLensProvider?.provideCodeLens(document, params, cancelToken), services, - DocumentState.IndexedReferences + requiredState )); } -export function addWorkspaceSymbolHandler(connection: Connection, services: LangiumSharedServices): void { +export function addWorkspaceSymbolHandler(connection: Connection, services: LangiumSharedServices, requiredState: ServiceRequirement = WorkspaceState.IndexedContent): void { const workspaceSymbolProvider = services.lsp.WorkspaceSymbolProvider; if (workspaceSymbolProvider) { + + if (isDocumentState(requiredState) || requiredState.type === 'document') { + throw new Error('Workspace symbol requests are independent of a certain document, so the given document-specific required state is invalid. Provide a service requirement of type "workspace".'); + } + const documentBuilder = services.workspace.DocumentBuilder; connection.onWorkspaceSymbol(async (params, token) => { try { - await documentBuilder.waitUntil(DocumentState.IndexedContent, token); + await documentBuilder.waitUntil(requiredState.state, token); return await workspaceSymbolProvider.getSymbols(params, token); } catch (err) { return responseError(err); @@ -565,7 +614,7 @@ export function addWorkspaceSymbolHandler(connection: Connection, services: Lang if (resolveWorkspaceSymbol) { connection.onWorkspaceSymbolResolve(async (workspaceSymbol, token) => { try { - await documentBuilder.waitUntil(DocumentState.IndexedContent, token); + await documentBuilder.waitUntil(requiredState.state, token); return await resolveWorkspaceSymbol(workspaceSymbol, token); } catch (err) { return responseError(err); @@ -575,7 +624,7 @@ export function addWorkspaceSymbolHandler(connection: Connection, services: Lang } } -export function addCallHierarchyHandler(connection: Connection, services: LangiumSharedServices): void { +export function addCallHierarchyHandler(connection: Connection, services: LangiumSharedServices, requiredState: ServiceRequirement = WorkspaceState.IndexedReferences): void { connection.languages.callHierarchy.onPrepare(createServerRequestHandler( async (services, document, params, cancelToken) => { if (services.lsp?.CallHierarchyProvider) { @@ -585,7 +634,7 @@ export function addCallHierarchyHandler(connection: Connection, services: Langiu return null; }, services, - DocumentState.IndexedReferences + requiredState )); connection.languages.callHierarchy.onIncomingCalls(createHierarchyRequestHandler( @@ -596,7 +645,8 @@ export function addCallHierarchyHandler(connection: Connection, services: Langiu } return null; }, - services + services, + requiredState )); connection.languages.callHierarchy.onOutgoingCalls(createHierarchyRequestHandler( @@ -607,11 +657,12 @@ export function addCallHierarchyHandler(connection: Connection, services: Langiu } return null; }, - services + services, + requiredState )); } -export function addTypeHierarchyHandler(connection: Connection, sharedServices: LangiumSharedServices): void { +export function addTypeHierarchyHandler(connection: Connection, sharedServices: LangiumSharedServices, requiredState: ServiceRequirement = WorkspaceState.IndexedReferences): void { // Don't register type hierarchy handlers if no type hierarchy provider is registered if (!sharedServices.ServiceRegistry.all.some((services: LangiumCoreAndPartialLSPServices) => services.lsp?.TypeHierarchyProvider)) { return; @@ -624,7 +675,7 @@ export function addTypeHierarchyHandler(connection: Connection, sharedServices: return result ?? null; }, sharedServices, - DocumentState.IndexedReferences + requiredState ), ); @@ -634,7 +685,8 @@ export function addTypeHierarchyHandler(connection: Connection, sharedServices: const result = await services.lsp?.TypeHierarchyProvider?.supertypes(params, cancelToken); return result ?? null; }, - sharedServices + sharedServices, + requiredState ), ); @@ -644,7 +696,8 @@ export function addTypeHierarchyHandler(connection: Connection, sharedServices: const result = await services.lsp?.TypeHierarchyProvider?.subtypes(params, cancelToken); return result ?? null; }, - sharedServices + sharedServices, + requiredState ), ); } @@ -652,11 +705,12 @@ export function addTypeHierarchyHandler(connection: Connection, sharedServices: export function createHierarchyRequestHandler

( serviceCall: (services: LangiumCoreAndPartialLSPServices, params: P, cancelToken: CancellationToken) => HandlerResult, sharedServices: LangiumSharedServices, + requiredState?: ServiceRequirement ): ServerRequestHandler { const serviceRegistry = sharedServices.ServiceRegistry; return async (params: P, cancelToken: CancellationToken) => { const uri = URI.parse(params.item.uri); - const cancellationError = await waitUntilPhase(sharedServices, cancelToken, uri, DocumentState.IndexedReferences); + const cancellationError = await waitUntilPhase(sharedServices, cancelToken, uri, requiredState); if (cancellationError) { return cancellationError; } @@ -677,13 +731,13 @@ export function createHierarchyRequestHandler

( serviceCall: (services: LangiumCoreAndPartialLSPServices, document: LangiumDocument, params: P, cancelToken: CancellationToken) => HandlerResult, sharedServices: LangiumSharedServices, - targetState?: DocumentState + requiredState?: ServiceRequirement ): ServerRequestHandler { const documents = sharedServices.workspace.LangiumDocuments; const serviceRegistry = sharedServices.ServiceRegistry; return async (params: P, cancelToken: CancellationToken) => { const uri = URI.parse(params.textDocument.uri); - const cancellationError = await waitUntilPhase(sharedServices, cancelToken, uri, targetState); + const cancellationError = await waitUntilPhase(sharedServices, cancelToken, uri, requiredState); if (cancellationError) { return cancellationError; } @@ -705,13 +759,13 @@ export function createServerRequestHandler

( serviceCall: (services: LangiumCoreAndPartialLSPServices, document: LangiumDocument, params: P, cancelToken: CancellationToken) => HandlerResult, sharedServices: LangiumSharedServices, - targetState?: DocumentState + requiredState?: ServiceRequirement ): RequestHandler { const documents = sharedServices.workspace.LangiumDocuments; const serviceRegistry = sharedServices.ServiceRegistry; return async (params: P, cancelToken: CancellationToken) => { const uri = URI.parse(params.textDocument.uri); - const cancellationError = await waitUntilPhase(sharedServices, cancelToken, uri, targetState); + const cancellationError = await waitUntilPhase(sharedServices, cancelToken, uri, requiredState); if (cancellationError) { return cancellationError; } @@ -729,11 +783,17 @@ export function createRequestHandler

(services: LangiumSharedServices, cancelToken: CancellationToken, uri?: URI, targetState?: DocumentState): Promise | undefined> { - if (targetState !== undefined) { +async function waitUntilPhase(services: LangiumSharedServices, cancelToken: CancellationToken, uri?: URI, requiredState?: ServiceRequirement): Promise | undefined> { + if (requiredState !== undefined) { const documentBuilder = services.workspace.DocumentBuilder; try { - await documentBuilder.waitUntil(targetState, uri, cancelToken); + if (isDocumentState(requiredState)) { + await documentBuilder.waitUntil(requiredState, uri, cancelToken); + } else if (requiredState.type === 'document') { + await documentBuilder.waitUntil(requiredState.state, uri, cancelToken); + } else { + await documentBuilder.waitUntil(requiredState.state, cancelToken); + } } catch (err) { return responseError(err); }