/******************************************************************************
 * Copyright 2021 TypeFox GmbH
 * This program and the accompanying materials are made available under the
 * terms of the MIT License, which is available in the project root.
 ******************************************************************************/ import { CancellationToken , Connection , Diagnostic } from 'vscode-languageserver' ; import { URI } from 'vscode-uri' ; import { ServiceRegistry } from '../service-registry' ; import { LangiumSharedServices } from '../services' ; import { AstNode } from '../syntax-tree' ; import { MultiMap } from '../utils/collections' ; import { interruptAndCheck , MaybePromise } from '../utils/promise-util' ; import { IndexManager } from '../workspace/index-manager' ; import { DocumentState , LangiumDocument , LangiumDocuments } from './documents' ; export interface DocumentBuilder { param document param cancelToken throws build < T extends AstNode > ( document , cancelToken ? ) : Promise < BuildResult < T > > ; param changed param deleted param cancelToken see IndexManager . update see LangiumDocuments . invalidateDocument throws update ( changed , deleted , cancelToken ? ) : Promise < void > ; /**
     * Notify the given callback when a document update was triggered, but before any document
     * is rebuilt. Listeners to this event should not perform any long-running task.
     */ onUpdate ( callback ) : void ; /**
     * Notify the given callback when a set of documents has been built reaching a desired target state.
     */ onBuildPhase ( targetState , callback ) : void ; } export type DocumentUpdateListener = ( changed : URI [ ] , deleted : URI [ ] ) => void ; export type DocumentBuildListener = ( built : LangiumDocument [ ] , cancelToken : CancellationToken ) => Promise < void > ; export interface BuildResult < T extends AstNode = AstNode > { readonly document : LangiumDocument < T > ; readonly diagnostics : Diagnostic [ ] ; } export class DefaultDocumentBuilder implements DocumentBuilder { protected readonly connection ? : Connection ; protected readonly langiumDocuments : LangiumDocuments ; protected readonly indexManager : IndexManager ; protected readonly serviceRegistry : ServiceRegistry ; protected readonly updateListeners : DocumentUpdateListener [ ] = [ ] ; protected readonly buildPhaseListeners : MultiMap < DocumentState , DocumentBuildListener > = new MultiMap ( ) ; constructor ( services ) { this . connection = services . lsp . Connection ; this . langiumDocuments = services . workspace . LangiumDocuments ; this . indexManager = services . workspace . IndexManager ; this . serviceRegistry = services . ServiceRegistry ; } async build < T extends AstNode > ( document , cancelToken = CancellationToken . None ) { await this . buildDocuments ( [ document ] , cancelToken ) ; return { document , diagnostics : await this . validate ( document , cancelToken , true ) } ; } protected async validate ( document , cancelToken = CancellationToken . None , forceDiagnostics = false ) { let diagnostics = [ ] ; const validator = this . serviceRegistry . getServices ( document . uri ) . validation . DocumentValidator ; if ( forceDiagnostics || this . shouldValidate ( document ) ) { diagnostics = await validator . validateDocument ( document , cancelToken ) ; if ( this . connection ) { this . connection . sendDiagnostics ( { uri : document . textDocument . uri , diagnostics } ) ; } document . diagnostics = diagnostics ; document . state = DocumentState . Validated ; } return diagnostics ; } /**
     * Determine whether the given document should be validated during a build. The default
     * implementation validates whenever a client connection is available.
     */ protected shouldValidate ( document ) { return this . connection !== undefined ; } async update ( changed , deleted , cancelToken = CancellationToken . None ) { for ( const deletedDocument of deleted ) { this . langiumDocuments . invalidateDocument ( deletedDocument ) ; } this . indexManager . remove ( deleted ) ; for ( const changedUri of changed ) { this . langiumDocuments . invalidateDocument ( changedUri ) ; } for ( const listener of this . updateListeners ) { listener ( changed , deleted ) ; } await interruptAndCheck ( cancelToken ) ; const changedDocuments = changed . map ( e => this . langiumDocuments . getOrCreateDocument ( e ) ) ; const rebuildDocuments = this . collectDocuments ( changedDocuments , deleted ) ; await this . buildDocuments ( rebuildDocuments , cancelToken ) ; } onUpdate ( callback ) { this . updateListeners . push ( callback ) ; } protected collectDocuments ( changed , deleted ) { const allUris = changed . map ( e => e . uri ) . concat ( deleted ) ; const affected = this . indexManager . getAffectedDocuments ( allUris ) . toArray ( ) ; affected . forEach ( e => { const linker = this . serviceRegistry . getServices ( e . uri ) . references . Linker ; linker . unlink ( e ) ; e . state = DocumentState . Indexed ; } ) ; const docSet = new Set ( [ ... changed , ... affected , ... this . langiumDocuments . all . filter ( e => e . state < DocumentState . Validated ) ] ) ; return Array . from ( docSet ) ; } protected async buildDocuments ( documents , cancelToken ) { const toBeIndexed = documents . filter ( e => e . state < DocumentState . Indexed ) ; await this . indexManager . update ( toBeIndexed , cancelToken ) ; await this . notifyBuildPhase ( toBeIndexed , DocumentState . Indexed , cancelToken ) ; await this . runCancelable ( documents , DocumentState . Processed , cancelToken , doc => this . process ( doc , cancelToken ) ) ; await this . runCancelable ( documents , DocumentState . Linked , cancelToken , doc => this . serviceRegistry . getServices ( doc . uri ) . references . Linker . link ( doc , cancelToken ) ) ; await this . runCancelable ( documents , DocumentState . Validated , cancelToken , doc => this . validate ( doc , cancelToken ) ) ; } protected async runCancelable ( documents , targetState , cancelToken , callback ) { const filtered = documents . filter ( e => e . state < targetState ) ; for ( const document of filtered ) { await interruptAndCheck ( cancelToken ) ; await callback ( document ) ; } await this . notifyBuildPhase ( filtered , targetState , cancelToken ) ; } onBuildPhase ( targetState , callback ) { this . buildPhaseListeners . add ( targetState , callback ) ; } protected async notifyBuildPhase ( documents , state , cancelToken ) { const listeners = this . buildPhaseListeners . get ( state ) ; for ( const listener of listeners ) { await interruptAndCheck ( cancelToken ) ; await listener ( documents , cancelToken ) ; } } /**
     * Process the document by running precomputations. The default implementation precomputes the scope.
     */ protected async process ( document , cancelToken ) { const scopeComputation = this . serviceRegistry . getServices ( document . uri ) . references . ScopeComputation ; document . precomputedScopes = await scopeComputation . computeScope ( document , cancelToken ) ; document . state = DocumentState . Processed ; } }