src/goEnvironmentStatus.ts: allow to choose go from file browser

This adds an additional QuickPickItem to the go environment select quick pick menu
that opens a file browser when selected. The selected file should be an executable.

Fixes golang/vscode-go#490

Change-Id: I63b940af15e5b0cfed20f3c88146597e16cba45c
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/260003
Trust: Hyang-Ah Hana Kim <hyangah@gmail.com>
Trust: Suzy Mueller <suzmue@golang.org>
Run-TryBot: Hyang-Ah Hana Kim <hyangah@gmail.com>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Suzy Mueller <suzmue@golang.org>
diff --git a/src/goEnvironmentStatus.ts b/src/goEnvironmentStatus.ts
index 126fce7..e7242ba 100644
--- a/src/goEnvironmentStatus.ts
+++ b/src/goEnvironmentStatus.ts
@@ -18,7 +18,7 @@
 import { addGoStatus, goEnvStatusbarItem, outputChannel, removeGoStatus } from './goStatus';
 import { getFromGlobalState, getFromWorkspaceState, updateGlobalState, updateWorkspaceState } from './stateUtils';
 import { getBinPath, getGoConfig, getGoVersion, getTempFilePath, GoVersion, rmdirRecursive } from './util';
-import { correctBinname, getBinPathFromEnvVar, getCurrentGoRoot, pathExists } from './utils/pathUtils';
+import { correctBinname, executableFileExists, getBinPathFromEnvVar, getCurrentGoRoot, pathExists } from './utils/pathUtils';
 
 export class GoEnvironmentOption {
 	public static fromQuickPickItem({ description, label }: vscode.QuickPickItem): GoEnvironmentOption {
@@ -42,6 +42,10 @@
 	environmentVariableCollection = env;
 }
 
+// QuickPickItem names for chooseGoEnvironment menu.
+const CLEAR_SELECTION = '$(clear-all) Clear selection';
+const CHOOSE_FROM_FILE_BROWSER = '$(folder) Choose from file browser';
+
 /**
  * Present a command palette menu to the user to select their go binary
  */
@@ -77,8 +81,13 @@
 	const goSDKQuickPicks = goSDKOptions.map((op) => op.toQuickPickItem());
 
 	// dedup options by eliminating duplicate paths (description)
-	const clearOption: vscode.QuickPickItem = { label: 'Clear selection' };
-	const options = [clearOption, defaultQuickPick, ...goSDKQuickPicks, ...uninstalledQuickPicks]
+	const clearOption: vscode.QuickPickItem = { label: CLEAR_SELECTION };
+	const filePickerOption: vscode.QuickPickItem = {
+		label: CHOOSE_FROM_FILE_BROWSER,
+		description: 'Select the go binary to use',
+	};
+	// TODO(hyangah): Add separators after clearOption if github.com/microsoft/vscode#74967 is resolved.
+	const options = [filePickerOption, clearOption, defaultQuickPick, ...goSDKQuickPicks, ...uninstalledQuickPicks]
 		.reduce((opts, nextOption) => {
 			if (opts.find((op) => op.description === nextOption.description || op.label === nextOption.label)) {
 				return opts;
@@ -115,12 +124,39 @@
 	if (goOption.binpath?.startsWith('go get')) {
 		// start a loading indicator
 		await downloadGo(goOption);
-	} else if (goOption.label === 'Clear selection') {
+	} else if (goOption.label === CLEAR_SELECTION) {
 		if (!getSelectedGo()) {
 			return false;  // do nothing.
 		}
 		await updateWorkspaceState('selectedGo', undefined);
-		// TODO: goEnvStatusbarItem?
+	} else if (goOption.label === CHOOSE_FROM_FILE_BROWSER) {
+		const currentGOROOT = getCurrentGoRoot();
+		const defaultUri = currentGOROOT ? vscode.Uri.file(path.join(currentGOROOT, 'bin')) : undefined;
+
+		const newGoUris = await vscode.window.showOpenDialog({
+			canSelectFiles: true,
+			canSelectFolders: false,
+			canSelectMany: false,
+			defaultUri,
+		});
+		if (!newGoUris || newGoUris.length !== 1) {
+			return false;
+		}
+		const newGoUri = newGoUris[0];
+
+		if (defaultUri === newGoUri) {
+			return false;
+		}
+		if (!executableFileExists(newGoUri.path)) {
+			vscode.window.showErrorMessage(`${newGoUri.path} is not an executable`);
+			return false;
+		}
+		const newGo = await getGoVersion(newGoUri.path);
+		if (!newGo) {
+			vscode.window.showErrorMessage(`failed to get "${newGoUri.path} version", invalid Go binary`);
+			return false;
+		}
+		await updateWorkspaceState('selectedGo', new GoEnvironmentOption(newGo.binaryPath, formatGoVersion(newGo)));
 	} else {
 		// check that the given binary is not already at the beginning of the PATH
 		const go = await getGoVersion();
diff --git a/src/util.ts b/src/util.ts
index 279092c..083fcf9 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -326,12 +326,12 @@
  * Returns undefined if go version can't be determined because
  * go is not available or `go version` fails.
  */
-export async function getGoVersion(): Promise<GoVersion | undefined> {
+export async function getGoVersion(goBinPath?: string): Promise<GoVersion | undefined> {
 	// TODO(hyangah): limit the number of concurrent getGoVersion call.
 	// When the extension starts, at least 4 concurrent calls race
 	// and end up calling `go version`.
 
-	const goRuntimePath = getBinPath('go');
+	const goRuntimePath = goBinPath ?? getBinPath('go');
 
 	const warn = (msg: string) => {
 		outputChannel.appendLine(msg);
@@ -348,6 +348,7 @@
 		}
 		warn(`cached Go version (${JSON.stringify(cachedGoVersion)}) is invalid, recomputing`);
 	}
+	let goVersion: GoVersion;
 	try {
 		const env = toolExecutionEnvironment();
 		const docUri = vscode.window.activeTextEditor?.document.uri;
@@ -358,16 +359,19 @@
 			warn(`failed to run "${goRuntimePath} version": stdout: ${stdout}, stderr: ${stderr}`);
 			return;
 		}
-		cachedGoBinPath = goRuntimePath;
-		cachedGoVersion = new GoVersion(goRuntimePath, stdout);
-		if (!cachedGoVersion.isValid()) {
-			warn(`unable to determine version from the output of "${goRuntimePath} version": "${stdout}"`);
-		}
+		goVersion = new GoVersion(goRuntimePath, stdout);
 	} catch (err) {
 		warn(`failed to run "${goRuntimePath} version": ${err}`);
 		return;
 	}
-	return cachedGoVersion;
+	if (!goBinPath) {  // if getGoVersion was called with a given goBinPath, don't cache the result.
+		cachedGoBinPath = goRuntimePath;
+		cachedGoVersion = goVersion;
+		if (!cachedGoVersion.isValid()) {
+			warn(`unable to determine version from the output of "${goRuntimePath} version": "${goVersion.svString}"`);
+		}
+	}
+	return goVersion;
 }
 
 /**
diff --git a/src/utils/pathUtils.ts b/src/utils/pathUtils.ts
index f133df8..0b93650 100644
--- a/src/utils/pathUtils.ts
+++ b/src/utils/pathUtils.ts
@@ -132,7 +132,7 @@
 	return toolName;
 }
 
-function executableFileExists(filePath: string): boolean {
+export function executableFileExists(filePath: string): boolean {
 	let exists = true;
 	try {
 		exists = fs.statSync(filePath).isFile();