forked from DonJayamanne/pythonVSCode
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Display an informational Pylance prompt for existing users #16069
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Kim-Adeline Miguel (kimadeline)
merged 24 commits into
microsoft:default-language-server
from
kimadeline:startup-prompt
May 4, 2021
Merged
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
c729bfe
Export extension version memento
8b31ad4
Add localization strings
26fc28c
Add prompt check
3b22184
Add to the list of things triggering on activation
df73fae
Add tests
95f98a7
Rename file
1c7c2d8
Remove unsupported newlines
23002d8
Fix localization + re-add newlines
8d4c741
Change to be a non-blocking diagnostic check
953a765
Change localization key
389b483
Update memento on close instead of just on ok
54b76d6
Fix localization
f0bb2ad
Add initialMementoValue handler
7d1313c
Links
f3ca882
Fix tests
2959731
Fix tests
985f533
Set PYLANCE_PROMPT_MEMENTO to false
098a7a9
Remove unused line
82286d3
Set to true directly
221e852
Remove extra updateMemento calls
7d1036d
I can't read
adabea9
Period
808d47f
Run in foreground
1c808aa
Add handling test
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
91 changes: 91 additions & 0 deletions
91
src/client/application/diagnostics/checks/pylanceDefault.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<MessageCommandPrompt>, | ||
| @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, | ||
| ) { | ||
| super([DiagnosticCodes.PylanceDefaultDiagnostic], serviceContainer, disposableRegistry, true); | ||
| } | ||
|
|
||
| public async diagnose(resource: Resource): Promise<IDiagnostic[]> { | ||
| if (!(await this.shouldShowPrompt())) { | ||
| return []; | ||
| } | ||
|
|
||
| return [new PylanceDefaultDiagnostic(Diagnostics.pylanceDefaultMessage(), resource)]; | ||
| } | ||
|
|
||
| protected async onHandle(diagnostics: IDiagnostic[]): Promise<void> { | ||
| 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<boolean> { | ||
| 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; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
162 changes: 162 additions & 0 deletions
162
src/test/application/diagnostics/checks/pylanceDefault.unit.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<IServiceContainer>; | ||
| let diagnosticService: IDiagnosticsService; | ||
| let filterService: typemoq.IMock<IDiagnosticFilterService>; | ||
| let messageHandler: typemoq.IMock<IDiagnosticHandlerService<MessageCommandPrompt>>; | ||
| let startPage: typemoq.IMock<IStartPage>; | ||
| let context: typemoq.IMock<IExtensionContext>; | ||
| let memento: typemoq.IMock<ExtensionContext['globalState']>; | ||
|
|
||
| setup(() => { | ||
| serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); | ||
| filterService = typemoq.Mock.ofType<IDiagnosticFilterService>(); | ||
| messageHandler = typemoq.Mock.ofType<IDiagnosticHandlerService<MessageCommandPrompt>>(); | ||
| startPage = typemoq.Mock.ofType<IStartPage>(); | ||
| context = typemoq.Mock.ofType<IExtensionContext>(); | ||
| memento = typemoq.Mock.ofType<ExtensionContext['globalState']>(); | ||
|
|
||
| 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()); | ||
|
kimadeline marked this conversation as resolved.
|
||
|
|
||
| await diagnosticService.handle([diagnostic]); | ||
|
|
||
| filterService.verifyAll(); | ||
| messageHandler.verifyAll(); | ||
| }); | ||
|
|
||
| test('PylanceDefaultDiagnosticService can handle PylanceDefaultDiagnostic diagnostics', async () => { | ||
| const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); | ||
| 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<IDiagnostic>(); | ||
| 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(); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.