Skip to content

Commit d724f74

Browse files
d3r3kkDonJayamanne
authored andcommitted
Add two new popups: One to switch to new LS, another to ask for feedback. (#2173)
1 parent 65ff9e3 commit d724f74

File tree

15 files changed

+542
-25
lines changed

15 files changed

+542
-25
lines changed

news/1 Enhancements/2127.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add two popups to the extension: one to ask users to move to the new language server, the other to request feedback from users of that language server.

src/client/activation/languageServer.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@
33

44
import { inject, injectable } from 'inversify';
55
import * as path from 'path';
6-
import { OutputChannel, Uri } from 'vscode';
7-
import { Disposable, LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-languageclient';
6+
import { CancellationToken, CompletionContext, OutputChannel, Position,
7+
TextDocument, Uri } from 'vscode';
8+
import { Disposable, LanguageClient, LanguageClientOptions,
9+
ProvideCompletionItemsSignature, ServerOptions } from 'vscode-languageclient';
810
import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types';
911
import { PythonSettings } from '../common/configSettings';
1012
import { isTestExecution, STANDARD_OUTPUT_CHANNEL } from '../common/constants';
1113
import { createDeferred, Deferred } from '../common/helpers';
1214
import { IFileSystem, IPlatformService } from '../common/platform/types';
1315
import { StopWatch } from '../common/stopWatch';
14-
import { IConfigurationService, IExtensionContext, ILogger, IOutputChannel, IPythonSettings } from '../common/types';
16+
import { BANNER_NAME_LS_SURVEY, IConfigurationService, IExtensionContext, ILogger,
17+
IOutputChannel, IPythonExtensionBanner, IPythonSettings } from '../common/types';
1518
import { IServiceContainer } from '../ioc/types';
1619
import {
1720
PYTHON_LANGUAGE_SERVER_DOWNLOADED,
@@ -51,6 +54,7 @@ export class LanguageServerExtensionActivator implements IExtensionActivator {
5154
private excludedFiles: string[] = [];
5255
private typeshedPaths: string[] = [];
5356
private loadExtensionArgs: {} | undefined;
57+
private surveyBanner: IPythonExtensionBanner;
5458
// tslint:disable-next-line:no-unused-variable
5559
private progressReporting: ProgressReporting | undefined;
5660

@@ -81,6 +85,8 @@ export class LanguageServerExtensionActivator implements IExtensionActivator {
8185
}
8286
));
8387

88+
this.surveyBanner = services.get<IPythonExtensionBanner>(IPythonExtensionBanner, BANNER_NAME_LS_SURVEY);
89+
8490
(this.configuration.getSettings() as PythonSettings).addListener('change', this.onSettingsChanged);
8591
}
8692

@@ -155,6 +161,7 @@ export class LanguageServerExtensionActivator implements IExtensionActivator {
155161
if (this.loadExtensionArgs) {
156162
this.languageClient!.sendRequest('python/loadExtension', this.loadExtensionArgs);
157163
}
164+
158165
this.startupCompleted.resolve();
159166
}
160167

@@ -250,6 +257,14 @@ export class LanguageServerExtensionActivator implements IExtensionActivator {
250257
testEnvironment: isTestExecution(),
251258
analysisUpdates: true,
252259
traceLogging
260+
},
261+
middleware: {
262+
provideCompletionItem: (document: TextDocument, position: Position, context: CompletionContext, token: CancellationToken, next: ProvideCompletionItemsSignature) => {
263+
if (this.surveyBanner) {
264+
this.surveyBanner.showBanner().ignoreErrors();
265+
}
266+
return next(document, position, context, token);
267+
}
253268
}
254269
};
255270
}

src/client/common/types.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,3 +267,23 @@ export const IBrowserService = Symbol('IBrowserService');
267267
export interface IBrowserService {
268268
launch(url: string): void;
269269
}
270+
271+
export const IExperimentalDebuggerBanner = Symbol('IExperimentalDebuggerBanner');
272+
export interface IExperimentalDebuggerBanner {
273+
enabled: boolean;
274+
initialize(): void;
275+
showBanner(): Promise<void>;
276+
shouldShowBanner(): Promise<boolean>;
277+
disable(): Promise<void>;
278+
launchSurvey(): Promise<void>;
279+
}
280+
281+
export const IPythonExtensionBanner = Symbol('IPythonExtensionBanner');
282+
export interface IPythonExtensionBanner {
283+
enabled: boolean;
284+
shownCount: Promise<number>;
285+
optionLabels: string[];
286+
showBanner(): Promise<void>;
287+
}
288+
export const BANNER_NAME_LS_SURVEY: string = 'LSSurveyBanner';
289+
export const BANNER_NAME_PROPOSE_LS: string = 'ProposeLS';

