diff --git a/.github/ISSUE_TEMPLATE/1_ds_bug_report.md b/.github/ISSUE_TEMPLATE/1_ds_bug_report.md index 37d7fc69cdec..8887effe1d52 100644 --- a/.github/ISSUE_TEMPLATE/1_ds_bug_report.md +++ b/.github/ISSUE_TEMPLATE/1_ds_bug_report.md @@ -33,7 +33,7 @@ _Please provide as much info as you readily know_ - **Jupyter server running:** Local | Remote | N/A - **Extension version:** 20YY.MM.#####-xxx - **VS Code version:** #.## -- **Setting python.jediEnabled:** true | false +- **Setting python.languageServer:** Jedi | Microsoft | None - **Python and/or Anaconda version:** #.#.# - **OS:** Windows | Mac | Linux (distro): - **Virtual environment:** conda | venv | virtualenv | N/A | ... diff --git a/.github/ISSUE_TEMPLATE/2_bug_report.md b/.github/ISSUE_TEMPLATE/2_bug_report.md index adbb8516ae32..339ed0cb6f90 100644 --- a/.github/ISSUE_TEMPLATE/2_bug_report.md +++ b/.github/ISSUE_TEMPLATE/2_bug_report.md @@ -15,7 +15,6 @@ labels: classify, type-bug - Type of virtual environment used (N/A | venv | virtualenv | conda | ...): XXX - Relevant/affected Python packages and their versions: XXX - Relevant/affected Python-related VS Code extensions and their versions: XXX -- Jedi or Language Server? (i.e. what is `"python.jediEnabled"` set to; more info #3977): XXX - Value of the `python.languageServer` setting: XXX ## Expected behaviour diff --git a/.github/release_plan.md b/.github/release_plan.md index 12c35bf0b96b..94906022d1b2 100644 --- a/.github/release_plan.md +++ b/.github/release_plan.md @@ -11,7 +11,7 @@ - [ ] Change the version in [`package.json`](https://github.com/Microsoft/vscode-python/blob/master/package.json) from a `-dev` suffix to `-rc` (🤖) - [ ] Run `npm install` to make sure [`package-lock.json`](https://github.com/Microsoft/vscode-python/blob/master/package.json) is up-to-date (🤖) - [ ] Update `requirements.txt` to point to latest release version of [ptvsd](https://github.com/microsoft/ptvsd). - - [ ] Update `languageServerVersion` in `package.json` to point to the latest version (???) of [the Language Server](https://github.com/Microsoft/python-language-server). + - [ ] Update `languageServerVersion` in `package.json` to point to the latest version of the [Language Server](https://github.com/Microsoft/python-language-server). - [ ] Update [`CHANGELOG.md`](https://github.com/Microsoft/vscode-python/blob/master/CHANGELOG.md) (🤖) - [ ] Run [`news`](https://github.com/Microsoft/vscode-python/tree/master/news) (typically `python news --final --update CHANGELOG.md | code-insiders -`) - [ ] Copy over the "Thanks" section from the previous release diff --git a/.vscode/settings.json b/.vscode/settings.json index 64e62ac725c8..a07e6550fd3e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -45,7 +45,7 @@ "source.fixAll.eslint": true, "source.fixAll.tslint": true }, - "python.jediEnabled": false, + "python.languageServer": "Microsoft", "python.analysis.logLevel": "Trace", "python.analysis.downloadChannel": "beta", "python.linting.pylintEnabled": false, diff --git a/news/.vscode/settings.json b/news/.vscode/settings.json index e70c9f336c57..2b759d72bd35 100644 --- a/news/.vscode/settings.json +++ b/news/.vscode/settings.json @@ -1,5 +1,5 @@ { - "python.jediEnabled": false, + "python.languageServer": "Microsoft", "python.formatting.provider": "black", "editor.formatOnSave": true, "python.testing.pytestArgs": ["."], diff --git a/news/1 Enhancements/7010.md b/news/1 Enhancements/7010.md new file mode 100644 index 000000000000..721269ec5e2e --- /dev/null +++ b/news/1 Enhancements/7010.md @@ -0,0 +1 @@ +Removed `python.jediEnabled` setting in favor of `python.languageServer`. Instead of `"python.jediEnabled": true` please use `"python.languageServer": "Jedi"`. diff --git a/news/1 Enhancements/README.md b/news/1 Enhancements/README.md index ecc51777759d..2a159b65f4f0 100644 --- a/news/1 Enhancements/README.md +++ b/news/1 Enhancements/README.md @@ -1 +1,2 @@ Changes that add new features. + diff --git a/package.json b/package.json index 9501da29d5d7..920d0c73efa3 100644 --- a/package.json +++ b/package.json @@ -2121,12 +2121,6 @@ "description": "Whether to install Python modules globally when not using an environment.", "scope": "resource" }, - "python.jediEnabled": { - "type": "boolean", - "default": true, - "description": "Enables Jedi as IntelliSense engine instead of Microsoft Python Analysis Engine.", - "scope": "resource" - }, "python.jediMemoryLimit": { "type": "number", "default": 0, diff --git a/src/client/activation/activationService.ts b/src/client/activation/activationService.ts index 77c7e9e0d412..43e61f7c4a67 100644 --- a/src/client/activation/activationService.ts +++ b/src/client/activation/activationService.ts @@ -35,7 +35,6 @@ import { LanguageServerType } from './types'; -const jediEnabledSetting: keyof IPythonSettings = 'jediEnabled'; const languageServerSetting: keyof IPythonSettings = 'languageServer'; const workspacePathNameForGlobalWorkspaces = ''; @@ -130,32 +129,37 @@ export class LanguageServerExtensionActivationService } } @swallowExceptions('Send telemetry for Language Server current selection') - public async sendTelemetryForChosenLanguageServer(jediEnabled: boolean): Promise { - const state = this.stateFactory.createGlobalPersistentState('SWITCH_LS', undefined); - if (typeof state.value !== 'boolean') { - await state.updateValue(jediEnabled); + public async sendTelemetryForChosenLanguageServer(languageServer: LanguageServerType): Promise { + const state = this.stateFactory.createGlobalPersistentState( + 'SWITCH_LS', + undefined + ); + if (typeof state.value !== 'string') { + await state.updateValue(languageServer); } - if (state.value !== jediEnabled) { - await state.updateValue(jediEnabled); + if (state.value !== languageServer) { + await state.updateValue(languageServer); sendTelemetryEvent(EventName.PYTHON_LANGUAGE_SERVER_CURRENT_SELECTION, undefined, { - switchTo: jediEnabled + switchTo: languageServer }); } else { sendTelemetryEvent(EventName.PYTHON_LANGUAGE_SERVER_CURRENT_SELECTION, undefined, { - lsStartup: jediEnabled + lsStartup: languageServer }); } } /** - * Checks if user has not manually set `jediEnabled` setting + * Checks if user does not have any `languageServer` setting set. * @param resource - * @returns `true` if user has NOT manually added the setting and is using default configuration, `false` if user has `jediEnabled` setting added + * @returns `true` if user is using default configuration, `false` if user has `languageServer` setting added. */ public isJediUsingDefaultConfiguration(resource: Resource): boolean { - const settings = this.workspaceService.getConfiguration('python', resource).inspect('jediEnabled'); + const settings = this.workspaceService + .getConfiguration('python', resource) + .inspect('languageServer'); if (!settings) { - traceError('WorkspaceConfiguration.inspect returns `undefined` for setting `python.jediEnabled`'); + traceError('WorkspaceConfiguration.inspect returns `undefined` for setting `python.languageServer`'); return false; } return ( @@ -170,19 +174,21 @@ export class LanguageServerExtensionActivationService * @returns `true` if user is using jedi, `false` if user is using language server */ public useJedi(): boolean { + // Check if `languageServer` setting is missing (default configuration). if (this.isJediUsingDefaultConfiguration(this.resource)) { + // If user is assigned to an experiment (i.e. use LS), return false. if (this.abExperiments.inExperiment(LSEnabled)) { return false; } // Send telemetry if user is in control group this.abExperiments.sendTelemetryIfInExperiment(LSControl); + return true; // Do use Jedi as it is default. } + // Configuration is non-default, so `languageServer` should be present. const configurationService = this.serviceContainer.get(IConfigurationService); - let enabled = configurationService.getSettings(this.resource).jediEnabled; - const languageServerType = configurationService.getSettings(this.resource).languageServer; - enabled = enabled || languageServerType === LanguageServerType.Jedi; - this.sendTelemetryForChosenLanguageServer(enabled).ignoreErrors(); - return enabled; + const lstType = configurationService.getSettings(this.resource).languageServer; + this.sendTelemetryForChosenLanguageServer(lstType).ignoreErrors(); + return lstType === LanguageServerType.Jedi; } protected async onWorkspaceFoldersChanged() { @@ -296,7 +302,6 @@ export class LanguageServerExtensionActivationService ? this.workspaceService.workspaceFolders!.map((workspace) => workspace.uri) : [undefined]; if ( - workspacesUris.findIndex((uri) => event.affectsConfiguration(`python.${jediEnabledSetting}`, uri)) === -1 && workspacesUris.findIndex((uri) => event.affectsConfiguration(`python.${languageServerSetting}`, uri)) === -1 ) { return; diff --git a/src/client/activation/languageServer/languageServerFolderService.ts b/src/client/activation/languageServer/languageServerFolderService.ts index bc89bb95ecea..03ce4e88d1b5 100644 --- a/src/client/activation/languageServer/languageServerFolderService.ts +++ b/src/client/activation/languageServer/languageServerFolderService.ts @@ -7,11 +7,15 @@ import { inject, injectable } from 'inversify'; import { IApplicationEnvironment } from '../../common/application/types'; import { IServiceContainer } from '../../ioc/types'; import { LanguageServerFolderService } from '../common/languageServerFolderService'; +import { DotNetLanguageServerFolder } from '../types'; + +// Must match languageServerVersion* keys in package.json +const DotNetLanguageServerMinVersionKey = 'languageServerVersion'; @injectable() export class DotNetLanguageServerFolderService extends LanguageServerFolderService { constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - super(serviceContainer, 'languageServer'); + super(serviceContainer, DotNetLanguageServerFolder); } protected getMinimalLanguageServerVersion(): string { @@ -19,7 +23,7 @@ export class DotNetLanguageServerFolderService extends LanguageServerFolderServi try { const appEnv = this.serviceContainer.get(IApplicationEnvironment); if (appEnv) { - minVersion = appEnv.packageJson.languageServerVersion as string; + minVersion = appEnv.packageJson[DotNetLanguageServerMinVersionKey] as string; } // tslint:disable-next-line: no-empty } catch {} diff --git a/src/client/activation/node/languageServerFolderService.ts b/src/client/activation/node/languageServerFolderService.ts index dc81535c7bd3..8da206ac55c9 100644 --- a/src/client/activation/node/languageServerFolderService.ts +++ b/src/client/activation/node/languageServerFolderService.ts @@ -6,11 +6,12 @@ import { inject, injectable } from 'inversify'; import { IServiceContainer } from '../../ioc/types'; import { LanguageServerFolderService } from '../common/languageServerFolderService'; +import { NodeLanguageServerFolder } from '../types'; @injectable() export class NodeLanguageServerFolderService extends LanguageServerFolderService { constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - super(serviceContainer, 'nodeLanguageServer'); + super(serviceContainer, NodeLanguageServerFolder); } protected getMinimalLanguageServerVersion(): string { diff --git a/src/client/activation/types.ts b/src/client/activation/types.ts index 4fce2850ffd3..6b53c01f60e0 100644 --- a/src/client/activation/types.ts +++ b/src/client/activation/types.ts @@ -70,6 +70,9 @@ export enum LanguageServerType { None = 'None' } +export const DotNetLanguageServerFolder = 'languageServer'; +export const NodeLanguageServerFolder = 'nodeLanguageServer'; + // tslint:disable-next-line: interface-name export interface DocumentHandler { handleOpen(document: TextDocument): void; diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index d5464ddcc762..4d0fdd0913b1 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -86,7 +86,6 @@ export class PythonSettings implements IPythonSettings { } private static pythonSettings: Map = new Map(); public downloadLanguageServer = true; - public jediEnabled = true; public jediPath = ''; public jediMemoryLimit = 1024; public envFile = ''; @@ -223,20 +222,9 @@ export class PythonSettings implements IPythonSettings { this.downloadLanguageServer = systemVariables.resolveAny( pythonSettings.get('downloadLanguageServer', true) )!; - this.jediEnabled = systemVariables.resolveAny(pythonSettings.get('jediEnabled', true))!; this.autoUpdateLanguageServer = systemVariables.resolveAny( pythonSettings.get('autoUpdateLanguageServer', true) )!; - if (this.jediEnabled) { - // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion - this.jediPath = systemVariables.resolveAny(pythonSettings.get('jediPath'))!; - if (typeof this.jediPath === 'string' && this.jediPath.length > 0) { - this.jediPath = getAbsolutePath(systemVariables.resolveAny(this.jediPath), workspaceRoot); - } else { - this.jediPath = ''; - } - this.jediMemoryLimit = pythonSettings.get('jediMemoryLimit')!; - } let ls = pythonSettings.get('languageServer'); if (!ls) { @@ -244,6 +232,15 @@ export class PythonSettings implements IPythonSettings { } this.languageServer = systemVariables.resolveAny(ls)!; + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + this.jediPath = systemVariables.resolveAny(pythonSettings.get('jediPath'))!; + if (typeof this.jediPath === 'string' && this.jediPath.length > 0) { + this.jediPath = getAbsolutePath(systemVariables.resolveAny(this.jediPath), workspaceRoot); + } else { + this.jediPath = ''; + } + this.jediMemoryLimit = pythonSettings.get('jediMemoryLimit')!; + const envFileSetting = pythonSettings.get('envFile'); this.envFile = systemVariables.resolveAny(envFileSetting)!; sendSettingTelemetry(this.workspace, envFileSetting); diff --git a/src/client/common/featureDeprecationManager.ts b/src/client/common/featureDeprecationManager.ts index 51f923698eb6..870743ce0723 100644 --- a/src/client/common/featureDeprecationManager.ts +++ b/src/client/common/featureDeprecationManager.ts @@ -30,7 +30,7 @@ const deprecatedFeatures: DeprecatedFeatureInfo[] = [ { doNotDisplayPromptStateKey: 'SHOW_DEPRECATED_FEATURE_PROMPT_FOR_AUTO_COMPLETE_PRELOAD_MODULES', message: - "The setting 'python.autoComplete.preloadModules' is deprecated, please consider using the new Language Server ('python.jediEnabled = false').", + "The setting 'python.autoComplete.preloadModules' is deprecated, please consider using Microsoft Language Server ('python.languageServer' setting).", moreInfoUrl: 'https://github.com/Microsoft/vscode-python/issues/1704', setting: { setting: 'autoComplete.preloadModules' } } diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 57c2cee24b85..199c32cbb8ff 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -165,7 +165,6 @@ export interface IPythonSettings { readonly poetryPath: string; readonly insidersChannel: ExtensionChannels; readonly downloadLanguageServer: boolean; - readonly jediEnabled: boolean; readonly jediPath: string; readonly jediMemoryLimit: number; readonly devOptions: string[]; diff --git a/src/client/languageServices/proposeLanguageServerBanner.ts b/src/client/languageServices/proposeLanguageServerBanner.ts index c259f007282b..9f3202779bba 100644 --- a/src/client/languageServices/proposeLanguageServerBanner.ts +++ b/src/client/languageServices/proposeLanguageServerBanner.ts @@ -5,6 +5,7 @@ import { inject, injectable } from 'inversify'; import { ConfigurationTarget } from 'vscode'; +import { LanguageServerType } from '../activation/types'; import { IApplicationShell } from '../common/application/types'; import '../common/extensions'; import { IConfigurationService, IPersistentStateFactory, IPythonExtensionBanner } from '../common/types'; @@ -82,7 +83,7 @@ export class ProposeLanguageServerBanner implements IPythonExtensionBanner { const response = await this.appShell.showInformationMessage(this.bannerMessage, ...this.bannerLabels); switch (response) { case this.bannerLabels[ProposeLSLabelIndex.Yes]: { - await this.enableNewLanguageServer(); + await this.enableLanguageServer(); await this.disable(); break; } @@ -111,7 +112,12 @@ export class ProposeLanguageServerBanner implements IPythonExtensionBanner { .updateValue(false); } - public async enableNewLanguageServer(): Promise { - await this.configuration.updateSetting('jediEnabled', false, undefined, ConfigurationTarget.Global); + public async enableLanguageServer(): Promise { + await this.configuration.updateSetting( + 'languageServer', + LanguageServerType.Microsoft, + undefined, + ConfigurationTarget.Global + ); } } diff --git a/src/client/linters/linterAvailability.ts b/src/client/linters/linterAvailability.ts index 228ec7935403..f03643ecceb2 100644 --- a/src/client/linters/linterAvailability.ts +++ b/src/client/linters/linterAvailability.ts @@ -6,6 +6,7 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; import { Uri } from 'vscode'; +import { LanguageServerType } from '../activation/types'; import { IApplicationShell, IWorkspaceService } from '../common/application/types'; import '../common/extensions'; import { IFileSystem } from '../common/platform/types'; @@ -133,11 +134,11 @@ export class AvailableLinterActivator implements IAvailableLinterActivator { * * This is a feature of the vscode-python extension that will become enabled once the * Python Language Server becomes the default, replacing Jedi as the default. Testing - * the global default setting for `"python.jediEnabled": false` enables it. + * the global default setting for `"python.languageServer": !Jedi` enables it. * - * @returns true if the global default for python.jediEnabled is false. + * @returns true if the global default for python.languageServer is not Jedi. */ public get isFeatureEnabled(): boolean { - return !this.configService.getSettings().jediEnabled; + return this.configService.getSettings().languageServer !== LanguageServerType.Jedi; } } diff --git a/src/client/linters/linterInfo.ts b/src/client/linters/linterInfo.ts index c9632599b1b6..21ead3b13c70 100644 --- a/src/client/linters/linterInfo.ts +++ b/src/client/linters/linterInfo.ts @@ -3,6 +3,7 @@ import * as path from 'path'; import { Uri } from 'vscode'; +import { LanguageServerType } from '../activation/types'; import { IWorkspaceService } from '../common/application/types'; import { ExecutionInfo, IConfigurationService, Product } from '../common/types'; import { ILinterInfo, LinterId } from './types'; @@ -85,14 +86,20 @@ export class PylintLinterInfo extends LinterInfo { super(Product.pylint, 'pylint', configService, configFileNames); } public isEnabled(resource?: Uri): boolean { - const enabled = super.isEnabled(resource); - if (!enabled || this.configService.getSettings(resource).jediEnabled) { + // We want to be sure the setting is not default since default is `true` and hence + // missing setting yields `true`. When setting is missing and LS is non-Jedi, + // we want default to be `false`. So inspection here makes sure we are not getting + // `true` because there is no setting and LS is active. + const enabled = super.isEnabled(resource); // Is it enabled by settings? + const usingJedi = this.configService.getSettings(resource).languageServer === LanguageServerType.Jedi; + if (usingJedi) { + // In Jedi case adhere to default behavior. Missing setting means `enabled`. return enabled; } - // If we're using new LS, then by default Pylint is disabled (unless the user provides a value). - const inspection = this.workspaceService - .getConfiguration('python', resource) - .inspect('linting.pylintEnabled'); + // If we're using LS, then by default Pylint is disabled unless user provided + // the value. We have to resort to direct inspection of settings here. + const configuration = this.workspaceService.getConfiguration('python', resource); + const inspection = configuration.inspect(this.enabledSettingName); if ( !inspection || (inspection.globalValue === undefined && diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index b26a13df591d..37763b71e5fd 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -6,6 +6,7 @@ import * as stackTrace from 'stack-trace'; // tslint:disable-next-line: import-name import TelemetryReporter from 'vscode-extension-telemetry/lib/telemetryReporter'; +import { LanguageServerType } from '../activation/types'; import { DiagnosticCodes } from '../application/diagnostics/constants'; import { IWorkspaceService } from '../common/application/types'; import { AppinsightsKey, isTestExecution, PVSC_EXTENSION_ID } from '../common/constants'; @@ -1118,11 +1119,11 @@ export interface IEventNamePropertyMapping { /** * The startup value of the language server setting */ - lsStartup?: boolean; + lsStartup?: LanguageServerType; /** - * Used to track switch between LS and Jedi. Carries the final state after the switch. + * Used to track switch between language servers. Carries the final state after the switch. */ - switchTo?: boolean; + switchTo?: LanguageServerType; }; /** * Telemetry event sent with details after attempting to download LS @@ -1216,7 +1217,7 @@ export interface IEventNamePropertyMapping { lsVersion?: string; }; /** - * Telemetry event sent when user specified None to the language server and jediEnabled is false. + * Telemetry event sent when user specified None to the language server. */ [EventName.PYTHON_LANGUAGE_SERVER_NONE]: never | undefined; /** diff --git a/src/client/testing/common/updateTestSettings.ts b/src/client/testing/common/updateTestSettings.ts index b1a6d7750420..1f5c79faf32c 100644 --- a/src/client/testing/common/updateTestSettings.ts +++ b/src/client/testing/common/updateTestSettings.ts @@ -4,8 +4,9 @@ 'use strict'; import { inject, injectable } from 'inversify'; +import { applyEdits, Edit, findNodeAtLocation, FormattingOptions, getNodeValue, modify, parseTree } from 'jsonc-parser'; import * as path from 'path'; -import { IExtensionActivationService } from '../../activation/types'; +import { IExtensionActivationService, LanguageServerType } from '../../activation/types'; import { IApplicationEnvironment, IWorkspaceService } from '../../common/application/types'; import '../../common/extensions'; import { traceDecorators, traceError } from '../../common/logger'; @@ -13,6 +14,8 @@ import { IFileSystem } from '../../common/platform/types'; import { Resource } from '../../common/types'; import { swallowExceptions } from '../../common/utils/decorators'; +// tslint:disable-next-line:no-suspicious-comment +// TODO: rename the class since it is not used just for test settings @injectable() export class UpdateTestSettingService implements IExtensionActivationService { constructor( @@ -49,8 +52,10 @@ export class UpdateTestSettingService implements IExtensionActivationService { ); return result.filter((item) => item.needsFixing).map((item) => item.file); } + // fixLanguageServerSetting provided for tests so not all tests have to + // deal with potential whitespace changes. @swallowExceptions('Failed to update settings.json') - public async fixSettingInFile(filePath: string) { + public async fixSettingInFile(filePath: string, fixLanguageServerSetting = true): Promise { let fileContents = await this.fs.readFile(filePath); const setting = new RegExp('"python\\.unitTest', 'g'); @@ -63,11 +68,6 @@ export class UpdateTestSettingService implements IExtensionActivationService { fileContents = fileContents.replace(setting_pytest_args, '.pytestArgs"'); fileContents = fileContents.replace(setting_pytest_path, '.pytestPath"'); - const setting_microsoftLanguageServer = new RegExp('\\.languageServer": "microsoft"', 'g'); - const setting_JediLanguageServer = new RegExp('\\.languageServer": "jedi"', 'g'); - fileContents = fileContents.replace(setting_microsoftLanguageServer, '.jediEnabled": false'); - fileContents = fileContents.replace(setting_JediLanguageServer, '.jediEnabled": true'); - const setting_pep8_args = new RegExp('\\.(? { - [true, false].forEach((jediIsEnabled) => { - suite(`Test activation - ${jediIsEnabled ? 'Jedi is enabled' : 'Jedi is disabled'}`, () => { - let serviceContainer: TypeMoq.IMock; - let pythonSettings: TypeMoq.IMock; - let appShell: TypeMoq.IMock; - let cmdManager: TypeMoq.IMock; - let workspaceService: TypeMoq.IMock; - let platformService: TypeMoq.IMock; - let lsNotSupportedDiagnosticService: TypeMoq.IMock; - let stateFactory: TypeMoq.IMock; - let state: TypeMoq.IMock>; - let experiments: TypeMoq.IMock; - let workspaceConfig: TypeMoq.IMock; - let interpreterService: TypeMoq.IMock; - let interpreterChangedHandler!: Function; - setup(() => { - serviceContainer = TypeMoq.Mock.ofType(); - appShell = TypeMoq.Mock.ofType(); - workspaceService = TypeMoq.Mock.ofType(); - cmdManager = TypeMoq.Mock.ofType(); - platformService = TypeMoq.Mock.ofType(); - stateFactory = TypeMoq.Mock.ofType(); - state = TypeMoq.Mock.ofType>(); - const configService = TypeMoq.Mock.ofType(); - pythonSettings = TypeMoq.Mock.ofType(); - experiments = TypeMoq.Mock.ofType(); - const langFolderServiceMock = TypeMoq.Mock.ofType(); - const folderVer: FolderVersionPair = { - path: '', - version: new SemVer('1.2.3') - }; - lsNotSupportedDiagnosticService = TypeMoq.Mock.ofType(); - workspaceService.setup((w) => w.hasWorkspaceFolders).returns(() => false); - workspaceService.setup((w) => w.workspaceFolders).returns(() => []); - configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); - interpreterService = TypeMoq.Mock.ofType(); - const disposable = TypeMoq.Mock.ofType(); - interpreterService - .setup((i) => i.onDidChangeInterpreter(TypeMoq.It.isAny())) - .returns((cb) => { - interpreterChangedHandler = cb; - return disposable.object; - }); - langFolderServiceMock - .setup((l) => l.getCurrentLanguageServerDirectory()) - .returns(() => Promise.resolve(folderVer)); - stateFactory - .setup((f) => - f.createGlobalPersistentState( - TypeMoq.It.isValue('SWITCH_LS'), - TypeMoq.It.isAny(), - TypeMoq.It.isAny() - ) - ) - .returns(() => state.object); - state.setup((s) => s.value).returns(() => undefined); - state.setup((s) => s.updateValue(TypeMoq.It.isAny())).returns(() => Promise.resolve()); - const setting = { workspaceFolderValue: jediIsEnabled }; - workspaceConfig = TypeMoq.Mock.ofType(); - workspaceService - .setup((ws) => ws.getConfiguration('python', TypeMoq.It.isAny())) - .returns(() => workspaceConfig.object); - workspaceConfig.setup((c) => c.inspect('jediEnabled')).returns(() => setting as any); - const output = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IOutputChannel), TypeMoq.It.isAny())) - .returns(() => output.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) - .returns(() => workspaceService.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell))) - .returns(() => appShell.object); - serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IDisposableRegistry))).returns(() => []); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) - .returns(() => configService.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(ICommandManager))) - .returns(() => cmdManager.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IPlatformService))) - .returns(() => platformService.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService))) - .returns(() => interpreterService.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(ILanguageServerFolderService))) - .returns(() => langFolderServiceMock.object); - serviceContainer - .setup((s) => - s.get( - TypeMoq.It.isValue(IDiagnosticsService), - TypeMoq.It.isValue(LSNotSupportedDiagnosticServiceId) - ) - ) - .returns(() => lsNotSupportedDiagnosticService.object); - }); - - async function testActivation( - activationService: IExtensionActivationService, - activator: TypeMoq.IMock, - lsSupported: boolean = true, - activatorName: LanguageServerType = LanguageServerType.Jedi - ) { - activator - .setup((a) => a.start(undefined, undefined)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - activator.setup((a) => a.activate()).verifiable(TypeMoq.Times.once()); - - if (activatorName !== LanguageServerType.None && lsSupported && !jediIsEnabled) { - activatorName = LanguageServerType.Microsoft; - } - - let diagnostics: IDiagnostic[]; - if (!lsSupported && !jediIsEnabled) { - diagnostics = [TypeMoq.It.isAny()]; - } else { - diagnostics = []; - } - - lsNotSupportedDiagnosticService - .setup((l) => l.diagnose(undefined)) - .returns(() => Promise.resolve(diagnostics)); - lsNotSupportedDiagnosticService - .setup((l) => l.handle(TypeMoq.It.isValue(diagnostics))) - .returns(() => Promise.resolve()); - serviceContainer - .setup((c) => - c.get(TypeMoq.It.isValue(ILanguageServerActivator), TypeMoq.It.isValue(activatorName)) - ) - .returns(() => activator.object) - .verifiable(TypeMoq.Times.once()); - - experiments - .setup((ex) => ex.inExperiment(TypeMoq.It.isAny())) - .returns(() => false) - .verifiable(TypeMoq.Times.never()); - - await activationService.activate(undefined); - - activator.verifyAll(); - serviceContainer.verifyAll(); - experiments.verifyAll(); - } - - async function testReloadMessage(settingName: string): Promise { - let callbackHandler!: (e: ConfigurationChangeEvent) => Promise; - let jediIsEnabledValueInSetting = jediIsEnabled; - workspaceService - .setup((w) => - w.onDidChangeConfiguration(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) - ) - .callback((cb) => (callbackHandler = cb)) - .returns(() => TypeMoq.Mock.ofType().object) - .verifiable(TypeMoq.Times.once()); - - pythonSettings.setup((p) => p.jediEnabled).returns(() => jediIsEnabledValueInSetting); - pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft); - const activator = TypeMoq.Mock.ofType(); - const activationService = new LanguageServerExtensionActivationService( - serviceContainer.object, - stateFactory.object, - experiments.object - ); - - workspaceService.verifyAll(); - await testActivation(activationService, activator); - - const event = TypeMoq.Mock.ofType(); - event - .setup((e) => - e.affectsConfiguration(TypeMoq.It.isValue(`python.${settingName}`), TypeMoq.It.isAny()) - ) - .returns(() => true) - .verifiable(TypeMoq.Times.atLeastOnce()); - appShell - .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isValue('Reload'))) - .returns(() => Promise.resolve('Reload')) - .verifiable(TypeMoq.Times.once()); - cmdManager - .setup((c) => c.executeCommand(TypeMoq.It.isValue('workbench.action.reloadWindow'))) - .verifiable(TypeMoq.Times.once()); - - // Toggle the value in the setting and invoke the callback. - jediIsEnabledValueInSetting = !jediIsEnabledValueInSetting; - await callbackHandler(event.object); - - event.verifyAll(); - appShell.verifyAll(); - cmdManager.verifyAll(); - } - - test('LS is supported', async () => { - pythonSettings.setup((p) => p.jediEnabled).returns(() => jediIsEnabled); - pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft); - const activator = TypeMoq.Mock.ofType(); - const activationService = new LanguageServerExtensionActivationService( - serviceContainer.object, - stateFactory.object, - experiments.object - ); - - await testActivation(activationService, activator, true); - }); - test('LS is not supported', async () => { - pythonSettings.setup((p) => p.jediEnabled).returns(() => jediIsEnabled); - pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft); - const activator = TypeMoq.Mock.ofType(); - const activationService = new LanguageServerExtensionActivationService( - serviceContainer.object, - stateFactory.object, - experiments.object - ); - - await testActivation(activationService, activator, false); - }); - - test('Activator must be activated', async () => { - pythonSettings.setup((p) => p.jediEnabled).returns(() => jediIsEnabled); - pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft); - const activator = TypeMoq.Mock.ofType(); - const activationService = new LanguageServerExtensionActivationService( - serviceContainer.object, - stateFactory.object, - experiments.object - ); - - await testActivation(activationService, activator); - }); - test('Activator must be deactivated', async () => { - pythonSettings.setup((p) => p.jediEnabled).returns(() => jediIsEnabled); - pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft); - const activator = TypeMoq.Mock.ofType(); - const activationService = new LanguageServerExtensionActivationService( - serviceContainer.object, - stateFactory.object, - experiments.object - ); - - await testActivation(activationService, activator); - - activator.setup((a) => a.dispose()).verifiable(TypeMoq.Times.once()); - - activationService.dispose(); - activator.verifyAll(); - }); - test('No language service', async () => { - pythonSettings.setup((p) => p.jediEnabled).returns(() => false); - pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.None); - const activator = TypeMoq.Mock.ofType(); - const activationService = new LanguageServerExtensionActivationService( - serviceContainer.object, - stateFactory.object, - experiments.object - ); - await testActivation(activationService, activator, false, LanguageServerType.None); - }); - test('Prompt user to reload VS Code and reload, when jediEnabled setting is toggled', async () => { - await testReloadMessage('jediEnabled'); - }); - test('Prompt user to reload VS Code and reload, when languageServer setting is toggled', async () => { - await testReloadMessage('languageServer'); - }); - test('Prompt user to reload VS Code and do not reload, when setting is toggled', async () => { - let callbackHandler!: (e: ConfigurationChangeEvent) => Promise; - let jediIsEnabledValueInSetting = jediIsEnabled; - workspaceService - .setup((w) => - w.onDidChangeConfiguration(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) - ) - .callback((cb) => (callbackHandler = cb)) - .returns(() => TypeMoq.Mock.ofType().object) - .verifiable(TypeMoq.Times.once()); - - pythonSettings.setup((p) => p.jediEnabled).returns(() => jediIsEnabledValueInSetting); - pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft); - const activator = TypeMoq.Mock.ofType(); - const activationService = new LanguageServerExtensionActivationService( - serviceContainer.object, - stateFactory.object, - experiments.object - ); - - workspaceService.verifyAll(); - await testActivation(activationService, activator); - - const event = TypeMoq.Mock.ofType(); - event - .setup((e) => e.affectsConfiguration(TypeMoq.It.isValue('python.jediEnabled'), TypeMoq.It.isAny())) - .returns(() => true) - .verifiable(TypeMoq.Times.atLeastOnce()); - appShell - .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isValue('Reload'))) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.once()); - cmdManager - .setup((c) => c.executeCommand(TypeMoq.It.isValue('workbench.action.reloadWindow'))) - .verifiable(TypeMoq.Times.never()); - - // Toggle the value in the setting and invoke the callback. - jediIsEnabledValueInSetting = !jediIsEnabledValueInSetting; - await callbackHandler(event.object); - - event.verifyAll(); - appShell.verifyAll(); - cmdManager.verifyAll(); - }); - test('Do not prompt user to reload VS Code when setting is not toggled', async () => { - let callbackHandler!: (e: ConfigurationChangeEvent) => Promise; - workspaceService - .setup((w) => - w.onDidChangeConfiguration(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) - ) - .callback((cb) => (callbackHandler = cb)) - .returns(() => TypeMoq.Mock.ofType().object) - .verifiable(TypeMoq.Times.once()); - - pythonSettings.setup((p) => p.jediEnabled).returns(() => jediIsEnabled); - pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft); - const activator = TypeMoq.Mock.ofType(); - const activationService = new LanguageServerExtensionActivationService( - serviceContainer.object, - stateFactory.object, - experiments.object - ); - - workspaceService.verifyAll(); - await testActivation(activationService, activator); - - const event = TypeMoq.Mock.ofType(); - event - .setup((e) => e.affectsConfiguration(TypeMoq.It.isValue('python.jediEnabled'), TypeMoq.It.isAny())) - .returns(() => true) - .verifiable(TypeMoq.Times.atLeastOnce()); - appShell - .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isValue('Reload'))) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.never()); - cmdManager - .setup((c) => c.executeCommand(TypeMoq.It.isValue('workbench.action.reloadWindow'))) - .verifiable(TypeMoq.Times.never()); - - // Invoke the config changed callback. - await callbackHandler(event.object); - - event.verifyAll(); - appShell.verifyAll(); - cmdManager.verifyAll(); - }); - test('Do not prompt user to reload VS Code when setting is not changed', async () => { - let callbackHandler!: (e: ConfigurationChangeEvent) => Promise; - workspaceService - .setup((w) => - w.onDidChangeConfiguration(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) - ) - .callback((cb) => (callbackHandler = cb)) - .returns(() => TypeMoq.Mock.ofType().object) - .verifiable(TypeMoq.Times.once()); - - pythonSettings.setup((p) => p.jediEnabled).returns(() => jediIsEnabled); - pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft); - const activator = TypeMoq.Mock.ofType(); - const activationService = new LanguageServerExtensionActivationService( - serviceContainer.object, - stateFactory.object, - experiments.object - ); - - workspaceService.verifyAll(); - await testActivation(activationService, activator); - - const event = TypeMoq.Mock.ofType(); - event - .setup((e) => e.affectsConfiguration(TypeMoq.It.isValue('python.jediEnabled'), TypeMoq.It.isAny())) - .returns(() => false) - .verifiable(TypeMoq.Times.atLeastOnce()); - appShell - .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isValue('Reload'))) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.never()); - cmdManager - .setup((c) => c.executeCommand(TypeMoq.It.isValue('workbench.action.reloadWindow'))) - .verifiable(TypeMoq.Times.never()); - - // Invoke the config changed callback. - await callbackHandler(event.object); - - event.verifyAll(); - appShell.verifyAll(); - cmdManager.verifyAll(); - }); - test('More than one LS is created for multiple interpreters', async () => { - const interpreter1: PythonInterpreter = { - path: '/foo/bar/python', - sysPrefix: '1', - envName: '1', - sysVersion: '3.1.1.1', - architecture: Architecture.x64, - type: InterpreterType.Unknown - }; - const interpreter2: PythonInterpreter = { - path: '/foo/baz/python', - sysPrefix: '1', - envName: '2', - sysVersion: '3.1.1.1', - architecture: Architecture.x64, - type: InterpreterType.Unknown - }; - const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; - const activator = TypeMoq.Mock.ofType(); - activator - .setup((a) => a.start(TypeMoq.It.isValue(folder1.uri), TypeMoq.It.isValue(interpreter1))) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - activator - .setup((a) => a.start(TypeMoq.It.isValue(folder1.uri), TypeMoq.It.isValue(interpreter2))) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - activator.setup((a) => a.deactivate()).verifiable(TypeMoq.Times.never()); - activator.setup((a) => a.activate()).verifiable(TypeMoq.Times.never()); - activator - .setup((a) => a.dispose()) - .returns(noop) - .verifiable(TypeMoq.Times.exactly(2)); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(ILanguageServerActivator), TypeMoq.It.isAny())) - .returns(() => activator.object); - let diagnostics: IDiagnostic[]; - if (!jediIsEnabled) { - diagnostics = [TypeMoq.It.isAny()]; - } else { - diagnostics = []; - } - lsNotSupportedDiagnosticService - .setup((l) => l.diagnose(undefined)) - .returns(() => Promise.resolve(diagnostics)); - lsNotSupportedDiagnosticService - .setup((l) => l.handle(TypeMoq.It.isValue(diagnostics))) - .returns(() => Promise.resolve()); - - pythonSettings.setup((p) => p.jediEnabled).returns(() => jediIsEnabled); - pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft); - const activationService = new LanguageServerExtensionActivationService( - serviceContainer.object, - stateFactory.object, - experiments.object - ); - const ls1 = await activationService.get(folder1.uri, interpreter1); - const ls2 = await activationService.get(folder1.uri, interpreter2); - expect(ls1).not.to.be.equal(ls2, 'Interpreter does not create new LS'); - const ls3 = await activationService.get(undefined, interpreter1); - expect(ls1).to.be.equal(ls3, 'Interpreter does return same LS'); - ls3.dispose(); - ls1.dispose(); - ls2.dispose(); - activator.verifyAll(); - }); - test('Changing interpreter will activate a new LS', async () => { - const interpreter1: PythonInterpreter = { - path: '/foo/bar/python', - sysPrefix: '1', - envName: '1', - sysVersion: '3.1.1.1', - architecture: Architecture.x64, - type: InterpreterType.Unknown - }; - const interpreter2: PythonInterpreter = { - path: '/foo/baz/python', - sysPrefix: '1', - envName: '2', - sysVersion: '3.1.1.1', - architecture: Architecture.x64, - type: InterpreterType.Unknown - }; - let getActiveCount = 0; - interpreterService - .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) - .returns(() => { - if (getActiveCount % 2 === 0) { - getActiveCount += 1; - return Promise.resolve(interpreter1); - } - getActiveCount += 1; - return Promise.resolve(interpreter2); - }); - const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; - const activator = TypeMoq.Mock.ofType(); - activator - .setup((a) => a.start(TypeMoq.It.isValue(folder1.uri), TypeMoq.It.isValue(interpreter1))) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - activator - .setup((a) => a.start(TypeMoq.It.isValue(folder1.uri), TypeMoq.It.isValue(interpreter2))) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - let connectCount = 0; - activator - .setup((a) => a.activate()) - .returns(() => { - connectCount = connectCount + 1; - }); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(ILanguageServerActivator), TypeMoq.It.isAny())) - .returns(() => activator.object); - let diagnostics: IDiagnostic[]; - if (!jediIsEnabled) { - diagnostics = [TypeMoq.It.isAny()]; - } else { - diagnostics = []; - } - lsNotSupportedDiagnosticService - .setup((l) => l.diagnose(undefined)) - .returns(() => Promise.resolve(diagnostics)); - lsNotSupportedDiagnosticService - .setup((l) => l.handle(TypeMoq.It.isValue(diagnostics))) - .returns(() => Promise.resolve()); - - pythonSettings.setup((p) => p.jediEnabled).returns(() => jediIsEnabled); - pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft); - const activationService = new LanguageServerExtensionActivationService( - serviceContainer.object, - stateFactory.object, - experiments.object - ); - await activationService.activate(folder1.uri); - await interpreterChangedHandler(); - activator.verifyAll(); - - // Hold onto the second item and switch two more times. Verify that - // reconnect happens - const server = await activationService.get(folder1.uri); - await interpreterChangedHandler(); - expect(connectCount).to.be.equal(3, 'Reconnect is not happening'); - await interpreterChangedHandler(); - expect(connectCount).to.be.equal(4, 'Reconnect is not happening'); - server.dispose(); - }); - if (!jediIsEnabled) { - test('Revert to jedi when LS activation fails', async () => { - pythonSettings.setup((p) => p.jediEnabled).returns(() => jediIsEnabled); - pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft); - const activatorDotNet = TypeMoq.Mock.ofType(); - const activatorJedi = TypeMoq.Mock.ofType(); - const activationService = new LanguageServerExtensionActivationService( - serviceContainer.object, - stateFactory.object, - experiments.object - ); - const diagnostics: IDiagnostic[] = []; - lsNotSupportedDiagnosticService - .setup((l) => l.diagnose(undefined)) - .returns(() => Promise.resolve(diagnostics)); - lsNotSupportedDiagnosticService - .setup((l) => l.handle(TypeMoq.It.isValue(diagnostics))) - .returns(() => Promise.resolve()); - serviceContainer - .setup((c) => - c.get( - TypeMoq.It.isValue(ILanguageServerActivator), - TypeMoq.It.isValue(LanguageServerType.Microsoft) + [LanguageServerType.Jedi, LanguageServerType.Microsoft].forEach((languageServerType) => { + suite( + `Test activation - ${ + languageServerType === LanguageServerType.Jedi ? 'Jedi is enabled' : 'Jedi is disabled' + }`, + () => { + let serviceContainer: TypeMoq.IMock; + let pythonSettings: TypeMoq.IMock; + let appShell: TypeMoq.IMock; + let cmdManager: TypeMoq.IMock; + let workspaceService: TypeMoq.IMock; + let platformService: TypeMoq.IMock; + let lsNotSupportedDiagnosticService: TypeMoq.IMock; + let stateFactory: TypeMoq.IMock; + let state: TypeMoq.IMock>; + let experiments: TypeMoq.IMock; + let workspaceConfig: TypeMoq.IMock; + let interpreterService: TypeMoq.IMock; + let interpreterChangedHandler!: Function; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType(); + appShell = TypeMoq.Mock.ofType(); + workspaceService = TypeMoq.Mock.ofType(); + cmdManager = TypeMoq.Mock.ofType(); + platformService = TypeMoq.Mock.ofType(); + stateFactory = TypeMoq.Mock.ofType(); + state = TypeMoq.Mock.ofType>(); + const configService = TypeMoq.Mock.ofType(); + pythonSettings = TypeMoq.Mock.ofType(); + experiments = TypeMoq.Mock.ofType(); + const langFolderServiceMock = TypeMoq.Mock.ofType(); + const folderVer: FolderVersionPair = { + path: '', + version: new SemVer('1.2.3') + }; + lsNotSupportedDiagnosticService = TypeMoq.Mock.ofType(); + workspaceService.setup((w) => w.hasWorkspaceFolders).returns(() => false); + workspaceService.setup((w) => w.workspaceFolders).returns(() => []); + configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + interpreterService = TypeMoq.Mock.ofType(); + const disposable = TypeMoq.Mock.ofType(); + interpreterService + .setup((i) => i.onDidChangeInterpreter(TypeMoq.It.isAny())) + .returns((cb) => { + interpreterChangedHandler = cb; + return disposable.object; + }); + langFolderServiceMock + .setup((l) => l.getCurrentLanguageServerDirectory()) + .returns(() => Promise.resolve(folderVer)); + stateFactory + .setup((f) => + f.createGlobalPersistentState( + TypeMoq.It.isValue('SWITCH_LS'), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() ) ) - .returns(() => activatorDotNet.object) - .verifiable(TypeMoq.Times.once()); - activatorDotNet - .setup((a) => a.start(undefined, undefined)) - .returns(() => Promise.reject(new Error(''))) - .verifiable(TypeMoq.Times.once()); + .returns(() => state.object); + state.setup((s) => s.value).returns(() => undefined); + state.setup((s) => s.updateValue(TypeMoq.It.isAny())).returns(() => Promise.resolve()); + workspaceConfig = TypeMoq.Mock.ofType(); + workspaceService + .setup((ws) => ws.getConfiguration('python', TypeMoq.It.isAny())) + .returns(() => workspaceConfig.object); + const output = TypeMoq.Mock.ofType(); serviceContainer - .setup((c) => - c.get( - TypeMoq.It.isValue(ILanguageServerActivator), - TypeMoq.It.isValue(LanguageServerType.Jedi) + .setup((c) => c.get(TypeMoq.It.isValue(IOutputChannel), TypeMoq.It.isAny())) + .returns(() => output.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell))) + .returns(() => appShell.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IDisposableRegistry))).returns(() => []); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) + .returns(() => configService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ICommandManager))) + .returns(() => cmdManager.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IPlatformService))) + .returns(() => platformService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService))) + .returns(() => interpreterService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ILanguageServerFolderService))) + .returns(() => langFolderServiceMock.object); + serviceContainer + .setup((s) => + s.get( + TypeMoq.It.isValue(IDiagnosticsService), + TypeMoq.It.isValue(LSNotSupportedDiagnosticServiceId) ) ) - .returns(() => activatorJedi.object) - .verifiable(TypeMoq.Times.once()); - activatorJedi - .setup((a) => a.start(undefined, undefined)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - activatorJedi - .setup((a) => a.activate()) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - - await activationService.activate(undefined); - - activatorDotNet.verifyAll(); - activatorJedi.verifyAll(); - serviceContainer.verifyAll(); + .returns(() => lsNotSupportedDiagnosticService.object); }); - async function testActivationOfResource( + + async function testActivation( activationService: IExtensionActivationService, activator: TypeMoq.IMock, - resource: Resource + lsSupported: boolean = true, + activatorName: LanguageServerType = LanguageServerType.Jedi ) { activator - .setup((a) => a.start(TypeMoq.It.isValue(resource), undefined)) + .setup((a) => a.start(undefined, undefined)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); activator.setup((a) => a.activate()).verifiable(TypeMoq.Times.once()); + + if ( + activatorName !== LanguageServerType.None && + lsSupported && + activatorName !== LanguageServerType.Jedi + ) { + activatorName = LanguageServerType.Microsoft; + } + + let diagnostics: IDiagnostic[]; + if (!lsSupported && activatorName !== LanguageServerType.Jedi) { + diagnostics = [TypeMoq.It.isAny()]; + } else { + diagnostics = []; + } + lsNotSupportedDiagnosticService .setup((l) => l.diagnose(undefined)) - .returns(() => Promise.resolve([])); + .returns(() => Promise.resolve(diagnostics)); lsNotSupportedDiagnosticService - .setup((l) => l.handle(TypeMoq.It.isValue([]))) + .setup((l) => l.handle(TypeMoq.It.isValue(diagnostics))) .returns(() => Promise.resolve()); serviceContainer .setup((c) => - c.get( - TypeMoq.It.isValue(ILanguageServerActivator), - TypeMoq.It.isValue(LanguageServerType.Microsoft) - ) + c.get(TypeMoq.It.isValue(ILanguageServerActivator), TypeMoq.It.isValue(activatorName)) ) .returns(() => activator.object) - .verifiable(TypeMoq.Times.atLeastOnce()); - workspaceService - .setup((w) => w.getWorkspaceFolderIdentifier(resource, '')) - .returns(() => resource!.fsPath) - .verifiable(TypeMoq.Times.atLeastOnce()); + .verifiable(TypeMoq.Times.once()); + experiments .setup((ex) => ex.inExperiment(TypeMoq.It.isAny())) .returns(() => false) .verifiable(TypeMoq.Times.never()); - await activationService.activate(resource); + await activationService.activate(undefined); activator.verifyAll(); serviceContainer.verifyAll(); - workspaceService.verifyAll(); experiments.verifyAll(); } - test('Activator is disposed if activated workspace is removed', async () => { - pythonSettings.setup((p) => p.jediEnabled).returns(() => jediIsEnabled); - pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft); - let workspaceFoldersChangedHandler!: Function; + + async function testReloadMessage(settingName: string): Promise { + let callbackHandler!: (e: ConfigurationChangeEvent) => Promise; workspaceService - .setup((w) => w.onDidChangeWorkspaceFolders(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .callback((cb) => (workspaceFoldersChangedHandler = cb)) - .returns(() => TypeMoq.Mock.ofType().object) + .setup((w) => + w.onDidChangeConfiguration(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + ) + .callback((cb) => (callbackHandler = cb)) + .returns(() => TypeMoq.Mock.ofType().object) .verifiable(TypeMoq.Times.once()); + + pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft); + const activator = TypeMoq.Mock.ofType(); const activationService = new LanguageServerExtensionActivationService( serviceContainer.object, stateFactory.object, experiments.object ); + workspaceService.verifyAll(); - expect(workspaceFoldersChangedHandler).not.to.be.equal(undefined, 'Handler not set'); - const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; - const folder2 = { name: 'two', uri: Uri.parse('two'), index: 2 }; - const folder3 = { name: 'three', uri: Uri.parse('three'), index: 3 }; - - const activator1 = TypeMoq.Mock.ofType(); - await testActivationOfResource(activationService, activator1, folder1.uri); - const activator2 = TypeMoq.Mock.ofType(); - await testActivationOfResource(activationService, activator2, folder2.uri); - const activator3 = TypeMoq.Mock.ofType(); - await testActivationOfResource(activationService, activator3, folder3.uri); - - //Now remove folder3 - workspaceService.reset(); - workspaceService.setup((w) => w.workspaceFolders).returns(() => [folder1, folder2]); - workspaceService - .setup((w) => w.getWorkspaceFolderIdentifier(folder1.uri, '')) - .returns(() => folder1.uri.fsPath) - .verifiable(TypeMoq.Times.atLeastOnce()); - workspaceService - .setup((w) => w.getWorkspaceFolderIdentifier(folder2.uri, '')) - .returns(() => folder2.uri.fsPath) + await testActivation(activationService, activator); + + const event = TypeMoq.Mock.ofType(); + event + .setup((e) => + e.affectsConfiguration(TypeMoq.It.isValue(`python.${settingName}`), TypeMoq.It.isAny()) + ) + .returns(() => true) .verifiable(TypeMoq.Times.atLeastOnce()); - activator1.setup((d) => d.dispose()).verifiable(TypeMoq.Times.never()); - activator2.setup((d) => d.dispose()).verifiable(TypeMoq.Times.never()); - activator3.setup((d) => d.dispose()).verifiable(TypeMoq.Times.once()); - await workspaceFoldersChangedHandler.call(activationService); - workspaceService.verifyAll(); - activator3.verifyAll(); + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isValue('Reload'))) + .returns(() => Promise.resolve('Reload')) + .verifiable(TypeMoq.Times.once()); + cmdManager + .setup((c) => c.executeCommand(TypeMoq.It.isValue('workbench.action.reloadWindow'))) + .verifiable(TypeMoq.Times.once()); + + // Toggle the value in the setting and invoke the callback. + languageServerType = + languageServerType === LanguageServerType.Jedi + ? LanguageServerType.Microsoft + : LanguageServerType.Jedi; + await callbackHandler(event.object); + + event.verifyAll(); + appShell.verifyAll(); + cmdManager.verifyAll(); + } + + test('LS is supported', async () => { + pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft); + const activator = TypeMoq.Mock.ofType(); + const activationService = new LanguageServerExtensionActivationService( + serviceContainer.object, + stateFactory.object, + experiments.object + ); + + await testActivation(activationService, activator, true); }); - } else { - test('Jedi is only started once', async () => { - pythonSettings.setup((p) => p.jediEnabled).returns(() => jediIsEnabled); + test('LS is not supported', async () => { pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft); - const activator1 = TypeMoq.Mock.ofType(); + const activator = TypeMoq.Mock.ofType(); const activationService = new LanguageServerExtensionActivationService( serviceContainer.object, stateFactory.object, experiments.object ); - const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; - const folder2 = { name: 'two', uri: Uri.parse('two'), index: 2 }; - serviceContainer - .setup((c) => - c.get( - TypeMoq.It.isValue(ILanguageServerActivator), - TypeMoq.It.isValue(LanguageServerType.Jedi) - ) + + await testActivation(activationService, activator, false); + }); + + test('Activator must be activated', async () => { + pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft); + const activator = TypeMoq.Mock.ofType(); + const activationService = new LanguageServerExtensionActivationService( + serviceContainer.object, + stateFactory.object, + experiments.object + ); + + await testActivation(activationService, activator); + }); + test('Activator must be deactivated', async () => { + pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft); + const activator = TypeMoq.Mock.ofType(); + const activationService = new LanguageServerExtensionActivationService( + serviceContainer.object, + stateFactory.object, + experiments.object + ); + + await testActivation(activationService, activator); + + activator.setup((a) => a.dispose()).verifiable(TypeMoq.Times.once()); + + activationService.dispose(); + activator.verifyAll(); + }); + test('No language service', async () => { + pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.None); + const activator = TypeMoq.Mock.ofType(); + const activationService = new LanguageServerExtensionActivationService( + serviceContainer.object, + stateFactory.object, + experiments.object + ); + await testActivation(activationService, activator, false, LanguageServerType.None); + }); + test('Prompt user to reload VS Code and reload, when languageServer setting is toggled', async () => { + await testReloadMessage('languageServer'); + }); + test('Do not prompt user to reload VS Code when setting is not changed', async () => { + let callbackHandler!: (e: ConfigurationChangeEvent) => Promise; + workspaceService + .setup((w) => + w.onDidChangeConfiguration(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) ) - .returns(() => activator1.object) - .verifiable(TypeMoq.Times.once()); - activator1 - .setup((a) => a.start(folder1.uri, undefined)) - .returns(() => Promise.resolve()) + .callback((cb) => (callbackHandler = cb)) + .returns(() => TypeMoq.Mock.ofType().object) .verifiable(TypeMoq.Times.once()); - experiments - .setup((ex) => ex.inExperiment(TypeMoq.It.isAny())) + + pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft); + const activator = TypeMoq.Mock.ofType(); + const activationService = new LanguageServerExtensionActivationService( + serviceContainer.object, + stateFactory.object, + experiments.object + ); + + workspaceService.verifyAll(); + await testActivation(activationService, activator); + + const event = TypeMoq.Mock.ofType(); + event + .setup((e) => + e.affectsConfiguration(TypeMoq.It.isValue('python.languageServer'), TypeMoq.It.isAny()) + ) .returns(() => false) + .verifiable(TypeMoq.Times.atLeastOnce()); + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isValue('Reload'))) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + cmdManager + .setup((c) => c.executeCommand(TypeMoq.It.isValue('workbench.action.reloadWindow'))) .verifiable(TypeMoq.Times.never()); - await activationService.activate(folder1.uri); - activator1.verifyAll(); - activator1.verify((a) => a.activate(), TypeMoq.Times.once()); - serviceContainer.verifyAll(); - experiments.verifyAll(); - const activator2 = TypeMoq.Mock.ofType(); + // Invoke the config changed callback. + await callbackHandler(event.object); + + event.verifyAll(); + appShell.verifyAll(); + cmdManager.verifyAll(); + }); + test('More than one LS is created for multiple interpreters if LS is "Microsoft"', async () => { + const interpreter1: PythonInterpreter = { + path: '/foo/bar/python', + sysPrefix: '1', + envName: '1', + sysVersion: '3.1.1.1', + architecture: Architecture.x64, + type: InterpreterType.Unknown + }; + const interpreter2: PythonInterpreter = { + path: '/foo/baz/python', + sysPrefix: '1', + envName: '2', + sysVersion: '3.1.1.1', + architecture: Architecture.x64, + type: InterpreterType.Unknown + }; + const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; + const activator = TypeMoq.Mock.ofType(); + activator + .setup((a) => a.start(TypeMoq.It.isValue(folder1.uri), TypeMoq.It.isValue(interpreter1))) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + activator + .setup((a) => a.start(TypeMoq.It.isValue(folder1.uri), TypeMoq.It.isValue(interpreter2))) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + activator.setup((a) => a.deactivate()).verifiable(TypeMoq.Times.never()); + activator.setup((a) => a.activate()).verifiable(TypeMoq.Times.never()); + activator + .setup((a) => a.dispose()) + .returns(noop) + .verifiable(TypeMoq.Times.exactly(2)); serviceContainer - .setup((c) => - c.get( - TypeMoq.It.isValue(ILanguageServerActivator), - TypeMoq.It.isValue(LanguageServerType.Jedi) - ) - ) - .returns(() => activator2.object) + .setup((c) => c.get(TypeMoq.It.isValue(ILanguageServerActivator), TypeMoq.It.isAny())) + .returns(() => activator.object); + + lsNotSupportedDiagnosticService + .setup((l) => l.diagnose(undefined)) + .returns(() => Promise.resolve([])); + lsNotSupportedDiagnosticService + .setup((l) => l.handle(TypeMoq.It.isValue([]))) + .returns(() => Promise.resolve()); + + pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft); + const activationService = new LanguageServerExtensionActivationService( + serviceContainer.object, + stateFactory.object, + experiments.object + ); + const ls1 = await activationService.get(folder1.uri, interpreter1); + const ls2 = await activationService.get(folder1.uri, interpreter2); + expect(ls1).not.to.be.equal(ls2, 'Interpreter does not create new LS'); + const ls3 = await activationService.get(undefined, interpreter1); + expect(ls1).to.be.equal(ls3, 'Interpreter does return same LS'); + ls3.dispose(); + ls1.dispose(); + ls2.dispose(); + activator.verifyAll(); + }); + test('Changing interpreter will activate a new LS if it is "Microsoft"', async () => { + const interpreter1: PythonInterpreter = { + path: '/foo/bar/python', + sysPrefix: '1', + envName: '1', + sysVersion: '3.1.1.1', + architecture: Architecture.x64, + type: InterpreterType.Unknown + }; + const interpreter2: PythonInterpreter = { + path: '/foo/baz/python', + sysPrefix: '1', + envName: '2', + sysVersion: '3.1.1.1', + architecture: Architecture.x64, + type: InterpreterType.Unknown + }; + let getActiveCount = 0; + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => { + if (getActiveCount % 2 === 0) { + getActiveCount += 1; + return Promise.resolve(interpreter1); + } + getActiveCount += 1; + return Promise.resolve(interpreter2); + }); + const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; + const activator = TypeMoq.Mock.ofType(); + activator + .setup((a) => a.start(TypeMoq.It.isValue(folder1.uri), TypeMoq.It.isValue(interpreter1))) + .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); - activator2 - .setup((a) => a.start(folder2.uri, undefined)) + activator + .setup((a) => a.start(TypeMoq.It.isValue(folder1.uri), TypeMoq.It.isValue(interpreter2))) .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.never()); - activator2.setup((a) => a.activate()).verifiable(TypeMoq.Times.never()); - experiments - .setup((ex) => ex.inExperiment(TypeMoq.It.isAny())) - .returns(() => false) - .verifiable(TypeMoq.Times.never()); - await activationService.activate(folder2.uri); - serviceContainer.verifyAll(); - activator1.verifyAll(); - activator1.verify((a) => a.activate(), TypeMoq.Times.exactly(2)); - activator2.verifyAll(); - experiments.verifyAll(); + .verifiable(TypeMoq.Times.once()); + let connectCount = 0; + activator + .setup((a) => a.activate()) + .returns(() => { + connectCount = connectCount + 1; + }); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ILanguageServerActivator), TypeMoq.It.isAny())) + .returns(() => activator.object); + const diagnostics: IDiagnostic[] = [TypeMoq.It.isAny()]; + lsNotSupportedDiagnosticService + .setup((l) => l.diagnose(undefined)) + .returns(() => Promise.resolve(diagnostics)); + lsNotSupportedDiagnosticService + .setup((l) => l.handle(TypeMoq.It.isValue(diagnostics))) + .returns(() => Promise.resolve()); + + pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft); + const activationService = new LanguageServerExtensionActivationService( + serviceContainer.object, + stateFactory.object, + experiments.object + ); + await activationService.activate(folder1.uri); + await interpreterChangedHandler(); + activator.verifyAll(); + + // Hold onto the second item and switch two more times. Verify that + // reconnect happens + const server = await activationService.get(folder1.uri); + await interpreterChangedHandler(); + expect(connectCount).to.be.equal(3, 'Reconnect is not happening'); + await interpreterChangedHandler(); + expect(connectCount).to.be.equal(4, 'Reconnect is not happening'); + server.dispose(); }); + if (languageServerType !== LanguageServerType.Jedi) { + test('Revert to jedi when LS activation fails', async () => { + pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft); + const activatorLS = TypeMoq.Mock.ofType(); + const activatorJedi = TypeMoq.Mock.ofType(); + const activationService = new LanguageServerExtensionActivationService( + serviceContainer.object, + stateFactory.object, + experiments.object + ); + const diagnostics: IDiagnostic[] = []; + lsNotSupportedDiagnosticService + .setup((l) => l.diagnose(undefined)) + .returns(() => Promise.resolve(diagnostics)); + lsNotSupportedDiagnosticService + .setup((l) => l.handle(TypeMoq.It.isValue(diagnostics))) + .returns(() => Promise.resolve()); + serviceContainer + .setup((c) => + c.get( + TypeMoq.It.isValue(ILanguageServerActivator), + TypeMoq.It.isValue(LanguageServerType.Microsoft) + ) + ) + .returns(() => activatorLS.object) + .verifiable(TypeMoq.Times.once()); + activatorLS + .setup((a) => a.start(undefined, undefined)) + .returns(() => Promise.reject(new Error(''))) + .verifiable(TypeMoq.Times.once()); + serviceContainer + .setup((c) => + c.get( + TypeMoq.It.isValue(ILanguageServerActivator), + TypeMoq.It.isValue(LanguageServerType.Jedi) + ) + ) + .returns(() => activatorJedi.object) + .verifiable(TypeMoq.Times.once()); + activatorJedi + .setup((a) => a.start(undefined, undefined)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + activatorJedi + .setup((a) => a.activate()) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await activationService.activate(undefined); + + activatorLS.verifyAll(); + activatorJedi.verifyAll(); + serviceContainer.verifyAll(); + }); + async function testActivationOfResource( + activationService: IExtensionActivationService, + activator: TypeMoq.IMock, + resource: Resource + ) { + activator + .setup((a) => a.start(TypeMoq.It.isValue(resource), undefined)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + activator.setup((a) => a.activate()).verifiable(TypeMoq.Times.once()); + lsNotSupportedDiagnosticService + .setup((l) => l.diagnose(undefined)) + .returns(() => Promise.resolve([])); + lsNotSupportedDiagnosticService + .setup((l) => l.handle(TypeMoq.It.isValue([]))) + .returns(() => Promise.resolve()); + serviceContainer + .setup((c) => + c.get( + TypeMoq.It.isValue(ILanguageServerActivator), + TypeMoq.It.isValue(LanguageServerType.Microsoft) + ) + ) + .returns(() => activator.object) + .verifiable(TypeMoq.Times.atLeastOnce()); + workspaceService + .setup((w) => w.getWorkspaceFolderIdentifier(resource, '')) + .returns(() => resource!.fsPath) + .verifiable(TypeMoq.Times.atLeastOnce()); + experiments + .setup((ex) => ex.inExperiment(TypeMoq.It.isAny())) + .returns(() => false) + .verifiable(TypeMoq.Times.never()); + + await activationService.activate(resource); + + activator.verifyAll(); + serviceContainer.verifyAll(); + workspaceService.verifyAll(); + experiments.verifyAll(); + } + test('Activator is disposed if activated workspace is removed and LS is "Microsoft"', async () => { + pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft); + let workspaceFoldersChangedHandler!: Function; + workspaceService + .setup((w) => w.onDidChangeWorkspaceFolders(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback((cb) => (workspaceFoldersChangedHandler = cb)) + .returns(() => TypeMoq.Mock.ofType().object) + .verifiable(TypeMoq.Times.once()); + const activationService = new LanguageServerExtensionActivationService( + serviceContainer.object, + stateFactory.object, + experiments.object + ); + workspaceService.verifyAll(); + expect(workspaceFoldersChangedHandler).not.to.be.equal(undefined, 'Handler not set'); + const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; + const folder2 = { name: 'two', uri: Uri.parse('two'), index: 2 }; + const folder3 = { name: 'three', uri: Uri.parse('three'), index: 3 }; + + const activator1 = TypeMoq.Mock.ofType(); + await testActivationOfResource(activationService, activator1, folder1.uri); + const activator2 = TypeMoq.Mock.ofType(); + await testActivationOfResource(activationService, activator2, folder2.uri); + const activator3 = TypeMoq.Mock.ofType(); + await testActivationOfResource(activationService, activator3, folder3.uri); + + //Now remove folder3 + workspaceService.reset(); + workspaceService.setup((w) => w.workspaceFolders).returns(() => [folder1, folder2]); + workspaceService + .setup((w) => w.getWorkspaceFolderIdentifier(folder1.uri, '')) + .returns(() => folder1.uri.fsPath) + .verifiable(TypeMoq.Times.atLeastOnce()); + workspaceService + .setup((w) => w.getWorkspaceFolderIdentifier(folder2.uri, '')) + .returns(() => folder2.uri.fsPath) + .verifiable(TypeMoq.Times.atLeastOnce()); + activator1.setup((d) => d.dispose()).verifiable(TypeMoq.Times.never()); + activator2.setup((d) => d.dispose()).verifiable(TypeMoq.Times.never()); + activator3.setup((d) => d.dispose()).verifiable(TypeMoq.Times.once()); + await workspaceFoldersChangedHandler.call(activationService); + workspaceService.verifyAll(); + activator3.verifyAll(); + }); + } else { + test('Jedi is only started once', async () => { + pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Jedi); + const activator1 = TypeMoq.Mock.ofType(); + const activationService = new LanguageServerExtensionActivationService( + serviceContainer.object, + stateFactory.object, + experiments.object + ); + const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; + const folder2 = { name: 'two', uri: Uri.parse('two'), index: 2 }; + serviceContainer + .setup((c) => + c.get( + TypeMoq.It.isValue(ILanguageServerActivator), + TypeMoq.It.isValue(LanguageServerType.Jedi) + ) + ) + .returns(() => activator1.object) + .verifiable(TypeMoq.Times.once()); + activator1 + .setup((a) => a.start(folder1.uri, undefined)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + experiments + .setup((ex) => ex.inExperiment(TypeMoq.It.isAny())) + .returns(() => false) + .verifiable(TypeMoq.Times.never()); + await activationService.activate(folder1.uri); + activator1.verifyAll(); + activator1.verify((a) => a.activate(), TypeMoq.Times.once()); + serviceContainer.verifyAll(); + experiments.verifyAll(); + + const activator2 = TypeMoq.Mock.ofType(); + serviceContainer + .setup((c) => + c.get( + TypeMoq.It.isValue(ILanguageServerActivator), + TypeMoq.It.isValue(LanguageServerType.Jedi) + ) + ) + .returns(() => activator2.object) + .verifiable(TypeMoq.Times.once()); + activator2 + .setup((a) => a.start(folder2.uri, undefined)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + activator2.setup((a) => a.activate()).verifiable(TypeMoq.Times.never()); + experiments + .setup((ex) => ex.inExperiment(TypeMoq.It.isAny())) + .returns(() => false) + .verifiable(TypeMoq.Times.never()); + await activationService.activate(folder2.uri); + serviceContainer.verifyAll(); + activator1.verifyAll(); + activator1.verify((a) => a.activate(), TypeMoq.Times.exactly(2)); + activator2.verifyAll(); + experiments.verifyAll(); + }); + } } - }); + ); }); suite('Test sendTelemetryForChosenLanguageServer()', () => { @@ -791,7 +691,7 @@ suite('Language Server Activation - ActivationService', () => { let platformService: TypeMoq.IMock; let lsNotSupportedDiagnosticService: TypeMoq.IMock; let stateFactory: TypeMoq.IMock; - let state: TypeMoq.IMock>; + let state: TypeMoq.IMock>; let experiments: TypeMoq.IMock; let workspaceConfig: TypeMoq.IMock; let interpreterService: TypeMoq.IMock; @@ -802,7 +702,7 @@ suite('Language Server Activation - ActivationService', () => { cmdManager = TypeMoq.Mock.ofType(); platformService = TypeMoq.Mock.ofType(); stateFactory = TypeMoq.Mock.ofType(); - state = TypeMoq.Mock.ofType>(); + state = TypeMoq.Mock.ofType>(); const configService = TypeMoq.Mock.ofType(); pythonSettings = TypeMoq.Mock.ofType(); experiments = TypeMoq.Mock.ofType(); @@ -875,9 +775,9 @@ suite('Language Server Activation - ActivationService', () => { .returns(() => undefined) .verifiable(TypeMoq.Times.exactly(2)); state - .setup((s) => s.updateValue(TypeMoq.It.isValue(true))) + .setup((s) => s.updateValue(TypeMoq.It.isValue(LanguageServerType.Jedi))) .returns(() => { - state.setup((s) => s.value).returns(() => true); + state.setup((s) => s.value).returns(() => LanguageServerType.Jedi); return Promise.resolve(); }) .verifiable(TypeMoq.Times.once()); @@ -887,7 +787,7 @@ suite('Language Server Activation - ActivationService', () => { stateFactory.object, experiments.object ); - await activationService.sendTelemetryForChosenLanguageServer(true); + await activationService.sendTelemetryForChosenLanguageServer(LanguageServerType.Jedi); state.verifyAll(); }); @@ -895,10 +795,10 @@ suite('Language Server Activation - ActivationService', () => { state.reset(); state .setup((s) => s.value) - .returns(() => true) + .returns(() => LanguageServerType.Jedi) .verifiable(TypeMoq.Times.exactly(2)); state - .setup((s) => s.updateValue(TypeMoq.It.isValue(false))) + .setup((s) => s.updateValue(TypeMoq.It.isValue(LanguageServerType.Microsoft))) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); @@ -907,7 +807,7 @@ suite('Language Server Activation - ActivationService', () => { stateFactory.object, experiments.object ); - await activationService.sendTelemetryForChosenLanguageServer(false); + await activationService.sendTelemetryForChosenLanguageServer(LanguageServerType.Microsoft); state.verifyAll(); }); @@ -915,10 +815,10 @@ suite('Language Server Activation - ActivationService', () => { state.reset(); state .setup((s) => s.value) - .returns(() => false) + .returns(() => LanguageServerType.Microsoft) .verifiable(TypeMoq.Times.exactly(2)); state - .setup((s) => s.updateValue(TypeMoq.It.isValue(true))) + .setup((s) => s.updateValue(TypeMoq.It.isValue(LanguageServerType.Jedi))) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); @@ -927,7 +827,7 @@ suite('Language Server Activation - ActivationService', () => { stateFactory.object, experiments.object ); - await activationService.sendTelemetryForChosenLanguageServer(true); + await activationService.sendTelemetryForChosenLanguageServer(LanguageServerType.Jedi); state.verifyAll(); }); @@ -935,7 +835,7 @@ suite('Language Server Activation - ActivationService', () => { state.reset(); state .setup((s) => s.value) - .returns(() => true) + .returns(() => LanguageServerType.Jedi) .verifiable(TypeMoq.Times.exactly(2)); state .setup((s) => s.updateValue(TypeMoq.It.isAny())) @@ -947,7 +847,7 @@ suite('Language Server Activation - ActivationService', () => { stateFactory.object, experiments.object ); - await activationService.sendTelemetryForChosenLanguageServer(true); + await activationService.sendTelemetryForChosenLanguageServer(LanguageServerType.Jedi); state.verifyAll(); }); @@ -1039,7 +939,7 @@ suite('Language Server Activation - ActivationService', () => { .returns(() => lsNotSupportedDiagnosticService.object); }); - test('If default value of jedi is being used, and LSEnabled experiment is enabled, then return false', async () => { + test('If default value (Jedi) of language server is being used, and LSEnabled experiment is enabled, then return false', async () => { const settings = {}; experiments .setup((ex) => ex.inExperiment(LSEnabled)) @@ -1050,7 +950,7 @@ suite('Language Server Activation - ActivationService', () => { .returns(() => undefined) .verifiable(TypeMoq.Times.never()); workspaceConfig - .setup((c) => c.inspect('jediEnabled')) + .setup((c) => c.inspect('languageServer')) .returns(() => settings as any) .verifiable(TypeMoq.Times.once()); @@ -1067,7 +967,7 @@ suite('Language Server Activation - ActivationService', () => { experiments.verifyAll(); }); - test('If default value of jedi is being used, and LSEnabled experiment is disabled, then send telemetry if user is in Experiment LSControl and return python settings value (which will always be true as default value is true)', async () => { + test('If default value of languageServer (Jedi) is being used, and LSEnabled experiment is disabled, then send telemetry if user is in Experiment LSControl and useJedi() should be true)', async () => { const settings = {}; experiments .setup((ex) => ex.inExperiment(LSEnabled)) @@ -1078,13 +978,9 @@ suite('Language Server Activation - ActivationService', () => { .returns(() => undefined) .verifiable(TypeMoq.Times.once()); workspaceConfig - .setup((c) => c.inspect('jediEnabled')) + .setup((c) => c.inspect('languageServer')) .returns(() => settings as any) .verifiable(TypeMoq.Times.once()); - pythonSettings - .setup((p) => p.jediEnabled) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); const activationService = new LanguageServerExtensionActivationService( serviceContainer.object, @@ -1101,17 +997,17 @@ suite('Language Server Activation - ActivationService', () => { }); suite( - 'If default value of jedi is not being used, then no experiments are used, and python settings value is returned', + 'If default value of languageServer (Jedi) is not being used, then no experiments are used, and python settings value is returned', async () => { [ { - testName: 'Returns false when python settings value is false', - pythonSettingsValue: false, + testName: 'Returns false when python settings value is Microsoft', + pythonSettingsValue: LanguageServerType.Microsoft, expectedResult: false }, { - testName: 'Returns true when python settings value is true', - pythonSettingsValue: true, + testName: 'Returns true when python settings value is Jedi', + pythonSettingsValue: LanguageServerType.Jedi, expectedResult: true } ].forEach((testParams) => { @@ -1126,11 +1022,11 @@ suite('Language Server Activation - ActivationService', () => { .returns(() => undefined) .verifiable(TypeMoq.Times.never()); workspaceConfig - .setup((c) => c.inspect('jediEnabled')) + .setup((c) => c.inspect('languageServer')) .returns(() => settings as any) .verifiable(TypeMoq.Times.once()); pythonSettings - .setup((p) => p.jediEnabled) + .setup((p) => p.languageServer) .returns(() => testParams.pythonSettingsValue) .verifiable(TypeMoq.Times.once()); @@ -1141,8 +1037,8 @@ suite('Language Server Activation - ActivationService', () => { ); const result = activationService.useJedi(); expect(result).to.equal( - testParams.pythonSettingsValue, - `Return value should be ${testParams.pythonSettingsValue}` + testParams.expectedResult, + `Return value should be ${testParams.expectedResult}` ); pythonSettings.verifyAll(); @@ -1263,7 +1159,7 @@ suite('Language Server Activation - ActivationService', () => { test(testName, async () => { workspaceConfig.reset(); workspaceConfig - .setup((c) => c.inspect('jediEnabled')) + .setup((c) => c.inspect('languageServer')) .returns(() => settings as any) .verifiable(TypeMoq.Times.once()); @@ -1284,7 +1180,7 @@ suite('Language Server Activation - ActivationService', () => { test('Returns false for settings = undefined', async () => { workspaceConfig.reset(); workspaceConfig - .setup((c) => c.inspect('jediEnabled')) + .setup((c) => c.inspect('languageServer')) .returns(() => undefined as any) .verifiable(TypeMoq.Times.once()); diff --git a/src/test/application/diagnostics/checks/updateTestSettings.unit.test.ts b/src/test/application/diagnostics/checks/updateTestSettings.unit.test.ts index 8699426016bc..819b27b9f8cb 100644 --- a/src/test/application/diagnostics/checks/updateTestSettings.unit.test.ts +++ b/src/test/application/diagnostics/checks/updateTestSettings.unit.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { expect } from 'chai'; import * as path from 'path'; import * as sinon from 'sinon'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { anyString, anything, instance, mock, verify, when } from 'ts-mockito'; import { Uri } from 'vscode'; import { ApplicationEnvironment } from '../../../../client/common/application/applicationEnvironment'; import { IApplicationEnvironment, IWorkspaceService } from '../../../../client/common/application/types'; @@ -195,26 +195,68 @@ suite('Application Diagnostics - Check Test Settings', () => { '{"python.pythonPath":"1234", "python.unittest.unitTestArgs":[], "python.unitTest.pytestArgs":[], "python.testing.pytestArgs":[], "python.testing.pytestPath":[]}', expectedContents: '{"python.pythonPath":"1234", "python.unittest.unitTestArgs":[], "python.testing.pytestArgs":[], "python.testing.pytestArgs":[], "python.testing.pytestPath":[]}' + } + ].forEach((item) => { + test(item.testTitle, async () => { + when(fs.readFile(__filename)).thenResolve(item.contents); + when(fs.writeFile(__filename, anything())).thenResolve(); + + const actualContent = await diagnosticService.fixSettingInFile(__filename, false); + + verify(fs.readFile(__filename)).once(); + verify(fs.writeFile(__filename, anyString())).once(); + expect(actualContent).to.be.equal(item.expectedContents); + }); + }); + + [ + { + testTitle: 'No jediEnabled setting.', + contents: '{}', + expectedContent: '{ "python.languageServer": "Jedi" }' + }, + { + testTitle: 'jediEnabled: true, no languageServer setting', + contents: '{ "python.jediEnabled": true }', + expectedContent: '{"python.languageServer": "Jedi"}' }, { - testTitle: 'Should replace python.jediEnabled.', - expectedContents: '{"python.jediEnabled": false}', - contents: '{"python.languageServer": "microsoft"}' + testTitle: 'jediEnabled: true, languageServer setting present', + contents: '{ "python.jediEnabled": true }', + expectedContent: '{"python.languageServer": "Jedi"}' }, { - testTitle: 'Should replace python.jediEnabled.', - expectedContents: '{"python.jediEnabled": true}', - contents: '{"python.languageServer": "jedi"}' + testTitle: 'jediEnabled: false, no languageServer setting', + contents: '{ "python.jediEnabled": false }', + expectedContent: '{"python.languageServer": "Microsoft"}' + }, + { + testTitle: 'jediEnabled: false, languageServer is Microsoft', + contents: '{ "python.jediEnabled": false, "python.languageServer": "Microsoft" }', + expectedContent: '{"python.languageServer": "Microsoft"}' + }, + { + testTitle: 'jediEnabled: false, languageServer is None', + contents: '{ "python.jediEnabled": false, "python.languageServer": "None" }', + expectedContent: '{"python.languageServer": "None"}' + }, + { + testTitle: 'jediEnabled: false, languageServer is Jedi', + contents: '{ "python.jediEnabled": false, "python.languageServer": "Jedi" }', + expectedContent: '{"python.languageServer": "Jedi"}' } ].forEach((item) => { test(item.testTitle, async () => { when(fs.readFile(__filename)).thenResolve(item.contents); - when(fs.writeFile(__filename, anything())).thenResolve(); - await diagnosticService.fixSettingInFile(__filename); + const actualContent = await diagnosticService.fixSettingInFile(__filename); + expect(nows(actualContent)).to.equal(nows(item.expectedContent)); verify(fs.readFile(__filename)).once(); - verify(fs.writeFile(__filename, item.expectedContents)).once(); }); }); + + function nows(s: string): string { + return s.replace(/\s*/g, ''); + } }); diff --git a/src/test/common.ts b/src/test/common.ts index b6a6fb43ad56..22cd720f639f 100644 --- a/src/test/common.ts +++ b/src/test/common.ts @@ -42,6 +42,7 @@ export enum OSType { export type PythonSettingKeys = | 'workspaceSymbols.enabled' | 'pythonPath' + | 'languageServer' | 'linting.lintOnSave' | 'linting.enabled' | 'linting.pylintEnabled' @@ -61,7 +62,6 @@ export type PythonSettingKeys = | 'testing.pytestEnabled' | 'testing.unittestEnabled' | 'envFile' - | 'jediEnabled' | 'linting.ignorePatterns' | 'terminal.activateEnvironment'; diff --git a/src/test/common/configSettings/configSettings.unit.test.ts b/src/test/common/configSettings/configSettings.unit.test.ts index 87b913b8b2be..b022ced14144 100644 --- a/src/test/common/configSettings/configSettings.unit.test.ts +++ b/src/test/common/configSettings/configSettings.unit.test.ts @@ -66,16 +66,14 @@ suite('Python Settings', async () => { 'envFile', 'poetryPath', 'insidersChannel', - 'defaultInterpreterPath' + 'defaultInterpreterPath', + 'jediPath' ]) { config .setup((c) => c.get(name)) // tslint:disable-next-line:no-any .returns(() => (sourceSettings as any)[name]); } - if (sourceSettings.jediEnabled) { - config.setup((c) => c.get('jediPath')).returns(() => sourceSettings.jediPath); - } for (const name of ['venvFolders']) { config .setup((c) => c.get(name)) @@ -84,7 +82,7 @@ suite('Python Settings', async () => { } // boolean settings - for (const name of ['downloadLanguageServer', 'jediEnabled', 'autoUpdateLanguageServer']) { + for (const name of ['downloadLanguageServer', 'autoUpdateLanguageServer']) { config .setup((c) => c.get(name, true)) // tslint:disable-next-line:no-any @@ -98,10 +96,7 @@ suite('Python Settings', async () => { } // number settings - if (sourceSettings.jediEnabled) { - config.setup((c) => c.get('jediMemoryLimit')).returns(() => sourceSettings.jediMemoryLimit); - } - + config.setup((c) => c.get('jediMemoryLimit')).returns(() => sourceSettings.jediMemoryLimit); // Language server type settings config.setup((c) => c.get('languageServer')).returns(() => sourceSettings.languageServer); @@ -153,7 +148,7 @@ suite('Python Settings', async () => { }); suite('Boolean settings', async () => { - ['downloadLanguageServer', 'jediEnabled', 'autoUpdateLanguageServer', 'globalModuleInstallation'].forEach( + ['downloadLanguageServer', 'autoUpdateLanguageServer', 'globalModuleInstallation'].forEach( async (settingName) => { testIfValueIsUpdated(settingName, true); } diff --git a/src/test/datascience/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts index ebb24859bfed..f92604b5b24b 100644 --- a/src/test/datascience/dataScienceIocContainer.ts +++ b/src/test/datascience/dataScienceIocContainer.ts @@ -1526,7 +1526,6 @@ export class DataScienceIocContainer extends UnitTestIocContainer { disableJupyterAutoStart: true, widgetScriptSources: ['jsdelivr.com', 'unpkg.com'] }; - pythonSettings.jediEnabled = false; pythonSettings.downloadLanguageServer = false; const folders = ['Envs', '.virtualenvs']; pythonSettings.venvFolders = folders; diff --git a/src/test/datascience/intellisense.unit.test.ts b/src/test/datascience/intellisense.unit.test.ts index 44855b188e0b..c125513a050a 100644 --- a/src/test/datascience/intellisense.unit.test.ts +++ b/src/test/datascience/intellisense.unit.test.ts @@ -7,6 +7,7 @@ import * as uuid from 'uuid/v4'; import { instance, mock } from 'ts-mockito'; import { Uri } from 'vscode'; +import { LanguageServerType } from '../../client/activation/types'; import { IWorkspaceService } from '../../client/common/application/types'; import { PythonSettings } from '../../client/common/configSettings'; import { IFileSystem } from '../../client/common/platform/types'; @@ -68,7 +69,7 @@ suite('DataScience Intellisense Unit Tests', () => { notebookProvider = TypeMoq.Mock.ofType(); const variableProvider = mock(JupyterVariables); - pythonSettings.jediEnabled = false; + pythonSettings.languageServer = LanguageServerType.Microsoft; configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings); workspaceService.setup((w) => w.rootPath).returns(() => '/foo/bar'); fileSystem diff --git a/src/test/linters/common.ts b/src/test/linters/common.ts index e010c8aab750..4c5d03f066ef 100644 --- a/src/test/linters/common.ts +++ b/src/test/linters/common.ts @@ -5,6 +5,7 @@ import * as os from 'os'; import * as TypeMoq from 'typemoq'; import { DiagnosticSeverity, TextDocument, Uri, WorkspaceFolder } from 'vscode'; +import { LanguageServerType } from '../../client/activation/types'; import { IApplicationShell, IWorkspaceService } from '../../client/common/application/types'; import { Product } from '../../client/common/installer/productInstaller'; import { ProductNames } from '../../client/common/installer/productNames'; @@ -318,7 +319,7 @@ export class BaseTestFixture { }) .returns(() => Promise.resolve(undefined)); - this.pythonSettings.setup((s) => s.jediEnabled).returns(() => true); + this.pythonSettings.setup((s) => s.languageServer).returns(() => LanguageServerType.Jedi); } private initData(): void { diff --git a/src/test/linters/lint.manager.unit.test.ts b/src/test/linters/lint.manager.unit.test.ts index fabe314e2b68..be23cef27d9b 100644 --- a/src/test/linters/lint.manager.unit.test.ts +++ b/src/test/linters/lint.manager.unit.test.ts @@ -11,6 +11,8 @@ import { IConfigurationService, IPythonSettings } from '../../client/common/type import { IServiceContainer } from '../../client/ioc/types'; import { LinterManager } from '../../client/linters/linterManager'; +const workspaceService = TypeMoq.Mock.ofType(); + // setup class instance class TestLinterManager extends LinterManager { public enableUnconfiguredLintersCallCount: number = 0; @@ -23,17 +25,27 @@ class TestLinterManager extends LinterManager { function getServiceContainerMockForLinterManagerTests(): TypeMoq.IMock { // setup test mocks const serviceContainerMock = TypeMoq.Mock.ofType(); - const configMock = TypeMoq.Mock.ofType(); + const pythonSettingsMock = TypeMoq.Mock.ofType(); + const configMock = TypeMoq.Mock.ofType(); configMock.setup((cm) => cm.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettingsMock.object); serviceContainerMock.setup((c) => c.get(IConfigurationService)).returns(() => configMock.object); + const pythonConfig = { + // tslint:disable-next-line:no-empty + inspect: () => {} + }; + workspaceService + .setup((x) => x.getConfiguration('python', TypeMoq.It.isAny())) + // tslint:disable-next-line:no-any + .returns(() => pythonConfig as any); + serviceContainerMock.setup((c) => c.get(IWorkspaceService)).returns(() => workspaceService.object); + return serviceContainerMock; } // tslint:disable-next-line:max-func-body-length suite('Lint Manager Unit Tests', () => { - const workspaceService = TypeMoq.Mock.ofType(); test('Linter manager isLintingEnabled checks availability when silent = false.', async () => { // set expectations const expectedCallCount = 1; diff --git a/src/test/linters/lint.multilinter.test.ts b/src/test/linters/lint.multilinter.test.ts index 9d2336ab0761..20d3ea5a54f6 100644 --- a/src/test/linters/lint.multilinter.test.ts +++ b/src/test/linters/lint.multilinter.test.ts @@ -5,6 +5,7 @@ import * as assert from 'assert'; import * as path from 'path'; import { ConfigurationTarget, DiagnosticCollection, Uri, window, workspace } from 'vscode'; +import { LanguageServerType } from '../../client/activation/types'; import { ICommandManager } from '../../client/common/application/types'; import { Product } from '../../client/common/installer/productInstaller'; import { PythonToolExecutionService } from '../../client/common/process/pythonToolService'; @@ -91,6 +92,7 @@ suite('Linting - Multiple Linters Enabled Test', () => { await closeActiveWindows(); const document = await workspace.openTextDocument(path.join(pythoFilesPath, 'print.py')); await window.showTextDocument(document); + await configService.updateSetting('languageServer', LanguageServerType.Jedi, workspaceUri); await configService.updateSetting('linting.enabled', true, workspaceUri); await configService.updateSetting('linting.pylintUseMinimalCheckers', false, workspaceUri); await configService.updateSetting('linting.pylintEnabled', true, workspaceUri); diff --git a/src/test/linters/lint.multiroot.test.ts b/src/test/linters/lint.multiroot.test.ts index 302361099680..4caa373f8f58 100644 --- a/src/test/linters/lint.multiroot.test.ts +++ b/src/test/linters/lint.multiroot.test.ts @@ -1,6 +1,7 @@ import * as assert from 'assert'; import * as path from 'path'; import { CancellationTokenSource, ConfigurationTarget, OutputChannel, Uri, workspace } from 'vscode'; +import { LanguageServerType } from '../../client/activation/types'; import { PythonSettings } from '../../client/common/configSettings'; import { CTagsProductPathService, @@ -133,6 +134,12 @@ suite('Multiroot Linting', () => { async function runTest(product: Product, global: boolean, wks: boolean, setting: string): Promise { const config = ioc.serviceContainer.get(IConfigurationService); + await config.updateSetting( + 'languageServer', + LanguageServerType.Jedi, + Uri.file(multirootPath), + ConfigurationTarget.Global + ); await Promise.all([ config.updateSetting(setting, global, Uri.file(multirootPath), ConfigurationTarget.Global), config.updateSetting(setting, wks, Uri.file(multirootPath), ConfigurationTarget.Workspace) diff --git a/src/test/linters/lint.provider.test.ts b/src/test/linters/lint.provider.test.ts index ff4ae1c34953..d5f9d3f3220a 100644 --- a/src/test/linters/lint.provider.test.ts +++ b/src/test/linters/lint.provider.test.ts @@ -5,6 +5,7 @@ import { Container } from 'inversify'; import * as TypeMoq from 'typemoq'; import * as vscode from 'vscode'; +import { LanguageServerType } from '../../client/activation/types'; import { IApplicationShell, IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; import { PersistentStateFactory } from '../../client/common/persistentState'; import { IFileSystem } from '../../client/common/platform/types'; @@ -50,6 +51,7 @@ suite('Linting - Provider', () => { let appShell: TypeMoq.IMock; let linterInstaller: TypeMoq.IMock; let workspaceService: TypeMoq.IMock; + let workspaceConfig: TypeMoq.IMock; suiteSetup(initialize); setup(async () => { @@ -80,6 +82,7 @@ suite('Linting - Provider', () => { settings = TypeMoq.Mock.ofType(); settings.setup((x) => x.linting).returns(() => lintSettings.object); + settings.setup((p) => p.languageServer).returns(() => LanguageServerType.Jedi); configService = TypeMoq.Mock.ofType(); configService.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); @@ -87,7 +90,14 @@ suite('Linting - Provider', () => { appShell = TypeMoq.Mock.ofType(); linterInstaller = TypeMoq.Mock.ofType(); + workspaceService = TypeMoq.Mock.ofType(); + workspaceConfig = TypeMoq.Mock.ofType(); + workspaceService + .setup((w) => w.getConfiguration('python', TypeMoq.It.isAny())) + .returns(() => workspaceConfig.object); + workspaceService.setup((w) => w.getConfiguration('python')).returns(() => workspaceConfig.object); + serviceManager.addSingletonInstance(IApplicationShell, appShell.object); serviceManager.addSingletonInstance(IInstaller, linterInstaller.object); serviceManager.addSingletonInstance(IWorkspaceService, workspaceService.object); diff --git a/src/test/linters/linter.availability.unit.test.ts b/src/test/linters/linter.availability.unit.test.ts index 55583d229534..39f51fb0e4d6 100644 --- a/src/test/linters/linter.availability.unit.test.ts +++ b/src/test/linters/linter.availability.unit.test.ts @@ -8,6 +8,7 @@ import * as path from 'path'; import { anything, instance, mock, verify, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; import { Uri, WorkspaceConfiguration, WorkspaceFolder } from 'vscode'; +import { LanguageServerType } from '../../client/activation/types'; import { ApplicationShell } from '../../client/common/application/applicationShell'; import { IApplicationShell, IWorkspaceService } from '../../client/common/application/types'; import { WorkspaceService } from '../../client/common/application/workspace'; @@ -29,9 +30,9 @@ import { IAvailableLinterActivator, ILinterInfo } from '../../client/linters/typ // tslint:disable:max-func-body-length no-any suite('Linter Availability Provider tests', () => { - test('Availability feature is disabled when global default for jediEnabled=true.', async () => { + test('Availability feature is disabled when global default for languageServer === Jedi.', async () => { // set expectations - const jediEnabledValue = true; + const languageServerValue = LanguageServerType.Jedi; const expectedResult = false; // arrange @@ -42,7 +43,7 @@ suite('Linter Availability Provider tests', () => { configServiceMock, factoryMock ] = getDependenciesForAvailabilityTests(); - setupConfigurationServiceForJediSettingsTest(jediEnabledValue, configServiceMock); + setupConfigurationServiceForJediSettingsTest(languageServerValue, configServiceMock); // call const availabilityProvider = new AvailableLinterActivator( @@ -56,14 +57,14 @@ suite('Linter Availability Provider tests', () => { // check expectaions expect(availabilityProvider.isFeatureEnabled).is.equal( expectedResult, - 'Avaialability feature should be disabled when python.jediEnabled is true' + 'Avaialability feature should be disabled when python.languageServer is Jedi' ); workspaceServiceMock.verifyAll(); }); - test('Availability feature is enabled when global default for jediEnabled=false.', async () => { + test('Availability feature is enabled when global default for languageServer is Microsoft.', async () => { // set expectations - const jediEnabledValue = false; + const languageServerValue = LanguageServerType.Microsoft; const expectedResult = true; // arrange @@ -74,7 +75,7 @@ suite('Linter Availability Provider tests', () => { configServiceMock, factoryMock ] = getDependenciesForAvailabilityTests(); - setupConfigurationServiceForJediSettingsTest(jediEnabledValue, configServiceMock); + setupConfigurationServiceForJediSettingsTest(languageServerValue, configServiceMock); const availabilityProvider = new AvailableLinterActivator( appShellMock.object, @@ -86,7 +87,7 @@ suite('Linter Availability Provider tests', () => { expect(availabilityProvider.isFeatureEnabled).is.equal( expectedResult, - 'Avaialability feature should be enabled when python.jediEnabled defaults to false' + 'Avaialability feature should be enabled when python.languageServer defaults to non-Jedi' ); workspaceServiceMock.verifyAll(); }); @@ -385,7 +386,7 @@ suite('Linter Availability Provider tests', () => { // Options to test the implementation of the IAvailableLinterActivator. // All options default to values that would otherwise allow the prompt to appear. class AvailablityTestOverallOptions { - public jediEnabledValue: boolean = false; + public languageServerValue = LanguageServerType.Microsoft; public pylintUserEnabled?: boolean; public pylintWorkspaceEnabled?: boolean; public pylintWorkspaceFolderEnabled?: boolean; @@ -433,7 +434,7 @@ suite('Linter Availability Provider tests', () => { .returns(async () => options.linterIsInstalled) .verifiable(TypeMoq.Times.atLeastOnce()); - setupConfigurationServiceForJediSettingsTest(options.jediEnabledValue, configServiceMock); + setupConfigurationServiceForJediSettingsTest(options.languageServerValue, configServiceMock); setupWorkspaceMockForLinterConfiguredTests( options.pylintUserEnabled, options.pylintWorkspaceEnabled, @@ -460,7 +461,7 @@ suite('Linter Availability Provider tests', () => { test('Overall implementation does not change configuration when feature disabled', async () => { // set expectations const testOpts = new AvailablityTestOverallOptions(); - testOpts.jediEnabledValue = true; + testOpts.languageServerValue = LanguageServerType.Jedi; const expectedResult = false; // arrange @@ -469,7 +470,7 @@ suite('Linter Availability Provider tests', () => { // perform test expect(expectedResult).to.equal( result, - 'promptIfLinterAvailable should not change any configuration when python.jediEnabled is true.' + 'promptIfLinterAvailable should not change any configuration when python.languageServer is Jedi.' ); }); @@ -803,14 +804,14 @@ function setupWorkspaceMockForLinterConfiguredTests( } function setupConfigurationServiceForJediSettingsTest( - jediEnabledValue: boolean, + languageServerValue: LanguageServerType, configServiceMock: TypeMoq.IMock ): [TypeMoq.IMock, TypeMoq.IMock] { if (!configServiceMock) { configServiceMock = TypeMoq.Mock.ofType(); } const pythonSettings = TypeMoq.Mock.ofType(); - pythonSettings.setup((ps) => ps.jediEnabled).returns(() => jediEnabledValue); + pythonSettings.setup((ps) => ps.languageServer).returns(() => languageServerValue); configServiceMock.setup((cs) => cs.getSettings()).returns(() => pythonSettings.object); return [configServiceMock, pythonSettings]; diff --git a/src/test/linters/linterinfo.unit.test.ts b/src/test/linters/linterinfo.unit.test.ts index e989361336f8..39b2e9550677 100644 --- a/src/test/linters/linterinfo.unit.test.ts +++ b/src/test/linters/linterinfo.unit.test.ts @@ -7,55 +7,58 @@ import { expect } from 'chai'; import { anything, instance, mock, when } from 'ts-mockito'; +import { LanguageServerType } from '../../client/activation/types'; import { WorkspaceService } from '../../client/common/application/workspace'; import { ConfigurationService } from '../../client/common/configuration/service'; import { PylintLinterInfo } from '../../client/linters/linterInfo'; suite('Linter Info - Pylint', () => { + const workspace = mock(WorkspaceService); + const config = mock(ConfigurationService); + test('Test disabled when Pylint is explicitly disabled', async () => { - const config = mock(ConfigurationService); - const workspaceService = mock(WorkspaceService); - const linterInfo = new PylintLinterInfo(instance(config), instance(workspaceService), []); + const linterInfo = new PylintLinterInfo(instance(config), instance(workspace), []); - when(config.getSettings(anything())).thenReturn({ linting: { pylintEnabled: false } } as any); + when(config.getSettings(anything())).thenReturn({ + linting: { pylintEnabled: false }, + languageServer: LanguageServerType.Jedi + } as any); expect(linterInfo.isEnabled()).to.be.false; }); test('Test disabled when Jedi is enabled and Pylint is explicitly disabled', async () => { - const config = mock(ConfigurationService); - const workspaceService = mock(WorkspaceService); - const linterInfo = new PylintLinterInfo(instance(config), instance(workspaceService), []); + const linterInfo = new PylintLinterInfo(instance(config), instance(workspace), []); when(config.getSettings(anything())).thenReturn({ linting: { pylintEnabled: false }, - jediEnabled: true + languageServer: LanguageServerType.Jedi } as any); expect(linterInfo.isEnabled()).to.be.false; }); test('Test enabled when Jedi is enabled and Pylint is explicitly enabled', async () => { - const config = mock(ConfigurationService); - const workspaceService = mock(WorkspaceService); - const linterInfo = new PylintLinterInfo(instance(config), instance(workspaceService), []); + const linterInfo = new PylintLinterInfo(instance(config), instance(workspace), []); - when(config.getSettings(anything())).thenReturn({ linting: { pylintEnabled: true }, jediEnabled: true } as any); + when(config.getSettings(anything())).thenReturn({ + linting: { pylintEnabled: true }, + languageServer: LanguageServerType.Jedi + } as any); expect(linterInfo.isEnabled()).to.be.true; }); test('Test disabled when using Language Server and Pylint is not configured', async () => { - const config = mock(ConfigurationService); - const workspaceService = mock(WorkspaceService); - const linterInfo = new PylintLinterInfo(instance(config), instance(workspaceService), []); + const linterInfo = new PylintLinterInfo(instance(config), instance(workspace), []); - const inspection = {}; - const pythonConfig = { - inspect: () => inspection - }; when(config.getSettings(anything())).thenReturn({ linting: { pylintEnabled: true }, - jediEnabled: false + languageServer: LanguageServerType.Microsoft } as any); - when(workspaceService.getConfiguration('python', anything())).thenReturn(pythonConfig as any); + + const pythonConfig = { + // tslint:disable-next-line:no-empty + inspect: () => {} + }; + when(workspace.getConfiguration('python', anything())).thenReturn(pythonConfig as any); expect(linterInfo.isEnabled()).to.be.false; }); @@ -77,6 +80,7 @@ suite('Linter Info - Pylint', () => { suite('Test is enabled when using Language Server and Pylint is configured', () => { testsForisEnabled.forEach((testParams) => { test(testParams.testName, async () => { + // tslint:disable-next-line:no-shadowed-variable const config = mock(ConfigurationService); const workspaceService = mock(WorkspaceService); const linterInfo = new PylintLinterInfo(instance(config), instance(workspaceService), []); @@ -86,7 +90,7 @@ suite('Linter Info - Pylint', () => { }; when(config.getSettings(anything())).thenReturn({ linting: { pylintEnabled: true }, - jediEnabled: false + languageServer: LanguageServerType.Microsoft } as any); when(workspaceService.getConfiguration('python', anything())).thenReturn(pythonConfig as any); diff --git a/src/test/linters/pylint.test.ts b/src/test/linters/pylint.test.ts index c8017d136970..02b19a7ef9bd 100644 --- a/src/test/linters/pylint.test.ts +++ b/src/test/linters/pylint.test.ts @@ -6,7 +6,16 @@ import { Container } from 'inversify'; import * as os from 'os'; import * as path from 'path'; import * as TypeMoq from 'typemoq'; -import { CancellationTokenSource, DiagnosticSeverity, OutputChannel, TextDocument, Uri, WorkspaceFolder } from 'vscode'; +import { + CancellationTokenSource, + DiagnosticSeverity, + OutputChannel, + TextDocument, + Uri, + WorkspaceConfiguration, + WorkspaceFolder +} from 'vscode'; +import { LanguageServerType } from '../../client/activation/types'; import { IWorkspaceService } from '../../client/common/application/types'; import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; import { IPythonToolExecutionService } from '../../client/common/process/types'; @@ -34,6 +43,8 @@ suite('Linting - Pylint', () => { let workspace: TypeMoq.IMock; let execService: TypeMoq.IMock; let config: TypeMoq.IMock; + let workspaceConfig: TypeMoq.IMock; + let pythonSettings: TypeMoq.IMock; let serviceContainer: ServiceContainer; setup(() => { @@ -67,7 +78,16 @@ suite('Linting - Pylint', () => { IInterpreterAutoSeletionProxyService, MockAutoSelectionService ); + + pythonSettings = TypeMoq.Mock.ofType(); + pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Jedi); + config = TypeMoq.Mock.ofType(); + config.setup((c) => c.getSettings()).returns(() => pythonSettings.object); + + workspaceConfig = TypeMoq.Mock.ofType(); + workspace.setup((w) => w.getConfiguration('python')).returns(() => workspaceConfig.object); + serviceManager.addSingletonInstance(IConfigurationService, config.object); const linterManager = new LinterManager(serviceContainer, workspace.object); serviceManager.addSingletonInstance(ILinterManager, linterManager); @@ -198,6 +218,7 @@ suite('Linting - Pylint', () => { const settings = TypeMoq.Mock.ofType(); settings.setup((x) => x.linting).returns(() => lintSettings); + settings.setup((x) => x.languageServer).returns(() => LanguageServerType.Jedi); config.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); await pylinter.lint(document.object, new CancellationTokenSource().token); @@ -244,6 +265,7 @@ suite('Linting - Pylint', () => { const settings = TypeMoq.Mock.ofType(); settings.setup((x) => x.linting).returns(() => lintSettings); + settings.setup((x) => x.languageServer).returns(() => LanguageServerType.Jedi); config.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); const messages = await pylinter.lint(document.object, new CancellationTokenSource().token); diff --git a/src/test/performance/settings.json b/src/test/performance/settings.json index 809ebf6ab2f4..ffc9d2a990cd 100644 --- a/src/test/performance/settings.json +++ b/src/test/performance/settings.json @@ -1 +1 @@ -{ "python.jediEnabled": true } +{ "python.languageServer": "Jedi" } diff --git a/src/test/performanceTest.ts b/src/test/performanceTest.ts index d489c76dd011..dbc02cc3652c 100644 --- a/src/test/performanceTest.ts +++ b/src/test/performanceTest.ts @@ -22,6 +22,7 @@ import * as download from 'download'; import * as fs from 'fs-extra'; import * as path from 'path'; import * as request from 'request'; +import { LanguageServerType } from '../client/activation/types'; import { EXTENSION_ROOT_DIR, PVSC_EXTENSION_ID } from '../client/common/constants'; import { unzip } from './common'; @@ -72,7 +73,9 @@ class TestRunner { await this.runPerfTest(devLogFiles, releaseLogFiles, languageServerLogFiles); } private async enableLanguageServer(enable: boolean) { - const settings = `{ "python.jediEnabled": ${!enable} }`; + const settings = `{ "python.languageServer": "${ + enable ? LanguageServerType.Microsoft : LanguageServerType.Jedi + }" }`; await fs.writeFile(path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'performance', 'settings.json'), settings); } diff --git a/src/test/smoke/common.ts b/src/test/smoke/common.ts index 61ad0dbdf3b8..4c8fa12b97cd 100644 --- a/src/test/smoke/common.ts +++ b/src/test/smoke/common.ts @@ -33,13 +33,13 @@ async function getLanguageServerFolders(): Promise { export function isJediEnabled() { const resource = vscode.workspace.workspaceFolders![0].uri; const settings = vscode.workspace.getConfiguration('python', resource); - return settings.get('jediEnabled') === true; + return settings.get('languageServer') === 'Jedi'; } export async function enableJedi(enable: boolean | undefined) { if (isJediEnabled() === enable) { return; } - await updateSetting('jediEnabled', enable); + await updateSetting('languageServer', 'Jedi'); } export async function openFileAndWaitForLS(file: string): Promise { const textDocument = await vscode.workspace.openTextDocument(file); diff --git a/src/test/smokeTest.ts b/src/test/smokeTest.ts index 815648c22706..79e44f1dcf3d 100644 --- a/src/test/smokeTest.ts +++ b/src/test/smokeTest.ts @@ -31,7 +31,8 @@ class TestRunner { await this.launchTest(env); } private async enableLanguageServer(enable: boolean) { - const settings = `{ "python.jediEnabled": ${!enable} }`; + // When running smoke tests, we won't have access to unbundled files. + const settings = `{ "python.languageServer": ${enable ? '"Microsoft"' : '"Jedi"'} }`; await fs.ensureDir( path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'smokeTests', '.vscode') );