Skip to content

Commit 154cf5e

Browse files
atscottKeen Yee Liau
authored andcommitted
perf: Avoid making Angular-related decisions for files not in an Angular project (#1259)
This commit updates our client-side short-circuit logic to provide an additiona short circuit that avoids tokenizing typescript files when they are not within a project that uses Angular. fixes #1237
1 parent 4eed99b commit 154cf5e

File tree

4 files changed

+115
-31
lines changed

4 files changed

+115
-31
lines changed

client/src/client.ts

Lines changed: 67 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import * as lsp from 'vscode-languageclient/node';
1313

1414
import {ProjectLoadingFinish, ProjectLoadingStart, SuggestIvyLanguageService, SuggestIvyLanguageServiceParams, SuggestStrictMode, SuggestStrictModeParams} from '../common/notifications';
1515
import {NgccProgress, NgccProgressToken, NgccProgressType} from '../common/progress';
16-
import {GetTcbRequest} from '../common/requests';
16+
import {GetTcbRequest, IsInAngularProject} from '../common/requests';
1717

1818
import {isInsideComponentDecorator, isInsideInlineTemplateRegion} from './embedded_support';
1919
import {ProgressReporter} from './progress-reporter';
@@ -30,6 +30,9 @@ export class AngularLanguageClient implements vscode.Disposable {
3030
private readonly outputChannel: vscode.OutputChannel;
3131
private readonly clientOptions: lsp.LanguageClientOptions;
3232
private readonly name = 'Angular Language Service';
33+
private readonly virtualDocumentContents = new Map<string, string>();
34+
/** A map that indicates whether Angular could be found in the file's project. */
35+
private readonly fileToIsInAngularProjectMap = new Map<string, boolean>();
3336

3437
constructor(private readonly context: vscode.ExtensionContext) {
3538
this.outputChannel = vscode.window.createOutputChannel(this.name);
@@ -55,36 +58,71 @@ export class AngularLanguageClient implements vscode.Disposable {
5558
provideDefinition: async (
5659
document: vscode.TextDocument, position: vscode.Position,
5760
token: vscode.CancellationToken, next: lsp.ProvideDefinitionSignature) => {
58-
if (isInsideComponentDecorator(document, position)) {
61+
if (await this.isInAngularProject(document) &&
62+
isInsideComponentDecorator(document, position)) {
5963
return next(document, position, token);
6064
}
6165
},
6266
provideTypeDefinition: async (
6367
document: vscode.TextDocument, position: vscode.Position,
6468
token: vscode.CancellationToken, next) => {
65-
if (isInsideInlineTemplateRegion(document, position)) {
69+
if (await this.isInAngularProject(document) &&
70+
isInsideInlineTemplateRegion(document, position)) {
6671
return next(document, position, token);
6772
}
6873
},
6974
provideHover: async (
7075
document: vscode.TextDocument, position: vscode.Position,
7176
token: vscode.CancellationToken, next: lsp.ProvideHoverSignature) => {
72-
if (isInsideInlineTemplateRegion(document, position)) {
73-
return next(document, position, token);
77+
if (!(await this.isInAngularProject(document)) ||
78+
!isInsideInlineTemplateRegion(document, position)) {
79+
return;
7480
}
81+
return next(document, position, token);
7582
},
7683
provideCompletionItem: async (
7784
document: vscode.TextDocument, position: vscode.Position,
7885
context: vscode.CompletionContext, token: vscode.CancellationToken,
7986
next: lsp.ProvideCompletionItemsSignature) => {
80-
if (isInsideInlineTemplateRegion(document, position)) {
81-
return next(document, position, context, token);
87+
// If not in inline template, do not perform request forwarding
88+
if (!(await this.isInAngularProject(document)) ||
89+
!isInsideInlineTemplateRegion(document, position)) {
90+
return;
8291
}
92+
return next(document, position, context, token);
8393
}
8494
}
8595
};
8696
}
8797

98+
private async isInAngularProject(doc: vscode.TextDocument): Promise<boolean> {
99+
if (this.client === null) {
100+
return false;
101+
}
102+
const uri = doc.uri.toString();
103+
if (this.fileToIsInAngularProjectMap.has(uri)) {
104+
return this.fileToIsInAngularProjectMap.get(uri)!;
105+
}
106+
107+
try {
108+
const response = await this.client.sendRequest(IsInAngularProject, {
109+
textDocument: this.client.code2ProtocolConverter.asTextDocumentIdentifier(doc),
110+
});
111+
this.fileToIsInAngularProjectMap.set(uri, response);
112+
return response;
113+
} catch {
114+
return false;
115+
}
116+
}
117+
118+
private createVirtualHtmlDoc(document: vscode.TextDocument): vscode.Uri {
119+
const originalUri = document.uri.toString();
120+
const vdocUri = vscode.Uri.file(encodeURIComponent(originalUri) + '.html')
121+
.with({scheme: 'angular-embedded-content', authority: 'html'});
122+
this.virtualDocumentContents.set(vdocUri.toString(), document.getText());
123+
return vdocUri;
124+
}
125+
88126
/**
89127
* Spin up the language server in a separate process and establish a connection.
90128
*/
@@ -171,7 +209,8 @@ export class AngularLanguageClient implements vscode.Disposable {
171209
}
172210

173211
function registerNotificationHandlers(client: lsp.LanguageClient): vscode.Disposable {
174-
const disposable1 = client.onNotification(ProjectLoadingStart, () => {
212+
const disposables: vscode.Disposable[] = [];
213+
disposables.push(client.onNotification(ProjectLoadingStart, () => {
175214
vscode.window.withProgress(
176215
{
177216
location: vscode.ProgressLocation.Window,
@@ -181,27 +220,26 @@ function registerNotificationHandlers(client: lsp.LanguageClient): vscode.Dispos
181220
client.onNotification(ProjectLoadingFinish, resolve);
182221
}),
183222
);
184-
});
223+
}));
185224

186-
const disposable2 =
187-
client.onNotification(SuggestStrictMode, async (params: SuggestStrictModeParams) => {
188-
const openTsConfig = 'Open tsconfig.json';
189-
// Markdown is not generally supported in `showInformationMessage()`,
190-
// but links are supported. See
191-
// https://github.com/microsoft/vscode/issues/20595#issuecomment-281099832
192-
const selection = await vscode.window.showInformationMessage(
193-
'Some language features are not available. To access all features, enable ' +
194-
'[strictTemplates](https://angular.io/guide/angular-compiler-options#stricttemplates) in ' +
195-
'[angularCompilerOptions](https://angular.io/guide/angular-compiler-options).',
196-
openTsConfig,
197-
);
198-
if (selection === openTsConfig) {
199-
const document = await vscode.workspace.openTextDocument(params.configFilePath);
200-
vscode.window.showTextDocument(document);
201-
}
202-
});
225+
disposables.push(client.onNotification(SuggestStrictMode, async (params: SuggestStrictModeParams) => {
226+
const openTsConfig = 'Open tsconfig.json';
227+
// Markdown is not generally supported in `showInformationMessage()`,
228+
// but links are supported. See
229+
// https://github.com/microsoft/vscode/issues/20595#issuecomment-281099832
230+
const selection = await vscode.window.showInformationMessage(
231+
'Some language features are not available. To access all features, enable ' +
232+
'[strictTemplates](https://angular.io/guide/angular-compiler-options#stricttemplates) in ' +
233+
'[angularCompilerOptions](https://angular.io/guide/angular-compiler-options).',
234+
openTsConfig,
235+
);
236+
if (selection === openTsConfig) {
237+
const document = await vscode.workspace.openTextDocument(params.configFilePath);
238+
vscode.window.showTextDocument(document);
239+
}
240+
}));
203241

204-
const disposable3 = client.onNotification(
242+
disposables.push(client.onNotification(
205243
SuggestIvyLanguageService, async (params: SuggestIvyLanguageServiceParams) => {
206244
const config = vscode.workspace.getConfiguration();
207245
if (config.get('angular.enable-experimental-ivy-prompt') === false) {
@@ -221,9 +259,9 @@ function registerNotificationHandlers(client: lsp.LanguageClient): vscode.Dispos
221259
config.update(
222260
'angular.enable-experimental-ivy-prompt', false, vscode.ConfigurationTarget.Global);
223261
}
224-
});
262+
}));
225263

226-
return vscode.Disposable.from(disposable1, disposable2, disposable3);
264+
return vscode.Disposable.from(...disposables);
227265
}
228266

229267
function registerProgressHandlers(client: lsp.LanguageClient) {

common/requests.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,11 @@ export interface GetTcbResponse {
2121
content: string;
2222
selections: lsp.Range[]
2323
}
24+
25+
export const IsInAngularProject =
26+
new lsp.RequestType<IsInAngularProjectParams, boolean, /* error */ void>(
27+
'angular/isAngularCoreInOwningProject');
28+
29+
export interface IsInAngularProjectParams {
30+
textDocument: lsp.TextDocumentIdentifier;
31+
}

integration/lsp/ivy_spec.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {URI} from 'vscode-uri';
1414

1515
import {ProjectLanguageService, ProjectLanguageServiceParams, SuggestStrictMode, SuggestStrictModeParams} from '../../common/notifications';
1616
import {NgccProgress, NgccProgressToken, NgccProgressType} from '../../common/progress';
17-
import {GetTcbRequest} from '../../common/requests';
17+
import {GetTcbRequest, IsInAngularProject} from '../../common/requests';
1818

1919
import {APP_COMPONENT, createConnection, createTracer, FOO_COMPONENT, FOO_TEMPLATE, initializeServer, openTextDocument, PROJECT_PATH, TSCONFIG} from './test_utils';
2020

@@ -380,6 +380,23 @@ describe('Angular Ivy language server', () => {
380380
expect(response).toBeDefined();
381381
});
382382
});
383+
384+
it('detects an Angular project', async () => {
385+
openTextDocument(client, FOO_TEMPLATE);
386+
await waitForNgcc(client);
387+
const templateResponse = await client.sendRequest(IsInAngularProject, {
388+
textDocument: {
389+
uri: `file://${FOO_TEMPLATE}`,
390+
}
391+
});
392+
expect(templateResponse).toBe(true);
393+
const componentResponse = await client.sendRequest(IsInAngularProject, {
394+
textDocument: {
395+
uri: `file://${FOO_COMPONENT}`,
396+
}
397+
});
398+
expect(componentResponse).toBe(true);
399+
})
383400
});
384401