src/client/common/utils.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22
// tslint:disable: no-any one-line no-suspicious-comment prefer-template prefer-const no-unnecessary-callback-wrapper no-function-expression no-string-literal no-control-regex no-shadowed-variable
33

4+
import * as crypto from 'crypto';
45
import * as fs from 'fs';
56
import * as os from 'os';
67
import * as path from 'path';
@@ -111,3 +112,18 @@ export function arePathsSame(path1: string, path2: string) {
111112
return path1 === path2;
112113
}
113114
}
115+
116+
function getRandom(): number {
117+
let num: number = 0;
118+
119+
const buf: Buffer = crypto.randomBytes(2);
120+
num = (buf.readUInt8(0) << 8) + buf.readUInt8(1);
121+
122+
const maxValue: number = Math.pow(16, 4) - 1;
123+
return (num / maxValue);
124+
}
125+
126+
export function getRandomBetween(min: number = 0, max: number = 10): number {
127+
const randomVal: number = getRandom();
128+
return min + (randomVal * (max - min));
129+
}

src/client/debugger/banner.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import { inject, injectable } from 'inversify';
88
import { Disposable } from 'vscode';
99
import { IApplicationEnvironment, IApplicationShell, IDebugService } from '../common/application/types';
1010
import '../common/extensions';
11-
import { IBrowserService, IDisposableRegistry, ILogger, IPersistentStateFactory } from '../common/types';
11+
import { IBrowserService, IDisposableRegistry, IExperimentalDebuggerBanner,
12+
ILogger, IPersistentStateFactory } from '../common/types';
1213
import { IServiceContainer } from '../ioc/types';
1314
import { ExperimentalDebuggerType } from './Common/constants';
14-
import { IExperimentalDebuggerBanner } from './types';
1515

