Skip to content

Commit 3f5f0cd

Browse files
committed
src/goExplorer.ts: add a go tree view to the explorer
Adds a tree view for Go to the explorer. Initially the view contains only select entries about the go env settings for the currently open workspace and file - GOPRIVATE, GOMOD, GOWORK, GOENV, and any value set with toolsEnvVars from the go workspace configuration. Snapshot: https://drive.google.com/file/d/1q0whHB5wSV0Q_pzMr9TM1-E7GxRE5zru/view?usp=sharing For #2049 Change-Id: I5c5f78913626f5f6d59de90368fff4bd5ffcb33e Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/388435 Trust: Jamal Carvalho <[email protected]> Run-TryBot: Jamal Carvalho <[email protected]> TryBot-Result: kokoro <[email protected]> Reviewed-by: Hyang-Ah Hana Kim <[email protected]>
1 parent 8ab268f commit 3f5f0cd

File tree

5 files changed

+282
-0
lines changed

5 files changed

+282
-0
lines changed

docs/commands.md

+8
Original file line numberDiff line numberDiff line change
@@ -242,3 +242,11 @@ Reset keys in workspace state to undefined.
242242
### `Go: Reset Global State`
243243

244244
Reset keys in global state to undefined.
245+
246+
### `Go Explorer: Refresh`
247+
248+
Refresh the Go explorer. Only available as a menu item in the explorer.
249+
250+
### `Go Explorer: Open File`
251+
252+
Open a file from the Go explorer. Only available as a menu item in the explorer.

package-lock.json

