goLanguageServer: add logic to prompt users to opt back into gopls
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 <rstambler@golang.org>
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/src/goLanguageServer.ts b/src/goLanguageServer.ts
index 6a62d9e..50d24fa 100644
--- a/src/goLanguageServer.ts
+++ b/src/goLanguageServer.ts
@@ -20,14 +20,12 @@
CompletionItemKind,
ConfigurationParams,
ConfigurationRequest,
- DocumentSelector,
ErrorAction,
HandleDiagnosticsSignature,
InitializeError,
Message,
ProvideCodeLensesSignature,
ProvideCompletionItemsSignature,
- ProvideDocumentLinksSignature,
ResponseError,
RevealOutputChannelOn
} from 'vscode-languageclient';
@@ -56,7 +54,7 @@
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,
@@ -130,15 +128,17 @@
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)) {
@@ -167,14 +167,13 @@
// 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;
@@ -190,11 +189,40 @@
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();
@@ -204,6 +232,76 @@
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.
@@ -987,6 +1085,9 @@
if (cfg.version !== '') {
return cfg.version;
}
+ if (cfg.path === '') {
+ return null;
+ }
const execFile = util.promisify(cp.execFile);
let output: any;
try {
@@ -1222,8 +1323,29 @@
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 {};
}
@@ -1260,18 +1382,6 @@
}
}
-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,
@@ -1332,7 +1442,7 @@
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');
diff --git a/src/goModules.ts b/src/goModules.ts
index 902e7af..8ae79c8 100644
--- a/src/goModules.ts
+++ b/src/goModules.ts
@@ -73,16 +73,9 @@
);
}
- 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;
diff --git a/test/gopls/survey.test.ts b/test/gopls/survey.test.ts
index 2f04128..3e667db 100644
--- a/test/gopls/survey.test.ts
+++ b/test/gopls/survey.test.ts
@@ -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', () => {
@@ -82,3 +83,37 @@
});
});
});
+
+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);
+
+ });
+ });
+});