From 85b1a3aabc0013a89b20869f1cc8b1ac4a2667d7 Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com> Date: Mon, 3 May 2021 13:17:08 -0700 Subject: [PATCH 1/4] Bundle Pylance as part of an extension pack (not a hard dependency) (#16077) * extension pack * Undo package.json change * Add extension pack link at build time * Forgot to update all names --- .github/actions/build-vsix/action.yml | 3 +++ gulpfile.js | 15 +++++++++++++++ package.json | 1 + 3 files changed, 19 insertions(+) diff --git a/.github/actions/build-vsix/action.yml b/.github/actions/build-vsix/action.yml index a3b9d0312afe..318dd5b8da15 100644 --- a/.github/actions/build-vsix/action.yml +++ b/.github/actions/build-vsix/action.yml @@ -37,5 +37,8 @@ runs: - run: npm run addExtensionDependencies shell: bash + - run: npm run addExtensionPackDependencies + shell: bash + - run: npm run package shell: bash diff --git a/gulpfile.js b/gulpfile.js index 7ea0c9f9b4aa..9c7c16e9a28f 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -81,6 +81,10 @@ gulp.task('addExtensionDependencies', async () => { await addExtensionDependencies(); }); +gulp.task('addExtensionPackDependencies', async () => { + await addExtensionPackDependencies(); +}); + async function addExtensionDependencies() { // Update the package.json to add extension dependencies at build time so that // extension dependencies need not be installed during development @@ -92,6 +96,17 @@ async function addExtensionDependencies() { await fsExtra.writeFile('package.json', JSON.stringify(packageJson, null, 4), 'utf-8'); } +async function addExtensionPackDependencies() { + // Update the package.json to add extension pack dependencies at build time so that + // extension dependencies need not be installed during development + const packageJsonContents = await fsExtra.readFile('package.json', 'utf-8'); + const packageJson = JSON.parse(packageJsonContents); + packageJson.extensionPack = ['ms-python.vscode-pylance'].concat( + packageJson.extensionPack ? packageJson.extensionPack : [], + ); + await fsExtra.writeFile('package.json', JSON.stringify(packageJson, null, 4), 'utf-8'); +} + gulp.task('updateBuildNumber', async () => { await updateBuildNumber(argv); }); diff --git a/package.json b/package.json index de0b7c5775c1..1d3b03029875 100644 --- a/package.json +++ b/package.json @@ -2050,6 +2050,7 @@ "format-fix": "prettier --write 'src/**/*.ts' 'src/**/*.tsx' 'build/**/*.js' '.github/**/*.yml' gulpfile.js", "clean": "gulp clean", "addExtensionDependencies": "gulp addExtensionDependencies", + "addExtensionPackDependencies": "gulp addExtensionPackDependencies", "updateBuildNumber": "gulp updateBuildNumber", "verifyBundle": "gulp verifyBundle", "webpack": "webpack" From 32e478c57af16875e0590300bb1ca9251a3f04e3 Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com> Date: Tue, 4 May 2021 12:47:51 -0700 Subject: [PATCH 2/4] Update readme + license at build time (#16134) * Add license update * Update package.json description * Update readme * Add news file * Update header wording * let -> const * Wording * Consistenly use VS Code long form --- README.md | 7 +++++-- build/license-header.txt | 5 +++++ gulpfile.js | 9 +++++++++ news/1 Enhancements/16116.md | 1 + package.json | 2 +- 5 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 build/license-header.txt create mode 100644 news/1 Enhancements/16116.md diff --git a/README.md b/README.md index 68058b036d86..0423083ff469 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,12 @@ # Python extension for Visual Studio Code -A [Visual Studio Code](https://code.visualstudio.com/) [extension](https://marketplace.visualstudio.com/VSCode) with rich support for the [Python language](https://www.python.org/) (for all [actively supported versions](https://devguide.python.org/#status-of-python-branches) of the language: >=3.6), including features such as IntelliSense, linting, debugging, code navigation, code formatting, refactoring, variable explorer, test explorer, and more! +A [Visual Studio Code](https://code.visualstudio.com/) [extension](https://marketplace.visualstudio.com/VSCode) with rich support for the [Python language](https://www.python.org/) (for all [actively supported versions](https://devguide.python.org/#status-of-python-branches) of the language: >=3.6), including features such as IntelliSense (Pylance), linting, debugging, code navigation, code formatting, refactoring, variable explorer, test explorer, and more! -Additionally, the Python extension gives you an optimal and feature-rich experience for working with Jupyter notebooks through the [Jupyter extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter). +## Installed extensions +The Python extension will automatically install the [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) and [Jupyter](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter) extensions to give you the best experience when working with Python files and Jupyter notebooks. However, Pylance is an optional dependency, meaning the Python extension will remain fully functional if it fails to be installed. You can also [uninstall](https://code.visualstudio.com/docs/editor/extension-marketplace#_uninstall-an-extension) it at the expense of some features if you’re using a different language server. + +Extensions installed through the marketplace are subject to the [Marketplace Terms of Use](https://cdn.vsassets.io/v/M146_20190123.39/_content/Microsoft-Visual-Studio-Marketplace-Terms-of-Use.pdf). ## Quick start diff --git a/build/license-header.txt b/build/license-header.txt new file mode 100644 index 000000000000..d1575272d318 --- /dev/null +++ b/build/license-header.txt @@ -0,0 +1,5 @@ +PLEASE NOTE: This Python extension for Visual Studio Code has a hard dependency on the Jupyter extension for Visual Studio Code which is installed automatically alongside it. The Python extension for Visual Studio Code also holds an optional dependency on the Pylance extension for Visual Studio Code, which is also installed automatically but is separately licensed. + +All the source code for the Python extension for Visual Studio Code is available under the MIT License (given below) as is the source code for the Jupyter extension for Visual Studio Code. But the optional Pylance extension for Visual Studio Code is only available in binary form and it is not licensed under the MIT License. The Pylance extension for Visual Studio Code is licensed under a Microsoft proprietary license, the terms of which are available here: https://marketplace.visualstudio.com/items/ms-python.vscode-pylance/license. + +------------------------------------------------------------------------------ diff --git a/gulpfile.js b/gulpfile.js index 9c7c16e9a28f..c2aa43f4aceb 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -82,6 +82,7 @@ gulp.task('addExtensionDependencies', async () => { }); gulp.task('addExtensionPackDependencies', async () => { + await buildLicense(); await addExtensionPackDependencies(); }); @@ -107,6 +108,14 @@ async function addExtensionPackDependencies() { await fsExtra.writeFile('package.json', JSON.stringify(packageJson, null, 4), 'utf-8'); } +async function buildLicense() { + const headerPath = path.join(__dirname, 'build', 'license-header.txt'); + const licenseHeader = await fsExtra.readFile(headerPath, 'utf-8'); + const license = await fsExtra.readFile('LICENSE', 'utf-8'); + + await fsExtra.writeFile('LICENSE', `${licenseHeader}\n${license}`, 'utf-8'); +} + gulp.task('updateBuildNumber', async () => { await updateBuildNumber(argv); }); diff --git a/news/1 Enhancements/16116.md b/news/1 Enhancements/16116.md new file mode 100644 index 000000000000..f52f66a0b1d3 --- /dev/null +++ b/news/1 Enhancements/16116.md @@ -0,0 +1 @@ +Bundle Pylance with the extension as an optional dependency. diff --git a/package.json b/package.json index 1d3b03029875..9315aa776ab9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "python", "displayName": "Python", - "description": "Linting, Debugging (multi-threaded, remote), Intellisense, Jupyter Notebooks, code formatting, refactoring, unit tests, and more.", + "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), Jupyter Notebooks, code formatting, refactoring, unit tests, and more.", "version": "2021.5.0-dev", "featureFlags": { "usingNewInterpreterStorage": true From 619325b39dd3878a0558c4335796ea734e71adb3 Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com> Date: Tue, 4 May 2021 15:34:32 -0700 Subject: [PATCH 3/4] Display an informational Pylance prompt for existing users (#16069) * Export extension version memento * Add localization strings * Add prompt check * Add to the list of things triggering on activation * Add tests * Rename file * Remove unsupported newlines * Fix localization + re-add newlines * Change to be a non-blocking diagnostic check * Change localization key * Update memento on close instead of just on ok * Fix localization * Add initialMementoValue handler * Links * Fix tests * Fix tests * Set PYLANCE_PROMPT_MEMENTO to false * Remove unused line * Set to true directly * Remove extra updateMemento calls * I can't read * Period * Run in foreground * Add handling test --- package.nls.json | 1 + .../diagnostics/checks/pylanceDefault.ts | 91 ++++++++++ .../application/diagnostics/constants.ts | 1 + .../diagnostics/serviceRegistry.ts | 8 + src/client/common/startPage/startPage.ts | 11 +- src/client/common/startPage/types.ts | 1 + src/client/common/utils/localize.ts | 4 + .../checks/pylanceDefault.unit.test.ts | 162 ++++++++++++++++++ src/test/startPage/startPage.unit.test.ts | 3 +- src/test/startPage/startPageIocContainer.ts | 3 + 10 files changed, 280 insertions(+), 5 deletions(-) create mode 100644 src/client/application/diagnostics/checks/pylanceDefault.ts create mode 100644 src/test/application/diagnostics/checks/pylanceDefault.unit.test.ts diff --git a/package.nls.json b/package.nls.json index a88ba79a1fda..e3450302a6b1 100644 --- a/package.nls.json +++ b/package.nls.json @@ -133,6 +133,7 @@ "diagnostics.checkIsort5UpgradeGuide": "We found outdated configuration for sorting imports in this workspace. Check the [isort upgrade guide](https://aka.ms/AA9j5x4) to update your settings.", "diagnostics.yesUpdateLaunch": "Yes, update launch.json", "diagnostics.invalidTestSettings": "Your settings needs to be updated to change the setting \"python.unitTest.\" to \"python.testing.\", otherwise testing Python code using the extension may not work. Would you like to automatically update your settings now?", + "diagnostics.pylanceDefaultMessage": "The Python extension now includes Pylance to improve completions, code navigation, overall performance and much more! You can learn more about the update and learn to change your language server [here](https://aka.ms/new-python-bundle).\n\nRead Pylance’s license [here](https://marketplace.visualstudio.com/items/ms-python.vscode-pylance/license).", "Common.canceled": "Canceled", "Common.cancel": "Cancel", "Common.yesPlease": "Yes, please", diff --git a/src/client/application/diagnostics/checks/pylanceDefault.ts b/src/client/application/diagnostics/checks/pylanceDefault.ts new file mode 100644 index 000000000000..08b1ca2499e9 --- /dev/null +++ b/src/client/application/diagnostics/checks/pylanceDefault.ts @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// eslint-disable-next-line max-classes-per-file +import { inject, named } from 'inversify'; +import { DiagnosticSeverity } from 'vscode'; +import { IStartPage } from '../../../common/startPage/types'; +import { IDisposableRegistry, IExtensionContext, Resource } from '../../../common/types'; +import { Diagnostics, Common } from '../../../common/utils/localize'; +import { IServiceContainer } from '../../../ioc/types'; +import { BaseDiagnostic, BaseDiagnosticsService } from '../base'; +import { DiagnosticCodes } from '../constants'; +import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../promptHandler'; +import { DiagnosticScope, IDiagnostic, IDiagnosticHandlerService } from '../types'; + +export const PYLANCE_PROMPT_MEMENTO = 'pylanceDefaultPromptMemento'; + +export class PylanceDefaultDiagnostic extends BaseDiagnostic { + constructor(message: string, resource: Resource) { + super( + DiagnosticCodes.PylanceDefaultDiagnostic, + message, + DiagnosticSeverity.Information, + DiagnosticScope.Global, + resource, + ); + } +} + +export const PylanceDefaultDiagnosticServiceId = 'PylanceDefaultDiagnosticServiceId'; + +export class PylanceDefaultDiagnosticService extends BaseDiagnosticsService { + constructor( + @inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(IExtensionContext) private readonly context: IExtensionContext, + @inject(IStartPage) private readonly startPage: IStartPage, + @inject(IDiagnosticHandlerService) + @named(DiagnosticCommandPromptHandlerServiceId) + protected readonly messageService: IDiagnosticHandlerService, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + ) { + super([DiagnosticCodes.PylanceDefaultDiagnostic], serviceContainer, disposableRegistry, true); + } + + public async diagnose(resource: Resource): Promise { + if (!(await this.shouldShowPrompt())) { + return []; + } + + return [new PylanceDefaultDiagnostic(Diagnostics.pylanceDefaultMessage(), resource)]; + } + + protected async onHandle(diagnostics: IDiagnostic[]): Promise { + if (diagnostics.length === 0 || !this.canHandle(diagnostics[0])) { + return; + } + + const diagnostic = diagnostics[0]; + if (await this.filterService.shouldIgnoreDiagnostic(diagnostic.code)) { + return; + } + + const options = [{ prompt: Common.ok() }]; + + await this.messageService.handle(diagnostic, { + commandPrompts: options, + onClose: this.updateMemento.bind(this), + }); + } + + private async updateMemento() { + await this.context.globalState.update(PYLANCE_PROMPT_MEMENTO, true); + } + + private async shouldShowPrompt(): Promise { + const savedVersion: string | undefined = this.startPage.initialMementoValue; + const promptShown: boolean | undefined = this.context.globalState.get(PYLANCE_PROMPT_MEMENTO); + + // savedVersion being undefined means that this is the first time the user activates the extension, + // and we don't want to show the prompt to first-time users. + // We set PYLANCE_PROMPT_MEMENTO here to skip the prompt + // in case the user reloads the extension and savedVersion becomes set + if (savedVersion === undefined) { + await this.updateMemento(); + return false; + } + + // promptShown being undefined means that this is the first time we check if we should show the prompt. + return promptShown === undefined; + } +} diff --git a/src/client/application/diagnostics/constants.ts b/src/client/application/diagnostics/constants.ts index a5c8c69a53d3..f03564ff2c42 100644 --- a/src/client/application/diagnostics/constants.ts +++ b/src/client/application/diagnostics/constants.ts @@ -19,4 +19,5 @@ export enum DiagnosticCodes { ConsoleTypeDiagnostic = 'ConsoleTypeDiagnostic', ConfigPythonPathDiagnostic = 'ConfigPythonPathDiagnostic', UpgradeCodeRunnerDiagnostic = 'UpgradeCodeRunnerDiagnostic', + PylanceDefaultDiagnostic = 'PylanceDefaultDiagnostic', } diff --git a/src/client/application/diagnostics/serviceRegistry.ts b/src/client/application/diagnostics/serviceRegistry.ts index 1388a2bab248..1a513ff9255a 100644 --- a/src/client/application/diagnostics/serviceRegistry.ts +++ b/src/client/application/diagnostics/serviceRegistry.ts @@ -28,6 +28,7 @@ import { PowerShellActivationHackDiagnosticsService, PowerShellActivationHackDiagnosticsServiceId, } from './checks/powerShellActivation'; +import { PylanceDefaultDiagnosticService, PylanceDefaultDiagnosticServiceId } from './checks/pylanceDefault'; import { InvalidPythonInterpreterService, InvalidPythonInterpreterServiceId } from './checks/pythonInterpreter'; import { PythonPathDeprecatedDiagnosticService, @@ -92,6 +93,13 @@ export function registerTypes(serviceManager: IServiceManager, languageServerTyp UpgradeCodeRunnerDiagnosticService, UpgradeCodeRunnerDiagnosticServiceId, ); + + serviceManager.addSingleton( + IDiagnosticsService, + PylanceDefaultDiagnosticService, + PylanceDefaultDiagnosticServiceId, + ); + serviceManager.addSingleton(IDiagnosticsCommandFactory, DiagnosticsCommandFactory); serviceManager.addSingleton(IApplicationDiagnostics, ApplicationDiagnostics); diff --git a/src/client/common/startPage/startPage.ts b/src/client/common/startPage/startPage.ts index dae649bfc59b..d0cccf72d5b2 100644 --- a/src/client/common/startPage/startPage.ts +++ b/src/client/common/startPage/startPage.ts @@ -28,6 +28,8 @@ import { WebviewPanelHost } from './webviewPanelHost'; const startPageDir = path.join(EXTENSION_ROOT_DIR, 'out', 'startPage-ui', 'viewers'); +export const EXTENSION_VERSION_MEMENTO = 'extensionVersion'; + // Class that opens, disposes and handles messages and actions for the Python Extension Start Page. // It also runs when the extension activates. @injectable() @@ -39,6 +41,8 @@ export class StartPage extends WebviewPanelHost private actionTakenOnFirstTime = false; private firstTime = false; private webviewDidLoad = false; + public initialMementoValue: string | undefined = undefined; + constructor( @inject(IWebviewPanelProvider) provider: IWebviewPanelProvider, @inject(ICodeCssGenerator) cssGenerator: ICodeCssGenerator, @@ -66,6 +70,7 @@ export class StartPage extends WebviewPanelHost false, ); this.timer = new StopWatch(); + this.initialMementoValue = this.context.globalState.get(EXTENSION_VERSION_MEMENTO); } public async activate(): Promise { @@ -129,7 +134,7 @@ export class StartPage extends WebviewPanelHost sendTelemetryEvent(Telemetry.StartPageOpenBlankNotebook); this.setTelemetryFlags(); - const savedVersion: string | undefined = this.context.globalState.get('extensionVersion'); + const savedVersion: string | undefined = this.context.globalState.get(EXTENSION_VERSION_MEMENTO); if (savedVersion) { await this.commandManager.executeCommand( @@ -220,7 +225,7 @@ export class StartPage extends WebviewPanelHost // Public for testing public async extensionVersionChanged(): Promise { - const savedVersion: string | undefined = this.context.globalState.get('extensionVersion'); + const savedVersion: string | undefined = this.context.globalState.get(EXTENSION_VERSION_MEMENTO); const version: string = this.appEnvironment.packageJson.version; let shouldShowStartPage: boolean; @@ -239,7 +244,7 @@ export class StartPage extends WebviewPanelHost // savedVersion being undefined means this is the first time the user activates the extension. // if savedVersion != version, there was an update - await this.context.globalState.update('extensionVersion', version); + await this.context.globalState.update(EXTENSION_VERSION_MEMENTO, version); return shouldShowStartPage; } diff --git a/src/client/common/startPage/types.ts b/src/client/common/startPage/types.ts index ef50ad1f9189..639dca40232a 100644 --- a/src/client/common/startPage/types.ts +++ b/src/client/common/startPage/types.ts @@ -10,6 +10,7 @@ export type JSONArray = JSONValue[]; export const IStartPage = Symbol('IStartPage'); export interface IStartPage { + readonly initialMementoValue?: string; open(): Promise; extensionVersionChanged(): Promise; } diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 05458ccc3db7..4fd7eca4ec0f 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -67,6 +67,10 @@ export namespace Diagnostics { 'diagnostics.checkIsort5UpgradeGuide', 'We found outdated configuration for sorting imports in this workspace. Check the [isort upgrade guide](https://aka.ms/AA9j5x4) to update your settings.', ); + export const pylanceDefaultMessage = localize( + 'diagnostics.pylanceDefaultMessage', + 'The Python extension now includes Pylance to improve completions, code navigation, overall performance and much more! You can learn more about the update and learn to change your language server [here](https://aka.ms/new-python-bundle).\n\nRead Pylance’s license [here](https://marketplace.visualstudio.com/items/ms-python.vscode-pylance/license).', + ); } export namespace Common { diff --git a/src/test/application/diagnostics/checks/pylanceDefault.unit.test.ts b/src/test/application/diagnostics/checks/pylanceDefault.unit.test.ts new file mode 100644 index 000000000000..518f46a023f0 --- /dev/null +++ b/src/test/application/diagnostics/checks/pylanceDefault.unit.test.ts @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { expect } from 'chai'; +import * as typemoq from 'typemoq'; +import { ExtensionContext } from 'vscode'; +import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; +import { + PylanceDefaultDiagnostic, + PylanceDefaultDiagnosticService, + PYLANCE_PROMPT_MEMENTO, +} from '../../../../client/application/diagnostics/checks/pylanceDefault'; +import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; +import { MessageCommandPrompt } from '../../../../client/application/diagnostics/promptHandler'; +import { + IDiagnostic, + IDiagnosticFilterService, + IDiagnosticHandlerService, + IDiagnosticsService, +} from '../../../../client/application/diagnostics/types'; +import { IStartPage } from '../../../../client/common/startPage/types'; +import { IExtensionContext } from '../../../../client/common/types'; +import { Common, Diagnostics } from '../../../../client/common/utils/localize'; +import { IServiceContainer } from '../../../../client/ioc/types'; + +suite('Application Diagnostics - Pylance informational prompt', () => { + let serviceContainer: typemoq.IMock; + let diagnosticService: IDiagnosticsService; + let filterService: typemoq.IMock; + let messageHandler: typemoq.IMock>; + let startPage: typemoq.IMock; + let context: typemoq.IMock; + let memento: typemoq.IMock; + + setup(() => { + serviceContainer = typemoq.Mock.ofType(); + filterService = typemoq.Mock.ofType(); + messageHandler = typemoq.Mock.ofType>(); + startPage = typemoq.Mock.ofType(); + context = typemoq.Mock.ofType(); + memento = typemoq.Mock.ofType(); + + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IDiagnosticFilterService))) + .returns(() => filterService.object); + context.setup((c) => c.globalState).returns(() => memento.object); + + diagnosticService = new (class extends PylanceDefaultDiagnosticService { + // eslint-disable-next-line class-methods-use-this + public _clear() { + while (BaseDiagnosticsService.handledDiagnosticCodeKeys.length > 0) { + BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); + } + } + })(serviceContainer.object, context.object, startPage.object, messageHandler.object, []); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (diagnosticService as any)._clear(); + }); + + teardown(() => { + context.reset(); + memento.reset(); + }); + + function setupMementos(version?: string, promptShown?: boolean) { + startPage.setup((s) => s.initialMementoValue).returns(() => version); + memento.setup((m) => m.get(PYLANCE_PROMPT_MEMENTO)).returns(() => promptShown); + } + + test("Should display message if it's an existing installation of the extension and the prompt has not been shown yet", async () => { + setupMementos('1.0.0', undefined); + + const diagnostics = await diagnosticService.diagnose(undefined); + + assert.deepStrictEqual(diagnostics, [ + new PylanceDefaultDiagnostic(Diagnostics.pylanceDefaultMessage(), undefined), + ]); + }); + + test("Should return empty diagnostics if it's an existing installation of the extension and the prompt has been shown before", async () => { + setupMementos('1.0.0', true); + + const diagnostics = await diagnosticService.diagnose(undefined); + + assert.deepStrictEqual(diagnostics, []); + }); + + test("Should return empty diagnostics if it's a fresh installation of the extension", async () => { + setupMementos(undefined, undefined); + + const diagnostics = await diagnosticService.diagnose(undefined); + + assert.deepStrictEqual(diagnostics, []); + }); + + test('Should display a prompt when handling the diagnostic code', async () => { + const diagnostic = new PylanceDefaultDiagnostic(DiagnosticCodes.PylanceDefaultDiagnostic, undefined); + let messagePrompt: MessageCommandPrompt | undefined; + + messageHandler + .setup((f) => f.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) + .callback((_d, prompt: MessageCommandPrompt) => { + messagePrompt = prompt; + }) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + await diagnosticService.handle([diagnostic]); + + filterService.verifyAll(); + messageHandler.verifyAll(); + + assert.notDeepStrictEqual(messagePrompt, undefined); + assert.notDeepStrictEqual(messagePrompt!.onClose, undefined); + assert.deepStrictEqual(messagePrompt!.commandPrompts, [{ prompt: Common.ok() }]); + }); + + test('Should return empty diagnostics if the diagnostic code has been ignored', async () => { + const diagnostic = new PylanceDefaultDiagnostic(DiagnosticCodes.PylanceDefaultDiagnostic, undefined); + + filterService + .setup((f) => f.shouldIgnoreDiagnostic(typemoq.It.isValue(DiagnosticCodes.PylanceDefaultDiagnostic))) + .returns(() => Promise.resolve(true)) + .verifiable(typemoq.Times.once()); + + messageHandler.setup((f) => f.handle(typemoq.It.isAny(), typemoq.It.isAny())).verifiable(typemoq.Times.never()); + + await diagnosticService.handle([diagnostic]); + + filterService.verifyAll(); + messageHandler.verifyAll(); + }); + + test('PylanceDefaultDiagnosticService can handle PylanceDefaultDiagnostic diagnostics', async () => { + const diagnostic = typemoq.Mock.ofType(); + diagnostic + .setup((d) => d.code) + .returns(() => DiagnosticCodes.PylanceDefaultDiagnostic) + .verifiable(typemoq.Times.atLeastOnce()); + + const canHandle = await diagnosticService.canHandle(diagnostic.object); + + expect(canHandle).to.be.equal(true, 'Invalid value'); + diagnostic.verifyAll(); + }); + + test('PylanceDefaultDiagnosticService cannot handle non-PylanceDefaultDiagnostic diagnostics', async () => { + const diagnostic = typemoq.Mock.ofType(); + diagnostic + .setup((d) => d.code) + .returns(() => DiagnosticCodes.EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic) + .verifiable(typemoq.Times.atLeastOnce()); + + const canHandle = await diagnosticService.canHandle(diagnostic.object); + + expect(canHandle).to.be.equal(false, 'Invalid value'); + diagnostic.verifyAll(); + }); +}); diff --git a/src/test/startPage/startPage.unit.test.ts b/src/test/startPage/startPage.unit.test.ts index fd42dfd3944b..85799f349e82 100644 --- a/src/test/startPage/startPage.unit.test.ts +++ b/src/test/startPage/startPage.unit.test.ts @@ -38,7 +38,6 @@ suite('StartPage tests', () => { const dummySettings = new PythonSettings(undefined, new MockAutoSelectionService()); function setupVersions(savedVersion: string, actualVersion: string) { - context.setup((c) => c.globalState).returns(() => memento.object); memento.setup((m) => m.get(typemoq.It.isAnyString())).returns(() => savedVersion); memento .setup((m) => m.update(typemoq.It.isAnyString(), typemoq.It.isAnyString())) @@ -50,7 +49,6 @@ suite('StartPage tests', () => { } function reset() { - context.reset(); memento.reset(); appEnvironment.reset(); } @@ -69,6 +67,7 @@ suite('StartPage tests', () => { appEnvironment = typemoq.Mock.ofType(); memento = typemoq.Mock.ofType(); + context.setup((c) => c.globalState).returns(() => memento.object); configuration.setup((cs) => cs.getSettings(undefined)).returns(() => dummySettings); startPage = new StartPage( diff --git a/src/test/startPage/startPageIocContainer.ts b/src/test/startPage/startPageIocContainer.ts index 00af74000c56..abba95cc6606 100644 --- a/src/test/startPage/startPageIocContainer.ts +++ b/src/test/startPage/startPageIocContainer.ts @@ -15,6 +15,7 @@ import { ConfigurationChangeEvent, Disposable, EventEmitter, + ExtensionContext, FileSystemWatcher, Uri, WorkspaceFolder, @@ -249,7 +250,9 @@ export class StartPageIocContainer extends UnitTestIocContainer { this.serviceManager.add(IInstallationChannelManager, InstallationChannelManager); + const mockMemento = TypeMoq.Mock.ofType(); const mockExtensionContext = TypeMoq.Mock.ofType(); + mockExtensionContext.setup((m) => m.globalState).returns(() => mockMemento.object); mockExtensionContext.setup((m) => m.globalStoragePath).returns(() => os.tmpdir()); mockExtensionContext.setup((m) => m.extensionPath).returns(() => this.extensionRootPath || os.tmpdir()); this.serviceManager.addSingletonInstance(IExtensionContext, mockExtensionContext.object); From 151f8db524fcffc4eb4242115374ad5eba0bf64f Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 5 May 2021 10:19:40 -0700 Subject: [PATCH 4/4] Remove extension pack category (#16149) --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 9315aa776ab9..0fab208ed5d9 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,6 @@ "Linters", "Formatters", "Other", - "Extension Packs", "Data Science", "Machine Learning", "Notebooks"