Skip to content

Commit 24e3807

Browse files
authored
Improve workspace manager startup behavior (#1388)
1 parent cc1a49c commit 24e3807

File tree

6 files changed

+69
-40
lines changed

6 files changed

+69
-40
lines changed

packages/langium/src/lsp/document-update-handler.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type { DocumentBuilder } from '../workspace/document-builder.js';
1111
import type { TextDocument } from '../workspace/documents.js';
1212
import type { WorkspaceLock } from '../workspace/workspace-lock.js';
1313
import type { LangiumSharedServices } from './lsp-services.js';
14+
import type { WorkspaceManager } from '../workspace/workspace-manager.js';
1415

1516
/**
1617
* Shared service for handling text document changes and watching relevant files.
@@ -31,10 +32,12 @@ export interface DocumentUpdateHandler {
3132

3233
export class DefaultDocumentUpdateHandler implements DocumentUpdateHandler {
3334

35+
protected readonly workspaceManager: WorkspaceManager;
3436
protected readonly documentBuilder: DocumentBuilder;
3537
protected readonly workspaceLock: WorkspaceLock;
3638

3739
constructor(services: LangiumSharedServices) {
40+
this.workspaceManager = services.workspace.WorkspaceManager;
3841
this.documentBuilder = services.workspace.DocumentBuilder;
3942
this.workspaceLock = services.workspace.WorkspaceLock;
4043

@@ -70,7 +73,14 @@ export class DefaultDocumentUpdateHandler implements DocumentUpdateHandler {
7073
}
7174

7275
protected fireDocumentUpdate(changed: URI[], deleted: URI[]): void {
73-
this.workspaceLock.write(token => this.documentBuilder.update(changed, deleted, token));
76+
// Only fire the document update when the workspace manager is ready
77+
// Otherwise, we might miss the initial indexing of the workspace
78+
this.workspaceManager.ready.then(() => {
79+
this.workspaceLock.write(token => this.documentBuilder.update(changed, deleted, token));
80+
}).catch(err => {
81+
// This should never happen, but if it does, we want to know about it
82+
console.error('Workspace initialization failed. Could not perform document update.', err);
83+
});
7484
}
7585

7686
didChangeContent(change: TextDocumentChangeEvent<TextDocument>): void {

packages/langium/src/parser/async-parser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ export abstract class AbstractThreadedAsyncParser implements AsyncParser {
125125
if (index >= 0) {
126126
this.queue.splice(index, 1);
127127
}
128-
deferred.reject('OperationCancelled');
128+
deferred.reject(OperationCancelled);
129129
});
130130
this.queue.push(deferred);
131131
return deferred.promise;

packages/langium/src/workspace/configuration.ts

Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,17 @@
77
import type { ConfigurationItem, DidChangeConfigurationParams, DidChangeConfigurationRegistrationOptions, InitializeParams, InitializedParams } from 'vscode-languageserver-protocol';
88
import type { ServiceRegistry } from '../service-registry.js';
99
import type { LangiumSharedCoreServices } from '../services.js';
10+
import { Deferred } from '../utils/promise-utils.js';
1011

1112
/* eslint-disable @typescript-eslint/no-explicit-any */
1213

1314
export interface ConfigurationProvider {
1415

16+
/**
17+
* A promise that resolves when the configuration provider is ready to be used.
18+
*/
19+
readonly ready: Promise<void>;
20+
1521
/**
1622
* When used in a language server context, this method is called when the server receives
1723
* the `initialize` request.
@@ -52,14 +58,18 @@ export interface ConfigurationInitializedParams extends InitializedParams {
5258
export class DefaultConfigurationProvider implements ConfigurationProvider {
5359

5460
protected readonly serviceRegistry: ServiceRegistry;
61+
protected readonly _ready = new Deferred<void>();
5562
protected settings: Record<string, Record<string, any>> = {};
5663
protected workspaceConfig = false;
57-
protected fetchConfiguration: ((configurations: ConfigurationItem[]) => Promise<any>) | undefined;
5864

5965
constructor(services: LangiumSharedCoreServices) {
6066
this.serviceRegistry = services.ServiceRegistry;
6167
}
6268

69+
get ready(): Promise<void> {
70+
return this._ready.promise;
71+
}
72+
6373
initialize(params: InitializeParams): void {
6474
this.workspaceConfig = params.capabilities.workspace?.configuration ?? false;
6575
}
@@ -77,35 +87,22 @@ export class DefaultConfigurationProvider implements ConfigurationProvider {
7787
});
7888
}
7989

80-
// params.fetchConfiguration(...) is a function to be provided by the calling language server for the sake of
81-
// decoupling this implementation from the concrete LSP implementations, specifically the LSP Connection
82-
this.fetchConfiguration = params.fetchConfiguration;
83-
84-
// awaiting the fetch of the initial configuration data must not happen here, because it would block the
85-
// initialization of the language server, and may lead to out-of-order processing of subsequent messages from the language client;
86-
// fetching the initial configuration in a non-blocking manner might cause a read-before-write problem
87-
// in case the workspace initialization relies on that configuration data (which is the case for the grammar language server);
88-
// consequently, we fetch the initial configuration data on blocking manner on-demand, when the first configuration value is read,
89-
// see below in #getConfiguration(...);
90-
}
91-
}
90+
if (params.fetchConfiguration) {
91+
// params.fetchConfiguration(...) is a function to be provided by the calling language server for the sake of
92+
// decoupling this implementation from the concrete LSP implementations, specifically the LSP Connection
93+
const configToUpdate = this.serviceRegistry.all.map(lang => <ConfigurationItem>{
94+
// Fetch the configuration changes for all languages
95+
section: this.toSectionName(lang.LanguageMetaData.languageId)
96+
});
9297

93-
protected async initializeConfiguration(): Promise<void> {
94-
if (this.fetchConfiguration) {
95-
const configToUpdate = this.serviceRegistry.all.map(lang => <ConfigurationItem>{
96-
// Fetch the configuration changes for all languages
97-
section: this.toSectionName(lang.LanguageMetaData.languageId)
98-
});
99-
100-
// get workspace configurations (default scope URI)
101-
const configs = await this.fetchConfiguration(configToUpdate);
102-
configToUpdate.forEach((conf, idx) => {
103-
this.updateSectionConfiguration(conf.section!, configs[idx]);
104-
});
105-
106-
// reset the 'fetchConfiguration' to 'undefined' again in order to prevent further 'fetch' attempts
107-
this.fetchConfiguration = undefined;
98+
// get workspace configurations (default scope URI)
99+
const configs = await params.fetchConfiguration(configToUpdate);
100+
configToUpdate.forEach((conf, idx) => {
101+
this.updateSectionConfiguration(conf.section!, configs[idx]);
102+
});
103+
}
108104
}
105+
this._ready.resolve();
109106
}
110107

111108
/**
@@ -134,7 +131,7 @@ export class DefaultConfigurationProvider implements ConfigurationProvider {
134131
* @param configuration Configuration name
135132
*/
136133
async getConfiguration(language: string, configuration: string): Promise<any> {
137-
await this.initializeConfiguration();
134+
await this.ready;
138135

139136
const sectionName = this.toSectionName(language);
140137
if (this.settings[sectionName]) {

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -254,9 +254,9 @@ export class DefaultDocumentBuilder implements DocumentBuilder {
254254
protected async buildDocuments(documents: LangiumDocument[], options: BuildOptions, cancelToken: CancellationToken): Promise<void> {
255255
this.prepareBuild(documents, options);
256256
// 0. Parse content
257-
await this.runCancelable(documents, DocumentState.Parsed, cancelToken, doc => {
258-
this.langiumDocumentFactory.update(doc, cancelToken);
259-
});
257+
await this.runCancelable(documents, DocumentState.Parsed, cancelToken, doc =>
258+
this.langiumDocumentFactory.update(doc, cancelToken)
259+
);
260260
// 1. Index content
261261
await this.runCancelable(documents, DocumentState.IndexedContent, cancelToken, doc =>
262262
this.indexManager.updateContent(doc, cancelToken)
@@ -308,7 +308,7 @@ export class DefaultDocumentBuilder implements DocumentBuilder {
308308
}
309309

310310
protected async runCancelable(documents: LangiumDocument[], targetState: DocumentState, cancelToken: CancellationToken,
311-
callback: (document: LangiumDocument) => MaybePromise<void>): Promise<void> {
311+
callback: (document: LangiumDocument) => MaybePromise<unknown>): Promise<void> {
312312
const filtered = documents.filter(e => e.state < targetState);
313313
for (const document of filtered) {
314314
await interruptAndCheck(cancelToken);

packages/langium/src/workspace/workspace-manager.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type { WorkspaceFolder } from 'vscode-languageserver-types';
99
import type { ServiceRegistry } from '../service-registry.js';
1010
import type { LangiumSharedCoreServices } from '../services.js';
1111
import { CancellationToken } from '../utils/cancellation.js';
12-
import { interruptAndCheck } from '../utils/promise-utils.js';
12+
import { Deferred, interruptAndCheck } from '../utils/promise-utils.js';
1313
import { URI, UriUtils } from '../utils/uri-utils.js';
1414
import type { BuildOptions, DocumentBuilder } from './document-builder.js';
1515
import type { LangiumDocument, LangiumDocuments } from './documents.js';
@@ -29,6 +29,12 @@ export interface WorkspaceManager {
2929
/** The options used for the initial workspace build. */
3030
initialBuildOptions: BuildOptions | undefined;
3131

32+
/**
33+
* A promise that resolves when the workspace manager is ready to be used.
34+
* Use this to ensure that the workspace manager has finished its initialization.
35+
*/
36+
readonly ready: Promise<void>;
37+
3238
/**
3339
* When used in a language server context, this method is called when the server receives
3440
* the `initialize` request.
@@ -61,6 +67,7 @@ export class DefaultWorkspaceManager implements WorkspaceManager {
6167
protected readonly documentBuilder: DocumentBuilder;
6268
protected readonly fileSystemProvider: FileSystemProvider;
6369
protected readonly mutex: WorkspaceLock;
70+
protected readonly _ready = new Deferred<void>();
6471
protected folders?: WorkspaceFolder[];
6572

6673
constructor(services: LangiumSharedCoreServices) {
@@ -71,6 +78,10 @@ export class DefaultWorkspaceManager implements WorkspaceManager {
7178
this.mutex = services.workspace.WorkspaceLock;
7279
}
7380

81+
get ready(): Promise<void> {
82+
return this._ready.promise;
83+
}
84+
7485
initialize(params: InitializeParams): void {
7586
this.folders = params.workspaceFolders ?? undefined;
7687
}
@@ -82,6 +93,18 @@ export class DefaultWorkspaceManager implements WorkspaceManager {
8293
}
8394

8495
async initializeWorkspace(folders: WorkspaceFolder[], cancelToken = CancellationToken.None): Promise<void> {
96+
const documents = await this.performStartup(folders);
97+
// Only after creating all documents do we check whether we need to cancel the initialization
98+
// The document builder will later pick up on all unprocessed documents
99+
await interruptAndCheck(cancelToken);
100+
await this.documentBuilder.build(documents, this.initialBuildOptions, cancelToken);
101+
}
102+
103+
/**
104+
* Performs the uninterruptable startup sequence of the workspace manager.
105+
* This methods loads all documents in the workspace and other documents and returns them.
106+
*/
107+
protected async performStartup(folders: WorkspaceFolder[]): Promise<LangiumDocument[]> {
85108
const fileExtensions = this.serviceRegistry.all.flatMap(e => e.LanguageMetaData.fileExtensions);
86109
const documents: LangiumDocument[] = [];
87110
const collector = (document: LangiumDocument) => {
@@ -98,10 +121,8 @@ export class DefaultWorkspaceManager implements WorkspaceManager {
98121
folders.map(wf => [wf, this.getRootFolder(wf)] as [WorkspaceFolder, URI])
99122
.map(async entry => this.traverseFolder(...entry, fileExtensions, collector))
100123
);
101-
// Only after creating all documents do we check whether we need to cancel the initialization
102-
// The document builder will later pick up on all unprocessed documents
103-
await interruptAndCheck(cancelToken);
104-
await this.documentBuilder.build(documents, this.initialBuildOptions, cancelToken);
124+
this._ready.resolve();
125+
return documents;
105126
}
106127

107128
/**

packages/langium/test/workspace/configuration.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ describe('ConfigurationProvider', () => {
1414
const grammarServices = createLangiumGrammarServices(EmptyFileSystem).grammar;
1515
const langId = grammarServices.LanguageMetaData.languageId;
1616
const configs = grammarServices.shared.workspace.ConfigurationProvider;
17+
configs.initialized({});
1718
beforeEach(() => {
1819
(configs as any).settings = {};
1920
});

0 commit comments

Comments
 (0)