Skip to content

Commit 072534c

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://docs.google.com/document/d/1rrgPvQG6G9KfywhVfFhNvNOlwyRlx7c0TWw5WZKSvY8/edit?usp=sharing Close #976
1 parent ff5dcda commit 072534c

File tree

3 files changed

+57
-13
lines changed

3 files changed

+57
-13
lines changed

integration/lsp/ivy_spec.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,47 +8,45 @@
88

99
import {MessageConnection} from 'vscode-jsonrpc';
1010
import * as lsp from 'vscode-languageserver-protocol';
11-
import {APP_COMPONENT, createConnection, initializeServer, openTextDocument} from './test_utils';
11+
12+
import {APP_COMPONENT, createConnection, FOO_TEMPLATE, initializeServer, openTextDocument} from './test_utils';
1213

1314
describe('Angular Ivy language server', () => {
1415
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; /* 10 seconds */
1516

1617
let client: MessageConnection;
1718

18-
beforeEach(() => {
19+
beforeEach(async () => {
1920
client = createConnection({
2021
ivy: true,
2122
});
2223
client.listen();
24+
await initializeServer(client);
2325
});
2426

2527
afterEach(() => {
2628
client.dispose();
2729
});
2830

2931
it('should send ngcc notification after a project has finished loading', async () => {
30-
await initializeServer(client);
3132
openTextDocument(client, APP_COMPONENT);
3233
const configFilePath = await onRunNgccNotification(client);
3334
expect(configFilePath.endsWith('integration/project/tsconfig.json')).toBeTrue();
3435
});
3536

3637
it('should disable language service until ngcc has completed', async () => {
37-
await initializeServer(client);
3838
openTextDocument(client, APP_COMPONENT);
3939
const languageServiceEnabled = await onLanguageServiceStateNotification(client);
4040
expect(languageServiceEnabled).toBeFalse();
4141
});
4242

4343
it('should re-enable language service once ngcc has completed', async () => {
44-
await initializeServer(client);
4544
openTextDocument(client, APP_COMPONENT);
4645
const languageServiceEnabled = await waitForNgcc(client);
4746
expect(languageServiceEnabled).toBeTrue();
4847
});
4948

5049
it('should handle hover on inline template', async () => {
51-
await initializeServer(client);
5250
openTextDocument(client, APP_COMPONENT);
5351
const languageServiceEnabled = await waitForNgcc(client);
5452
expect(languageServiceEnabled).toBeTrue();
@@ -63,6 +61,30 @@ describe('Angular Ivy language server', () => {
6361
value: '(property) AppComponent.name: string',
6462
});
6563
});
64+
65+
it('should show existing diagnostics on external template', async () => {
66+
client.sendNotification(lsp.DidOpenTextDocumentNotification.type, {
67+
textDocument: {
68+
uri: `file://${FOO_TEMPLATE}`,
69+
languageId: 'typescript',
70+
version: 1,
71+
text: `{{ doesnotexist }}`,
72+
},
73+
});
74+
const languageServiceEnabled = await waitForNgcc(client);
75+
expect(languageServiceEnabled).toBeTrue();
76+
const diagnostics: lsp.Diagnostic[] = await new Promise(resolve => {
77+
client.onNotification(
78+
lsp.PublishDiagnosticsNotification.type, (params: lsp.PublishDiagnosticsParams) => {
79+
if (params.uri === `file://${FOO_TEMPLATE}`) {
80+
resolve(params.diagnostics);
81+
}
82+
});
83+
});
84+
expect(diagnostics.length).toBe(1);
85+
expect(diagnostics[0].message)
86+
.toBe(`Property 'doesnotexist' does not exist on type 'FooComponent'.`);
87+
});
6688
});
6789

6890
function onRunNgccNotification(client: MessageConnection): Promise<string> {

server/src/session.ts

Lines changed: 20 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, isTypeScriptFile, lspPositionToTsPosition, lspRangeToTsPositions, tsTextSpanToLspRange, uriToFilePath} from './utils';
1919

2020
export interface SessionOptions {
2121
host: ServerHost;
@@ -131,6 +131,24 @@ 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+
this.runGlobalAnalysisForNewlyLoadedProject(project);
138+
project.refreshDiagnostics(); // Show initial diagnostics
139+
}
140+
141+
/**
142+
* Invoke the compiler for the first time so that external templates get
143+
* matched to the project they belong to.
144+
*/
145+
private runGlobalAnalysisForNewlyLoadedProject(project: ts.server.Project) {
146+
if (!project.hasRoots()) {
147+
return;
148+
}
149+
const fileName = project.getRootScriptInfos()[0].fileName;
150+
// Getting semantic diagnostics will trigger a global analysis.
151+
project.getLanguageService().getSemanticDiagnostics(fileName);
134152
}
135153

136154
/**
@@ -289,7 +307,6 @@ export class Session {
289307
this.projectService.findProject(configFileName) :
290308
this.projectService.getScriptInfo(filePath)?.containingProjects.find(isConfiguredProject);
291309
if (!project) {
292-
this.error(`Failed to find project for ${filePath}`);
293310
return;
294311
}
295312
if (project.languageServiceEnabled) {
@@ -549,7 +566,7 @@ export class Session {
549566
return;
550567
}
551568

552-
if (this.ivy && project instanceof ts.server.ConfiguredProject) {
569+
if (this.ivy && isConfiguredProject(project)) {
553570
// Keep language service disabled until ngcc is completed.
554571
project.disableLanguageService();
555572
this.connection.sendNotification(notification.RunNgcc, {
@@ -590,7 +607,3 @@ export class Session {
590607
return false;
591608
}
592609
}
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: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,12 @@ 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+
}
82+
83+
export function isTypeScriptFile(fileName: string): boolean {
84+
return fileName.endsWith('.ts');
85+
}

0 commit comments

Comments
 (0)