Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/1 Enhancements/2127.md
Original file line number Diff line number Diff line change
@@ -0,0 +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.
21 changes: 18 additions & 3 deletions src/client/activation/languageServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@

import { inject, injectable } from 'inversify';
import * as path from 'path';
import { OutputChannel, Uri } from 'vscode';
import { Disposable, LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-languageclient';
import { CancellationToken, CompletionContext, OutputChannel, Position,
TextDocument, Uri } from 'vscode';
import { Disposable, LanguageClient, LanguageClientOptions,
ProvideCompletionItemsSignature, ServerOptions } from 'vscode-languageclient';
import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types';
import { PythonSettings } from '../common/configSettings';
import { isTestExecution, STANDARD_OUTPUT_CHANNEL } from '../common/constants';
import { createDeferred, Deferred } from '../common/helpers';
import { IFileSystem, IPlatformService } from '../common/platform/types';
import { StopWatch } from '../common/stopWatch';
import { IConfigurationService, IExtensionContext, ILogger, IOutputChannel, IPythonSettings } from '../common/types';
import { BANNER_NAME_LS_SURVEY, IConfigurationService, IExtensionContext, ILogger,
IOutputChannel, IPythonExtensionBanner, IPythonSettings } from '../common/types';
import { IServiceContainer } from '../ioc/types';
import {
PYTHON_LANGUAGE_SERVER_DOWNLOADED,
Expand Down Expand Up @@ -51,6 +54,7 @@ export class LanguageServerExtensionActivator implements IExtensionActivator {
private excludedFiles: string[] = [];
private typeshedPaths: string[] = [];
private loadExtensionArgs: {} | undefined;
private surveyBanner: IPythonExtensionBanner;
// tslint:disable-next-line:no-unused-variable
private progressReporting: ProgressReporting | undefined;

Expand Down Expand Up @@ -81,6 +85,8 @@ export class LanguageServerExtensionActivator implements IExtensionActivator {
}
));

this.surveyBanner = services.get<IPythonExtensionBanner>(IPythonExtensionBanner, BANNER_NAME_LS_SURVEY);

(this.configuration.getSettings() as PythonSettings).addListener('change', this.onSettingsChanged);
}

Expand Down Expand Up @@ -155,6 +161,7 @@ export class LanguageServerExtensionActivator implements IExtensionActivator {
if (this.loadExtensionArgs) {
this.languageClient!.sendRequest('python/loadExtension', this.loadExtensionArgs);
}

this.startupCompleted.resolve();
}

Expand Down Expand Up @@ -250,6 +257,14 @@ export class LanguageServerExtensionActivator implements IExtensionActivator {
testEnvironment: isTestExecution(),
analysisUpdates: true,
traceLogging
},
middleware: {
provideCompletionItem: (document: TextDocument, position: Position, context: CompletionContext, token: CancellationToken, next: ProvideCompletionItemsSignature) => {
if (this.surveyBanner) {
this.surveyBanner.showBanner().ignoreErrors();
}
return next(document, position, context, token);
}
}
};
}
Expand Down
20 changes: 20 additions & 0 deletions src/client/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,3 +267,23 @@ export const IBrowserService = Symbol('IBrowserService');
export interface IBrowserService {
launch(url: string): void;
}

export const IExperimentalDebuggerBanner = Symbol('IExperimentalDebuggerBanner');
export interface IExperimentalDebuggerBanner {
enabled: boolean;
initialize(): void;
showBanner(): Promise<void>;
shouldShowBanner(): Promise<boolean>;
disable(): Promise<void>;
launchSurvey(): Promise<void>;
}

export const IPythonExtensionBanner = Symbol('IPythonExtensionBanner');
export interface IPythonExtensionBanner {
enabled: boolean;
shownCount: Promise<number>;
optionLabels: string[];
showBanner(): Promise<void>;
}
export const BANNER_NAME_LS_SURVEY: string = 'LSSurveyBanner';
export const BANNER_NAME_PROPOSE_LS: string = 'ProposeLS';
16 changes: 16 additions & 0 deletions src/client/common/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';
// 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

import * as crypto from 'crypto';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
Expand Down Expand Up @@ -111,3 +112,18 @@ export function arePathsSame(path1: string, path2: string) {
return path1 === path2;
}
}

function getRandom(): number {
let num: number = 0;

const buf: Buffer = crypto.randomBytes(2);
num = (buf.readUInt8(0) << 8) + buf.readUInt8(1);

const maxValue: number = Math.pow(16, 4) - 1;
return (num / maxValue);
}

