Skip to content

Commit faa454e

Browse files
aviralgarg05h9jiang
authored andcommitted
extension: support follow cursor and sorting in package outline
Package Outline now supports follow cursor, sort by name, and sort by position, matching the controls available in Outline. This change switches the Package Outline view to a TreeView so the active symbol can be revealed as the cursor moves. It also adds view actions for follow cursor and both sort modes, and keeps symbol ordering stable by name or source position. The cursor matching logic also handles receiver methods returned by gopls.package_symbols, where methods may be grouped under a type without being nested inside that type's source range. Fixes #3998 Change-Id: I0ed9cc1526dd84bac44a29cec918b53465b81274 GitHub-Last-Rev: 6f9c67f GitHub-Pull-Request: #4008 Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/761181 Reviewed-by: Madeline Kalil <mkalil@google.com> LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Hongxiang Jiang <hxjiang@golang.org> Reviewed-by: Carlos Amedee <carlos@golang.org>
1 parent fecc313 commit faa454e

4 files changed

Lines changed: 312 additions & 53 deletions

File tree

docs/commands.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ See the currently set GOROOT.
3939

4040
List all the Go tools being used by this extension along with their locations.
4141

42+
### `Package Outline: Sort By Name`
43+
44+
Sort Package Outline symbols alphabetically.
45+
46+
### `Package Outline: Sort By Position`
47+
48+
Sort Package Outline symbols by source position.
49+
4250
### `Go: Test Function At Cursor`
4351

4452
Runs a unit test at the cursor.

extension/package.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,16 @@
223223
"title": "Go: Locate Configured Go Tools",
224224
"description": "List all the Go tools being used by this extension along with their locations."
225225
},
226+
{
227+
"command": "go.packageOutline.sortByName",
228+
"title": "Package Outline: Sort By Name",
229+
"description": "Sort Package Outline symbols alphabetically."
230+
},
231+
{
232+
"command": "go.packageOutline.sortByPosition",
233+
"title": "Package Outline: Sort By Position",
234+
"description": "Sort Package Outline symbols by source position."
235+
},
226236
{
227237
"command": "go.test.cursor",
228238
"title": "Go: Test Function At Cursor",
@@ -3846,6 +3856,16 @@
38463856
"command": "go.explorer.refresh",
38473857
"when": "view == go.explorer",
38483858
"group": "navigation"
3859+
},
3860+
{
3861+
"command": "go.packageOutline.sortByName",
3862+
"when": "view == go.package.outline && go.packageOutline.sortOrder != 'name'",
3863+
"group": "2_packageOutline"
3864+
},
3865+
{
3866+
"command": "go.packageOutline.sortByPosition",
3867+
"when": "view == go.package.outline && go.packageOutline.sortOrder != 'position'",
3868+
"group": "2_packageOutline"
38493869
}
38503870
],
38513871
"view/item/context": [

extension/src/goPackageOutline.ts

Lines changed: 200 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ interface PackageSymbolsCommandResult {
1212
Symbols: PackageSymbolData[];
1313
}
1414

15+
enum PackageOutlineSortOrder {
16+
Position = 'position',
17+
Name = 'name'
18+
}
19+
1520
export class GoPackageOutlineProvider implements vscode.TreeDataProvider<PackageSymbol> {
1621
private _onDidChangeTreeData: vscode.EventEmitter<PackageSymbol | undefined> = new vscode.EventEmitter<
1722
PackageSymbol | undefined
@@ -21,13 +26,30 @@ export class GoPackageOutlineProvider implements vscode.TreeDataProvider<Package
2126

2227
public result?: PackageSymbolsCommandResult;
2328
public activeDocument?: vscode.TextDocument;
29+
public view?: vscode.TreeView<PackageSymbol>;
30+
31+
private readonly collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
32+
private packageSymbols: PackageSymbol[] = [];
33+
private packageItem = this.createPackageItem();
34+
private sortOrder = PackageOutlineSortOrder.Position;
35+
private lastRevealedSymbol?: PackageSymbol;
2436

2537
static setup(ctx: vscode.ExtensionContext) {
2638
const provider = new this(ctx);
27-
const {
28-
window: { registerTreeDataProvider }
29-
} = vscode;
30-
ctx.subscriptions.push(registerTreeDataProvider('go.package.outline', provider));
39+
provider.view = vscode.window.createTreeView('go.package.outline', {
40+
treeDataProvider: provider,
41+
showCollapseAll: true
42+
});
43+
ctx.subscriptions.push(provider.view);
44+
ctx.subscriptions.push(
45+
vscode.commands.registerCommand('go.packageOutline.sortByName', () =>
46+
provider.setSortOrder(PackageOutlineSortOrder.Name)
47+
),
48+
vscode.commands.registerCommand('go.packageOutline.sortByPosition', () =>
49+
provider.setSortOrder(PackageOutlineSortOrder.Position)
50+
)
51+
);
52+
provider.updateContextKeys();
3153
return provider;
3254
}
3355

@@ -51,60 +73,43 @@ export class GoPackageOutlineProvider implements vscode.TreeDataProvider<Package
5173
this.reload(e?.document);
5274
})
5375
);
76+
ctx.subscriptions.push(
77+
vscode.window.onDidChangeTextEditorSelection((e) => {
78+
void this.revealActiveSymbol(e.textEditor);
79+
})
80+
);
5481
}
5582

