Skip to content

Commit 1ca74ed

Browse files
committed
fix: match external templates to respective projects on startup
Currently, Ivy LS is not able to match external projects to their external templates if no TS files are open. This is because Ivy LS does not implement `getExternalFiles()`. To fix this, we take a different route: After ngcc has run, we send diagnostics for all the open files. But in the case where all open files are external templates, we first get diagnostics for a root TS file, then process the rest. Processing a root TS file triggers a global analysis, during which we match the external templates to their project. For a more detailed explanation, see https://github.com/angular/vscode-ng-language-service/wiki/Project-Matching-for-External-Templates Close #976
1 parent ff5dcda commit 1ca74ed

File tree

3 files changed

+58
-13
lines changed

3 files changed

+58
-13
lines changed

integration/lsp/ivy_spec.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,49 +6,48 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import ts = require('typescript');
910
import {MessageConnection} from 'vscode-jsonrpc';
1011
import * as lsp from 'vscode-languageserver-protocol';
11-
import {APP_COMPONENT, createConnection, initializeServer, openTextDocument} from './test_utils';
12+
13+
import {APP_COMPONENT, createConnection, FOO_TEMPLATE, initializeServer, openTextDocument} from './test_utils';
1214

1315
describe('Angular Ivy language server', () => {
1416
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; /* 10 seconds */
1517

1618
let client: MessageConnection;
1719

18-
beforeEach(() => {
20+
beforeEach(async () => {
1921
client = createConnection({
2022
ivy: true,
2123
});
2224
client.listen();
25+
await initializeServer(client);
2326
});
2427

2528
afterEach(() => {
2629
client.dispose();
2730
});
2831

2932
it('should send ngcc notification after a project has finished loading', async () => {
30-
await initializeServer(client);
3133
openTextDocument(client, APP_COMPONENT);
3234
const configFilePath = await onRunNgccNotification(client);
3335
expect(configFilePath.endsWith('integration/project/tsconfig.json')).toBeTrue();
3436
});
3537

3638
it('should disable language service until ngcc has completed', async () => {
37-
await initializeServer(client);
3839
openTextDocument(client, APP_COMPONENT);
3940
const languageServiceEnabled = await onLanguageServiceStateNotification(client);
4041
expect(languageServiceEnabled).toBeFalse();
4142
});
4243

4344
it('should re-enable language service once ngcc has completed', async () => {
44-
await initializeServer(client);
4545
openTextDocument(client, APP_COMPONENT);
4646
const languageServiceEnabled = await waitForNgcc(client);
4747
expect(languageServiceEnabled).toBeTrue();
4848
});
4949

5050
it('should handle hover on inline template', async () => {
51-
await initializeServer(client);
5251
openTextDocument(client, APP_COMPONENT);
5352
const languageServiceEnabled = await waitForNgcc(client);
5453
expect(languageServiceEnabled).toBeTrue();
@@ -63,6 +62,23 @@ describe('Angular Ivy language server', () => {
6362
value: '(property) AppComponent.name: string',
6463
});
6564
});
65+
66+
it('should show existing diagnostics on external template', async () => {
67+
client.sendNotification(lsp.DidOpenTextDocumentNotification.type, {
68+
textDocument: {
69+
uri: `file://${FOO_TEMPLATE}`,
70+
languageId: 'typescript',
71+
version: 1,
72+
text: `{{ doesnotexist }}`,
73+
},
74+
});
75+
const languageServiceEnabled = await waitForNgcc(client);
76+
expect(languageServiceEnabled).toBeTrue();
77+
const diagnostics = await getDiagnosticsForFile(client, FOO_TEMPLATE);
78+
expect(diagnostics.length).toBe(1);
79+
expect(diagnostics[0].message)
80+
.toBe(`Property 'doesnotexist' does not exist on type 'FooComponent'.`);
81+
});
6682
});
6783

6884
function onRunNgccNotification(client: MessageConnection): Promise<string> {
@@ -87,6 +103,16 @@ function onLanguageServiceStateNotification(client: MessageConnection): Promise<
87103
});
88104
}
89105