export function getRandomBetween(min: number = 0, max: number = 10): number {
const randomVal: number = getRandom();
return min + (randomVal * (max - min));
}
4 changes: 2 additions & 2 deletions src/client/debugger/banner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import { inject, injectable } from 'inversify';
import { Disposable } from 'vscode';
import { IApplicationEnvironment, IApplicationShell, IDebugService } from '../common/application/types';
import '../common/extensions';
import { IBrowserService, IDisposableRegistry, ILogger, IPersistentStateFactory } from '../common/types';
import { IBrowserService, IDisposableRegistry, IExperimentalDebuggerBanner,
ILogger, IPersistentStateFactory } from '../common/types';
import { IServiceContainer } from '../ioc/types';
import { ExperimentalDebuggerType } from './Common/constants';
import { IExperimentalDebuggerBanner } from './types';

export enum PersistentStateKeys {
ShowBanner = 'ShowBanner',
Expand Down
9 changes: 7 additions & 2 deletions src/client/debugger/serviceRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,19 @@ import { FileSystem } from '../common/platform/fileSystem';
import { PlatformService } from '../common/platform/platformService';
import { IFileSystem, IPlatformService } from '../common/platform/types';
import { CurrentProcess } from '../common/process/currentProcess';
import { ICurrentProcess, ISocketServer } from '../common/types';
import { BANNER_NAME_LS_SURVEY, BANNER_NAME_PROPOSE_LS, ICurrentProcess,
IExperimentalDebuggerBanner, IPythonExtensionBanner, ISocketServer } from '../common/types';
import { ServiceContainer } from '../ioc/container';
import { ServiceManager } from '../ioc/serviceManager';
import { IServiceContainer, IServiceManager } from '../ioc/types';
import { LanguageServerSurveyBanner } from '../languageServices/languageServerSurveyBanner';
import { ProposeLanguageServerBanner } from '../languageServices/proposeLanguageServerBanner';
import { ExperimentalDebuggerBanner } from './banner';
import { DebugStreamProvider } from './Common/debugStreamProvider';
import { ProtocolLogger } from './Common/protocolLogger';
import { ProtocolParser } from './Common/protocolParser';
import { ProtocolMessageWriter } from './Common/protocolWriter';
import { IDebugStreamProvider, IExperimentalDebuggerBanner, IProtocolLogger, IProtocolMessageWriter, IProtocolParser } from './types';
import { IDebugStreamProvider, IProtocolLogger, IProtocolMessageWriter, IProtocolParser } from './types';

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

export function registerTypes(serviceManager: IServiceManager) {
serviceManager.addSingleton<IExperimentalDebuggerBanner>(IExperimentalDebuggerBanner, ExperimentalDebuggerBanner);
serviceManager.addSingleton<IPythonExtensionBanner>(IPythonExtensionBanner, LanguageServerSurveyBanner, BANNER_NAME_LS_SURVEY);
serviceManager.addSingleton<IPythonExtensionBanner>(IPythonExtensionBanner, ProposeLanguageServerBanner, BANNER_NAME_PROPOSE_LS);
}
10 changes: 0 additions & 10 deletions src/client/debugger/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,3 @@ export interface IProtocolMessageWriter {
}

export const IDebugConfigurationProvider = Symbol('DebugConfigurationProvider');

export const IExperimentalDebuggerBanner = Symbol('IExperimentalDebuggerBanner');
export interface IExperimentalDebuggerBanner {
enabled: boolean;
initialize(): void;
showBanner(): Promise<void>;
shouldShowBanner(): Promise<boolean>;
disable(): Promise<void>;
launchSurvey(): Promise<void>;
}
6 changes: 4 additions & 2 deletions src/client/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ import { registerTypes as platformRegisterTypes } from './common/platform/servic
import { registerTypes as processRegisterTypes } from './common/process/serviceRegistry';
import { registerTypes as commonRegisterTypes } from './common/serviceRegistry';
import { ITerminalHelper } from './common/terminal/types';
import { GLOBAL_MEMENTO, IConfigurationService, IDisposableRegistry, IExtensionContext, ILogger, IMemento, IOutputChannel, IPersistentStateFactory, WORKSPACE_MEMENTO } from './common/types';
import { GLOBAL_MEMENTO, IConfigurationService, IDisposableRegistry,
IExperimentalDebuggerBanner, IExtensionContext, ILogger, IMemento, IOutputChannel,
IPersistentStateFactory, WORKSPACE_MEMENTO } from './common/types';
import { registerTypes as variableRegisterTypes } from './common/variables/serviceRegistry';
import { AttachRequestArguments, LaunchRequestArguments } from './debugger/Common/Contracts';
import { BaseConfigurationProvider } from './debugger/configProviders/baseProvider';
import { registerTypes as debugConfigurationRegisterTypes } from './debugger/configProviders/serviceRegistry';
import { registerTypes as debuggerRegisterTypes } from './debugger/serviceRegistry';
import { IDebugConfigurationProvider, IExperimentalDebuggerBanner } from './debugger/types';
import { IDebugConfigurationProvider } from './debugger/types';
import { registerTypes as formattersRegisterTypes } from './formatters/serviceRegistry';
import { IInterpreterSelector } from './interpreter/configuration/types';
import { ICondaService, IInterpreterService, PythonInterpreter } from './interpreter/contracts';
Expand Down
143 changes: 143 additions & 0 deletions src/client/languageServices/languageServerSurveyBanner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';

import { inject, injectable } from 'inversify';
import { IApplicationShell } from '../common/application/types';
import '../common/extensions';
import { IBrowserService, IPersistentStateFactory,
IPythonExtensionBanner } from '../common/types';
import { getRandomBetween } from '../common/utils';

// persistent state names, exported to make use of in testing
export enum LSSurveyStateKeys {
ShowBanner = 'ShowLSSurveyBanner',
ShowAttemptCounter = 'LSSurveyShowAttempt',
ShowAfterCompletionCount = 'LSSurveyShowCount'
}

enum LSSurveyLabelIndex {
Yes,
No
}

/*
This class represents a popup that will ask our users for some feedback after
a specific event occurs N times.
*/
@injectable()
export class LanguageServerSurveyBanner implements IPythonExtensionBanner {
private disabledInCurrentSession: boolean = false;
private minCompletionsBeforeShow: number;
private maxCompletionsBeforeShow: number;
private isInitialized: boolean = false;
private bannerMessage: string = 'Can you please take 2 minutes to tell us how the Experimental Debugger is working for you?';
private bannerLabels: string [] = [ 'Yes, take survey now', 'No, thanks'];

constructor(
@inject(IApplicationShell) private appShell: IApplicationShell,
@inject(IPersistentStateFactory) private persistentState: IPersistentStateFactory,
@inject(IBrowserService) private browserService: IBrowserService,
showAfterMinimumEventsCount: number = 100,
showBeforeMaximumEventsCount: number = 500)
{
this.minCompletionsBeforeShow = showAfterMinimumEventsCount;
this.maxCompletionsBeforeShow = showBeforeMaximumEventsCount;
this.initialize();
}

public initialize(): void {
if (this.isInitialized) {
return;
}
this.isInitialized = true;

if (this.minCompletionsBeforeShow >= this.maxCompletionsBeforeShow) {
this.disable().ignoreErrors();
}
}

public get optionLabels(): string[] {
return this.bannerLabels;
}

public get shownCount(): Promise<number> {
return this.getPythonLSLaunchCounter();
}

public get enabled(): boolean {
return this.persistentState.createGlobalPersistentState<boolean>(LSSurveyStateKeys.ShowBanner, true).value;
}

public async showBanner(): Promise<void> {
if (!this.enabled || this.disabledInCurrentSession) {
return;
}

const launchCounter: number = await this.incrementPythonLanguageServiceLaunchCounter();
const show = await this.shouldShowBanner(launchCounter);
if (!show) {
return;
}

const response = await this.appShell.showInformationMessage(this.bannerMessage, ...this.bannerLabels);
switch (response) {
case this.bannerLabels[LSSurveyLabelIndex.Yes]:
{
await this.launchSurvey();
await this.disable();
break;
}
case this.bannerLabels[LSSurveyLabelIndex.No]: {
await this.disable();
break;
}
default: {
// Disable for the current session.
this.disabledInCurrentSession = true;
}
}
}

public async shouldShowBanner(launchCounter?: number): Promise<boolean> {
if (!this.enabled || this.disabledInCurrentSession) {
return false;
}

if (! launchCounter) {
launchCounter = await this.getPythonLSLaunchCounter();
}
const threshold: number = await this.getPythonLSLaunchThresholdCounter();

return launchCounter >= threshold;
}

public async disable(): Promise<void> {
await this.persistentState.createGlobalPersistentState<boolean>(LSSurveyStateKeys.ShowBanner, false).updateValue(false);
}

public async launchSurvey(): Promise<void> {
const launchCounter = await this.getPythonLSLaunchCounter();
this.browserService.launch(`https://www.research.net/r/LJZV9BZ?n=${launchCounter}`);
}

private async incrementPythonLanguageServiceLaunchCounter(): Promise<number> {
const state = this.persistentState.createGlobalPersistentState<number>(LSSurveyStateKeys.ShowAttemptCounter, 0);
await state.updateValue(state.value + 1);
return state.value;
}

private async getPythonLSLaunchCounter(): Promise<number> {
const state = this.persistentState.createGlobalPersistentState<number>(LSSurveyStateKeys.ShowAttemptCounter, 0);
return state.value;
}

private async getPythonLSLaunchThresholdCounter(): Promise<number> {
const state = this.persistentState.createGlobalPersistentState<number | undefined>(LSSurveyStateKeys.ShowAfterCompletionCount, undefined);
if (state.value === undefined) {
await state.updateValue(getRandomBetween(this.minCompletionsBeforeShow, this.maxCompletionsBeforeShow));
}
return state.value!;
}
}
Loading