Skip to content

Commit 0b125c4

Browse files
committed
feat: Prompt to use the Ivy Language Service if VE is detected
In order to drive adoption of the new Ivy Language Service before it becomes the default in v12, this commit adds a prompt for users to enable it if they are using `@angular/core` v9+. Fixes #1095
1 parent ebf5662 commit 0b125c4

File tree

8 files changed

+86
-16
lines changed

8 files changed

+86
-16
lines changed

client/src/client.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import * as path from 'path';
1111
import * as vscode from 'vscode';
1212
import * as lsp from 'vscode-languageclient/node';
1313

14-
import {ProjectLoadingFinish, ProjectLoadingStart, SuggestStrictMode, SuggestStrictModeParams} from '../common/notifications';
14+
import {ProjectLoadingFinish, ProjectLoadingStart, SuggestIvyLanguageService, SuggestIvyLanguageServiceParams, SuggestStrictMode, SuggestStrictModeParams} from '../common/notifications';
1515
import {NgccProgress, NgccProgressToken, NgccProgressType} from '../common/progress';
1616

1717
import {ProgressReporter} from './progress-reporter';
@@ -136,7 +136,29 @@ function registerNotificationHandlers(
136136
}
137137
});
138138

139-
context.subscriptions.push(disposable1, disposable2);
139+
const disposable3 = client.onNotification(
140+
SuggestIvyLanguageService, async (params: SuggestIvyLanguageServiceParams) => {
141+
const config = vscode.workspace.getConfiguration();
142+
if (config.get('angular.enable-experimental-ivy-prompt') === false) {
143+
return;
144+
}
145+
146+
const enableIvy = 'Enable';
147+
const doNotPromptAgain = 'Do not show this again';
148+
const selection = await vscode.window.showInformationMessage(
149+
params.message,
150+
enableIvy,
151+
doNotPromptAgain,
152+
);
153+
if (selection === enableIvy) {
154+
config.update('angular.experimental-ivy', true, vscode.ConfigurationTarget.Global);
155+
} else if (selection === doNotPromptAgain) {
156+
config.update(
157+
'angular.enable-experimental-ivy-prompt', false, vscode.ConfigurationTarget.Global);
158+
}
159+
});
160+
161+
context.subscriptions.push(disposable1, disposable2, disposable3);
140162
}
141163

142164
function registerProgressHandlers(client: lsp.LanguageClient, context: vscode.ExtensionContext) {

common/notifications.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,10 @@ export interface SuggestStrictModeParams {
2626

2727
export const SuggestStrictMode =
2828
new NotificationType<SuggestStrictModeParams>('angular/suggestStrictMode');
29+
30+
export interface SuggestIvyLanguageServiceParams {
31+
message: string;
32+
}
33+
34+
export const SuggestIvyLanguageService =
35+
new NotificationType<SuggestIvyLanguageServiceParams>('angular/suggestIvyLanguageServiceMode');

integration/lsp/test_utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export function createConnection(serverOptions: ServerOptions): MessageConnectio
3030
'--tsProbeLocations',
3131
PACKAGE_ROOT,
3232
'--ngProbeLocations',
33-
SERVER_PATH,
33+
[SERVER_PATH, PROJECT_PATH].join(','),
3434
];
3535
if (serverOptions.ivy) {
3636
argv.push('--experimental-ivy');

integration/lsp/viewengine_spec.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

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

1314
describe('Angular language server', () => {
@@ -107,6 +108,12 @@ describe('Angular language server', () => {
107108
expect(diagnostics.length).toBe(1);
108109
expect(diagnostics[0].message).toContain(`Identifier 'doesnotexist' is not defined.`);
109110
});
111+
112+
it('should prompt to enable Ivy Language Service', async () => {
113+
openTextDocument(client, APP_COMPONENT);
114+
const message = await onSuggestIvyLanguageService(client);
115+
expect(message).toContain('Would you like to enable the new Ivy-native language service');
116+
});
110117
});
111118

112119
describe('initialization', () => {
@@ -138,3 +145,11 @@ describe('initialization', () => {
138145
client.dispose();
139146
});
140147
});
148+
149+
function onSuggestIvyLanguageService(client: MessageConnection): Promise<string> {
150+
return new Promise(resolve => {
151+
client.onNotification(SuggestIvyLanguageService, (params: SuggestIvyLanguageServiceParams) => {
152+
resolve(params.message);
153+
});
154+
});
155+
}

package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@
5757
"default": false,
5858
"description": "This is an experimental feature that enables the Ivy language service."
5959
},
60+
"angular.enable-experimental-ivy-prompt": {
61+
"type": "boolean",
62+
"default": true,
63+
"description": "Prompt to enable the Ivy language service for the workspace when View Engine is in use."
64+
},
6065
"angular.trace.server": {
6166
"type": "string",
6267
"scope": "window",
@@ -159,4 +164,4 @@
159164
"type": "git",
160165
"url": "https://github.com/angular/vscode-ng-language-service"
161166
}
162-
}
167+
}

server/src/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const session = new Session({
3737
host,
3838
logger,
3939
ngPlugin: ng.name,
40-
ngProbeLocation: ng.resolvedPath,
40+
resolvedNgLsPath: ng.resolvedPath,
4141
ivy: options.ivy,
4242
logToConsole: options.logToConsole,
4343
});