106+
function getDiagnosticsForFile(client: MessageConnection, fileName: string): Promise<lsp.Diagnostic[]> {
107+
return new Promise(resolve => {
108+
client.onNotification(lsp.PublishDiagnosticsNotification.type, (params: lsp.PublishDiagnosticsParams) => {
109+
if (params.uri === `file://${fileName}`) {
110+
resolve(params.diagnostics);
111+
}
112+
});
113+
});
114+
}
115+
90116
async function waitForNgcc(client: MessageConnection): Promise<boolean> {
91117
const configFilePath = await onRunNgccNotification(client);
92118
// We run ngcc before the test, so no need to do anything here.

server/src/session.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {tsCompletionEntryToLspCompletionItem} from './completion';
1515
import {tsDiagnosticToLspDiagnostic} from './diagnostic';
1616
import {Logger} from './logger';
1717
import {ServerHost} from './server_host';
18-
import {filePathToUri, lspPositionToTsPosition, lspRangeToTsPositions, tsTextSpanToLspRange, uriToFilePath} from './utils';
18+
import {filePathToUri, isConfiguredProject, lspPositionToTsPosition, lspRangeToTsPositions, tsTextSpanToLspRange, uriToFilePath} from './utils';
1919

2020
export interface SessionOptions {
2121
host: ServerHost;
@@ -131,6 +131,25 @@ export class Session {
131131
// project as dirty to force update the graph.
132132
project.markAsDirty();
133133
this.info(`Enabling Ivy language service for ${project.projectName}.`);
134+
135+
// Send diagnostics since we skipped this step when opening the file
136+
// (because language service was disabled while waiting for ngcc).
137+
// First, make sure the Angular project is complete.
138+
this.runGlobalAnalysisForNewlyLoadedProject(project);
139+
project.refreshDiagnostics(); // Show initial diagnostics
140+
}
141+
142+
/**
143+
* Invoke the compiler for the first time so that external templates get
144+
* matched to the project they belong to.
145+
*/
146+
private runGlobalAnalysisForNewlyLoadedProject(project: ts.server.Project) {
147+
if (!project.hasRoots()) {
148+
return;
149+
}
150+
const fileName = project.getRootScriptInfos()[0].fileName;
151+
// Getting semantic diagnostics will trigger a global analysis.
152+
project.getLanguageService().getSemanticDiagnostics(fileName);
134153
}
135154

136155
/**
@@ -289,7 +308,6 @@ export class Session {
289308
this.projectService.findProject(configFileName) :
290309
this.projectService.getScriptInfo(filePath)?.containingProjects.find(isConfiguredProject);
291310
if (!project) {
292-
this.error(`Failed to find project for ${filePath}`);
293311
return;
294312
}
295313
if (project.languageServiceEnabled) {
@@ -549,7 +567,7 @@ export class Session {
549567
return;
550568
}
551569

552-
if (this.ivy && project instanceof ts.server.ConfiguredProject) {
570+
if (this.ivy && isConfiguredProject(project)) {
553571
// Keep language service disabled until ngcc is completed.
554572
project.disableLanguageService();
555573
this.connection.sendNotification(notification.RunNgcc, {
@@ -590,7 +608,3 @@ export class Session {
590608
return false;
591609
}
592610
}
593-
594-
function isConfiguredProject(project: ts.server.Project): project is ts.server.ConfiguredProject {
595-
return project.projectKind === ts.server.ProjectKind.Configured;
596-
}

server/src/utils.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,8 @@ export function lspRangeToTsPositions(
7474
const end = lspPositionToTsPosition(scriptInfo, range.end);
7575
return [start, end];
7676
}
77+
78+
export function isConfiguredProject(project: ts.server.Project):
79+
project is ts.server.ConfiguredProject {
80+
return project.projectKind === ts.server.ProjectKind.Configured;
81+
}

0 commit comments

Comments
 (0)