license import * as fs from 'fs' ; import * as path from 'path' ; import * as vscode from 'coc.nvim' ; import { ProjectLoadingFinish , ProjectLoadingStart , SuggestStrictMode , SuggestStrictModeParams } from './common/notifications' ; import { NgccProgress , NgccProgressToken , NgccProgressType } from './common/progress' ; import { GetComponentsWithTemplateFile , GetTcbRequest , GetTemplateLocationForComponent , IsInAngularProject } from './common/requests' ; import { provideCompletionItem } from './middleware/provideCompletionItem' ; import { resolve , Version } from './common/resolver' ; import { isInsideComponentDecorator , isInsideInlineTemplateRegion , isInsideStringLiteral } from './embedded_support' ; import { ProgressReporter } from './progress-reporter' ; import { code2ProtocolConverter , protocol2CodeConverter } from './common/utils' ; import { DocumentUri } from 'vscode-languageserver-protocol' ; interface GetTcbResponse { uri : vscode . Uri ; content : string ; selections : vscode . Range [ ] ; } type GetComponentsForOpenExternalTemplateResponse = Array < { uri : DocumentUri ; range : vscode . Range ; } > ; export class AngularLanguageClient implements vscode . Disposable { private client : vscode . LanguageClient | null = null ; private readonly disposables : vscode . Disposable [ ] = [ ] ; private readonly outputChannel : vscode . OutputChannel ; private readonly clientOptions : vscode . LanguageClientOptions ; private readonly name = 'Angular Language Service' ; private readonly virtualDocumentContents = new Map < string , string > ( ) ; /** A map that indicates whether Angular could be found in the file's project. */ private readonly fileToIsInAngularProjectMap = new Map < string , boolean > ( ) ; constructor ( private readonly context ) { vscode . workspace . registerTextDocumentContentProvider ( 'angular-embedded-content' , { provideTextDocumentContent : uri => { return this . virtualDocumentContents . get ( uri . toString ( ) ) ; } } ) ; this . outputChannel = vscode . window . createOutputChannel ( this . name ) ; this . clientOptions = { documentSelector : [ { scheme : 'file' , language : 'html' } , { scheme : 'file' , language : 'typescript' } , ] , synchronize : { fileEvents : [ vscode . workspace . createFileSystemWatcher ( '**/tsconfig.json' ) , ] } , revealOutputChannelOn : vscode . RevealOutputChannelOn . Never , outputChannel : this . outputChannel , middleware : { prepareRename : async ( document , position , token , next ) => { if ( await this . isInAngularProject ( document ) && isInsideStringLiteral ( document , position ) ) { return next ( document , position , token ) ; } } , provideDefinition : async ( document , position , token , next ) => { if ( await this . isInAngularProject ( document ) && isInsideComponentDecorator ( document , position ) ) { return next ( document , position , token ) ; } } , provideTypeDefinition : async ( document , position , token , next ) => { if ( await this . isInAngularProject ( document ) && isInsideInlineTemplateRegion ( document , position ) ) { return next ( document , position , token ) ; } } , provideHover : async ( document , position , token , next ) => { if ( ! ( await this . isInAngularProject ( document ) ) || ! isInsideInlineTemplateRegion ( document , position ) ) { return ; } const angularResultsPromise = next ( document , position , token ) ; return angularResultsPromise ; } , provideSignatureHelp : async ( document , position , context , token , next ) => { if ( await this . isInAngularProject ( document ) && isInsideInlineTemplateRegion ( document , position ) ) { return next ( document , position , context , token ) ; } } , provideCompletionItem : async ( document , position , context , token , next ) => { if ( ! ( await this . isInAngularProject ( document ) ) || ! isInsideInlineTemplateRegion ( document , position ) ) { return ; } const angularCompletionsPromise = next ( document , position , context , token ) as Promise < vscode . CompletionItem [ ] | null | undefined > ; return angularCompletionsPromise ; } } } ; } private async isInAngularProject ( doc ) { if ( this . client === null ) { return false ; } const uri = doc . uri . toString ( ) ; if ( this . fileToIsInAngularProjectMap . has ( uri ) ) { return this . fileToIsInAngularProjectMap . get ( uri ) ! ; } try { const response = await this . client . sendRequest ( IsInAngularProject , { textDocument : code2ProtocolConverter . asTextDocumentIdentifier ( doc ) , } ) ; if ( response === null ) { return false ; } this . fileToIsInAngularProjectMap . set ( uri , response ) ; return response ; } catch { return false ; } } private createVirtualHtmlDoc ( document ) { const originalUri = document . uri . toString ( ) ; const vdocUri = vscode . Uri . file ( encodeURIComponent ( originalUri ) + '.html' ) . with ( { scheme : 'angular-embedded-content' , authority : 'html' } ) ; this . virtualDocumentContents . set ( vdocUri . toString ( ) , document . getText ( ) ) ; return vdocUri ; } /**
     * Spin up the language server in a separate process and establish a connection.
     */ async start ( ) { if ( this . client !== null ) { throw new Error ( `An existing client is running. Call stop() first.` ) ; } const serverOptions = { run : getServerOptions ( this . context , false ) , debug : getServerOptions ( this . context , true ) , } ; const forceDebug = process . env [ 'NG_DEBUG' ] === 'true' ; this . client = new vscode . LanguageClient ( 'angular' , this . name , serverOptions , this . clientOptions , forceDebug ) ; this . disposables . push ( this . client . start ( ) ) ; await this . client . onReady ( ) ; this . disposables . push ( registerNotificationHandlers ( this . client ) ) ; this . disposables . push ( registerProgressHandlers ( this . client ) ) ; } /**
     * Kill the language client and perform some clean ups.
     */ async stop ( ) { if ( this . client === null ) { return ; } await this . client . stop ( ) ; this . outputChannel . clear ( ) ; this . dispose ( ) ; this . client = null ; this . fileToIsInAngularProjectMap . clear ( ) ; this . virtualDocumentContents . clear ( ) ; } /**
    * Requests a template typecheck block at the current cursor location in the
    * specified editor.
    */ async getTcbUnderCursor ( ) { if ( this . client === null ) { return undefined ; } const doc = await vscode . workspace . document ; if ( ! doc ) { return ; } const cursor = await vscode . window . getCursorPosition ( ) ; if ( ! cursor ) { return ; } const textDocument = doc . textDocument ; const c2pConverter = code2ProtocolConverter ; const response = await this . client . sendRequest ( GetTcbRequest , { textDocument : c2pConverter . asTextDocumentIdentifier ( textDocument ) , position : cursor , } ) ; if ( response === null ) { return undefined ; } const p2cConverter = protocol2CodeConverter ; return { uri : p2cConverter . asUri ( response . uri ) , content : response . content , selections : response . selections || [ ] } ; } get initializeResult ( ) : vscode . InitializeResult | undefined { return this . client ?. initializeResult ; } async getComponentsForOpenExternalTemplate ( textDocument ) { if ( this . client === null ) { return undefined ; } const response = await this . client . sendRequest ( GetComponentsWithTemplateFile , { textDocument : code2ProtocolConverter . asTextDocumentIdentifier ( textDocument ) , } ) ; if ( response === undefined ) { return undefined ; } return response ; } async getTemplateLocationForComponent ( document ) { if ( this . client === null ) { return null ; } const position = await vscode . window . getCursorPosition ( ) ; const c2pConverter = code2ProtocolConverter ; const response = await this . client . sendRequest ( GetTemplateLocationForComponent , { textDocument : c2pConverter . asTextDocumentIdentifier ( document . textDocument ) , position : c2pConverter . asPosition ( position ) , } ) ; if ( response === null ) { return null ; } const p2cConverter = protocol2CodeConverter ; return vscode . Location . create ( p2cConverter . asUri ( response . uri ) . toString ( ) , p2cConverter . asRange ( response . range ) ) ; } dispose ( ) { for ( let d = this . disposables . pop ( ) ; d !== undefined ; d = this . disposables . pop ( ) ) { d . dispose ( ) ; } } } function registerNotificationHandlers ( client ) { let task ; client . onNotification ( ProjectLoadingStart , ( ) => { const statusBar = vscode . window . createStatusBarItem ( 0 , { progress : true } ) ; statusBar . text = 'Angular' ; statusBar . show ( ) ; task = { resolve : ( ) => { statusBar . isProgress = false ; statusBar . hide ( ) ; statusBar . dispose ( ) ; } } ; client . onNotification ( ProjectLoadingFinish , ( ) => { task . resolve ( ) ; task = undefined ; } ) ; } ) ; const disposable1 = vscode . Disposable . create ( ( ) => { if ( task ) { task . resolve ( ) ; task = undefined ; } } ) ; client . onNotification ( SuggestStrictMode , async ( params ) => { const config = vscode . workspace . getConfiguration ( ) ; if ( config . get ( 'angular.enable-strict-mode-prompt' ) === false ) { return ; } const openTsConfig = 'Open tsconfig.json' ; const doNotPromptAgain = 'Do not show this again' ; const selection = await vscode . window . showInformationMessage ( 'Some language features are not available. To access all features, enable ' + '[strictTemplates](https://angular.io/guide/angular-compiler-options#stricttemplates) in ' + '[angularCompilerOptions](https://angular.io/guide/angular-compiler-options).' , openTsConfig , doNotPromptAgain ) ; if ( selection === openTsConfig ) { await vscode . workspace . openResource ( params . configFilePath ) ; } else if ( selection === doNotPromptAgain ) { config . update ( 'angular.enable-strict-mode-prompt' , false , ( vscode as any ) . ConfigurationTarget ?. Global ) ; } } ) ; return disposable1 ; } function registerProgressHandlers ( client ) { const progressReporters = new Map < string , ProgressReporter > ( ) ; const disposable = client . onProgress ( NgccProgressType , NgccProgressToken , async ( params ) => { const { configFilePath } = params ; if ( ! progressReporters . has ( configFilePath ) ) { const reporter = new ProgressReporter ( ) ; progressReporters . set ( configFilePath , reporter ) ; } const reporter = progressReporters . get ( configFilePath ) ! ; if ( params . done ) { reporter . finish ( ) ; progressReporters . delete ( configFilePath ) ; if ( ! params . success ) { const selection = await vscode . window . showErrorMessage ( `Angular extension might not work correctly because ngcc operation failed. ` + `Try to invoke ngcc manually by running 'npm/yarn run ngcc'. ` + `Please see the extension output for more information.` , 'Show output' ) ; if ( selection ) { client . outputChannel . show ( ) ; } } } else { reporter . report ( params . message ) ; } } ) ; const reporterDisposer = vscode . Disposable . create ( ( ) => { for ( const reporter of progressReporters . values ( ) ) { reporter . finish ( ) ; } disposable . dispose ( ) ; } ) ; return reporterDisposer ; } param configName param bundled function getProbeLocations ( bundled ) { const locations = [ ] ; locations . push ( bundled ) ; const workspaceFolders = vscode . workspace . workspaceFolders || [ ] ; for ( const folder of workspaceFolders ) { locations . push ( vscode . Uri . parse ( folder . uri ) . fsPath ) ; } return locations ; } param ctx function constructArgs ( ctx , viewEngine ) { const config = vscode . workspace . getConfiguration ( ) ; const args = [ '--logToConsole' ] ; const ngLog = config . get ( 'angular.log' , 'off' ) ; if ( ngLog !== 'off' ) { const logFile = path . join ( ctx . storagePath , 'nglangsvc.log' ) ; args . push ( '--logFile' , logFile ) ; args . push ( '--logVerbosity' , ngLog ) ; } const ngProbeLocations = getProbeLocations ( ctx . extensionPath ) ; if ( viewEngine ) { args . push ( '--viewEngine' ) ; args . push ( '--ngProbeLocations' , [ path . join ( ctx . extensionPath , 'v12_language_service' ) , ... ngProbeLocations , ] . join ( ',' ) ) ; } else { args . push ( '--ngProbeLocations' , ngProbeLocations . join ( ',' ) ) ; } const includeAutomaticOptionalChainCompletions = config . get < boolean > ( 'angular.suggest.includeAutomaticOptionalChainCompletions' ) ; if ( includeAutomaticOptionalChainCompletions ) { args . push ( '--includeAutomaticOptionalChainCompletions' ) ; } const includeCompletionsWithSnippetText = config . get < boolean > ( 'angular.suggest.includeCompletionsWithSnippetText' ) ; if ( includeCompletionsWithSnippetText ) { args . push ( '--includeCompletionsWithSnippetText' ) ; } const tsdk = config . get ( 'typescript.tsdk' , null ) ; const tsProbeLocations = [ tsdk , ... getProbeLocations ( ctx . extensionPath ) ] ; args . push ( '--tsProbeLocations' , tsProbeLocations . join ( ',' ) ) ; return args ; } function getServerOptions ( ctx , debug ) { const prodEnv = { } ; const devEnv = { ... prodEnv , NG_DEBUG : true , } ; const config = vscode . workspace . getConfiguration ( ) ; const viewEngine = config . get ( 'angular.view-engine' ) || ! allProjectsSupportIvy ( ) ; const args = constructArgs ( ctx , viewEngine ) ; const prodBundle = ctx . asAbsolutePath ( path . join ( 'node_modules' , '@angular' , 'language-server' ) ) ; const devBundle = ctx . asAbsolutePath ( path . join ( 'node_modules' , '@angular' , 'language-server' ) ) ; const latestServerModule = debug && fs . existsSync ( devBundle ) ? devBundle : prodBundle ; const v12ServerModule = ctx . asAbsolutePath ( path . join ( 'v12_language_service' , 'node_modules' , '@angular' , 'language-server' ) ) ; const module = viewEngine ? v12ServerModule : latestServerModule ; const prodExecArgv = [ ] ; const devExecArgv = [ '--nolazy' , '--inspect=6009' , ] ; return { module , transport : vscode . TransportKind . ipc , args , options : { env : debug ? devEnv : prodEnv , execArgv : debug ? devExecArgv : prodExecArgv , } , } ; } function allProjectsSupportIvy ( ) { const workspaceFolders = vscode . workspace . workspaceFolders || [ ] ; for ( const workspaceFolder of workspaceFolders ) { const angularCore = resolve ( '@angular/core' , vscode . Uri . parse ( workspaceFolder . uri ) . fsPath ) ; if ( angularCore ?. version . greaterThanOrEqual ( new Version ( '9' ) ) === false ) { return false ; } } return true ; }