Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Perform read lock on LSP requests #1640

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions examples/domainmodel/src/language-server/domain-model-scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { AstNode, AstNodeDescription, LangiumDocument, PrecomputedScopes }
import type { DomainModelServices } from './domain-model-module.js';
import type { QualifiedNameProvider } from './domain-model-naming.js';
import type { Domainmodel, PackageDeclaration } from './generated/ast.js';
import { AstUtils, Cancellation, DefaultScopeComputation, interruptAndCheck, MultiMap } from 'langium';
import { AstUtils, DefaultScopeComputation, MultiMap, CancellationToken } from 'langium';
import { isType, isPackageDeclaration } from './generated/ast.js';

export class DomainModelScopeComputation extends DefaultScopeComputation {
Expand All @@ -23,10 +23,10 @@ export class DomainModelScopeComputation extends DefaultScopeComputation {
/**
* Exports only types (`DataType or `Entity`) with their qualified names.
*/
override async computeExports(document: LangiumDocument, cancelToken = Cancellation.CancellationToken.None): Promise<AstNodeDescription[]> {
override async computeExports(document: LangiumDocument, cancelToken = CancellationToken.None): Promise<AstNodeDescription[]> {
const descr: AstNodeDescription[] = [];
for (const modelNode of AstUtils.streamAllContents(document.parseResult.value)) {
await interruptAndCheck(cancelToken);
await cancelToken.check();
if (isType(modelNode)) {
let name = this.nameProvider.getName(modelNode);
if (name) {
Expand All @@ -40,17 +40,17 @@ export class DomainModelScopeComputation extends DefaultScopeComputation {
return descr;
}

override async computeLocalScopes(document: LangiumDocument, cancelToken = Cancellation.CancellationToken.None): Promise<PrecomputedScopes> {
override async computeLocalScopes(document: LangiumDocument, cancelToken = CancellationToken.None): Promise<PrecomputedScopes> {
const model = document.parseResult.value as Domainmodel;
const scopes = new MultiMap<AstNode, AstNodeDescription>();
await this.processContainer(model, scopes, document, cancelToken);
return scopes;
}

protected async processContainer(container: Domainmodel | PackageDeclaration, scopes: PrecomputedScopes, document: LangiumDocument, cancelToken: Cancellation.CancellationToken): Promise<AstNodeDescription[]> {
protected async processContainer(container: Domainmodel | PackageDeclaration, scopes: PrecomputedScopes, document: LangiumDocument, cancelToken: CancellationToken): Promise<AstNodeDescription[]> {
const localDescriptions: AstNodeDescription[] = [];
for (const element of container.elements) {
await interruptAndCheck(cancelToken);
await cancelToken.check();
if (isType(element) && element.name) {
const description = this.descriptions.createDescription(element, element.name, document);
localDescriptions.push(description);
Expand Down
8 changes: 4 additions & 4 deletions packages/langium-sprotty/src/diagram-server-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
* terms of the MIT License, which is available in the project root.
******************************************************************************/

import type { CancellationToken, Connection } from 'vscode-languageserver';
import type { Connection } from 'vscode-languageserver';
import type { ActionMessage, DiagramOptions, DiagramServer, RequestModelAction } from 'sprotty-protocol';
import type { LangiumDocument, ServiceRegistry, URI } from 'langium';
import type { CancellationToken, LangiumDocument, ServiceRegistry, URI } from 'langium';
import type { LangiumSprottyServices, LangiumSprottySharedServices } from './sprotty-services.js';
import type { LangiumDiagramGeneratorArguments } from './diagram-generator.js';
import { isRequestAction, RejectAction } from 'sprotty-protocol';
import { DocumentState, UriUtils, interruptAndCheck, stream } from 'langium';
import { DocumentState, UriUtils, stream } from 'langium';
import { DiagramActionNotification } from './lsp.js';

/**
Expand Down Expand Up @@ -89,7 +89,7 @@ export class DefaultDiagramServerManager implements DiagramServerManager {

protected async updateDiagrams(documents: Map<LangiumDocument, DiagramServer[]>, cancelToken: CancellationToken): Promise<void> {
while (documents.size > 0) {
await interruptAndCheck(cancelToken);
await cancelToken.check();
const [firstEntry] = documents;
const [document, diagramServers] = firstEntry;
const language = this.serviceRegistry.getServices(document.uri) as LangiumSprottyServices;
Expand Down
3 changes: 1 addition & 2 deletions packages/langium/src/lsp/inlay-hint-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { CancellationToken } from '../utils/cancellation.js';
import type { MaybePromise } from '../utils/promise-utils.js';
import type { LangiumDocument } from '../workspace/documents.js';
import { streamAst } from '../utils/ast-utils.js';
import { interruptAndCheck } from '../utils/promise-utils.js';

export type InlayHintAcceptor = (inlayHint: InlayHint) => void;

Expand All @@ -33,7 +32,7 @@ export abstract class AbstractInlayHintProvider implements InlayHintProvider {
const inlayHints: InlayHint[] = [];
const acceptor: InlayHintAcceptor = hint => inlayHints.push(hint);
for (const node of streamAst(root, { range: params.range })) {
await interruptAndCheck(cancelToken);
await cancelToken.check();
this.computeInlayHint(node, acceptor);
}
return inlayHints;
Expand Down
64 changes: 46 additions & 18 deletions packages/langium/src/lsp/language-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import type {
CallHierarchyIncomingCallsParams,
CallHierarchyOutgoingCallsParams,
CancellationToken,
CancellationToken as LSPCancellationToken,
Connection,
Disposable,
Event,
Expand All @@ -31,14 +31,15 @@ import type {
import { DidChangeConfigurationNotification, Emitter, LSPErrorCodes, ResponseError, TextDocumentSyncKind } from 'vscode-languageserver-protocol';
import { eagerLoad } from '../dependency-injection.js';
import type { LangiumCoreServices } from '../services.js';
import { isOperationCancelled } from '../utils/promise-utils.js';
import { URI } from '../utils/uri-utils.js';
import type { ConfigurationInitializedParams } from '../workspace/configuration.js';
import { DocumentState, type LangiumDocument } from '../workspace/documents.js';
import { mergeCompletionProviderOptions } from './completion/completion-provider.js';
import type { LangiumSharedServices, PartialLangiumLSPServices } from './lsp-services.js';
import { DefaultSemanticTokenOptions } from './semantic-token-provider.js';
import { mergeSignatureHelpOptions } from './signature-help-provider.js';
import { WorkspaceLockPriority } from '../workspace/workspace-lock.js';
import { CancellationToken, isOperationCancelled } from '../utils/cancellation.js';

export interface LanguageServer {
initialize(params: InitializeParams): Promise<InitializeResult>
Expand Down Expand Up @@ -518,7 +519,7 @@ export function addExecuteCommandHandler(connection: Connection, services: Langi
if (commandHandler) {
connection.onExecuteCommand(async (params, token) => {
try {
return await commandHandler.executeCommand(params.command, params.arguments ?? [], token);
return await commandHandler.executeCommand(params.command, params.arguments ?? [], CancellationToken.create(token));
} catch (err) {
return responseError(err);
}
Expand Down Expand Up @@ -552,12 +553,15 @@ export function addCodeLensHandler(connection: Connection, services: LangiumShar

export function addWorkspaceSymbolHandler(connection: Connection, services: LangiumSharedServices): void {
const workspaceSymbolProvider = services.lsp.WorkspaceSymbolProvider;
const lock = services.workspace.WorkspaceLock;
if (workspaceSymbolProvider) {
const documentBuilder = services.workspace.DocumentBuilder;
connection.onWorkspaceSymbol(async (params, token) => {
try {
await documentBuilder.waitUntil(DocumentState.IndexedContent, token);
return await workspaceSymbolProvider.getSymbols(params, token);
const cancellationToken = CancellationToken.create(token);
await documentBuilder.waitUntil(DocumentState.IndexedContent, cancellationToken);
const result = await lock.read(() => workspaceSymbolProvider.getSymbols(params, cancellationToken), WorkspaceLockPriority.Immediate);
return result;
} catch (err) {
return responseError(err);
}
Expand All @@ -566,8 +570,10 @@ export function addWorkspaceSymbolHandler(connection: Connection, services: Lang
if (resolveWorkspaceSymbol) {
connection.onWorkspaceSymbolResolve(async (workspaceSymbol, token) => {
try {
await documentBuilder.waitUntil(DocumentState.IndexedContent, token);
return await resolveWorkspaceSymbol(workspaceSymbol, token);
const cancellationToken = CancellationToken.create(token);
await documentBuilder.waitUntil(DocumentState.IndexedContent, cancellationToken);
const result = await lock.read(() => resolveWorkspaceSymbol(workspaceSymbol, cancellationToken), WorkspaceLockPriority.Immediate);
return result;
} catch (err) {
return responseError(err);
}
Expand Down Expand Up @@ -654,10 +660,12 @@ export function createHierarchyRequestHandler<P extends TypeHierarchySupertypesP
serviceCall: (services: LangiumCoreAndPartialLSPServices, params: P, cancelToken: CancellationToken) => HandlerResult<R, E>,
sharedServices: LangiumSharedServices,
): ServerRequestHandler<P, R, PR, E> {
const lock = sharedServices.workspace.WorkspaceLock;
const serviceRegistry = sharedServices.ServiceRegistry;
return async (params: P, cancelToken: CancellationToken) => {
return async (params: P, cancelToken: LSPCancellationToken) => {
const uri = URI.parse(params.item.uri);
const cancellationError = await waitUntilPhase<E>(sharedServices, cancelToken, uri, DocumentState.IndexedReferences);
const token = CancellationToken.create(cancelToken);
const cancellationError = await waitUntilPhase<E>(sharedServices, token, uri, DocumentState.IndexedReferences);
if (cancellationError) {
return cancellationError;
}
Expand All @@ -668,7 +676,9 @@ export function createHierarchyRequestHandler<P extends TypeHierarchySupertypesP
}
const language = serviceRegistry.getServices(uri);
try {
return await serviceCall(language, params, cancelToken);
// Give this priority, since we already waited until the target state
const result = await lock.read(async () => await serviceCall(language, params, token), WorkspaceLockPriority.Immediate);
return result;
} catch (err) {
return responseError<E>(err);
}
Expand All @@ -681,10 +691,12 @@ export function createServerRequestHandler<P extends { textDocument: TextDocumen
targetState?: DocumentState
): ServerRequestHandler<P, R, PR, E> {
const documents = sharedServices.workspace.LangiumDocuments;
const lock = sharedServices.workspace.WorkspaceLock;
const serviceRegistry = sharedServices.ServiceRegistry;
return async (params: P, cancelToken: CancellationToken) => {
return async (params: P, cancelToken: LSPCancellationToken) => {
const uri = URI.parse(params.textDocument.uri);
const cancellationError = await waitUntilPhase<E>(sharedServices, cancelToken, uri, targetState);
const token = CancellationToken.create(cancelToken);
const cancellationError = await waitUntilPhase<E>(sharedServices, token, uri, targetState);
if (cancellationError) {
return cancellationError;
}
Expand All @@ -694,9 +706,16 @@ export function createServerRequestHandler<P extends { textDocument: TextDocumen
return responseError<E>(new Error(errorText));
}
const language = serviceRegistry.getServices(uri);
const document = documents.getDocument(uri);
if (!document) {
const errorText = `Could not find document for uri: '${uri}'`;
console.debug(errorText);
return responseError<E>(new Error(errorText));
}
try {
const document = await documents.getOrCreateDocument(uri);
return await serviceCall(language, document, params, cancelToken);
// Give this priority, since we already waited until the target state
const result = await lock.read(async () => await serviceCall(language, document, params, token), WorkspaceLockPriority.Immediate);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Detail: the async / await in the inline function is not necessary because simply returning the promise returned by serviceCall has the same effect.

return result;
} catch (err) {
return responseError<E>(err);
}
Expand All @@ -709,10 +728,12 @@ export function createRequestHandler<P extends { textDocument: TextDocumentIdent
targetState?: DocumentState
): RequestHandler<P, R | null, E> {
const documents = sharedServices.workspace.LangiumDocuments;
const lock = sharedServices.workspace.WorkspaceLock;
const serviceRegistry = sharedServices.ServiceRegistry;
return async (params: P, cancelToken: CancellationToken) => {
return async (params: P, cancelToken: LSPCancellationToken) => {
const uri = URI.parse(params.textDocument.uri);
const cancellationError = await waitUntilPhase<E>(sharedServices, cancelToken, uri, targetState);
const token = CancellationToken.create(cancelToken);
const cancellationError = await waitUntilPhase<E>(sharedServices, token, uri, targetState);
if (cancellationError) {
return cancellationError;
}
Expand All @@ -721,9 +742,16 @@ export function createRequestHandler<P extends { textDocument: TextDocumentIdent
return null;
}
const language = serviceRegistry.getServices(uri);
const document = documents.getDocument(uri);
if (!document) {
const errorText = `Could not find document for uri: '${uri}'`;
console.debug(errorText);
return responseError<E>(new Error(errorText));
}
try {
const document = await documents.getOrCreateDocument(uri);
return await serviceCall(language, document, params, cancelToken);
// Give this priority, since we already waited until the target state
const result = await lock.read(async () => await serviceCall(language, document, params, token), WorkspaceLockPriority.Immediate);
return result;
} catch (err) {
return responseError<E>(err);
}
Expand Down
3 changes: 1 addition & 2 deletions packages/langium/src/lsp/semantic-token-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { streamAst } from '../utils/ast-utils.js';
import { inRange } from '../utils/cst-utils.js';
import { findNodeForKeyword, findNodeForProperty, findNodesForKeyword, findNodesForProperty } from '../utils/grammar-utils.js';
import type { MaybePromise } from '../utils/promise-utils.js';
import { interruptAndCheck } from '../utils/promise-utils.js';
import type { LangiumDocument } from '../workspace/documents.js';
import type { LangiumServices } from './lsp-services.js';

Expand Down Expand Up @@ -287,7 +286,7 @@ export abstract class AbstractSemanticTokenProvider implements SemanticTokenProv
do {
result = treeIterator.next();
if (!result.done) {
await interruptAndCheck(cancelToken);
await cancelToken.check();
const node = result.value;
if (this.highlightElement(node, acceptor) === 'prune') {
treeIterator.prune();
Expand Down
3 changes: 1 addition & 2 deletions packages/langium/src/lsp/workspace-symbol-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import type { AstNodeDescription } from '../syntax-tree.js';
import type { NodeKindProvider } from './node-kind-provider.js';
import type { FuzzyMatcher } from './fuzzy-matcher.js';
import { CancellationToken } from '../utils/cancellation.js';
import { interruptAndCheck } from '../utils/promise-utils.js';

/**
* Shared service for handling workspace symbols requests.
Expand Down Expand Up @@ -58,7 +57,7 @@ export class DefaultWorkspaceSymbolProvider implements WorkspaceSymbolProvider {
const workspaceSymbols: WorkspaceSymbol[] = [];
const query = params.query.toLowerCase();
for (const description of this.indexManager.allElements()) {
await interruptAndCheck(cancelToken);
await cancelToken.check();
if (this.fuzzyMatcher.match(query, description.name)) {
const symbol = this.getWorkspaceSymbol(description);
if (symbol) {
Expand Down
4 changes: 2 additions & 2 deletions packages/langium/src/parser/async-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
* terms of the MIT License, which is available in the project root.
******************************************************************************/

import type { CancellationToken } from '../utils/cancellation.js';
import { OperationCancelled, type CancellationToken } from '../utils/cancellation.js';
import type { LangiumCoreServices } from '../services.js';
import type { AstNode } from '../syntax-tree.js';
import type { LangiumParser, ParseResult } from './langium-parser.js';
import type { Hydrator } from '../serializer/hydrator.js';
import type { Event } from '../utils/event.js';
import { Deferred, OperationCancelled } from '../utils/promise-utils.js';
import { Deferred } from '../utils/promise-utils.js';
import { Emitter } from '../utils/event.js';

/**
Expand Down
3 changes: 1 addition & 2 deletions packages/langium/src/references/linker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import type { ScopeProvider } from './scope-provider.js';
import { CancellationToken } from '../utils/cancellation.js';
import { isAstNode, isAstNodeDescription, isLinkingError } from '../syntax-tree.js';
import { findRootNode, streamAst, streamReferences } from '../utils/ast-utils.js';
import { interruptAndCheck } from '../utils/promise-utils.js';
import { DocumentState } from '../workspace/documents.js';

/**
Expand Down Expand Up @@ -91,7 +90,7 @@ export class DefaultLinker implements Linker {

async link(document: LangiumDocument, cancelToken = CancellationToken.None): Promise<void> {
for (const node of streamAst(document.parseResult.value)) {
await interruptAndCheck(cancelToken);
await cancelToken.check();
streamReferences(node).forEach(ref => this.doLink(ref, document));
}
}
Expand Down
5 changes: 2 additions & 3 deletions packages/langium/src/references/scope-computation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import type { NameProvider } from './name-provider.js';
import { CancellationToken } from '../utils/cancellation.js';
import { streamAllContents, streamContents } from '../utils/ast-utils.js';
import { MultiMap } from '../utils/collections.js';
import { interruptAndCheck } from '../utils/promise-utils.js';

/**
* Language-specific service for precomputing global and local scopes. The service methods are executed
Expand Down Expand Up @@ -94,7 +93,7 @@ export class DefaultScopeComputation implements ScopeComputation {

this.exportNode(parentNode, exports, document);
for (const node of children(parentNode)) {
await interruptAndCheck(cancelToken);
await cancelToken.check();
this.exportNode(node, exports, document);
}
return exports;
Expand All @@ -116,7 +115,7 @@ export class DefaultScopeComputation implements ScopeComputation {
const scopes = new MultiMap<AstNode, AstNodeDescription>();
// Here we navigate the full AST - local scopes shall be available in the whole document
for (const node of streamAllContents(rootNode)) {
await interruptAndCheck(cancelToken);
await cancelToken.check();
this.processNode(node, document, scopes);
}
return scopes;
Expand Down
Loading