Skip to content

Commit a4fbc33

Browse files
kyliauKeen Yee Liau
authored andcommitted
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 dc69e28 commit a4fbc33

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
@@ -11,47 +11,44 @@ import * as lsp from 'vscode-languageserver-protocol';
1111

1212
import {NgccComplete, ProjectLanguageService, ProjectLanguageServiceParams, RunNgcc, RunNgccParams} from '../../common/notifications';
1313

14-
import {APP_COMPONENT, createConnection, initializeServer, openTextDocument} from './test_utils';
14+
import {APP_COMPONENT, createConnection, FOO_TEMPLATE, initializeServer, openTextDocument} from './test_utils';
1515

1616
describe('Angular Ivy language server', () => {
1717
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; /* 10 seconds */
1818

1919
let client: MessageConnection;
2020

21-
beforeEach(() => {
21+
beforeEach(async () => {
2222
client = createConnection({
2323
ivy: true,
2424
});
2525
client.listen();
26+
await initializeServer(client);
2627
});
2728

2829
afterEach(() => {
2930
client.dispose();
3031
});
3132

3233
it('should send ngcc notification after a project has finished loading', async () => {
33-
await initializeServer(client);
3434
openTextDocument(client, APP_COMPONENT);
3535
const configFilePath = await onRunNgccNotification(client);
3636
expect(configFilePath.endsWith('integration/project/tsconfig.json')).toBeTrue();
3737
});
3838

3939
it('should disable language service until ngcc has completed', async () => {
40-
await initializeServer(client);
4140
openTextDocument(client, APP_COMPONENT);
4241
const languageServiceEnabled = await onLanguageServiceStateNotification(client);
4342
expect(languageServiceEnabled).toBeFalse();
4443
});
4544

4645
it('should re-enable language service once ngcc has completed', async () => {
47-
await initializeServer(client);
4846
openTextDocument(client, APP_COMPONENT);
4947
const languageServiceEnabled = await waitForNgcc(client);
5048
expect(languageServiceEnabled).toBeTrue();
5149
});
5250

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

7185
function onRunNgccNotification(client: MessageConnection): Promise<string> {
@@ -84,6 +98,18 @@ function onLanguageServiceStateNotification(client: MessageConnection): Promise<
8498
});
8599
}
86100

101+
function getDiagnosticsForFile(
102+
client: MessageConnection, fileName: string): Promise<lsp.Diagnostic[]> {
103+
return new Promise(resolve => {
104+
client.onNotification(
105+
lsp.PublishDiagnosticsNotification.type, (params: lsp.PublishDiagnosticsParams) => {
106+
if (params.uri === `file://${fileName}`) {
107+
resolve(params.diagnostics);
108+
}
109+
});
110+
});
111+
}
112+
87113
async function waitForNgcc(client: MessageConnection): Promise<boolean> {
88114
const configFilePath = await onRunNgccNotification(client);
89115
// 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 * as notification from '../common/notifications';
1515
import {tsCompletionEntryToLspCompletionItem} from './completion';
1616
import {tsDiagnosticToLspDiagnostic} from './diagnostic';
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;
@@ -132,6 +132,25 @@ export class Session {
132132
// project as dirty to force update the graph.
133133
project.markAsDirty();
134134
this.info(`Enabling Ivy language service for ${project.projectName}.`);
135+
136+
// Send diagnostics since we skipped this step when opening the file
137+
// (because language service was disabled while waiting for ngcc).
138+
// First, make sure the Angular project is complete.
139+
this.runGlobalAnalysisForNewlyLoadedProject(project);
140+
project.refreshDiagnostics(); // Show initial diagnostics
141+
}
142+
143+
/**
144+
* Invoke the compiler for the first time so that external templates get
145+
* matched to the project they belong to.
146+
*/
147+
private runGlobalAnalysisForNewlyLoadedProject(project: ts.server.Project) {
148+
if (!project.hasRoots()) {
149+
return;
150+
}
151+
const fileName = project.getRootScriptInfos()[0].fileName;
152+
// Getting semantic diagnostics will trigger a global analysis.
153+
project.getLanguageService().getSemanticDiagnostics(fileName);
135154
}
136155

137156
/**
@@ -295,7 +314,6 @@ export class Session {
295314
this.projectService.findProject(configFileName) :
296315
this.projectService.getScriptInfo(filePath)?.containingProjects.find(isConfiguredProject);
297316
if (!project) {
298-
this.error(`Failed to find project for ${filePath}`);
299317
return;
300318
}
301319
if (project.languageServiceEnabled) {
@@ -574,7 +592,7 @@ export class Session {
574592
return;
575593
}
576594

577-
if (this.ivy && project instanceof ts.server.ConfiguredProject) {
595+
if (this.ivy && isConfiguredProject(project)) {
578596
// Keep language service disabled until ngcc is completed.
579597
project.disableLanguageService();
580598
this.connection.sendNotification(notification.RunNgcc, {
@@ -615,7 +633,3 @@ export class Session {
615633
return false;
616634
}
617635
}
618-
619-
function isConfiguredProject(project: ts.server.Project): project is ts.server.ConfiguredProject {
620-
return project.projectKind === ts.server.ProjectKind.Configured;
621-
}

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)