Skip to content

Commit

Permalink
extension/src: add separator in command go.environment.choose
Browse files Browse the repository at this point in the history
Go: Choose Go Environment command will separate the options in three
parts:
- Fixed options: selecting go from file browser & clear selection.
- Locally discovered: go currently being selected or available in
$HOME/sdk.
- Downloadable: go version that is downloadable from golang.org/dl.

UI screenshot before:
https://github.com/user-attachments/assets/094fe705-886f-4074-a564-b8021208ebe2
UI screenshot after:
https://github.com/user-attachments/assets/6f5858f9-9c78-4610-9fa0-da30c78bb7c9

https://google.github.io/styleguide/tsguide.html#comments-documentation
- JSDoc for documentation, line comments for implementation.
- Method/Function comment should be written as if there is an implied
"This method ..." before it.

For #3697

Change-Id: I779bbd978103974b7c3e68a56820934312cdce04
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/654215
LUCI-TryBot-Result: Go LUCI <[email protected]>
Auto-Submit: Hongxiang Jiang <[email protected]>
kokoro-CI: kokoro <[email protected]>
Reviewed-by: Peter Weinberger <[email protected]>
  • Loading branch information
h9jiang authored and gopherbot committed Mar 3, 2025
1 parent 1579d6e commit 25da974
Showing 1 changed file with 102 additions and 71 deletions.
173 changes: 102 additions & 71 deletions extension/src/goEnvironmentStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,9 @@ function canChooseGoEnvironment() {

return { ok: true };
}

