@@ -13,9 +13,9 @@ import type { MaybePromise } from '../utils/promise-utils.js';
1313import type { Deferred } from '../utils/promise-utils.js' ;
1414import type { ValidationOptions } from '../validation/document-validator.js' ;
1515import type { IndexManager } from '../workspace/index-manager.js' ;
16- import type { LangiumDocument , LangiumDocuments , LangiumDocumentFactory } from './documents.js' ;
16+ import type { LangiumDocument , LangiumDocuments , LangiumDocumentFactory , TextDocumentProvider } from './documents.js' ;
1717import { MultiMap } from '../utils/collections.js' ;
18- import { OperationCancelled , interruptAndCheck } from '../utils/promise-utils.js' ;
18+ import { OperationCancelled , interruptAndCheck , isOperationCancelled } from '../utils/promise-utils.js' ;
1919import { stream } from '../utils/stream.js' ;
2020import type { URI } from '../utils/uri-utils.js' ;
2121import { ValidationCategory } from '../validation/validation-registry.js' ;
@@ -78,10 +78,22 @@ export interface DocumentBuilder {
7878 onUpdate ( callback : DocumentUpdateListener ) : Disposable ;
7979
8080 /**
81- * Notify the given callback when a set of documents has been built reaching a desired target state.
81+ * Notify the given callback when a set of documents has been built reaching the specified target state.
8282 */
8383 onBuildPhase ( targetState : DocumentState , callback : DocumentBuildListener ) : Disposable ;
8484
85+ /**
86+ * Notify the specified callback when a document has been built reaching the specified target state.
87+ * Unlike {@link onBuildPhase} the listener is called for every single document.
88+ *
89+ * There are two main advantages compared to {@link onBuildPhase}:
90+ * 1. If the build is cancelled, {@link onDocumentPhase} will still fire for documents that have reached a specific state.
91+ * Meanwhile, {@link onBuildPhase} won't fire for that state.
92+ * 2. The {@link DocumentBuilder} ensures that all {@link DocumentPhaseListener} instances are called for a built document.
93+ * Even if the build is cancelled before those listeners were called.
94+ */
95+ onDocumentPhase ( targetState : DocumentState , callback : DocumentPhaseListener ) : Disposable ;
96+
8597 /**
8698 * Wait until the workspace has reached the specified state for all documents.
8799 *
@@ -105,6 +117,7 @@ export interface DocumentBuilder {
105117
106118export type DocumentUpdateListener = ( changed : URI [ ] , deleted : URI [ ] ) => void | Promise < void >
107119export type DocumentBuildListener = ( built : LangiumDocument [ ] , cancelToken : CancellationToken ) => void | Promise < void >
120+ export type DocumentPhaseListener = ( built : LangiumDocument , cancelToken : CancellationToken ) => void | Promise < void >
108121export class DefaultDocumentBuilder implements DocumentBuilder {
109122
110123 updateBuildOptions : BuildOptions = {
@@ -116,17 +129,20 @@ export class DefaultDocumentBuilder implements DocumentBuilder {
116129
117130 protected readonly langiumDocuments : LangiumDocuments ;
118131 protected readonly langiumDocumentFactory : LangiumDocumentFactory ;
132+ protected readonly textDocuments : TextDocumentProvider | undefined ;
119133 protected readonly indexManager : IndexManager ;
120134 protected readonly serviceRegistry : ServiceRegistry ;
121135 protected readonly updateListeners : DocumentUpdateListener [ ] = [ ] ;
122136 protected readonly buildPhaseListeners = new MultiMap < DocumentState , DocumentBuildListener > ( ) ;
137+ protected readonly documentPhaseListeners = new MultiMap < DocumentState , DocumentPhaseListener > ( ) ;
123138 protected readonly buildState = new Map < string , DocumentBuildState > ( ) ;
124139 protected readonly documentBuildWaiters = new Map < string , Deferred < void > > ( ) ;
125140 protected currentState = DocumentState . Changed ;
126141
127142 constructor ( services : LangiumSharedCoreServices ) {
128143 this . langiumDocuments = services . workspace . LangiumDocuments ;
129144 this . langiumDocumentFactory = services . workspace . LangiumDocumentFactory ;
145+ this . textDocuments = services . workspace . TextDocuments ;
130146 this . indexManager = services . workspace . IndexManager ;
131147 this . serviceRegistry = services . ServiceRegistry ;
132148 }
@@ -209,22 +225,49 @@ export class DefaultDocumentBuilder implements DocumentBuilder {
209225 // Only allow interrupting the execution after all state changes are done
210226 await interruptAndCheck ( cancelToken ) ;
211227
212- // Collect all documents that we should rebuild
213- const rebuildDocuments = this . langiumDocuments . all
214- . filter ( doc =>
215- // This includes those that were reported as changed and those that we selected for relinking
216- doc . state < DocumentState . Linked
217- // This includes those for which a previous build has been cancelled
218- || ! this . buildState . get ( doc . uri . toString ( ) ) ?. completed
219- )
220- . toArray ( ) ;
228+ // Collect and sort all documents that we should rebuild
229+ const rebuildDocuments = this . sortDocuments (
230+ this . langiumDocuments . all
231+ . filter ( doc =>
232+ // This includes those that were reported as changed and those that we selected for relinking
233+ doc . state < DocumentState . Linked
234+ // This includes those for which a previous build has been cancelled
235+ || ! this . buildState . get ( doc . uri . toString ( ) ) ?. completed
236+ )
237+ . toArray ( )
238+ ) ;
221239 await this . buildDocuments ( rebuildDocuments , this . updateBuildOptions , cancelToken ) ;
222240 }
223241
224242 protected async emitUpdate ( changed : URI [ ] , deleted : URI [ ] ) : Promise < void > {
225243 await Promise . all ( this . updateListeners . map ( listener => listener ( changed , deleted ) ) ) ;
226244 }
227245
246+ /**
247+ * Sort the given documents by priority. By default, documents with an open text document are prioritized.
248+ * This is useful to ensure that visible documents show their diagnostics before all other documents.
249+ *
250+ * This improves the responsiveness in large workspaces as users usually don't care about diagnostics
251+ * in files that are currently not opened in the editor.
252+ */
253+ protected sortDocuments ( documents : LangiumDocument [ ] ) : LangiumDocument [ ] {
254+ const hasTextDocument = new Map < LangiumDocument , boolean > ( ) ;
255+ for ( const doc of documents ) {
256+ hasTextDocument . set ( doc , Boolean ( this . textDocuments ?. get ( doc . uri . toString ( ) ) ) ) ;
257+ }
258+ return documents . sort ( ( a , b ) => {
259+ const aHasDoc = hasTextDocument . get ( a ) ;
260+ const bHasDoc = hasTextDocument . get ( b ) ;
261+ if ( aHasDoc && ! bHasDoc ) {
262+ return - 1 ;
263+ } else if ( ! aHasDoc && bHasDoc ) {
264+ return 1 ;
265+ } else {
266+ return 0 ;
267+ }
268+ } ) ;
269+ }
270+
228271 /**
229272 * Check whether the given document should be relinked after changes were found in the given URIs.
230273 */
@@ -314,6 +357,7 @@ export class DefaultDocumentBuilder implements DocumentBuilder {
314357 await interruptAndCheck ( cancelToken ) ;
315358 await callback ( document ) ;
316359 document . state = targetState ;
360+ await this . notifyDocumentPhase ( document , targetState , cancelToken ) ;
317361 }
318362 await this . notifyBuildPhase ( filtered , targetState , cancelToken ) ;
319363 this . currentState = targetState ;
@@ -326,6 +370,13 @@ export class DefaultDocumentBuilder implements DocumentBuilder {
326370 } ) ;
327371 }
328372
373+ onDocumentPhase ( targetState : DocumentState , callback : DocumentPhaseListener ) : Disposable {
374+ this . documentPhaseListeners . add ( targetState , callback ) ;
375+ return Disposable . create ( ( ) => {
376+ this . documentPhaseListeners . delete ( targetState , callback ) ;
377+ } ) ;
378+ }
379+
329380 waitUntil ( state : DocumentState , cancelToken ?: CancellationToken ) : Promise < void > ;
330381 waitUntil ( state : DocumentState , uri ?: URI , cancelToken ?: CancellationToken ) : Promise < URI | undefined > ;
331382 waitUntil ( state : DocumentState , uriOrToken ?: URI | CancellationToken , cancelToken ?: CancellationToken ) : Promise < URI | undefined | void > {
@@ -366,6 +417,21 @@ export class DefaultDocumentBuilder implements DocumentBuilder {
366417 } ) ;
367418 }
368419
420+ protected async notifyDocumentPhase ( document : LangiumDocument , state : DocumentState , cancelToken : CancellationToken ) : Promise < void > {
421+ const listeners = this . documentPhaseListeners . get ( state ) ;
422+ for ( const listener of listeners ) {
423+ try {
424+ await listener ( document , cancelToken ) ;
425+ } catch ( err ) {
426+ // Ignore cancellation errors
427+ // We want to finish the listeners before throwing
428+ if ( ! isOperationCancelled ( err ) ) {
429+ throw err ;
430+ }
431+ }
432+ }
433+ }
434+
369435 protected async notifyBuildPhase ( documents : LangiumDocument [ ] , state : DocumentState , cancelToken : CancellationToken ) : Promise < void > {
370436 if ( documents . length === 0 ) {
371437 // Don't notify when no document has been processed
0 commit comments