Skip to content

Commit

Permalink
goLanguageServer: add logic to prompt users to opt back into gopls
Browse files Browse the repository at this point in the history
Users who have explicitly disabled gopls may have done so a long time
ago when it wasn't yet stabilized. We should occasionally prompt them
to opt in now that it's on by default.

Set up a prompt that will show up until the user makes a selection. If
they opt out, direct them to a survey to share feedback.

This works per-workspace, so users will be re-prompted if they open a
different workspace with gopls disabled.

I used MessageItems in the vscode.window.showInformationMessage
function because sinon refused to stub any other signature.

Change-Id: Id46e88ab1e0a44740e777588288c27ddb4da93a6
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/289090
Trust: Rebecca Stambler <[email protected]>
Run-TryBot: Rebecca Stambler <[email protected]>
TryBot-Result: kokoro <[email protected]>
Reviewed-by: Hyang-Ah Hana Kim <[email protected]>
  • Loading branch information
stamblerre committed Feb 10, 2021
1 parent 48c4af1 commit 95dc50c
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 42 deletions.
172 changes: 141 additions & 31 deletions src/goLanguageServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,12 @@ import {
CompletionItemKind,
ConfigurationParams,
ConfigurationRequest,
DocumentSelector,
ErrorAction,
HandleDiagnosticsSignature,
InitializeError,
Message,
ProvideCodeLensesSignature,
ProvideCompletionItemsSignature,
ProvideDocumentLinksSignature,
ResponseError,
RevealOutputChannelOn
} from 'vscode-languageclient';
Expand Down Expand Up @@ -56,7 +54,7 @@ import { GoCompletionItemProvider } from './goSuggest';
import { GoWorkspaceSymbolProvider } from './goSymbol';
import { getTool, Tool } from './goTools';
import { GoTypeDefinitionProvider } from './goTypeDefinition';
import { getFromGlobalState, updateGlobalState } from './stateUtils';
import { getFromGlobalState, getFromWorkspaceState, getWorkspaceState, updateGlobalState, updateWorkspaceState } from './stateUtils';
import {
getBinPath,
getCheckForToolsUpdatesConfig,
Expand Down Expand Up @@ -130,15 +128,17 @@ export async function startLanguageServerWithFallback(ctx: vscode.ExtensionConte
const goConfig = getGoConfig();
const cfg = buildLanguageServerConfig(goConfig);

// We have some extra prompts for gopls users and for people who have opted
// out of gopls.
if (activation) {
scheduleGoplsSuggestions();
}

// If the language server is gopls, we enable a few additional features.
// These include prompting for updates and surveys.
if (cfg.serverName === 'gopls') {
const tool = getTool(cfg.serverName);
if (tool) {
if (activation) {
scheduleGoplsSuggestions(tool);
}

// If the language server is turned on because it is enabled by default,
// make sure that the user is using a new enough version.
if (cfg.enabled && languageServerUsingDefault(goConfig)) {
Expand Down Expand Up @@ -167,14 +167,13 @@ export async function startLanguageServerWithFallback(ctx: vscode.ExtensionConte
// suggestions. We check user's gopls versions once per day to prompt users to
// update to the latest version. We also check if we should prompt users to
// fill out the survey.
function scheduleGoplsSuggestions(tool: Tool) {
const update = async () => {
setTimeout(update, timeDay);

const cfg = buildLanguageServerConfig(getGoConfig());
if (!cfg.enabled) {
return;
}
function scheduleGoplsSuggestions() {
// Some helper functions.
const usingGopls = (cfg: LanguageServerConfig): boolean => {
return cfg.enabled && cfg.serverName === 'gopls';
};
const installGopls = async (cfg: LanguageServerConfig) => {
const tool = getTool('gopls');
const versionToUpdate = await shouldUpdateLanguageServer(tool, cfg);
if (!versionToUpdate) {
return;
Expand All @@ -190,11 +189,40 @@ function scheduleGoplsSuggestions(tool: Tool) {
promptForUpdatingTool(tool.name, versionToUpdate);
}
};
const update = async () => {
setTimeout(update, timeDay);

let cfg = buildLanguageServerConfig(getGoConfig());
if (!usingGopls(cfg)) {
// This shouldn't happen, but if the user has a non-gopls language
// server enabled, we shouldn't prompt them to change.
if (cfg.serverName !== '') {
return;
}
// Check if the configuration is set in the workspace.
const useLanguageServer = getGoConfig().inspect('useLanguageServer');
let workspace: boolean;
if (useLanguageServer.workspaceFolderValue === false || useLanguageServer.workspaceValue === false) {
workspace = true;
}
// Prompt the user to enable gopls and record what actions they took.
let optOutCfg = getGoplsOptOutConfig(workspace);
optOutCfg = await promptAboutGoplsOptOut(optOutCfg);
flushGoplsOptOutConfig(optOutCfg, workspace);
// Check if the language server has now been enabled, and if so,
// it will be installed below.
cfg = buildLanguageServerConfig(getGoConfig());
if (!cfg.enabled) {
return;
}
}
await installGopls(cfg);
};
const survey = async () => {
setTimeout(survey, timeDay);

const cfg = buildLanguageServerConfig(getGoConfig());
if (!cfg.enabled) {
if (!usingGopls(cfg)) {
return;
}
maybePromptForGoplsSurvey();
Expand All @@ -204,6 +232,76 @@ function scheduleGoplsSuggestions(tool: Tool) {
setTimeout(survey, 30 * timeMinute);
}

export interface GoplsOptOutConfig {
prompt?: boolean;
lastDatePrompted?: Date;
}

const goplsOptOutConfigKey = 'goplsOptOutConfig';

function getGoplsOptOutConfig(workspace: boolean): GoplsOptOutConfig {
return getStateConfig(goplsOptOutConfigKey, workspace) as GoplsOptOutConfig;
}

function flushGoplsOptOutConfig(cfg: GoplsOptOutConfig, workspace: boolean) {
if (workspace) {
updateWorkspaceState(goplsOptOutConfigKey, JSON.stringify(cfg));
}
updateGlobalState(goplsOptOutConfigKey, JSON.stringify(cfg));
}

export async function promptAboutGoplsOptOut(cfg: GoplsOptOutConfig): Promise<GoplsOptOutConfig> {
if (cfg.prompt === false) {
return cfg;
}
// Prompt the user ~once a month.
if (cfg.lastDatePrompted && daysBetween(new Date(), cfg.lastDatePrompted) < 30) {
return cfg;
}
cfg.lastDatePrompted = new Date();
const selected = await vscode.window.showInformationMessage(`We noticed that you have disabled the language server.
It has [stabilized](https://blog.golang.org/gopls-vscode-go) and is now enabled by default in this extension.
Would you like to enable it now?`, { title: 'Enable' }, { title: 'Not now' }, { title: 'Never' });
if (!selected) {
return cfg;
}
switch (selected.title) {
case 'Enable':
// Change the user's Go configuration to enable the language server.
// Remove the setting entirely, since it's on by default now.
const goConfig = getGoConfig();
await goConfig.update('useLanguageServer', undefined, vscode.ConfigurationTarget.Global);
if (goConfig.inspect('useLanguageServer').workspaceValue === false) {
await goConfig.update('useLanguageServer', undefined, vscode.ConfigurationTarget.Workspace);
}
if (goConfig.inspect('useLanguageServer').workspaceFolderValue === false) {
await goConfig.update('useLanguageServer', undefined, vscode.ConfigurationTarget.WorkspaceFolder);
}

cfg.prompt = false;
break;
case 'Not now':
cfg.prompt = true;
break;
case 'Never':
cfg.prompt = false;
await promptForGoplsOptOutSurvey();
break;
}
return cfg;
}

async function promptForGoplsOptOutSurvey() {
const selected = await vscode.window.showInformationMessage(`No problem. Would you be willing to tell us why you have opted out of the language server?`, { title: 'Yes' }, { title: 'No' });
switch (selected.title) {
case 'Yes':
await vscode.env.openExternal(vscode.Uri.parse(`https://forms.gle/hwC8CncV7Cyc2yBN6`));
break;
case 'No':
break;
}
}

async function startLanguageServer(ctx: vscode.ExtensionContext, config: LanguageServerConfig): Promise<boolean> {
// If the client has already been started, make sure to clear existing
// diagnostics and stop it.
Expand Down Expand Up @@ -987,6 +1085,9 @@ export const getLocalGoplsVersion = async (cfg: LanguageServerConfig) => {
if (cfg.version !== '') {
return cfg.version;
}
if (cfg.path === '') {
return null;
}
const execFile = util.promisify(cp.execFile);
let output: any;
try {
Expand Down Expand Up @@ -1222,8 +1323,29 @@ Would you be willing to fill out a quick survey about your experience with gopls

export const goplsSurveyConfig = 'goplsSurveyConfig';

function getSurveyConfig(surveyConfigKey = goplsSurveyConfig): SurveyConfig {
const saved = getFromGlobalState(surveyConfigKey);
function getSurveyConfig(): SurveyConfig {
return getStateConfig(goplsSurveyConfig) as SurveyConfig;
}

export function resetSurveyConfig() {
flushSurveyConfig(null);
}

function flushSurveyConfig(cfg: SurveyConfig) {
if (cfg) {
updateGlobalState(goplsSurveyConfig, JSON.stringify(cfg));
} else {
updateGlobalState(goplsSurveyConfig, null); // reset
}
}

function getStateConfig(globalStateKey: string, workspace?: boolean): any {
let saved: any;
if (workspace === true) {
saved = getFromWorkspaceState(globalStateKey);
} else {
saved = getFromGlobalState(globalStateKey);
}
if (saved === undefined) {
return {};
}
Expand Down Expand Up @@ -1260,18 +1382,6 @@ export async function showSurveyConfig() {
}
}

export function resetSurveyConfig() {
flushSurveyConfig(null);
}

function flushSurveyConfig(cfg: SurveyConfig) {
if (cfg) {
updateGlobalState(goplsSurveyConfig, JSON.stringify(cfg));
} else {
updateGlobalState(goplsSurveyConfig, null); // reset
}
}

// errorKind refers to the different possible kinds of gopls errors.
enum errorKind {
initializationFailure,
Expand Down Expand Up @@ -1332,7 +1442,7 @@ async function suggestGoplsIssueReport(msg: string, reason: errorKind, initializ
selected = await vscode.window.showInformationMessage(`The extension was unable to start the language server.
You may have an invalid value in your "go.languageServerFlags" setting.
It is currently set to [${languageServerFlags}]. Please correct the setting by navigating to Preferences -> Settings.`,
'Open settings', 'I need more help.');
'Open settings', 'I need more help.');
switch (selected) {
case 'Open settings':
await vscode.commands.executeCommand('workbench.action.openSettings', 'go.languageServerFlags');
Expand Down
13 changes: 3 additions & 10 deletions src/goModules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,9 @@ export async function getModFolderPath(fileuri: vscode.Uri, isDir?: boolean): Pr
);
}

if (goConfig['useLanguageServer'] === false) {
const promptMsg = 'For better performance using Go modules, you can try the experimental Go language server, gopls.';
promptToUpdateToolForModules('gopls', promptMsg, goConfig)
.then((choseToUpdate) => {
if (choseToUpdate || goConfig['formatTool'] !== 'goreturns') {
return;
}
const promptFormatToolMsg = `The goreturns tool does not support Go modules. Please update the "formatTool" setting to "goimports".`;
promptToUpdateToolForModules('switchFormatToolToGoimports', promptFormatToolMsg, goConfig);
});
if (goConfig['useLanguageServer'] === false && goConfig['formatTool'] !== 'goreturns') {
const promptFormatToolMsg = `The goreturns tool does not support Go modules. Please update the "formatTool" setting to "goimports".`;
promptToUpdateToolForModules('switchFormatToolToGoimports', promptFormatToolMsg, goConfig);
}
}
packagePathToGoModPathMap[pkgPath] = goModEnvResult;
Expand Down
37 changes: 36 additions & 1 deletion test/gopls/survey.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

import * as assert from 'assert';
import sinon = require('sinon');
import { shouldPromptForGoplsSurvey, SurveyConfig } from '../../src/goLanguageServer';
import vscode = require('vscode');
import { GoplsOptOutConfig, promptAboutGoplsOptOut, shouldPromptForGoplsSurvey, SurveyConfig } from '../../src/goLanguageServer';

suite('gopls survey tests', () => {
test('prompt for survey', () => {
Expand Down Expand Up @@ -82,3 +83,37 @@ suite('gopls survey tests', () => {
});
});
});

suite('gopls opt out', () => {
let sandbox: sinon.SinonSandbox;

setup(() => {
sandbox = sinon.createSandbox();
});

teardown(() => {
sandbox.restore();
});

const testCases: [GoplsOptOutConfig, string, number][] = [
// No saved config, different choices in the first dialog box.
[{}, 'Enable', 1],
[{}, 'Not now', 1],
[{}, 'Never', 2],
// // Saved config, doesn't matter what the user chooses.
[{ prompt: false, }, '', 0],
[{ prompt: false, lastDatePrompted: new Date() }, '', 0],
[{ prompt: true, }, '', 1],
[{ prompt: true, lastDatePrompted: new Date() }, '', 0],
];

testCases.map(async ([testConfig, choice, wantCount], i) => {
test(`opt out: ${i}`, async () => {
const stub = sandbox.stub(vscode.window, 'showInformationMessage').resolves({ title: choice });

await promptAboutGoplsOptOut(testConfig);
assert.strictEqual(stub.callCount, wantCount);

});
});
});

0 comments on commit 95dc50c

Please sign in to comment.