5683
getTreeItem(element: PackageSymbol) {
5784
return element;
5885
}
5986

87+
// TreeView.reveal uses getParent to expand the path to nested symbols.
88+
getParent(element: PackageSymbol): PackageSymbol | undefined {
89+
return element.parent;
90+
}
91+
6092
rootItems(): Promise<PackageSymbol[]> {
61-
const list = Array<PackageSymbol>();
62-
// Add a tree item to display the current package name. Its "command" value will be undefined and thus
63-
// will not link anywhere when clicked
64-
list.push(
65-
new PackageSymbol(
66-
{
67-
name: this.result?.PackageName ? 'Current Package: ' + this.result.PackageName : '',
68-
detail: '',
69-
kind: 0,
70-
range: new vscode.Range(0, 0, 0, 0),
71-
selectionRange: new vscode.Range(0, 0, 0, 0),
72-
children: [],
73-
file: 0
74-
},
75-
[],
76-
vscode.TreeItemCollapsibleState.None
77-
)
78-
);
79-
const res = this.result;
80-
if (res) {
81-
res.Symbols?.forEach((d) =>
82-
list.push(
83-
new PackageSymbol(
84-
d,
85-
res.Files ?? [],
86-
d.children?.length > 0
87-
? vscode.TreeItemCollapsibleState.Collapsed
88-
: vscode.TreeItemCollapsibleState.None
89-
)
90-
)
91-
);
92-
}
93-
return new Promise((resolve) => resolve(list));
93+
return Promise.resolve([this.packageItem, ...this.sortSymbols(this.packageSymbols)]);
9494
}
9595

9696
getChildren(element?: PackageSymbol): Thenable<PackageSymbol[] | undefined> {
9797
// getChildren is called with null element when TreeDataProvider first loads
9898
if (!element) {
9999
return this.rootItems();
100100
}
101-
return Promise.resolve(element.children);
101+
return Promise.resolve(this.sortSymbols(element.children));
102102
}
103103

