license import * as fs from 'fs' ; import * as path from 'path' ; import * as vscode from 'vscode' ; import * as lsp from 'vscode-languageclient/node' ; import { ProjectLoadingFinish , ProjectLoadingStart , SuggestStrictMode , SuggestStrictModeParams } from '../common/notifications' ; import { NgccProgress , NgccProgressToken , NgccProgressType } from '../common/progress' ; import { GetComponentsWithTemplateFile , GetTcbRequest , GetTemplateLocationForComponent , IsInAngularProject } from '../common/requests' ; import { resolve , Version } from '../common/resolver' ; import { isInsideComponentDecorator , isInsideInlineTemplateRegion , isInsideStringLiteral } from './embedded_support' ; import { ProgressReporter } from './progress-reporter' ; interface GetTcbResponse { uri : vscode . Uri ; content : string ; selections : vscode . Range [ ] ; } export class AngularLanguageClient implements vscode . Disposable { private client : lsp . LanguageClient | null = null ; private readonly disposables : vscode . Disposable [ ] = [ ] ; private readonly outputChannel : vscode . OutputChannel ; private readonly clientOptions : lsp . 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 : lsp . 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 ) ; if ( document . languageId === 'typescript' ) { const vdocUri = this . createVirtualHtmlDoc ( document ) ; const htmlProviderResultsPromise = vscode . commands . executeCommand < vscode . Hover [ ] > ( 'vscode.executeHoverProvider' , vdocUri , position ) ; const [ angularResults , htmlProviderResults ] = await Promise . all ( [ angularResultsPromise , htmlProviderResultsPromise ] ) ; return angularResults ?? htmlProviderResults ?. [ 0 ] ; } 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 > ; if ( document . languageId === 'typescript' ) { const vdocUri = this . createVirtualHtmlDoc ( document ) ; const htmlProviderCompletionsPromise = vscode . commands . executeCommand < vscode . CompletionList > ( 'vscode.executeCompletionItemProvider' , vdocUri , position , context . triggerCharacter ) ; const [ angularCompletions , htmlProviderCompletions ] = await Promise . all ( [ angularCompletionsPromise , htmlProviderCompletionsPromise ] ) ; return [ ... ( angularCompletions ?? [ ] ) , ... ( htmlProviderCompletions ?. items ?? [ ] ) ] ; } 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 : this . client . 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 lsp . 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 ( textEditor ) { if ( this . client === null ) { return undefined ; } const c2pConverter = this . client . code2ProtocolConverter ; const response = await this . client . sendRequest ( GetTcbRequest , { textDocument : c2pConverter . asTextDocumentIdentifier ( textEditor . document ) , position : c2pConverter . asPosition ( textEditor . selection . active ) , } ) ; if ( response === null ) { return undefined ; } const p2cConverter = this . client . protocol2CodeConverter ; return { uri : p2cConverter . asUri ( response . uri ) , content : response . content , selections : p2cConverter . asRanges ( response . selections ) , } ; } get initializeResult ( ) : lsp . InitializeResult | undefined { return this . client ?. initializeResult ; } async getComponentsForOpenExternalTemplate ( textEditor ) { if ( this . client === null ) { return undefined ; } const response = await this . client . sendRequest ( GetComponentsWithTemplateFile , { textDocument : this . client . code2ProtocolConverter . asTextDocumentIdentifier ( textEditor . document ) , } ) ; if ( response === undefined ) { return undefined ; } const p2cConverter = this . client . protocol2CodeConverter ; return response . map ( v => new vscode . Location ( p2cConverter . asUri ( v . uri ) , p2cConverter . asRange ( v . range ) ) ) ; } async getTemplateLocationForComponent ( textEditor ) { if ( this . client === null ) { return null ; } const c2pConverter = this . client . code2ProtocolConverter ; const response = await this . client . sendRequest ( GetTemplateLocationForComponent , { textDocument : c2pConverter . asTextDocumentIdentifier ( textEditor . document ) , position : c2pConverter . asPosition ( textEditor . selection . active ) , } ) ; if ( response === null ) { return null ; } const p2cConverter = this . client . protocol2CodeConverter ; return new vscode . Location ( p2cConverter . asUri ( response . uri ) , p2cConverter . asRange ( response . range ) ) ; } dispose ( ) { for ( let d = this . disposables . pop ( ) ; d !== undefined ; d = this . disposables . pop ( ) ) { d . dispose ( ) ; } } } function registerNotificationHandlers ( client ) { const disposables = [ ] ; disposables . push ( client . onNotification ( ProjectLoadingStart , ( ) => { vscode . window . withProgress ( { location : vscode . ProgressLocation . Window , title : 'Initializing Angular language features' , } , ( ) => new Promise < void > ( ( resolve ) => { client . onNotification ( ProjectLoadingFinish , resolve ) ; } ) ) ; } ) ) ; disposables . push ( 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 again for this workspace' ; 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 ) { const document = await vscode . workspace . openTextDocument ( params . configFilePath ) ; vscode . window . showTextDocument ( document ) ; } else if ( selection === doNotPromptAgain ) { config . update ( 'angular.enable-strict-mode-prompt' , false , vscode . ConfigurationTarget . Workspace ) ; } } ) ) ; return vscode . Disposable . from ( ... disposables ) ; } function registerProgressHandlers ( client ) { const progressReporters = new Map < string , ProgressReporter > ( ) ; const disposable = client . onProgress ( NgccProgressType , NgccProgressToken , async ( params ) => { const { configFilePath } = params ; if ( ! progressReporters . has ( configFilePath ) ) { progressReporters . set ( configFilePath , new ProgressReporter ( ) ) ; } 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 . from ( { dispose ( ) { 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 ( 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 . logUri . fsPath , '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 ( ) ; let viewEngine = config . get ( 'angular.view-engine' ) || ! allProjectsSupportIvy ( ) ; if ( viewEngine && ! allProjectsSupportVE ( ) ) { viewEngine = false ; if ( config . get ( 'angular.view-engine' ) ) { vscode . window . showErrorMessage ( `The legacy View Engine option is enabled but the workspace contains a version 13 Angular project.` + ` Legacy View Engine will be disabled since support for it was dropped in v13.` ) ; } else if ( ! allProjectsSupportIvy ( ) && ! allProjectsSupportVE ( ) ) { vscode . window . showErrorMessage ( `The workspace contains a project that does not support legacy View Engine (Angular v13+) and a project that does not support the new current runtime (v8 and below).` + `The extension will not work for the legacy project in this workspace.` ) ; } } const args = constructArgs ( ctx , viewEngine ) ; const prodBundle = ctx . asAbsolutePath ( 'server' ) ; const devBundle = ctx . asAbsolutePath ( path . join ( 'dist' , 'server' , 'server.js' ) ) ; 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 : lsp . TransportKind . ipc , args , options : { env : debug ? devEnv : prodEnv , execArgv : debug ? devExecArgv : prodExecArgv , } , } ; } /**
 * Returns true if all projects in the workspace support Ivy LS, otherwise
 * return false.
 */ function allProjectsSupportIvy ( ) { const workspaceFolders = vscode . workspace . workspaceFolders || [ ] ; for ( const workspaceFolder of workspaceFolders ) { const angularCore = resolve ( '@angular/core' , workspaceFolder . uri . fsPath ) ; if ( angularCore ?. version . greaterThanOrEqual ( new Version ( '9' ) ) === false ) { return false ; } } return true ; } function allProjectsSupportVE ( ) { const workspaceFolders = vscode . workspace . workspaceFolders || [ ] ; for ( const workspaceFolder of workspaceFolders ) { const angularCore = resolve ( '@angular/core' , workspaceFolder . uri . fsPath ) ; if ( angularCore ?. version . greaterThanOrEqual ( new Version ( '13' ) ) === true ) { return false ; } } return true ; }