/******************************************************************************
 * 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 fs from 'fs' ; import path from 'path' ; import { CancellationToken , WorkspaceFolder } from 'vscode-languageserver' ; import { URI , Utils } from 'vscode-uri' ; import { ServiceRegistry } from '../service-registry' ; import { LangiumSharedServices } from '../services' ; import { AstNode , AstNodeDescription , AstReflection } from '../syntax-tree' ; import { getDocument } from '../utils/ast-util' ; import { interruptAndCheck } from '../utils/promise-util' ; import { stream , Stream } from '../utils/stream' ; import { ReferenceDescription } from './ast-descriptions' ; import { DocumentState , LangiumDocument , LangiumDocuments } from './documents' ; /**
 * The index manager is responsible for keeping metadata about symbols and cross-references
 * in the workspace. It is used to look up symbols in the global scope, mostly during linking
 * and completion.
 */ export interface IndexManager { param folders initializeWorkspace ( folders ) : Promise < void > ; param uris remove ( uris ) : void ; param document param cancelToken throws update ( documents , cancelToken ? ) : Promise < void > ; param uris getAffectedDocuments ( uris ) : Stream < LangiumDocument > ; param nodeType returns allElements ( nodeType ? ) : Stream < AstNodeDescription > ; param targetNode param astNodePath returns findAllReferences ( targetNode , astNodePath ) : Stream < ReferenceDescription > ; } export class DefaultIndexManager implements IndexManager { protected readonly serviceRegistry : ServiceRegistry ; protected readonly langiumDocuments : ( ) => LangiumDocuments ; protected readonly astReflection : AstReflection ; protected readonly simpleIndex : Map < string , AstNodeDescription [ ] > = new Map < string , AstNodeDescription [ ] > ( ) ; protected readonly referenceIndex : Map < string , ReferenceDescription [ ] > = new Map < string , ReferenceDescription [ ] > ( ) ; protected readonly globalScopeCache = new Map < string , AstNodeDescription [ ] > ( ) ; constructor ( services ) { this . serviceRegistry = services . ServiceRegistry ; this . astReflection = services . AstReflection ; this . langiumDocuments = ( ) => services . workspace . LangiumDocuments ; } findAllReferences ( targetNode , astNodePath ) { const targetDocUri = getDocument ( targetNode ) . uri ; const result = [ ] ; this . referenceIndex . forEach ( ( docRefs ) => { docRefs . forEach ( ( refDescr ) => { if ( refDescr . targetUri . toString ( ) === targetDocUri . toString ( ) && refDescr . targetPath === astNodePath ) { result . push ( refDescr ) ; } } ) ; } ) ; return stream ( result ) ; } allElements ( nodeType = '' ) { if ( ! this . globalScopeCache . has ( '' ) ) { this . globalScopeCache . set ( '' , Array . from ( this . simpleIndex . values ( ) ) . flat ( ) ) ; } const cached = this . globalScopeCache . get ( nodeType ) ; if ( cached ) { return stream ( cached ) ; } else { const elements = this . globalScopeCache . get ( '' ) ! . filter ( e => this . astReflection . isSubtype ( e . type , nodeType ) ) ; this . globalScopeCache . set ( nodeType , elements ) ; return stream ( elements ) ; } } remove ( uris ) { for ( const uri of uris ) { const uriString = uri . toString ( ) ; this . simpleIndex . delete ( uriString ) ; this . referenceIndex . delete ( uriString ) ; } } update ( documents , cancelToken = CancellationToken . None ) { return this . processDocuments ( documents , cancelToken ) ; } getAffectedDocuments ( uris ) { return this . langiumDocuments ( ) . all . filter ( e => { if ( uris . some ( uri => e . uri . toString ( ) === uri . toString ( ) ) ) { return false ; } for ( const uri of uris ) { if ( this . isAffected ( e , uri ) ) { return true ; } } return false ; } ) ; } /**
     * Determine whether the given document could be affected by a change of the document
     * identified by the given URI (second parameter).
     */ protected isAffected ( document , changed ) { const changedUriString = changed . toString ( ) ; const documentUri = document . uri . toString ( ) ; if ( document . references . some ( e => e . error ) ) { return true ; } const references = this . referenceIndex . get ( documentUri ) ; if ( references ) { return references . filter ( e => ! e . local ) . some ( e => e . targetUri . toString ( ) === changedUriString ) ; } return false ; } async initializeWorkspace ( folders ) { const documents = [ ] ; const allFileExtensions = this . serviceRegistry . all . flatMap ( e => e . LanguageMetaData . fileExtensions ) ; const fileFilter = ( uri ) => allFileExtensions . includes ( path . extname ( uri . path ) ) ; const collector = ( document ) => documents . push ( document ) ; await Promise . all ( folders . map ( folder => this . getRootFolder ( folder ) ) . map ( async ( folderPath ) => this . traverseFolder ( folderPath , fileFilter , collector ) ) ) ; await this . loadAdditionalDocuments ( folders , collector ) ; await this . processDocuments ( documents ) ; } /**
     * Load all additional documents that shall be visible in the context of the given workspace
     * folders and add them to the acceptor. This can be used to include built-in libraries of
     * your language, which can be either loaded from provided files or constructed in memory.
     */ protected loadAdditionalDocuments ( _folders , _acceptor ) { return Promise . resolve ( ) ; } /**
     * Determine the root folder of the source documents in the given workspace folder.
     * The default implementation returns the URI of the workspace folder, but you can override
     * this to return a subfolder like `src` instead.
     */ protected getRootFolder ( folder ) { return URI . parse ( folder . uri ) ; } /**
     * Traverse the file system folder identified by the given URI and its subFolders. All
     * contained files that match the filter are added to the acceptor.
     */ protected async traverseFolder ( folderPath , fileFilter , acceptor ) { const fsPath = folderPath . fsPath ; if ( ! fs . existsSync ( fsPath ) ) { console . error ( `File ${ folderPath } doesn't exist.` ) ; return ; } if ( this . skipFolder ( folderPath ) ) { return ; } const subFolders = await fs . promises . readdir ( fsPath , { withFileTypes : true } ) ; for ( const dir of subFolders ) { const uri = URI . file ( path . resolve ( fsPath , dir . name ) ) ; if ( dir . isDirectory ( ) ) { await this . traverseFolder ( uri , fileFilter , acceptor ) ; } else if ( fileFilter ( uri ) ) { const document = this . langiumDocuments ( ) . getOrCreateDocument ( uri ) ; acceptor ( document ) ; } } } /**
     * Determine whether the folder with the given path shall be skipped while indexing the workspace.
     */ protected skipFolder ( folderPath ) { const base = Utils . basename ( folderPath ) ; return base . startsWith ( '.' ) || base === 'node_modules' || base === 'out' ; } protected async processDocuments ( documents , cancelToken = CancellationToken . None ) { this . globalScopeCache . clear ( ) ; for ( const document of documents ) { const services = this . serviceRegistry . getServices ( document . uri ) ; const indexData = await services . index . AstNodeDescriptionProvider . createDescriptions ( document , cancelToken ) ; for ( const data of indexData ) { data . node = undefined ; } this . simpleIndex . set ( document . textDocument . uri , indexData ) ; await interruptAndCheck ( cancelToken ) ; } for ( const document of documents ) { const services = this . serviceRegistry . getServices ( document . uri ) ; this . referenceIndex . set ( document . textDocument . uri , await services . index . ReferenceDescriptionProvider . createDescriptions ( document , cancelToken ) ) ; await interruptAndCheck ( cancelToken ) ; document . state = DocumentState . Indexed ; } } }