server/src/session.ts

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,21 @@ import * as ts from 'typescript/lib/tsserverlibrary';
1010
import * as lsp from 'vscode-languageserver/node';
1111

1212
import {ServerOptions} from '../common/initialize';
13-
import {ProjectLanguageService, ProjectLoadingFinish, ProjectLoadingStart, SuggestStrictMode} from '../common/notifications';
13+
import {ProjectLanguageService, ProjectLoadingFinish, ProjectLoadingStart, SuggestIvyLanguageService, SuggestStrictMode} from '../common/notifications';
1414
import {NgccProgressToken, NgccProgressType} from '../common/progress';
1515

1616
import {readNgCompletionData, tsCompletionEntryToLspCompletionItem} from './completion';
1717
import {tsDiagnosticToLspDiagnostic} from './diagnostic';
1818
import {resolveAndRunNgcc} from './ngcc';
1919
import {ServerHost} from './server_host';
2020
import {filePathToUri, isConfiguredProject, lspPositionToTsPosition, lspRangeToTsPositions, tsTextSpanToLspRange, uriToFilePath} from './utils';
21+
import {resolve, Version} from './version_provider';
2122

2223
export interface SessionOptions {
2324
host: ServerHost;
2425
logger: ts.server.Logger;
2526
ngPlugin: string;
26-
ngProbeLocation: string;
27+
resolvedNgLsPath: string;
2728
ivy: boolean;
2829
logToConsole: boolean;
2930
}
@@ -44,6 +45,7 @@ export class Session {
4445
private readonly connection: lsp.Connection;
4546
private readonly projectService: ts.server.ProjectService;
4647
private readonly logger: ts.server.Logger;
48+
private readonly angularCoreVersionMap = new WeakMap<ts.server.Project, Version>();
4749
private readonly ivy: boolean;
4850
private readonly configuredProjToExternalProj = new Map<string, string>();
4951
private readonly logToConsole: boolean;
@@ -83,7 +85,7 @@ export class Session {
8385
suppressDiagnosticEvents: true,
8486
eventHandler: (e) => this.handleProjectServiceEvent(e),
8587
globalPlugins: [options.ngPlugin],
86-
pluginProbeLocations: [options.ngProbeLocation],
88+
pluginProbeLocations: [options.resolvedNgLsPath],
8789
allowLocalPluginLoads: false, // do not load plugins from tsconfig.json
8890
});
8991

@@ -836,7 +838,8 @@ export class Session {
836838
return;
837839
}
838840

839-
if (!this.checkIsAngularProject(project)) {
841+
const coreDts = this.checkIsAngularProject(project);
842+
if (coreDts === undefined) {
840843
return;
841844
}
842845

@@ -850,22 +853,40 @@ export class Session {
850853
} else {
851854
// Immediately enable Legacy/ViewEngine language service
852855
this.info(`Enabling VE language service for ${projectName}.`);
856+
this.promptToEnableIvyIfAvailable(project, coreDts);
857+
}
858+
}
859+
860+
private promptToEnableIvyIfAvailable(
861+
project: ts.server.Project, coreDts: ts.server.NormalizedPath) {
862+
let angularCoreVersion = this.angularCoreVersionMap.get(project);
863+
if (angularCoreVersion === undefined) {
864+
angularCoreVersion = resolve('@angular/core', coreDts)?.version;
865+
}
866+
867+
if (angularCoreVersion !== undefined && !this.ivy && angularCoreVersion.major >= 9) {
868+
this.connection.sendNotification(SuggestIvyLanguageService, {
869+
message:
870+
'Would you like to enable the new Ivy-native language service to get the latest features and bug fixes?',
871+
});
853872
}
854873
}
855874

856875
/**
857876
* Determine if the specified `project` is Angular, and disable the language
858877
* service if not.
878+
*
879+
* @returns The `ts.server.NormalizedPath` to the `@angular/core/core.d.ts` file.
859880
*/
860-
private checkIsAngularProject(project: ts.server.Project): boolean {
881+
private checkIsAngularProject(project: ts.server.Project): ts.server.NormalizedPath|undefined {
861882
const {projectName} = project;
862883
const NG_CORE = '@angular/core/core.d.ts';
863-
864-
const isAngularProject = project.hasRoots() && !project.isNonTsProject() &&
865-
project.getFileNames().some(f => f.endsWith(NG_CORE));
884+
const ngCoreDts = project.getFileNames().find(f => f.endsWith(NG_CORE));
885+
const isAngularProject =
886+
project.hasRoots() && !project.isNonTsProject() && ngCoreDts !== undefined;
866887

867888
if (isAngularProject) {
868-
return true;
889+
return ngCoreDts;
869890
}
870891

871892
project.disableLanguageService();
@@ -879,7 +900,7 @@ export class Session {
879900
`Please check your tsconfig.json to make sure 'node_modules' directory is not excluded.`);
880901
}
881902

882-
return false;
903+
return undefined;
883904
}
884905
}
885906

server/src/version_provider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ interface NodeModule {
2020
version: Version;
2121
}
2222

23-
function resolve(packageName: string, location: string, rootPackage?: string): NodeModule|
23+
export function resolve(packageName: string, location: string, rootPackage?: string): NodeModule|
2424
undefined {
2525
rootPackage = rootPackage || packageName;
2626
try {

0 commit comments

Comments
 (0)