src/goLanguageServer.ts: update gopls when gopls is turned on by default

When the gopls configuration turns on by default, the user may already
have an old gopls binary installed. If this is the case we want to be sure
to update it before enabling the language server.

Updates golang/vscode-go#938

Change-Id: I06a738b7a3bc5e06191269178ac3a2775d93eb16
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/274250
Trust: Suzy Mueller <suzmue@golang.org>
Trust: Hyang-Ah Hana Kim <hyangah@gmail.com>
Run-TryBot: Suzy Mueller <suzmue@golang.org>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/src/goInstallTools.ts b/src/goInstallTools.ts
index e395ec6..d34b4ff 100644
--- a/src/goInstallTools.ts
+++ b/src/goInstallTools.ts
@@ -94,10 +94,14 @@
  * @param missing array of tool names and optionally, their versions to be installed.
  *                If a tool's version is not specified, it will install the latest.
  * @param goVersion version of Go that affects how to install the tool. (e.g. modules vs legacy GOPATH mode)
+ * @returns a list of tools that failed to install.
  */
-export async function installTools(missing: ToolAtVersion[], goVersion: GoVersion): Promise<void> {
+export async function installTools(
+	missing: ToolAtVersion[],
+	goVersion: GoVersion
+): Promise<{ tool: ToolAtVersion, reason: string }[]> {
 	if (!missing) {
-		return;
+		return [];
 	}
 
 	outputChannel.show();
@@ -173,6 +177,7 @@
 			outputChannel.appendLine(`${failure.tool.name}: ${failure.reason} `);
 		}
 	}
+	return failures;
 }
 
 export async function installTool(
diff --git a/src/goLanguageServer.ts b/src/goLanguageServer.ts
index b0e82f4..d0bef14 100644
--- a/src/goLanguageServer.ts
+++ b/src/goLanguageServer.ts
@@ -41,7 +41,7 @@
 import { GoHoverProvider } from './goExtraInfo';
 import { GoDocumentFormattingEditProvider } from './goFormat';
 import { GoImplementationProvider } from './goImplementations';
-import { promptForMissingTool, promptForUpdatingTool } from './goInstallTools';
+import { installTools, promptForMissingTool, promptForUpdatingTool } from './goInstallTools';
 import { parseLiveFile } from './goLiveErrors';
 import { restartLanguageServer } from './goMain';
 import { GO_MODE } from './goMode';
@@ -52,10 +52,17 @@
 import { outputChannel, updateLanguageServerIconGoStatusBar } from './goStatus';
 import { GoCompletionItemProvider } from './goSuggest';
 import { GoWorkspaceSymbolProvider } from './goSymbol';
-import { getTool, Tool } from './goTools';
+import { getTool, Tool, ToolAtVersion } from './goTools';
 import { GoTypeDefinitionProvider } from './goTypeDefinition';
 import { getFromGlobalState, updateGlobalState } from './stateUtils';
-import { getBinPath, getCurrentGoPath, getGoConfig, getGoplsConfig, getWorkspaceFolderPath } from './util';
+import {
+	getBinPath,
+	getCurrentGoPath,
+	getGoConfig,
+	getGoplsConfig,
+	getGoVersion,
+	getWorkspaceFolderPath
+} from './util';
 import { getToolFromToolPath } from './utils/pathUtils';
 
 export interface LanguageServerConfig {
@@ -107,14 +114,27 @@
 	}
 	languageServerStartInProgress = true;
 
-	const cfg = buildLanguageServerConfig(getGoConfig());
+	const goConfig = getGoConfig();
+	const cfg = buildLanguageServerConfig(goConfig);
 
 	// If the language server is gopls, we enable a few additional features.
 	// These include prompting for updates and surveys.
-	if (activation && cfg.serverName === 'gopls') {
+	if (cfg.serverName === 'gopls') {
 		const tool = getTool(cfg.serverName);
 		if (tool) {
-			scheduleGoplsSuggestions(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)) {
+				const updated = await forceUpdateGopls(tool, cfg);
+				if (updated) {
+					// restartLanguageServer will be called when the new version of gopls was installed.
+					return;
+				}
+			}
 		}
 	}
 
@@ -807,6 +827,50 @@
 	return semver.lt(usersVersionSemver, latestVersion) ? latestVersion : null;
 }
 
