diff --git a/client/src/client.ts b/client/src/client.ts index 138342cf1a..1719ea984c 100644 --- a/client/src/client.ts +++ b/client/src/client.ts @@ -13,7 +13,7 @@ import * as lsp from 'vscode-languageclient/node'; import {ProjectLoadingFinish, ProjectLoadingStart, SuggestIvyLanguageService, SuggestIvyLanguageServiceParams, SuggestStrictMode, SuggestStrictModeParams} from '../common/notifications'; import {NgccProgress, NgccProgressToken, NgccProgressType} from '../common/progress'; -import {GetTcbRequest} from '../common/requests'; +import {GetTcbRequest, IsInAngularProject} from '../common/requests'; import {isInsideComponentDecorator, isInsideInlineTemplateRegion} from './embedded_support'; import {ProgressReporter} from './progress-reporter'; @@ -30,6 +30,9 @@ export class AngularLanguageClient implements vscode.Disposable { private readonly outputChannel: vscode.OutputChannel; private readonly clientOptions: lsp.LanguageClientOptions; private readonly name = 'Angular Language Service'; + private readonly virtualDocumentContents = new Map(); + /** A map that indicates whether Angular could be found in the file's project. */ + private readonly fileToIsInAngularProjectMap = new Map(); constructor(private readonly context: vscode.ExtensionContext) { this.outputChannel = vscode.window.createOutputChannel(this.name); @@ -55,36 +58,71 @@ export class AngularLanguageClient implements vscode.Disposable { provideDefinition: async ( document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, next: lsp.ProvideDefinitionSignature) => { - if (isInsideComponentDecorator(document, position)) { + if (await this.isInAngularProject(document) && + isInsideComponentDecorator(document, position)) { return next(document, position, token); } }, provideTypeDefinition: async ( document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, next) => { - if (isInsideInlineTemplateRegion(document, position)) { + if (await this.isInAngularProject(document) && + isInsideInlineTemplateRegion(document, position)) { return next(document, position, token); } }, provideHover: async ( document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, next: lsp.ProvideHoverSignature) => { - if (isInsideInlineTemplateRegion(document, position)) { - return next(document, position, token); + if (!(await this.isInAngularProject(document)) || + !isInsideInlineTemplateRegion(document, position)) { + return; } + return next(document, position, token); }, provideCompletionItem: async ( document: vscode.TextDocument, position: vscode.Position, context: vscode.CompletionContext, token: vscode.CancellationToken, next: lsp.ProvideCompletionItemsSignature) => { - if (isInsideInlineTemplateRegion(document, position)) { - return next(document, position, context, token); + // If not in inline template, do not perform request forwarding + if (!(await this.isInAngularProject(document)) || + !isInsideInlineTemplateRegion(document, position)) { + return; } + return next(document, position, context, token); } } }; } + private async isInAngularProject(doc: vscode.TextDocument): Promise { + 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), + }); + this.fileToIsInAngularProjectMap.set(uri, response); + return response; + } catch { + return false; + } + } + + private createVirtualHtmlDoc(document: vscode.TextDocument): vscode.Uri { + 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. */ @@ -171,7 +209,8 @@ export class AngularLanguageClient implements vscode.Disposable { } function registerNotificationHandlers(client: lsp.LanguageClient): vscode.Disposable { - const disposable1 = client.onNotification(ProjectLoadingStart, () => { + const disposables: vscode.Disposable[] = []; + disposables.push(client.onNotification(ProjectLoadingStart, () => { vscode.window.withProgress( { location: vscode.ProgressLocation.Window, @@ -181,27 +220,26 @@ function registerNotificationHandlers(client: lsp.LanguageClient): vscode.Dispos client.onNotification(ProjectLoadingFinish, resolve); }), ); - }); + })); - const disposable2 = - client.onNotification(SuggestStrictMode, async (params: SuggestStrictModeParams) => { - const openTsConfig = 'Open tsconfig.json'; - // Markdown is not generally supported in `showInformationMessage()`, - // but links are supported. See - // https://github.com/microsoft/vscode/issues/20595#issuecomment-281099832 - 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, - ); - if (selection === openTsConfig) { - const document = await vscode.workspace.openTextDocument(params.configFilePath); - vscode.window.showTextDocument(document); - } - }); + disposables.push(client.onNotification(SuggestStrictMode, async (params: SuggestStrictModeParams) => { + const openTsConfig = 'Open tsconfig.json'; + // Markdown is not generally supported in `showInformationMessage()`, + // but links are supported. See + // https://github.com/microsoft/vscode/issues/20595#issuecomment-281099832 + 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, + ); + if (selection === openTsConfig) { + const document = await vscode.workspace.openTextDocument(params.configFilePath); + vscode.window.showTextDocument(document); + } + })); - const disposable3 = client.onNotification( + disposables.push(client.onNotification( SuggestIvyLanguageService, async (params: SuggestIvyLanguageServiceParams) => { const config = vscode.workspace.getConfiguration(); if (config.get('angular.enable-experimental-ivy-prompt') === false) { @@ -221,9 +259,9 @@ function registerNotificationHandlers(client: lsp.LanguageClient): vscode.Dispos config.update( 'angular.enable-experimental-ivy-prompt', false, vscode.ConfigurationTarget.Global); } - }); + })); - return vscode.Disposable.from(disposable1, disposable2, disposable3); + return vscode.Disposable.from(...disposables); } function registerProgressHandlers(client: lsp.LanguageClient) { diff --git a/common/requests.ts b/common/requests.ts index a93c80e78d..de996bc041 100644 --- a/common/requests.ts +++ b/common/requests.ts @@ -21,3 +21,11 @@ export interface GetTcbResponse { content: string; selections: lsp.Range[] } + +export const IsInAngularProject = + new lsp.RequestType( + 'angular/isAngularCoreInOwningProject'); + +export interface IsInAngularProjectParams { + textDocument: lsp.TextDocumentIdentifier; +} \ No newline at end of file diff --git a/integration/lsp/ivy_spec.ts b/integration/lsp/ivy_spec.ts index 5e4a56ba24..cd73fe6bb9 100644 --- a/integration/lsp/ivy_spec.ts +++ b/integration/lsp/ivy_spec.ts @@ -14,7 +14,7 @@ import {URI} from 'vscode-uri'; import {ProjectLanguageService, ProjectLanguageServiceParams, SuggestStrictMode, SuggestStrictModeParams} from '../../common/notifications'; import {NgccProgress, NgccProgressToken, NgccProgressType} from '../../common/progress'; -import {GetTcbRequest} from '../../common/requests'; +import {GetTcbRequest, IsInAngularProject} from '../../common/requests'; import {APP_COMPONENT, createConnection, createTracer, FOO_COMPONENT, FOO_TEMPLATE, initializeServer, openTextDocument, PROJECT_PATH, TSCONFIG} from './test_utils'; @@ -380,6 +380,23 @@ describe('Angular Ivy language server', () => { expect(response).toBeDefined(); }); }); + + it('detects an Angular project', async () => { + openTextDocument(client, FOO_TEMPLATE); + await waitForNgcc(client); + const templateResponse = await client.sendRequest(IsInAngularProject, { + textDocument: { + uri: `file://${FOO_TEMPLATE}`, + } + }); + expect(templateResponse).toBe(true); + const componentResponse = await client.sendRequest(IsInAngularProject, { + textDocument: { + uri: `file://${FOO_COMPONENT}`, + } + }); + expect(componentResponse).toBe(true); + }) }); function onNgccProgress(client: MessageConnection): Promise { diff --git a/server/src/session.ts b/server/src/session.ts index 1c951c4a4b..da5c413519 100644 --- a/server/src/session.ts +++ b/server/src/session.ts @@ -15,7 +15,7 @@ import * as lsp from 'vscode-languageserver/node'; import {ServerOptions} from '../common/initialize'; import {ProjectLanguageService, ProjectLoadingFinish, ProjectLoadingStart, SuggestIvyLanguageService, SuggestStrictMode} from '../common/notifications'; import {NgccProgressToken, NgccProgressType} from '../common/progress'; -import {GetTcbParams, GetTcbRequest, GetTcbResponse} from '../common/requests'; +import {GetTcbParams, GetTcbRequest, GetTcbResponse, IsInAngularProject, IsInAngularProjectParams} from '../common/requests'; import {readNgCompletionData, tsCompletionEntryToLspCompletionItem} from './completion'; import {tsDiagnosticToLspDiagnostic} from './diagnostic'; @@ -164,6 +164,27 @@ export class Session { conn.onCompletion(p => this.onCompletion(p)); conn.onCompletionResolve(p => this.onCompletionResolve(p)); conn.onRequest(GetTcbRequest, p => this.onGetTcb(p)); + conn.onRequest(IsInAngularProject, p => this.isInAngularProject(p)); + } + + private isInAngularProject(params: IsInAngularProjectParams): boolean { + const filePath = uriToFilePath(params.textDocument.uri); + if (!filePath) { + return false; + } + const scriptInfo = this.projectService.getScriptInfo(filePath); + if (!scriptInfo) { + return false; + } + const project = this.projectService.getDefaultProjectForFile( + scriptInfo.fileName, + false // ensureProject + ); + if (!project) { + return false; + } + const angularCore = project.getFileNames().find(isAngularCore); + return angularCore !== undefined; } private onGetTcb(params: GetTcbParams): GetTcbResponse|undefined {