104104
async reload(e?: vscode.TextDocument) {
105105
if (e?.languageId !== 'go' || e?.uri?.scheme !== 'file') {
106106
this.result = undefined;
107107
this.activeDocument = undefined;
108+
this.packageSymbols = [];
109+
this.packageItem = this.createPackageItem();
110+
this.lastRevealedSymbol = undefined;
111+
vscode.commands.executeCommand('setContext', 'go.showPackageOutline', false);
112+
this._onDidChangeTreeData.fire(undefined);
108113
return;
109114
}
110115
this.activeDocument = e;
@@ -113,15 +118,135 @@ export class GoPackageOutlineProvider implements vscode.TreeDataProvider<Package
113118
URI: e.uri.toString()
114119
})) as PackageSymbolsCommandResult;
115120
this.result = res;
121+
this.packageSymbols = this.createPackageSymbols(res);
122+
this.packageItem = this.createPackageItem(res.PackageName);
123+
this.lastRevealedSymbol = undefined;
116124
// Show the Package Outline explorer if the request returned symbols for the current package
117125
vscode.commands.executeCommand('setContext', 'go.showPackageOutline', res?.Symbols?.length > 0);
118126
this._onDidChangeTreeData.fire(undefined);
127+
await this.revealActiveSymbol(vscode.window.activeTextEditor);
119128
} catch (e) {
129+
this.result = undefined;
130+
this.packageSymbols = [];
131+
this.packageItem = this.createPackageItem();
132+
this.lastRevealedSymbol = undefined;
120133
// Hide the Package Outline explorer
121134
vscode.commands.executeCommand('setContext', 'go.showPackageOutline', false);
135+
this._onDidChangeTreeData.fire(undefined);
122136
console.log('ERROR', e);
123137
}
124138
}
139+
140+
private createPackageSymbols(res: PackageSymbolsCommandResult): PackageSymbol[] {
141+
return (res.Symbols ?? []).map(
142+
(symbol) =>
143+
new PackageSymbol(
144+
symbol,
145+
res.Files ?? [],
146+
symbol.children?.length > 0
147+
? vscode.TreeItemCollapsibleState.Collapsed
148+
: vscode.TreeItemCollapsibleState.None
149+
)
150+
);
151+
}
152+
153+
private createPackageItem(packageName?: string): PackageSymbol {
154+
return new PackageSymbol(
155+
{
156+
name: packageName ? 'Current Package: ' + packageName : '',
157+
detail: '',
158+
kind: 0,
159+
range: new vscode.Range(0, 0, 0, 0),
160+
selectionRange: new vscode.Range(0, 0, 0, 0),
161+
children: [],
162+
file: 0
163+
},
164+
[],
165+
vscode.TreeItemCollapsibleState.None
166+
);
167+
}
168+
169+
private sortSymbols(symbols: readonly PackageSymbol[]): PackageSymbol[] {
170+
return [...symbols].sort((a, b) => this.compareSymbols(a, b));
171+
}
172+
173+
// Sort alphabetically when requested, otherwise preserve source order.
174+
private compareSymbols(a: PackageSymbol, b: PackageSymbol): number {
175+
if (this.sortOrder === PackageOutlineSortOrder.Name) {
176+
const byName = this.collator.compare(a.symbolName, b.symbolName);
177+
if (byName !== 0) {
178+
return byName;
179+
}
180+
return this.compareByPosition(a, b);
181+
}
182+
const byPosition = this.compareByPosition(a, b);
183+
if (byPosition !== 0) {
184+
return byPosition;
185+
}
186+
return this.collator.compare(a.symbolName, b.symbolName);
187+
}
188+
189+
private compareByPosition(a: PackageSymbol, b: PackageSymbol): number {
190+
if (a.fileIndex !== b.fileIndex) {
191+
return a.fileIndex - b.fileIndex;
192+
}
193+
if (a.range.start.line !== b.range.start.line) {
194+
return a.range.start.line - b.range.start.line;
195+
}
196+
return a.range.start.character - b.range.start.character;
197+
}
198+
199+
private setSortOrder(sortOrder: PackageOutlineSortOrder) {
200+
if (this.sortOrder === sortOrder) {
201+
return;
202+
}
203+
this.sortOrder = sortOrder;
204+
vscode.commands.executeCommand('setContext', 'go.packageOutline.sortOrder', sortOrder);
205+
this.lastRevealedSymbol = undefined;
206+
this._onDidChangeTreeData.fire(undefined);
207+
void this.revealActiveSymbol(vscode.window.activeTextEditor);
208+
}
209+
210+
private updateContextKeys() {
211+
vscode.commands.executeCommand('setContext', 'go.packageOutline.sortOrder', this.sortOrder);
212+
}
213+
214+
private async revealActiveSymbol(editor?: vscode.TextEditor) {
215+
if (!this.view || !editor || editor.document !== this.activeDocument) {
216+
return;
217+
}
218+
const symbol = this.findSymbolAtPosition(this.packageSymbols, editor.document.uri, editor.selection.active);
219+
if (!symbol) {
220+
this.lastRevealedSymbol = undefined;
221+
return;
222+
}
223+
if (symbol === this.lastRevealedSymbol) {
224+
return;
225+
}
226+
this.lastRevealedSymbol = symbol;
227+
try {
228+
await this.view.reveal(symbol, { expand: true, select: true });
229+
} catch (e) {
230+
console.log('ERROR', e);
231+
}
232+
}
233+
234+
private findSymbolAtPosition(
235+
symbols: readonly PackageSymbol[],
236+
uri: vscode.Uri,
237+
position: vscode.Position
238+
): PackageSymbol | undefined {
239+
for (const symbol of symbols) {
240+
const childMatch = this.findSymbolAtPosition(symbol.children, uri, position);
241+
if (childMatch) {
242+
return childMatch;
243+
}
244+
if (symbol.contains(uri, position)) {
245+
return symbol;
246+
}
247+
}
248+
return undefined;
249+
}
125250
}
126251