385402
function onNgccProgress(client: MessageConnection): Promise<string> {

server/src/session.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import * as lsp from 'vscode-languageserver/node';
1515
import {ServerOptions} from '../common/initialize';
1616
import {ProjectLanguageService, ProjectLoadingFinish, ProjectLoadingStart, SuggestIvyLanguageService, SuggestStrictMode} from '../common/notifications';
1717
import {NgccProgressToken, NgccProgressType} from '../common/progress';
18-
import {GetTcbParams, GetTcbRequest, GetTcbResponse} from '../common/requests';
18+
import {GetTcbParams, GetTcbRequest, GetTcbResponse, IsInAngularProject, IsInAngularProjectParams} from '../common/requests';
1919

2020
import {readNgCompletionData, tsCompletionEntryToLspCompletionItem} from './completion';
2121
import {tsDiagnosticToLspDiagnostic} from './diagnostic';
@@ -164,6 +164,27 @@ export class Session {
164164
conn.onCompletion(p => this.onCompletion(p));
165165
conn.onCompletionResolve(p => this.onCompletionResolve(p));
166166
conn.onRequest(GetTcbRequest, p => this.onGetTcb(p));
167+
conn.onRequest(IsInAngularProject, p => this.isInAngularProject(p));
168+
}
169+
170+
private isInAngularProject(params: IsInAngularProjectParams): boolean {
171+
const filePath = uriToFilePath(params.textDocument.uri);
172+
if (!filePath) {
173+
return false;
174+
}
175+
const scriptInfo = this.projectService.getScriptInfo(filePath);
176+
if (!scriptInfo) {
177+
return false;
178+
}
179+
const project = this.projectService.getDefaultProjectForFile(
180+
scriptInfo.fileName,
181+
false // ensureProject
182+
);
183+
if (!project) {
184+
return false;
185+
}
186+
const angularCore = project.getFileNames().find(isAngularCore);
187+
return angularCore !== undefined;
167188
}
168189

169190
private onGetTcb(params: GetTcbParams): GetTcbResponse|undefined {

0 commit comments

Comments
 (0)