1616
export enum PersistentStateKeys {
1717
ShowBanner = 'ShowBanner',

src/client/debugger/serviceRegistry.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,19 @@ import { FileSystem } from '../common/platform/fileSystem';
99
import { PlatformService } from '../common/platform/platformService';
1010
import { IFileSystem, IPlatformService } from '../common/platform/types';
1111
import { CurrentProcess } from '../common/process/currentProcess';
12-
import { ICurrentProcess, ISocketServer } from '../common/types';
12+
import { BANNER_NAME_LS_SURVEY, BANNER_NAME_PROPOSE_LS, ICurrentProcess,
13+
IExperimentalDebuggerBanner, IPythonExtensionBanner, ISocketServer } from '../common/types';
1314
import { ServiceContainer } from '../ioc/container';
1415
import { ServiceManager } from '../ioc/serviceManager';
1516
import { IServiceContainer, IServiceManager } from '../ioc/types';
17+
import { LanguageServerSurveyBanner } from '../languageServices/languageServerSurveyBanner';
18+
import { ProposeLanguageServerBanner } from '../languageServices/proposeLanguageServerBanner';
1619
import { ExperimentalDebuggerBanner } from './banner';
1720
import { DebugStreamProvider } from './Common/debugStreamProvider';
1821
import { ProtocolLogger } from './Common/protocolLogger';
1922
import { ProtocolParser } from './Common/protocolParser';
2023
import { ProtocolMessageWriter } from './Common/protocolWriter';
21-
import { IDebugStreamProvider, IExperimentalDebuggerBanner, IProtocolLogger, IProtocolMessageWriter, IProtocolParser } from './types';
24+
import { IDebugStreamProvider, IProtocolLogger, IProtocolMessageWriter, IProtocolParser } from './types';
2225

2326
export function initializeIoc(): IServiceContainer {
2427
const cont = new Container();
@@ -42,4 +45,6 @@ function registerDebuggerTypes(serviceManager: IServiceManager) {
4245

4346
export function registerTypes(serviceManager: IServiceManager) {
4447
serviceManager.addSingleton<IExperimentalDebuggerBanner>(IExperimentalDebuggerBanner, ExperimentalDebuggerBanner);
48+
serviceManager.addSingleton<IPythonExtensionBanner>(IPythonExtensionBanner, LanguageServerSurveyBanner, BANNER_NAME_LS_SURVEY);
49+
serviceManager.addSingleton<IPythonExtensionBanner>(IPythonExtensionBanner, ProposeLanguageServerBanner, BANNER_NAME_PROPOSE_LS);
4550
}

src/client/debugger/types.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,3 @@ export interface IProtocolMessageWriter {
3838
}
3939

4040
export const IDebugConfigurationProvider = Symbol('DebugConfigurationProvider');
41-
42-
export const IExperimentalDebuggerBanner = Symbol('IExperimentalDebuggerBanner');
43-
export interface IExperimentalDebuggerBanner {
44-
enabled: boolean;
45-
initialize(): void;
46-
showBanner(): Promise<void>;
47-
shouldShowBanner(): Promise<boolean>;
48-
disable(): Promise<void>;
49-
launchSurvey(): Promise<void>;
50-
}

src/client/extension.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,15 @@ import { registerTypes as platformRegisterTypes } from './common/platform/servic
2626
import { registerTypes as processRegisterTypes } from './common/process/serviceRegistry';
2727
import { registerTypes as commonRegisterTypes } from './common/serviceRegistry';
2828
import { ITerminalHelper } from './common/terminal/types';
29-
import { GLOBAL_MEMENTO, IConfigurationService, IDisposableRegistry, IExtensionContext, ILogger, IMemento, IOutputChannel, IPersistentStateFactory, WORKSPACE_MEMENTO } from './common/types';
29+
import { GLOBAL_MEMENTO, IConfigurationService, IDisposableRegistry,
30+
IExperimentalDebuggerBanner, IExtensionContext, ILogger, IMemento, IOutputChannel,
31+
IPersistentStateFactory, WORKSPACE_MEMENTO } from './common/types';
3032
import { registerTypes as variableRegisterTypes } from './common/variables/serviceRegistry';
3133
import { AttachRequestArguments, LaunchRequestArguments } from './debugger/Common/Contracts';
3234
import { BaseConfigurationProvider } from './debugger/configProviders/baseProvider';
3335
import { registerTypes as debugConfigurationRegisterTypes } from './debugger/configProviders/serviceRegistry';
3436
import { registerTypes as debuggerRegisterTypes } from './debugger/serviceRegistry';
35-
import { IDebugConfigurationProvider, IExperimentalDebuggerBanner } from './debugger/types';
37+
import { IDebugConfigurationProvider } from './debugger/types';
3638
import { registerTypes as formattersRegisterTypes } from './formatters/serviceRegistry';
3739
import { IInterpreterSelector } from './interpreter/configuration/types';
3840
import { ICondaService, IInterpreterService, PythonInterpreter } from './interpreter/contracts';
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { inject, injectable } from 'inversify';
7+
import { IApplicationShell } from '../common/application/types';
8+
import '../common/extensions';
9+
import { IBrowserService, IPersistentStateFactory,
10+
IPythonExtensionBanner } from '../common/types';
11+
import { getRandomBetween } from '../common/utils';
12+
13+
// persistent state names, exported to make use of in testing
14+
export enum LSSurveyStateKeys {
15+
ShowBanner = 'ShowLSSurveyBanner',
16+
ShowAttemptCounter = 'LSSurveyShowAttempt',
17+
ShowAfterCompletionCount = 'LSSurveyShowCount'
18+
}
19+
20+
enum LSSurveyLabelIndex {
21+
Yes,
22+
No
23+
}
24+
25+
/*
26+
This class represents a popup that will ask our users for some feedback after
27+
a specific event occurs N times.
28+
*/
29+
@injectable()
30+
export class LanguageServerSurveyBanner implements IPythonExtensionBanner {
31+
private disabledInCurrentSession: boolean = false;
32+
private minCompletionsBeforeShow: number;
33+
private maxCompletionsBeforeShow: number;
34+
private isInitialized: boolean = false;
35+
private bannerMessage: string = 'Can you please take 2 minutes to tell us how the Experimental Debugger is working for you?';
36+
private bannerLabels: string [] = [ 'Yes, take survey now', 'No, thanks'];
37+
38+
constructor(
39+
@inject(IApplicationShell) private appShell: IApplicationShell,
40+
@inject(IPersistentStateFactory) private persistentState: IPersistentStateFactory,
41+
@inject(IBrowserService) private browserService: IBrowserService,
42+
showAfterMinimumEventsCount: number = 100,
43+
showBeforeMaximumEventsCount: number = 500)
44+
{
45+
this.minCompletionsBeforeShow = showAfterMinimumEventsCount;
46+
this.maxCompletionsBeforeShow = showBeforeMaximumEventsCount;
47+
this.initialize();
48+
}
49+
50+
public initialize(): void {
51+
if (this.isInitialized) {
52+
return;
53+
}
54+
this.isInitialized = true;
55+
56+
if (this.minCompletionsBeforeShow >= this.maxCompletionsBeforeShow) {
57+
this.disable().ignoreErrors();
58+
}
59+
}
60+
61+
public get optionLabels(): string[] {
62+
return this.bannerLabels;
63+
}
64+
65+
public get shownCount(): Promise<number> {
66+
return this.getPythonLSLaunchCounter();
67+
}
68+
69+
public get enabled(): boolean {
70+
return this.persistentState.createGlobalPersistentState<boolean>(LSSurveyStateKeys.ShowBanner, true).value;
71+
}
72+
73+
public async showBanner(): Promise<void> {
74+
if (!this.enabled || this.disabledInCurrentSession) {
75+
return;
76+
}
77+
78+
const launchCounter: number = await this.incrementPythonLanguageServiceLaunchCounter();
79+
const show = await this.shouldShowBanner(launchCounter);
80+
if (!show) {
81+
return;
82+
}
83+
84+
const response = await this.appShell.showInformationMessage(this.bannerMessage, ...this.bannerLabels);
85+
switch (response) {
86+
case this.bannerLabels[LSSurveyLabelIndex.Yes]:
87+
{
88+
await this.launchSurvey();
89+
await this.disable();
90+
break;
91+
}
92+
case this.bannerLabels[LSSurveyLabelIndex.No]: {
93+
await this.disable();
94+
break;
95+
}
96+
default: {
97+
// Disable for the current session.
98+
this.disabledInCurrentSession = true;
99+
}
100+
}
101+
}
102+
103+
public async shouldShowBanner(launchCounter?: number): Promise<boolean> {
104+
if (!this.enabled || this.disabledInCurrentSession) {
105+
return false;
106+
}
107+
108+
if (! launchCounter) {
109+
launchCounter = await this.getPythonLSLaunchCounter();
110+
}
111+
const threshold: number = await this.getPythonLSLaunchThresholdCounter();
112+
113+
return launchCounter >= threshold;
114+
}
115+
116+
public async disable(): Promise<void> {
117+
await this.persistentState.createGlobalPersistentState<boolean>(LSSurveyStateKeys.ShowBanner, false).updateValue(false);
118+
}
119+
120+
public async launchSurvey(): Promise<void> {
121+
const launchCounter = await this.getPythonLSLaunchCounter();
122+
this.browserService.launch(`https://www.research.net/r/LJZV9BZ?n=${launchCounter}`);
123+
}
124+
125+
private async incrementPythonLanguageServiceLaunchCounter(): Promise<number> {
126+
const state = this.persistentState.createGlobalPersistentState<number>(LSSurveyStateKeys.ShowAttemptCounter, 0);
127+
await state.updateValue(state.value + 1);
128+
return state.value;
129+
}
130+
131+
private async getPythonLSLaunchCounter(): Promise<number> {
132+
const state = this.persistentState.createGlobalPersistentState<number>(LSSurveyStateKeys.ShowAttemptCounter, 0);
133+
return state.value;
134+
}
135+
136+
private async getPythonLSLaunchThresholdCounter(): Promise<number> {
137+
const state = this.persistentState.createGlobalPersistentState<number | undefined>(LSSurveyStateKeys.ShowAfterCompletionCount, undefined);
138+
if (state.value === undefined) {
139+
await state.updateValue(getRandomBetween(this.minCompletionsBeforeShow, this.maxCompletionsBeforeShow));
140+
}
141+
return state.value!;
142+
}
143+
}

0 commit comments

Comments
 (0)