127252
interface PackageSymbolData {
@@ -168,12 +293,26 @@ interface PackageSymbolData {
168293
}
169294

170295
export class PackageSymbol extends vscode.TreeItem {
296+
public readonly children: PackageSymbol[];
297+
171298
constructor(
172299
private readonly data: PackageSymbolData,
173300
private readonly files: string[],
174-
public readonly collapsibleState: vscode.TreeItemCollapsibleState
301+
public readonly collapsibleState: vscode.TreeItemCollapsibleState,
302+
public readonly parent?: PackageSymbol
175303
) {
176304
super(data.name, collapsibleState);
305+
this.children = (data.children ?? []).map(
306+
(child) =>
307+
new PackageSymbol(
308+
child,
309+
files,
310+
child.children?.length > 0
311+
? vscode.TreeItemCollapsibleState.Collapsed
312+
: vscode.TreeItemCollapsibleState.None,
313+
this
314+
)
315+
);
177316
const file = files[data.file ?? 0];
178317
this.resourceUri = files && files.length > 0 ? vscode.Uri.parse(file) : undefined;
179318
const [icon, kind] = this.getSymbolInfo();
@@ -195,17 +334,28 @@ export class PackageSymbol extends vscode.TreeItem {
195334
: undefined;
196335
}
197336

198-
get children(): PackageSymbol[] | undefined {
199-
return this.data.children?.map(
200-
(c) =>
201-
new PackageSymbol(
202-
c,
203-
this.files,
204-
c.children?.length > 0
205-
? vscode.TreeItemCollapsibleState.Collapsed
206-
: vscode.TreeItemCollapsibleState.None
207-
)
208-
);
337+
get range(): vscode.Range {
338+
return this.data.range;
339+
}
340+
341+
get fileIndex(): number {
342+
return this.data.file ?? 0;
343+
}
344+
345+
get symbolName(): string {
346+
return this.data.name;
347+
}
348+
349+
contains(uri: vscode.Uri, position: vscode.Position): boolean {
350+
if (this.resourceUri?.toString() !== uri.toString()) {
351+
return false;
352+
}
353+
const { start, end } = this.range;
354+
const afterStart =
355+
start.line < position.line || (start.line === position.line && start.character <= position.character);
356+
const beforeEnd =
357+
end.line > position.line || (end.line === position.line && end.character >= position.character);
358+
return afterStart && beforeEnd;
209359
}
210360

211361
private getSymbolInfo(): [vscode.ThemeIcon | undefined, string] {

0 commit comments

Comments
 (0)