diff --git a/client/src/client.ts b/client/src/client.ts index 98e8e706ed..394fe23543 100644 --- a/client/src/client.ts +++ b/client/src/client.ts @@ -11,7 +11,7 @@ import * as path from 'path'; import * as vscode from 'vscode'; import * as lsp from 'vscode-languageclient/node'; -import {ProjectLoadingFinish, ProjectLoadingStart, SuggestStrictMode, SuggestStrictModeParams} from '../common/notifications'; +import {ProjectLoadingFinish, ProjectLoadingStart, SuggestIvyLanguageService, SuggestIvyLanguageServiceParams, SuggestStrictMode, SuggestStrictModeParams} from '../common/notifications'; import {NgccProgress, NgccProgressToken, NgccProgressType} from '../common/progress'; import {ProgressReporter} from './progress-reporter'; @@ -136,7 +136,29 @@ function registerNotificationHandlers( } }); - context.subscriptions.push(disposable1, disposable2); + const disposable3 = client.onNotification( + SuggestIvyLanguageService, async (params: SuggestIvyLanguageServiceParams) => { + const config = vscode.workspace.getConfiguration(); + if (config.get('angular.enable-experimental-ivy-prompt') === false) { + return; + } + + const enableIvy = 'Enable'; + const doNotPromptAgain = 'Do not show this again'; + const selection = await vscode.window.showInformationMessage( + params.message, + enableIvy, + doNotPromptAgain, + ); + if (selection === enableIvy) { + config.update('angular.experimental-ivy', true, vscode.ConfigurationTarget.Global); + } else if (selection === doNotPromptAgain) { + config.update( + 'angular.enable-experimental-ivy-prompt', false, vscode.ConfigurationTarget.Global); + } + }); + + context.subscriptions.push(disposable1, disposable2, disposable3); } function registerProgressHandlers(client: lsp.LanguageClient, context: vscode.ExtensionContext) { diff --git a/common/notifications.ts b/common/notifications.ts index dd05645f8d..4f7fd06fab 100644 --- a/common/notifications.ts +++ b/common/notifications.ts @@ -26,3 +26,10 @@ export interface SuggestStrictModeParams { export const SuggestStrictMode = new NotificationType('angular/suggestStrictMode'); + +export interface SuggestIvyLanguageServiceParams { + message: string; +} + +export const SuggestIvyLanguageService = + new NotificationType('angular/suggestIvyLanguageServiceMode'); diff --git a/integration/lsp/test_utils.ts b/integration/lsp/test_utils.ts index ad86dc7e55..f43f7ed154 100644 --- a/integration/lsp/test_utils.ts +++ b/integration/lsp/test_utils.ts @@ -30,7 +30,7 @@ export function createConnection(serverOptions: ServerOptions): MessageConnectio '--tsProbeLocations', PACKAGE_ROOT, '--ngProbeLocations', - SERVER_PATH, + [SERVER_PATH, PROJECT_PATH].join(','), ]; if (serverOptions.ivy) { argv.push('--experimental-ivy'); diff --git a/integration/lsp/viewengine_spec.ts b/integration/lsp/viewengine_spec.ts index e38109a52c..6e073e4689 100644 --- a/integration/lsp/viewengine_spec.ts +++ b/integration/lsp/viewengine_spec.ts @@ -8,6 +8,7 @@ import {MessageConnection} from 'vscode-jsonrpc'; import * as lsp from 'vscode-languageserver-protocol'; +import {SuggestIvyLanguageService, SuggestIvyLanguageServiceParams} from '../../common/notifications'; import {APP_COMPONENT, createConnection, FOO_TEMPLATE, initializeServer, openTextDocument} from './test_utils'; describe('Angular language server', () => { @@ -107,6 +108,12 @@ describe('Angular language server', () => { expect(diagnostics.length).toBe(1); expect(diagnostics[0].message).toContain(`Identifier 'doesnotexist' is not defined.`); }); + + it('should prompt to enable Ivy Language Service', async () => { + openTextDocument(client, APP_COMPONENT); + const message = await onSuggestIvyLanguageService(client); + expect(message).toContain('Would you like to enable the new Ivy-native language service'); + }); }); describe('initialization', () => { @@ -138,3 +145,11 @@ describe('initialization', () => { client.dispose(); }); }); + +function onSuggestIvyLanguageService(client: MessageConnection): Promise { + return new Promise(resolve => { + client.onNotification(SuggestIvyLanguageService, (params: SuggestIvyLanguageServiceParams) => { + resolve(params.message); + }); + }); +} diff --git a/package.json b/package.json index ac5e07b59c..54f7f77741 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,11 @@ "default": false, "description": "This is an experimental feature that enables the Ivy language service." }, + "angular.enable-experimental-ivy-prompt": { + "type": "boolean", + "default": true, + "description": "Prompt to enable the Ivy language service for the workspace when View Engine is in use." + }, "angular.trace.server": { "type": "string", "scope": "window", @@ -159,4 +164,4 @@ "type": "git", "url": "https://github.com/angular/vscode-ng-language-service" } -} +} \ No newline at end of file diff --git a/server/src/server.ts b/server/src/server.ts index c447338b90..895c7be70f 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -37,7 +37,7 @@ const session = new Session({ host, logger, ngPlugin: ng.name, - ngProbeLocation: ng.resolvedPath, + resolvedNgLsPath: ng.resolvedPath, ivy: options.ivy, logToConsole: options.logToConsole, }); diff --git a/server/src/session.ts b/server/src/session.ts index 987b24c8d5..2042a7d5fa 100644 --- a/server/src/session.ts +++ b/server/src/session.ts @@ -10,7 +10,7 @@ import * as ts from 'typescript/lib/tsserverlibrary'; import * as lsp from 'vscode-languageserver/node'; import {ServerOptions} from '../common/initialize'; -import {ProjectLanguageService, ProjectLoadingFinish, ProjectLoadingStart, SuggestStrictMode} from '../common/notifications'; +import {ProjectLanguageService, ProjectLoadingFinish, ProjectLoadingStart, SuggestIvyLanguageService, SuggestStrictMode} from '../common/notifications'; import {NgccProgressToken, NgccProgressType} from '../common/progress'; import {readNgCompletionData, tsCompletionEntryToLspCompletionItem} from './completion'; @@ -18,12 +18,13 @@ import {tsDiagnosticToLspDiagnostic} from './diagnostic'; import {resolveAndRunNgcc} from './ngcc'; import {ServerHost} from './server_host'; import {filePathToUri, isConfiguredProject, lspPositionToTsPosition, lspRangeToTsPositions, tsTextSpanToLspRange, uriToFilePath} from './utils'; +import {resolve, Version} from './version_provider'; export interface SessionOptions { host: ServerHost; logger: ts.server.Logger; ngPlugin: string; - ngProbeLocation: string; + resolvedNgLsPath: string; ivy: boolean; logToConsole: boolean; } @@ -44,6 +45,7 @@ export class Session { private readonly connection: lsp.Connection; private readonly projectService: ts.server.ProjectService; private readonly logger: ts.server.Logger; + private readonly angularCoreVersionMap = new WeakMap(); private readonly ivy: boolean; private readonly configuredProjToExternalProj = new Map(); private readonly logToConsole: boolean; @@ -83,7 +85,7 @@ export class Session { suppressDiagnosticEvents: true, eventHandler: (e) => this.handleProjectServiceEvent(e), globalPlugins: [options.ngPlugin], - pluginProbeLocations: [options.ngProbeLocation], + pluginProbeLocations: [options.resolvedNgLsPath], allowLocalPluginLoads: false, // do not load plugins from tsconfig.json }); @@ -836,7 +838,8 @@ export class Session { return; } - if (!this.checkIsAngularProject(project)) { + const coreDts = this.checkIsAngularProject(project); + if (coreDts === undefined) { return; } @@ -850,22 +853,40 @@ export class Session { } else { // Immediately enable Legacy/ViewEngine language service this.info(`Enabling VE language service for ${projectName}.`); + this.promptToEnableIvyIfAvailable(project, coreDts); + } + } + + private promptToEnableIvyIfAvailable( + project: ts.server.Project, coreDts: ts.server.NormalizedPath) { + let angularCoreVersion = this.angularCoreVersionMap.get(project); + if (angularCoreVersion === undefined) { + angularCoreVersion = resolve('@angular/core', coreDts)?.version; + } + + if (angularCoreVersion !== undefined && !this.ivy && angularCoreVersion.major >= 9) { + this.connection.sendNotification(SuggestIvyLanguageService, { + message: + 'Would you like to enable the new Ivy-native language service to get the latest features and bug fixes?', + }); } } /** * Determine if the specified `project` is Angular, and disable the language * service if not. + * + * @returns The `ts.server.NormalizedPath` to the `@angular/core/core.d.ts` file. */ - private checkIsAngularProject(project: ts.server.Project): boolean { + private checkIsAngularProject(project: ts.server.Project): ts.server.NormalizedPath|undefined { const {projectName} = project; const NG_CORE = '@angular/core/core.d.ts'; - - const isAngularProject = project.hasRoots() && !project.isNonTsProject() && - project.getFileNames().some(f => f.endsWith(NG_CORE)); + const ngCoreDts = project.getFileNames().find(f => f.endsWith(NG_CORE)); + const isAngularProject = + project.hasRoots() && !project.isNonTsProject() && ngCoreDts !== undefined; if (isAngularProject) { - return true; + return ngCoreDts; } project.disableLanguageService(); @@ -879,7 +900,7 @@ export class Session { `Please check your tsconfig.json to make sure 'node_modules' directory is not excluded.`); } - return false; + return undefined; } } diff --git a/server/src/version_provider.ts b/server/src/version_provider.ts index 587589b0b1..7711c7830d 100644 --- a/server/src/version_provider.ts +++ b/server/src/version_provider.ts @@ -20,7 +20,7 @@ interface NodeModule { version: Version; } -function resolve(packageName: string, location: string, rootPackage?: string): NodeModule| +export function resolve(packageName: string, location: string, rootPackage?: string): NodeModule| undefined { rootPackage = rootPackage || packageName; try {