Skip to content

Commit 1b02551

Browse files
committed
st-language: refined waitUntil with document URI to fire as soon as document reaches required state
added corresponding test
1 parent 38d5f16 commit 1b02551

File tree

3 files changed

+82
-13
lines changed

3 files changed

+82
-13
lines changed

packages/langium/src/lsp/language-server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,8 +731,10 @@ export function createRequestHandler<P extends { textDocument: TextDocumentIdent
731731

732732
async function waitUntilPhase<E>(services: LangiumSharedServices, cancelToken: CancellationToken, uri?: URI, targetState?: DocumentState): Promise<ResponseError<E> | undefined> {
733733
if (targetState !== undefined) {
734+
const workspaceManager = services.workspace.WorkspaceManager;
734735
const documentBuilder = services.workspace.DocumentBuilder;
735736
try {
737+
await workspaceManager.ready; // mandatory if awaiting the state of a document (uri !== undefined) while the LS is starting
736738
await documentBuilder.waitUntil(targetState, uri, cancelToken);
737739
} catch (err) {
738740
return responseError(err);

packages/langium/src/workspace/document-builder.ts

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* terms of the MIT License, which is available in the project root.
55
******************************************************************************/
66

7+
import { LSPErrorCodes, ResponseError } from 'vscode-languageserver-protocol';
78
import { CancellationToken } from '../utils/cancellation.js';
89
import { Disposable } from '../utils/disposable.js';
910
import type { ServiceRegistry } from '../service-registry.js';
@@ -490,8 +491,8 @@ export class DefaultDocumentBuilder implements DocumentBuilder {
490491
}
491492

492493
waitUntil(state: DocumentState, cancelToken?: CancellationToken): Promise<void>;
493-
waitUntil(state: DocumentState, uri?: URI, cancelToken?: CancellationToken): Promise<URI | undefined>;
494-
waitUntil(state: DocumentState, uriOrToken?: URI | CancellationToken, cancelToken?: CancellationToken): Promise<URI | undefined | void> {
494+
waitUntil(state: DocumentState, uri?: URI, cancelToken?: CancellationToken): Promise<URI>;
495+
waitUntil(state: DocumentState, uriOrToken?: URI | CancellationToken, cancelToken?: CancellationToken): Promise<URI | void> {
495496
let uri: URI | undefined = undefined;
496497
if (uriOrToken && 'path' in uriOrToken) {
497498
uri = uriOrToken;
@@ -500,26 +501,67 @@ export class DefaultDocumentBuilder implements DocumentBuilder {
500501
}
501502
cancelToken ??= CancellationToken.None;
502503
if (uri) {
503-
const document = this.langiumDocuments.getDocument(uri);
504-
if (document && document.state >= state) {
505-
return Promise.resolve(uri);
506-
}
504+
return this.awaitDocumentState(state, uri, cancelToken);
505+
506+
} else {
507+
return this.awaitBuilderState(state, cancelToken);
508+
}
509+
}
510+
511+
protected awaitDocumentState(state: DocumentState, uri: URI, cancelToken: CancellationToken): Promise<URI> {
512+
const document = this.langiumDocuments.getDocument(uri);
513+
if (!document) {
514+
return Promise.reject(
515+
new ResponseError(
516+
LSPErrorCodes.ServerCancelled,
517+
`No document found for URI: ${uri.toString()}`
518+
)
519+
);
520+
521+
} else if (document.state >= state) {
522+
return Promise.resolve(uri);
523+
524+
} else if (cancelToken.isCancellationRequested) {
525+
return Promise.reject(OperationCancelled);
526+
527+
} else if (this.currentState >= state && state > document.state) {
528+
// this would imply that the document has been excluded from linking or validation, for example;
529+
// this should never occur, the LS need to make sure that the affected document is properly built,
530+
// alternatively, the build state requirement need to be relaxed.
531+
return Promise.reject(
532+
new ResponseError(
533+
LSPErrorCodes.RequestFailed,
534+
`Document state of ${uri.toString()} is ${DocumentState[document.state]}, requiring ${DocumentState[state]}, but workspace state is already ${DocumentState[this.currentState]}. Returning undefined.`
535+
)
536+
);
507537
}
538+
return new Promise((resolve, reject) => {
539+
const buildDisposable = this.onDocumentPhase(state, (doc) => {
540+
if (doc.uri.toString() === uri.toString()) {
541+
buildDisposable.dispose();
542+
cancelDisposable.dispose();
543+
resolve(doc.uri);
544+
}
545+
});
546+
const cancelDisposable = cancelToken!.onCancellationRequested(() => {
547+
buildDisposable.dispose();
548+
cancelDisposable.dispose();
549+
reject(OperationCancelled);
550+
});
551+
});
552+
}
553+
554+
protected awaitBuilderState(state: DocumentState, cancelToken: CancellationToken): Promise<void> {
508555
if (this.currentState >= state) {
509-
return Promise.resolve(undefined);
556+
return Promise.resolve();
510557
} else if (cancelToken.isCancellationRequested) {
511558
return Promise.reject(OperationCancelled);
512559
}
513560
return new Promise((resolve, reject) => {
514561
const buildDisposable = this.onBuildPhase(state, () => {
515562
buildDisposable.dispose();
516563
cancelDisposable.dispose();
517-
if (uri) {
518-
const document = this.langiumDocuments.getDocument(uri);
519-
resolve(document?.uri);
520-
} else {
521-
resolve(undefined);
522-
}
564+
resolve();
523565
});
524566
const cancelDisposable = cancelToken!.onCancellationRequested(() => {
525567
buildDisposable.dispose();

packages/langium/test/workspace/document-builder.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,31 @@ describe('DefaultDocumentBuilder', () => {
497497
).resolves.toEqual(documentUri);
498498
});
499499

500+
test('`waitUntil` on document fires as soon as document reaches required state.', async () => {
501+
const services = await createServices();
502+
const documentFactory = services.shared.workspace.LangiumDocumentFactory;
503+
const documents = services.shared.workspace.LangiumDocuments;
504+
const builder = services.shared.workspace.DocumentBuilder;
505+
506+
const document = documentFactory.fromString<Model>(`
507+
foo 1 A
508+
foo 11 B
509+
bar A
510+
bar B
511+
`, URI.parse('file:///test1.txt'));
512+
documents.addDocument(document);
513+
const document2 = documentFactory.fromString<Model>('', URI.parse('file:///test2.txt'));
514+
documents.addDocument(document2);
515+
516+
const states: DocumentState[] = [];
517+
builder.waitUntil(DocumentState.Linked, document.uri).then(() => {
518+
states.push(document.state, document2.state);
519+
});
520+
521+
await builder.build(documents.all.toArray());
522+
expect(states).toEqual([ DocumentState.Linked, DocumentState.ComputedScopes ]);
523+
});
524+
500525
test('`onDocumentPhase` always triggers before the respective `onBuildPhase`', async () => {
501526
const services = await createServices();
502527
const documentFactory = services.shared.workspace.LangiumDocumentFactory;

0 commit comments

Comments
 (0)