+11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+41
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"vscode-debugprotocol": "1.45.0",
6262
"vscode-languageclient": "7.0.0",
6363
"vscode-languageserver-protocol": "3.16.0",
64+
"vscode-uri": "3.0.3",
6465
"web-request": "1.0.7"
6566
},
6667
"devDependencies": {
@@ -487,6 +488,20 @@
487488
"command": "go.global.resetState",
488489
"title": "Go: Reset Global State",
489490
"description": "Reset keys in global state to undefined."
491+
},
492+
{
493+
"command": "go.explorer.refresh",
494+
"title": "Go Explorer: Refresh",
495+
"description": "Refresh the Go explorer. Only available as a menu item in the explorer.",
496+
"category": "Explorer",
497+
"icon": "$(refresh)"
498+
},
499+
{
500+
"command": "go.explorer.open",
501+
"title": "Go Explorer: Open File",
502+
"description": "Open a file from the Go explorer. Only available as a menu item in the explorer.",
503+
"category": "Explorer",
504+
"icon": "$(go-to-file)"
490505
}
491506
],
492507
"breakpoints": [
@@ -2554,6 +2569,14 @@
25542569
{
25552570
"command": "go.test.deleteProfile",
25562571
"when": "false"
2572+
},
2573+
{
2574+
"command": "go.explorer.refresh",
2575+
"when": "false"
2576+
},
2577+
{
2578+
"command": "go.explorer.open",
2579+
"when": "false"
25572580
}
25582581
],
25592582
"editor/context": [
@@ -2655,14 +2678,32 @@
26552678
"group": "profile"
26562679
}
26572680
],
2681+
"view/title": [
2682+
{
2683+
"command": "go.explorer.refresh",
2684+
"when": "view == go.explorer",
2685+
"group": "navigation"
2686+
}
2687+
],
26582688
"view/item/context": [
26592689
{
26602690
"command": "go.test.deleteProfile",
26612691
"when": "viewItem == go:test:file"
2692+
},
2693+
{
2694+
"command": "go.explorer.open",
2695+
"when": "view == go.explorer && viewItem == go:explorer:envitem:file",
2696+
"group": "inline"
26622697
}
26632698
]
26642699
},
26652700
"views": {
2701+
"explorer": [
2702+
{
2703+
"id": "go.explorer",
2704+
"name": "go"
2705+
}
2706+
],
26662707
"test": [
26672708
{
26682709
"id": "go.test.profile",

src/goExplorer.ts

+219
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
/*---------------------------------------------------------
2+
* Copyright 2022 The Go Authors. All rights reserved.
3+
* Licensed under the MIT License. See LICENSE in the project root for license information.
4+
*--------------------------------------------------------*/
5+
import vscode = require('vscode');
6+
import vscodeUri = require('vscode-uri');
7+
import cp = require('child_process');
8+
import util = require('util');
9+
import os = require('os');
10+
import path = require('path');
11+
import { getGoConfig } from './config';
12+
import { getBinPath } from './util';
13+
import { toolExecutionEnvironment } from './goEnv';
14+
15+
/**
16+
* GoExplorerProvider provides data for the Go tree view in the Explorer
17+
* Tree View Container.
18+
*/
19+
export class GoExplorerProvider implements vscode.TreeDataProvider<vscode.TreeItem> {
20+
private goEnvCache = new Cache((uri) => GoEnv.get(uri ? vscode.Uri.parse(uri) : undefined), 1000 * 60);
21+
private activeFolder?: vscode.WorkspaceFolder;
22+
private activeDocument?: vscode.TextDocument;
23+
24+
static setup(ctx: vscode.ExtensionContext) {
25+
const provider = new this();
26+
ctx.subscriptions.push(vscode.window.registerTreeDataProvider('go.explorer', provider));
27+
ctx.subscriptions.push(vscode.commands.registerCommand('go.explorer.refresh', () => provider.update(true)));
28+
ctx.subscriptions.push(
29+
vscode.commands.registerCommand('go.explorer.open', (item: EnvTreeItem) => provider.open(item))
30+
);
31+
return provider;
32+
}
33+
34+
private _onDidChangeTreeData = new vscode.EventEmitter<vscode.TreeItem | void>();
35+
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
36+
37+
constructor() {
38+
this.update();
39+
vscode.window.onDidChangeActiveTextEditor(() => this.update());
40+
vscode.workspace.onDidChangeWorkspaceFolders(() => this.update());
41+
vscode.workspace.onDidChangeConfiguration(() => this.update(true));
42+
vscode.workspace.onDidCloseTextDocument((doc) => {
43+
if (!this.activeFolder) {
44+
this.goEnvCache.delete(vscodeUri.Utils.dirname(doc.uri).toString());
45+
}
46+
});
47+
}
48+
49+
getTreeItem(element: vscode.TreeItem) {
50+
return element;
51+
}
52+
53+
getChildren(element?: vscode.TreeItem) {
54+
if (isEnvTree(element)) {
55+
return this.envTreeItems(element.workspace);
56+
}
57+
return [this.envTree()];
58+
}
59+
60+
private update(clearCache = false) {
61+
if (clearCache) {
62+
this.goEnvCache.clear();
63+
}
64+
const { activeTextEditor } = vscode.window;
65+
const { getWorkspaceFolder, workspaceFolders } = vscode.workspace;
66+
this.activeDocument = activeTextEditor?.document;
67+
this.activeFolder = activeTextEditor?.document
68+
? getWorkspaceFolder(activeTextEditor.document.uri) || workspaceFolders?.[0]
69+
: workspaceFolders?.[0];
70+
this._onDidChangeTreeData.fire();
71+
}
72+
73+
private async open(item: EnvTreeItem) {
74+
if (typeof item.file === 'undefined') return;
75+
const doc = await vscode.workspace.openTextDocument(item.file);
76+
await vscode.window.showTextDocument(doc);
77+
}
78+
79+
private envTree() {
80+
if (this.activeFolder) {
81+
const { name, uri } = this.activeFolder;
82+
return new EnvTree(name, uri);
83+
}
84+
if (this.activeDocument) {
85+
const { fileName, uri } = this.activeDocument;
86+
return new EnvTree(path.basename(fileName), vscodeUri.Utils.dirname(uri));
87+
}
88+
return new EnvTree();
89+
}
90+
91+
private async envTreeItems(uri?: vscode.Uri) {
92+
let env: Record<string, string>;
93+
try {
94+
env = await this.goEnvCache.get(uri?.toString());
95+
} catch (e) {
96+
vscode.window.showErrorMessage(`Failed to run "go env": ${e.message}`);
97+
return;
98+
}
99+
const items = [];
100+
for (const [k, v] of Object.entries(env)) {
101+
if (v !== '') {
102+
items.push(new EnvTreeItem(k, v));
103+
}
104+
}
105+
return items;
106+
}
107+
}
108+
109+
function isEnvTree(item?: vscode.TreeItem): item is EnvTree {
110+
return item?.contextValue === 'go:explorer:env';
111+
}
112+
113+
class EnvTree implements vscode.TreeItem {
114+
label = 'env';
115+
contextValue = 'go:explorer:env';
116+
collapsibleState = vscode.TreeItemCollapsibleState.Expanded;
117+
iconPath = new vscode.ThemeIcon('folder-opened');
118+
constructor(public description = '', public workspace?: vscode.Uri) {}
119+
}
120+
121+
class EnvTreeItem implements vscode.TreeItem {
122+
file?: vscode.Uri;
123+
label: string;
124+
contextValue?: string;
125+
tooltip?: string;
126+
constructor(public key: string, public value: string) {
127+
this.label = `${key}=${value.replace(new RegExp(`^${os.homedir()}`), '~')}`;
128+
this.contextValue = 'go:explorer:envitem';
129+
if (GoEnv.fileVars.includes(key)) {
130+
this.contextValue = 'go:explorer:envitem:file';
131+
this.file = vscode.Uri.file(value);
132+
}
133+
this.tooltip = `${key}=${value}`;
134+
}
135+
}
136+
137+
class GoEnv {
138+
/**
139+
* get returns a subset of go env vars, the union of this.vars and values
140+
* set with toolsEnvVars in the go workspace config.
141+
* @param uri the directory from which to run go env.
142+
* @returns the output of running go env -json VAR1 VAR2...
143+
*/
144+
static async get(uri?: vscode.Uri) {
145+
const toolsEnv = await getGoConfig(uri)['toolsEnvVars'];
146+
const output = await this.go(['env', '-json', ...this.vars, ...Object.keys(toolsEnv)], uri);
147+
return JSON.parse(output) as Record<string, string>;
148+
}
149+
150+
/**
151+
* update writes to toolsEnvVars in the go workspace config.
152+
* @param vars a record of env vars to update.
153+
*/
154+
static async update(vars: Record<string, string>) {
155+
const config = getGoConfig();
156+
await config.update('toolsEnvVars', { ...config['toolsEnvVars'], ...vars });
157+
}
158+
159+
/**
160+
* reset removes entries from toolsEnvVars in the go workspace config.
161+
* @param vars env vars to reset.
162+
*/
163+
static async reset(vars: string[]) {
164+
const config = getGoConfig();
165+
const env = { ...config['toolsEnvVars'] };
166+
for (const v of vars) {
167+
delete env[v];
168+
}
169+
await config.update('toolsEnvVars', env);
170+
}
171+
172+
/** A list of env vars that point to files. */
173+
static fileVars = ['GOMOD', 'GOWORK', 'GOENV'];
174+
175+
/** The list of env vars that should always be visible if they contain a value. */
176+
private static vars = ['GOPRIVATE', 'GOMOD', 'GOWORK', 'GOENV'];
177+
178+
private static async go(args: string[], uri?: vscode.Uri) {
179+
const exec = util.promisify(cp.execFile);
180+
const goBin = getBinPath('go');
181+
const env = toolExecutionEnvironment(uri);
182+
const cwd = uri?.fsPath;
183+
const { stdout, stderr } = await exec(goBin, args, { env, cwd });
184+
if (stderr) {
185+
throw new Error(stderr);
186+
}
187+
return stdout;
188+
}
189+
}
190+
191+
interface CacheEntry<T> {
192+
entry: T;
193+
updatedAt: number;
194+
}
195+
196+
class Cache<T> {
197+
private cache = new Map<string, CacheEntry<T>>();
198+
199+
constructor(private fn: (key: string) => Promise<T>, private ttl: number) {}
200+
201+
async get(key: string, ttl = this.ttl) {
202+
const cache = this.cache.get(key);
203+
const useCache = cache && Date.now() - cache.updatedAt < ttl;
204+
if (useCache) {
205+
return cache.entry;
206+
}
207+
const entry = await this.fn(key);
208+
this.cache.set(key, { entry, updatedAt: Date.now() });
209+
return entry;
210+
}
211+
212+
clear() {
213+
return this.cache.clear();
214+
}
215+
216+
delete(key: string) {
217+
return this.cache.delete(key);
218+
}
219+
}

src/goMain.ts

+3
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ import { ExtensionAPI } from './export';
109109
import extensionAPI from './extensionAPI';
110110
import { GoTestExplorer, isVscodeTestingAPIAvailable } from './goTest/explore';
111111
import { killRunningPprof } from './goTest/profile';
112+
import { GoExplorerProvider } from './goExplorer';
112113

113114
export let buildDiagnosticCollection: vscode.DiagnosticCollection;
114115
export let lintDiagnosticCollection: vscode.DiagnosticCollection;
@@ -327,6 +328,8 @@ If you would like additional configuration for diagnostics from gopls, please se
327328
GoTestExplorer.setup(ctx);
328329
}
329330

331+
GoExplorerProvider.setup(ctx);
332+
330333
ctx.subscriptions.push(
331334
vscode.commands.registerCommand('go.subtest.cursor', (args) => {
332335
const goConfig = getGoConfig();

0 commit comments

Comments
 (0)