+/**
+ * forceUpdateGopls will make sure the user is using the latest version of `gopls`,
+ * when go.useLanguageServer is changed to true by default.
+ *
+ * @param tool	Object of type `Tool` for gopls tool.
+ * @param cfg	Object of type `Language Server Config` for the users language server
+ * 				configuration.
+ * @returns		true if the tool was updated
+ */
+async function forceUpdateGopls(
+	tool: Tool,
+	cfg: LanguageServerConfig,
+): Promise<boolean> {
+	const forceUpdatedGoplsKey = 'forceUpdateForGoplsOnDefault';
+	// forceUpdated is true when the process of updating has been succesfully completed.
+	const forceUpdated = getFromGlobalState(forceUpdatedGoplsKey, false);
+	// TODO: If we want to force update again, switch this to be a comparison for a newer version.
+	if (!!forceUpdated) {
+		return false;
+	}
+	// Update the state to the latest version to show the last version that was checked.
+	await updateGlobalState(forceUpdatedGoplsKey, tool.latestVersion);
+
+	const latestVersion = await shouldUpdateLanguageServer(tool, cfg);
+
+	if (!latestVersion) {
+		// The user is using a new enough version
+		return false;
+	}
+
+	const toolVersion = { ...tool, version: latestVersion }; // ToolWithVersion
+	const goVersion = await getGoVersion();
+	const failures = await installTools([toolVersion], goVersion);
+
+	// We successfully updated to the latest version.
+	if (failures.length === 0) {
+		return true;
+	}
+
+	// Failed to install the new version of gopls, warn the user.
+	vscode.window.showWarningMessage(`'gopls' is now enabled by default and you are using an old version. Please [update 'gopls'](https://github.com/golang/tools/blob/master/gopls/doc/user.md#installation) and restart the language server for the best experience.`);
+	return false;
+}
+
 // Copied from src/cmd/go/internal/modfetch.go.
 const pseudoVersionRE = /^v[0-9]+\.(0\.0-|\d+\.\d+-([^+]*\.)?0\.)\d{14}-[A-Za-z0-9]+(\+incompatible)?$/;
 
@@ -1448,7 +1512,7 @@
 
 export async function promptForLanguageServerDefaultChange(cfg: vscode.WorkspaceConfiguration) {
 	const useLanguageServer = cfg.inspect<boolean>('useLanguageServer');
-	if (useLanguageServer.globalValue !== undefined || useLanguageServer.workspaceValue !== undefined) {
+	if (!languageServerUsingDefault(cfg)) {
 		if (!cfg['useLanguageServer']) {  // ask users who explicitly disabled.
 			promptForLanguageServerOptOutSurvey();
 		}
@@ -1471,6 +1535,11 @@
 	updateGlobalState(promptedForLSDefaultChangeKey, true);
 }
 
+function languageServerUsingDefault(cfg: vscode.WorkspaceConfiguration): boolean {
+	const useLanguageServer = cfg.inspect<boolean>('useLanguageServer');
+	return useLanguageServer.globalValue === undefined && useLanguageServer.workspaceValue === undefined;
+}
+
 // Prompt users who disabled the language server and ask to file an issue.
 async function promptForLanguageServerOptOutSurvey() {
 	const promptedForLSOptOutSurveyKey = `promptedForLSOptOutSurvey`;
@@ -1513,4 +1582,5 @@
 	const version = vscode.extensions.getExtension(extensionId)?.packageJSON?.version;
 	const appName = vscode.env.appName;
 	return { version, appName };
+
 }
diff --git a/src/goStatus.ts b/src/goStatus.ts
index bcd4bbb..5e30ede 100644
--- a/src/goStatus.ts
+++ b/src/goStatus.ts
@@ -9,7 +9,7 @@
 import path = require('path');
 import vscode = require('vscode');
 import { formatGoVersion, GoEnvironmentOption, terminalCreationListener } from './goEnvironmentStatus';
-import { buildLanguageServerConfig, getLocalGoplsVersion, serverOutputChannel } from './goLanguageServer';
+import { buildLanguageServerConfig, getLocalGoplsVersion, languageServerIsRunning, serverOutputChannel } from './goLanguageServer';
 import { isGoFile } from './goMode';
 import { getModFolderPath, isModSupported } from './goModules';
 import { getGoConfig, getGoVersion } from './util';
@@ -48,7 +48,7 @@
 
 	// Get the gopls configuration
 	const cfg = buildLanguageServerConfig(getGoConfig());
-	if (cfg.serverName === 'gopls') {
+	if (languageServerIsRunning && cfg.serverName === 'gopls') {
 		const goplsVersion = await getLocalGoplsVersion(cfg);
 		options.push({label: `${languageServerIcon}Open 'gopls' trace`, description: `${goplsVersion}`});
 	}
@@ -102,7 +102,7 @@
 	// Assume if it is configured it is already running, since the
 	// icon will be updated on an attempt to start.
 	const cfg = buildLanguageServerConfig(getGoConfig());
-	updateLanguageServerIconGoStatusBar(true, cfg.serverName);
+	updateLanguageServerIconGoStatusBar(languageServerIsRunning, cfg.serverName);
 
 	showGoStatusBar();
 }