/**
* Present a command palette menu to the user to select their go binary
* Presents a command palette menu to the user to select their go binary.
*/
export const chooseGoEnvironment: CommandFactory = () => async () => {
if (!goEnvStatusbarItem) {
Expand All @@ -78,48 +79,63 @@ export const chooseGoEnvironment: CommandFactory = () => async () => {
return;
}

// fetch default go and uninstalled go versions
let defaultOption: GoEnvironmentOption | undefined;
let uninstalledOptions: GoEnvironmentOption[];
let goSDKOptions: GoEnvironmentOption[];
let options: vscode.QuickPickItem[] = [
// Option to choose go binary from file browser.
{
label: CHOOSE_FROM_FILE_BROWSER,
description: 'Select the go binary to use'
},
// Option to clear the existing selection.
{ label: CLEAR_SELECTION }
];
try {
[defaultOption, uninstalledOptions, goSDKOptions] = await Promise.all([
getDefaultGoOption(),
fetchDownloadableGoVersions(),
getSDKGoOptions()
]);
const seenDescriptions = new Set<string>();
const seenLabels = new Set<string>();
// addOption adds the option to the input array only if it is unique,
// based on its description and label.
const addOption = (options: GoEnvironmentOption[], option: GoEnvironmentOption | undefined) => {
if (option === undefined) {
return;
}
if (!seenDescriptions.has(option.description) && !seenLabels.has(option.label)) {
seenDescriptions.add(option.description);
seenLabels.add(option.label);
options.push(option);
}
};

const defaultOption = await Promise.resolve(getDefaultGoOption());
const goSDKOptions = await getSDKGoOptions();

const local: GoEnvironmentOption[] = [];
addOption(local, defaultOption);
goSDKOptions.forEach((option) => addOption(local, option));

if (local.length > 0) {
options.push({ kind: vscode.QuickPickItemKind.Separator, label: 'Locally discovered' });
options.push(...local);
}

const downloadableOptions = await getDownloadableGoVersions();
const downloadable: GoEnvironmentOption[] = [];
downloadableOptions.forEach((option) => addOption(downloadable, option));

if (downloadable.length > 0) {
options.push({ kind: vscode.QuickPickItemKind.Separator, label: 'Downloadable' });
options.push(...downloadable);
}
} catch (e) {
vscode.window.showErrorMessage((e as Error).message);
return;
}

// create quick pick items
const defaultQuickPick = defaultOption ? [defaultOption] : [];

// dedup options by eliminating duplicate paths (description)
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, ...goSDKOptions, ...uninstalledOptions].reduce(
(opts, nextOption) => {
if (opts.find((op) => op.description === nextOption.description || op.label === nextOption.label)) {
return opts;
}
return [...opts, nextOption];
},
[] as vscode.QuickPickItem[]
);

// get user's selection, return if none was made
// Get user's selection, return if none was made.
const selection = await vscode.window.showQuickPick<vscode.QuickPickItem>(options);
if (!selection) {
return;
}

// update currently selected go
// Update currently selected go.
try {
await setSelectedGo(selection);
} catch (e) {
Expand All @@ -128,17 +144,18 @@ export const chooseGoEnvironment: CommandFactory = () => async () => {
};

/**
* update the selected go path and label in the workspace state
* Updates the selected go path and label in the workspace state.
* @returns true if set successfully, false otherwise.
*/
export async function setSelectedGo(goOption: vscode.QuickPickItem, promptReload = true): Promise<boolean> {
if (!goOption) {
return false;
}

// if the selected go version is not installed, install it
// If the selected go version is not installed, install it.
if (goOption instanceof GoEnvironmentOption) {
const o = goOption.available ? (goOption as GoEnvironmentOption) : await downloadGo(goOption);
// check that the given binary is not already at the beginning of the PATH
// Check that the given binary is not already at the beginning of the PATH.
const go = await getGoVersion();
if (!!go && (go.binaryPath === o.binpath || 'Go ' + go.format() === o.label)) {
return false;
Expand Down Expand Up @@ -183,7 +200,8 @@ export async function setSelectedGo(goOption: vscode.QuickPickItem, promptReload
}
}
}
// prompt the user to reload the window.
// Show modal dialog to the user to reload the window, this require user's
// immediate attention.
// promptReload defaults to true and should only be false for tests.
if (promptReload) {
const choice = await vscode.window.showWarningMessage(
Expand All @@ -203,7 +221,9 @@ export async function setSelectedGo(goOption: vscode.QuickPickItem, promptReload
return true;
}

// downloadGo downloads the specified go version available in dl.golang.org.
/**
* Downloads the specified go version available in dl.golang.org.
*/
async function downloadGo(goOption: GoEnvironmentOption): Promise<GoEnvironmentOption> {
if (goOption.available) {
return Promise.resolve(goOption);
Expand Down Expand Up @@ -268,7 +288,9 @@ async function downloadGo(goOption: GoEnvironmentOption): Promise<GoEnvironmentO
);
}

// PATH value cached before addGoRuntimeBaseToPath modified.
/**
* PATH value cached before addGoRuntimeBaseToPath modified.
*/
let defaultPathEnv = '';

function pathEnvVarName(): string | undefined {
Expand All @@ -281,9 +303,11 @@ function pathEnvVarName(): string | undefined {
}
}

// addGoRuntimeBaseToPATH adds the given path to the front of the PATH environment variable.
// It removes duplicates.
// TODO: can we avoid changing PATH but utilize toolExecutionEnv?
/**
* addGoRuntimeBaseToPATH adds the given path to the front of the PATH
* environment variable. It removes duplicates.
* TODO: can we avoid changing PATH but utilize toolExecutionEnv?
*/
export function addGoRuntimeBaseToPATH(newGoRuntimeBase: string) {
if (!newGoRuntimeBase) {
return;
Expand All @@ -305,7 +329,7 @@ export function addGoRuntimeBaseToPATH(newGoRuntimeBase: string) {

outputChannel.debug(`addGoRuntimeBase(${newGoRuntimeBase}) when PATH=${defaultPathEnv}`);

// calling this multiple times will override the previous value.
// Calling this multiple times will override the previous value.
// environmentVariableCollection.clear();
if (process.platform !== 'darwin') {
environmentVariableCollection?.prepend(pathEnvVar, newGoRuntimeBase + path.delimiter);
Expand Down Expand Up @@ -339,12 +363,13 @@ export function addGoRuntimeBaseToPATH(newGoRuntimeBase: string) {
pathVars.unshift(newGoRuntimeBase);
process.env[pathEnvVar] = pathVars.join(path.delimiter);
}

// Clear terminal PATH environment modification previously installed
// using addGoRuntimeBaseToPATH.
// In particular, changes to vscode.EnvironmentVariableCollection persist across
// vscode sessions, so when we decide not to mutate PATH, we need to clear
// the preexisting changes.
/**
* Clears terminal PATH environment modification previously installed using
* addGoRuntimeBaseToPATH.
* In particular, changes to vscode.EnvironmentVariableCollection persist across
* vscode sessions, so when we decide not to mutate PATH, we need to clear the
* preexisting changes.
*/
export function clearGoRuntimeBaseFromPATH() {
if (terminalCreationListener) {
const l = terminalCreationListener;
Expand All @@ -366,12 +391,13 @@ function isTerminalOptions(
}

/**
* update the PATH variable in the given terminal to default to the currently selected Go
* Updates the PATH variable in the given terminal to default to the currently
* selected Go.
*/
export async function updateIntegratedTerminal(terminal: vscode.Terminal): Promise<void> {
if (
!terminal ||
// don't interfere if this terminal was created to run a Go task (goTaskProvider.ts).
// Don't interfere if this terminal was created to run a Go task (goTaskProvider.ts).
// Go task uses ProcessExecution which results in the terminal having `go` or `go.exe`
// as its shellPath.
(isTerminalOptions(terminal.creationOptions) &&
Expand All @@ -385,7 +411,7 @@ export async function updateIntegratedTerminal(terminal: vscode.Terminal): Promi
return;
}

// append the goroot to the beginning of the PATH so it takes precedence
// Append the goroot to the beginning of the PATH so it takes precedence.
// TODO: add support for more terminal names
if (vscode.env.shell.search(/(powershell|pwsh)$/i) !== -1) {
terminal.sendText(`$env:Path="${gorootBin};$env:Path"`, true);
Expand All @@ -400,14 +426,14 @@ export async function updateIntegratedTerminal(terminal: vscode.Terminal): Promi
}

/**
* retreive the current selected Go from the workspace state
* Retreives the current selected Go from the workspace state.
*/
export function getSelectedGo(): GoEnvironmentOption {
return getFromWorkspaceState('selectedGo');
}

/**
* return reference to the statusbar item
* @returns reference to the statusbar item.
*/
export function getGoEnvironmentStatusbarItem(): vscode.StatusBarItem {
return goEnvStatusbarItem;
Expand All @@ -427,44 +453,49 @@ export function formatGoVersion(version?: GoVersion): string {
}
}

/**
* @returns go versions available in `$HOME/sdk`.
*/
async function getSDKGoOptions(): Promise<GoEnvironmentOption[]> {
// get list of Go versions
// Get list of Go versions.
const sdkPath = path.join(os.homedir(), 'sdk');

if (!(await dirExists(sdkPath))) {
return [];
}
const readdir = promisify(fs.readdir);
const subdirs = await readdir(sdkPath);
// the dir happens to be the version, which will be used as the label
// the path is assembled and used as the description
// The dir happens to be the version, which will be used as the label.
// The path is assembled and used as the description.
return subdirs.map(
(dir: string) =>
new GoEnvironmentOption(path.join(sdkPath, dir, 'bin', correctBinname('go')), dir.replace('go', 'Go '))
);
}

export async function getDefaultGoOption(): Promise<GoEnvironmentOption | undefined> {
// make goroot default to go.goroot
// Make goroot default to "go.goroot" in vscode-go settings.
const goroot = getCurrentGoRoot();
if (!goroot) {
return undefined;
}

// set Go version and command
// Set Go version and command.
const version = await getGoVersion();
return new GoEnvironmentOption(path.join(goroot, 'bin', correctBinname('go')), formatGoVersion(version));
}

/**
* make a web request to get versions of Go
* Makes a web request to get versions of Go.
*/
interface GoVersionWebResult {
version: string;
stable: boolean;
}

async function fetchDownloadableGoVersions(): Promise<GoEnvironmentOption[]> {
/**
* @returns downloadable go versions from `golang.org/dl`.
*/
async function getDownloadableGoVersions(): Promise<GoEnvironmentOption[]> {
// TODO: use `go list -m --versions -json go` when go1.20+ is the minimum supported version.
// fetch information about what Go versions are available to install
let webResults;
Expand All @@ -478,13 +509,13 @@ async function fetchDownloadableGoVersions(): Promise<GoEnvironmentOption[]> {
if (!webResults) {
return [];
}
// turn the web result into GoEnvironmentOption model
return webResults.reduce((opts, result: GoVersionWebResult) => {
// Turn the web result into GoEnvironmentOption model.
return webResults.reduce((opts: GoEnvironmentOption[], result: GoVersionWebResult) => {
// TODO: allow downloading from different sites
const dlPath = `golang.org/dl/${result.version}`;
const label = result.version.replace('go', 'Go ');
return [...opts, new GoEnvironmentOption(dlPath, label, false)];
}, [] as GoEnvironmentOption[]);
}, []);
}

export const latestGoVersionKey = 'latestGoVersions';
Expand All @@ -501,10 +532,10 @@ export async function getLatestGoVersions(): Promise<GoEnvironmentOption[]> {
if (cachedResults && now - cachedResults.timestamp < timeout) {
results = cachedResults.goVersions;
} else {
// fetch the latest supported Go versions
// Fetch the latest supported Go versions.
try {
// fetch the latest Go versions and cache the results
results = await fetchDownloadableGoVersions();
// Fetch the latest Go versions and cache the results.
results = await getDownloadableGoVersions();
await updateGlobalState(latestGoVersionKey, {
timestamp: now,
goVersions: results
Expand Down Expand Up @@ -535,20 +566,20 @@ export async function offerToInstallLatestGoVersion(ctx: Pick<vscode.ExtensionCo

let options = await getLatestGoVersions();

// filter out Go versions the user has already dismissed
// Filter out Go versions the user has already dismissed.
let dismissedOptions: GoEnvironmentOption[];
dismissedOptions = await getFromGlobalState(dismissedGoVersionUpdatesKey);
if (dismissedOptions) {
options = options.filter((version) => !dismissedOptions.find((x) => x.label === version.label));
}

// compare to current go version.
// Compare to current go version.
const currentVersion = await getGoVersion();
if (currentVersion) {
options = options.filter((version) => currentVersion.lt(version.label));
}

// notify user that there is a newer version of Go available
// Notify user that there is a newer version of Go available.
if (options.length > 0) {
const versionsText = options.map((x) => x.label).join(', ');
const statusBarItem = addGoStatus(STATUS_BAR_ITEM_NAME);
Expand Down Expand Up @@ -576,7 +607,7 @@ export async function offerToInstallLatestGoVersion(ctx: Pick<vscode.ExtensionCo
const neverAgain = {
title: "Don't Show Again",
async command() {
// mark these versions as seen
// Mark these versions as seen.
dismissedOptions = await getFromGlobalState(dismissedGoVersionUpdatesKey);
if (!dismissedOptions) {
dismissedOptions = [];
Expand Down

0 comments on commit 25da974

Please sign in to comment.