import { flatten } from 'lodash' ; import { CancellationToken , CodeAction , CodeActionContext , Color , ColorInformation , ColorPresentation , CompletionContext , CompletionItem , CompletionList , DefinitionLink , Diagnostic , FormattingOptions , Hover , LinkedEditingRanges , Location , Position , Range , ReferenceContext , SelectionRange , SemanticTokens , SignatureHelp , SignatureHelpContext , SymbolInformation , TextDocumentContentChangeEvent , TextDocumentIdentifier , TextEdit , WorkspaceEdit } from 'vscode-languageserver' ; import { DocumentManager } from '../lib/documents' ; import { Logger } from '../logger' ; import { regexLastIndexOf } from '../utils' ; import { AppCompletionItem , FileRename , LSPProviderConfig , LSProvider , OnWatchFileChanges , OnWatchFileChangesPara , Plugin } from './interfaces' ; enum ExecuteMode { None , FirstNonNull , Collect } export class PluginHost implements LSProvider , OnWatchFileChanges { private plugins : Plugin [ ] = [ ] ; private pluginHostConfig : LSPProviderConfig = { filterIncompleteCompletions : true , definitionLinkSupport : false } ; private deferredRequests : Record < string , [ number , Promise < any > ] > = { } ; constructor ( private documentsManager ) { } initialize ( pluginHostConfig ) { this . pluginHostConfig = pluginHostConfig ; } register ( plugin ) { this . plugins . push ( plugin ) ; } didUpdateDocument ( ) { this . deferredRequests = { } ; } async getDiagnostics ( textDocument ) { const document = this . getDocument ( textDocument . uri ) ; if ( ( document . getFilePath ( ) ?. includes ( '/node_modules/' ) || document . getFilePath ( ) ?. includes ( '\\node_modules\\' ) ) && ! ( document . getFilePath ( ) ?. includes ( '/src/node_modules/' ) || document . getFilePath ( ) ?. includes ( '\\src\\node_modules\\' ) ) ) { return [ ] ; } return flatten ( await this . execute < Diagnostic [ ] > ( 'getDiagnostics' , [ document ] , ExecuteMode . Collect , 'high' ) ) ; } async doHover ( textDocument , position ) { const document = this . getDocument ( textDocument . uri ) ; return this . execute < Hover > ( 'doHover' , [ document , position ] , ExecuteMode . FirstNonNull , 'high' ) ; } async getCompletions ( textDocument , position , completionContext ? , cancellationToken ? ) { const document = this . getDocument ( textDocument . uri ) ; const completions = ( await this . execute < CompletionList > ( 'getCompletions' , [ document , position , completionContext , cancellationToken ] , ExecuteMode . Collect , 'high' ) ) . filter ( ( completion ) => completion != null ) ; let flattenedCompletions = flatten ( completions . map ( ( completion ) => completion . items ) ) ; const isIncomplete = completions . reduce ( ( incomplete , completion ) => incomplete || completion . isIncomplete , false as boolean ) ; if ( isIncomplete && this . pluginHostConfig . filterIncompleteCompletions ) { const offset = document . offsetAt ( position ) ; const text = document . getText ( ) . substring ( Math . max ( 0 , offset - 20 ) , offset ) ; const start = regexLastIndexOf ( text , /[\W\s]/g ) + 1 ; const filterValue = text . substring ( start ) . toLowerCase ( ) ; flattenedCompletions = flattenedCompletions . filter ( ( comp ) => comp . label . toLowerCase ( ) . includes ( filterValue ) ) ; } return CompletionList . create ( flattenedCompletions , isIncomplete ) ; } async resolveCompletion ( textDocument , completionItem , cancellationToken ) { const document = this . getDocument ( textDocument . uri ) ; const result = await this . execute < CompletionItem > ( 'resolveCompletion' , [ document , completionItem , cancellationToken ] , ExecuteMode . FirstNonNull , 'high' ) ; return result ?? completionItem ; } async formatDocument ( textDocument , options ) { const document = this . getDocument ( textDocument . uri ) ; return flatten ( await this . execute < TextEdit [ ] > ( 'formatDocument' , [ document , options ] , ExecuteMode . Collect , 'high' ) ) ; } async doTagComplete ( textDocument , position ) { const document = this . getDocument ( textDocument . uri ) ; return this . execute < string | null > ( 'doTagComplete' , [ document , position ] , ExecuteMode . FirstNonNull , 'high' ) ; } async getDocumentColors ( textDocument ) { const document = this . getDocument ( textDocument . uri ) ; return flatten ( await this . execute < ColorInformation [ ] > ( 'getDocumentColors' , [ document ] , ExecuteMode . Collect , 'low' ) ) ; } async getColorPresentations ( textDocument , range , color ) { const document = this . getDocument ( textDocument . uri ) ; return flatten ( await this . execute < ColorPresentation [ ] > ( 'getColorPresentations' , [ document , range , color ] , ExecuteMode . Collect , 'high' ) ) ; } async getDocumentSymbols ( textDocument , cancellationToken ) { const document = this . getDocument ( textDocument . uri ) ; return flatten ( await this . execute < SymbolInformation [ ] > ( 'getDocumentSymbols' , [ document , cancellationToken ] , ExecuteMode . Collect , 'low' ) ) ; } async getDefinitions ( textDocument , position ) { const document = this . getDocument ( textDocument . uri ) ; const definitions = flatten ( await this . execute < DefinitionLink [ ] > ( 'getDefinitions' , [ document , position ] , ExecuteMode . Collect , 'high' ) ) ; if ( this . pluginHostConfig . definitionLinkSupport ) { return definitions ; } else { return definitions . map ( ( def ) => < Location > { range : def . targetSelectionRange , uri : def . targetUri } ) ; } } async getCodeActions ( textDocument , range , context , cancellationToken ) { const document = this . getDocument ( textDocument . uri ) ; return flatten ( await this . execute < CodeAction [ ] > ( 'getCodeActions' , [ document , range , context , cancellationToken ] , ExecuteMode . Collect , 'high' ) ) ; } async executeCommand ( textDocument , command , args ? ) { const document = this . getDocument ( textDocument . uri ) ; return await this . execute < WorkspaceEdit > ( 'executeCommand' , [ document , command , args ] , ExecuteMode . FirstNonNull , 'high' ) ; } async updateImports ( fileRename ) { return await this . execute < WorkspaceEdit > ( 'updateImports' , [ fileRename ] , ExecuteMode . FirstNonNull , 'high' ) ; } async prepareRename ( textDocument , position ) { const document = this . getDocument ( textDocument . uri ) ; return await this . execute < any > ( 'prepareRename' , [ document , position ] , ExecuteMode . FirstNonNull , 'high' ) ; } async rename ( textDocument , position , newName ) { const document = this . getDocument ( textDocument . uri ) ; return await this . execute < any > ( 'rename' , [ document , position , newName ] , ExecuteMode . FirstNonNull , 'high' ) ; } async findReferences ( textDocument , position , context ) { const document = this . getDocument ( textDocument . uri ) ; return await this . execute < any > ( 'findReferences' , [ document , position , context ] , ExecuteMode . FirstNonNull , 'high' ) ; } async getSignatureHelp ( textDocument , position , context , cancellationToken ) { const document = this . getDocument ( textDocument . uri ) ; return await this . execute < any > ( 'getSignatureHelp' , [ document , position , context , cancellationToken ] , ExecuteMode . FirstNonNull , 'high' ) ; } see https async getSelectionRanges ( textDocument , positions ) { const document = this . getDocument ( textDocument . uri ) ; try { return Promise . all ( positions . map ( async ( position ) => { for ( const plugin of this . plugins ) { const range = await plugin . getSelectionRange ?. ( document , position ) ; if ( range ) { return range ; } } return SelectionRange . create ( Range . create ( position , position ) ) ; } ) ) ; } catch ( error ) { Logger . error ( error ) ; return null ; } } async getSemanticTokens ( textDocument , range ? , cancellationToken ? ) { const document = this . getDocument ( textDocument . uri ) ; return await this . execute < SemanticTokens > ( 'getSemanticTokens' , [ document , range , cancellationToken ] , ExecuteMode . FirstNonNull , 'low' ) ; } async getLinkedEditingRanges ( textDocument , position ) { const document = this . getDocument ( textDocument . uri ) ; return await this . execute < LinkedEditingRanges > ( 'getLinkedEditingRanges' , [ document , position ] , ExecuteMode . FirstNonNull , 'high' ) ; } getImplementation ( textDocument , position ) { const document = this . getDocument ( textDocument . uri ) ; return this . execute < Location [ ] | null > ( 'getImplementation' , [ document , position ] , ExecuteMode . FirstNonNull , 'high' ) ; } getTypeDefinition ( textDocument , position ) { const document = this . getDocument ( textDocument . uri ) ; return this . execute < Location [ ] | null > ( 'getTypeDefinition' , [ document , position ] , ExecuteMode . FirstNonNull , 'high' ) ; } onWatchFileChanges ( onWatchFileChangesParas ) { for ( const support of this . plugins ) { support . onWatchFileChanges ?. ( onWatchFileChangesParas ) ; } } updateTsOrJsFile ( fileName , changes ) { for ( const support of this . plugins ) { support . updateTsOrJsFile ?. ( fileName , changes ) ; } } private getDocument ( uri ) { const document = this . documentsManager . get ( uri ) ; if ( ! document ) { throw new Error ( 'Cannot call methods on an unopened document' ) ; } return document ; } private execute < T > ( name , args , mode , priority ) ; private execute < T > ( name , args , mode , priority ) ; private execute ( name , args , mode , priority ) ; private async execute < T > ( name , args , mode , priority ) { const plugins = this . plugins . filter ( ( plugin ) => typeof plugin [ name ] === 'function' ) ; if ( priority === 'low' ) { const debounce = async ( ) : Promise < boolean > => { const id = Math . random ( ) ; this . deferredRequests [ name ] = [ id , new Promise < void > ( ( resolve , reject ) => { setTimeout ( ( ) => { if ( ! this . deferredRequests [ name ] || this . deferredRequests [ name ] [ 0 ] === id ) { resolve ( ) ; } else { reject ( ) ; } } , 1000 ) ; } ) ] ; try { await this . deferredRequests [ name ] [ 1 ] ; if ( ! this . deferredRequests [ name ] ) { return debounce ( ) ; } return true ; } catch ( e ) { return false ; } } ; const shouldContinue = await debounce ( ) ; if ( ! shouldContinue ) { return ; } } switch ( mode ) { case ExecuteMode . FirstNonNull : for ( const plugin of plugins ) { const res = await this . tryExecutePlugin ( plugin , name , args , null ) ; if ( res != null ) { return res ; } } return null ; case ExecuteMode . Collect : return Promise . all ( plugins . map ( ( plugin ) => this . tryExecutePlugin ( plugin , name , args , [ ] ) ) ) ; case ExecuteMode . None : await Promise . all ( plugins . map ( ( plugin ) => this . tryExecutePlugin ( plugin , name , args , null ) ) ) ; return ; } } private async tryExecutePlugin ( plugin , fnName , args , failValue ) { try { return await plugin [ fnName ] ( ... args ) ; } catch ( e ) { Logger . error ( e ) ; return failValue ; } } }