Skip to content

Commit d45e21e

Browse files
committed
Index update event for structure view in VSCode
1 parent c6d80f0 commit d45e21e

File tree

7 files changed

+111
-131
lines changed

7 files changed

+111
-131
lines changed

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/JsonNodeHandler.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,8 @@ private void addChild(Consumer<Node> consumer) {
218218
}
219219

220220
private static void assignNodeId(Node n, Node p) {
221-
String textId = n.attributes.containsKey(TEXT) ? (String) n.attributes.get(TEXT) : "";
221+
String textId = n.attributes.containsKey(PROJECT_ID) ? (String) n.attributes.get(PROJECT_ID)
222+
: n.attributes.containsKey(TEXT) ? (String) n.attributes.get(TEXT) : "";
222223

223224
Location location = (Location) n.attributes.get(LOCATION);
224225
String locationId = location == null ? "" : "%s:%d:%d".formatted(location.getUri(), location.getRange().getStart().getLine(), location.getRange().getStart().getCharacter());

vscode-extensions/vscode-spring-boot/lib/api.d.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Event, Uri } from "vscode";
22
import { LanguageClient } from "vscode-languageclient/node";
3-
import { LiveProcess } from "./notification";
3+
import { IndexUpdateDetails, LiveProcess } from "./notification";
44
import {Location} from "vscode-languageclient";
55

66
export interface ExtensionAPI {
@@ -109,9 +109,9 @@ interface InjectionPoint {
109109

110110
interface SpringIndex {
111111
readonly beans: (params: BeansParams) => Promise<Bean[]>;
112-
readonly onSpringIndexUpdated: Event<void>;
112+
readonly onSpringIndexUpdated: Event<IndexUpdateDetails>;
113113
}
114114

115115
interface BeansParams {
116116
projectName: string;
117-
}
117+
}

vscode-extensions/vscode-spring-boot/lib/apiManager.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { commands } from "vscode";
22
import { Emitter, LanguageClient } from "vscode-languageclient/node";
3-
import {Bean, BeansParams, ExtensionAPI} from "./api";
3+
import { Bean, BeansParams, ExtensionAPI } from "./api";
44
import {
55
LiveProcess,
66
LiveProcessConnectedNotification,
@@ -9,6 +9,7 @@ import {
99
LiveProcessGcPausesMetricsUpdatedNotification,
1010
LiveProcessMemoryMetricsUpdatedNotification,
1111
SpringIndexUpdatedNotification,
12+
IndexUpdateDetails,
1213
} from "./notification";
1314
import {RequestType} from "vscode-languageclient";
1415

@@ -19,7 +20,7 @@ export class ApiManager {
1920
private onDidLiveProcessUpdateEmitter: Emitter<LiveProcess> = new Emitter<LiveProcess>();
2021
private onDidLiveProcessGcPausesMetricsUpdateEmitter: Emitter<LiveProcess> = new Emitter<LiveProcess>();
2122
private onDidLiveProcessMemoryMetricsUpdateEmitter: Emitter<LiveProcess> = new Emitter<LiveProcess>();
22-
private onSpringIndexUpdateEmitter: Emitter<void> = new Emitter<void>();
23+
private onSpringIndexUpdateEmitter: Emitter<IndexUpdateDetails> = new Emitter<IndexUpdateDetails>();
2324

2425
public constructor(client: LanguageClient) {
2526
const onDidLiveProcessConnect = this.onDidLiveProcessConnectEmitter.event;
@@ -61,7 +62,7 @@ export class ApiManager {
6162
client.onNotification(LiveProcessGcPausesMetricsUpdatedNotification.type, (process: LiveProcess) => this.onDidLiveProcessGcPausesMetricsUpdateEmitter.fire(process));
6263
client.onNotification(LiveProcessMemoryMetricsUpdatedNotification.type, (process: LiveProcess) => this.onDidLiveProcessMemoryMetricsUpdateEmitter.fire(process));
6364

64-
client.onNotification(SpringIndexUpdatedNotification.type, () => this.onSpringIndexUpdateEmitter.fire());
65+
client.onNotification(SpringIndexUpdatedNotification.type, (details: IndexUpdateDetails) => this.onSpringIndexUpdateEmitter.fire(details));
6566

6667
const beansRequestType = new RequestType<BeansParams, Bean[], void>('spring/index/beans');
6768
const beans = (params: BeansParams) => {
Lines changed: 12 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,36 @@
1-
import { Event, EventEmitter, ExtensionContext, ProviderResult, TreeDataProvider, TreeItem, TreeItemCollapsibleState, window } from "vscode";
1+
import { Event, EventEmitter, ExtensionContext, ProviderResult, TreeDataProvider, TreeItem, window } from "vscode";
22
import { StructureManager } from "./structure-tree-manager";
3-
import { SpringNode } from "./nodes";
3+
import { StereotypedNode } from "./nodes";
44

5-
export class ExplorerTreeProvider implements TreeDataProvider<SpringNode> {
5+
export class ExplorerTreeProvider implements TreeDataProvider<StereotypedNode> {
66

7-
private emitter: EventEmitter<undefined | SpringNode | SpringNode[]>;
8-
public readonly onDidChangeTreeData: Event<undefined | SpringNode | SpringNode[]>;
9-
private expansionStates: Map<string, TreeItemCollapsibleState> = new Map();
7+
private emitter: EventEmitter<undefined | StereotypedNode | StereotypedNode[]>;
8+
public readonly onDidChangeTreeData: Event<undefined | StereotypedNode | StereotypedNode[]>;
109

1110
constructor(private manager: StructureManager) {
12-
this.emitter = new EventEmitter<undefined | SpringNode | SpringNode[]>();
11+
this.emitter = new EventEmitter<undefined | StereotypedNode | StereotypedNode[]>();
1312
this.onDidChangeTreeData = this.emitter.event;
14-
this.manager.onDidChange(e => {
15-
// Expansion states are tracked via onDidExpandElement/onDidCollapseElement events
16-
this.emitter.fire(e);
17-
});
13+
this.manager.onDidChange(e => this.emitter.fire(e));
1814
}
1915

2016
createTreeView(context: ExtensionContext, viewId: string) {
21-
const treeView = window.createTreeView(viewId, { treeDataProvider: this, showCollapseAll: true });
22-
23-
// Track expansion/collapse events to preserve state across refreshes
24-
context.subscriptions.push(treeView.onDidExpandElement(e => {
25-
const nodeId = e.element.getNodeId();
26-
this.setExpansionState(nodeId, TreeItemCollapsibleState.Expanded);
27-
}));
28-
29-
context.subscriptions.push(treeView.onDidCollapseElement(e => {
30-
const nodeId = e.element.getNodeId();
31-
this.setExpansionState(nodeId, TreeItemCollapsibleState.Collapsed);
32-
}));
33-
17+
const treeView = window.createTreeView(viewId, { treeDataProvider: this, showCollapseAll: true });
3418
context.subscriptions.push(treeView);
3519
return treeView
3620
}
3721

38-
getTreeItem(element: SpringNode): TreeItem | Thenable<TreeItem> {
39-
const nodeId = element.getNodeId();
40-
const savedState = this.expansionStates.get(nodeId);
41-
return element.getTreeItem(savedState);
22+
getTreeItem(element: StereotypedNode): TreeItem | Thenable<TreeItem> {
23+
return element.getTreeItem();
4224
}
4325

44-
getChildren(element?: SpringNode): ProviderResult<SpringNode[]> {
26+
getChildren(element?: StereotypedNode): ProviderResult<StereotypedNode[]> {
4527
if (element) {
4628
return element.children;
4729
}
4830
return this.getRootElements();
4931
}
5032

51-
getRootElements(): ProviderResult<SpringNode[]> {
33+
getRootElements(): ProviderResult<StereotypedNode[]> {
5234
return this.manager.rootElements;
5335
}
54-
55-
56-
private getExpansionState(nodeId: string): TreeItemCollapsibleState | undefined {
57-
return this.expansionStates.get(nodeId);
58-
}
59-
60-
private setExpansionState(nodeId: string, state: TreeItemCollapsibleState): void {
61-
this.expansionStates.set(nodeId, state);
62-
}
63-
64-
// getParent?(element: SpringNode): ProviderResult<SpringNode> {
65-
// throw new Error("Method not implemented.");
66-
// }
67-
68-
// resolveTreeItem?(item: TreeItem, element: SpringNode, token: CancellationToken): ProviderResult<TreeItem> {
69-
// throw new Error("Method not implemented.");
70-
// }
71-
7236
}
Lines changed: 14 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,26 @@
11
import { TextDocumentShowOptions, ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri } from "vscode";
22
import { Location } from "vscode-languageclient";
33
import { LsStereoTypedNode } from "./structure-tree-manager";
4+
import * as ls from 'vscode-languageserver-protocol';
45

5-
export class SpringNode {
6-
constructor(public children: SpringNode[], protected parent?: SpringNode) {}
7-
8-
getTreeItem(savedState?: TreeItemCollapsibleState): TreeItem {
9-
const defaultState = savedState !== undefined ? savedState : TreeItemCollapsibleState.Collapsed;
10-
return new TreeItem("<node>", this.computeState(defaultState));
11-
}
12-
13-
computeState(defaultState: TreeItemCollapsibleState): TreeItemCollapsibleState {
14-
return Array.isArray(this.children) && this.children.length ? defaultState : TreeItemCollapsibleState.None;
15-
}
16-
17-
getNodeId(): string {
18-
return "<base-node>";
19-
}
20-
21-
protected getParentPath(): string {
22-
if (!this.parent) {
23-
return "";
24-
}
25-
26-
const parentText = this.parent.getNodeText();
27-
// Recursively get the full path of all ancestors up to the root
28-
const ancestorPath = this.parent.getParentPath();
29-
30-
return ancestorPath ? `${ancestorPath}/${parentText}` : parentText;
31-
}
32-
33-
protected getNodeText(): string {
34-
return "<node>";
35-
}
36-
}
37-
38-
export class StereotypedNode extends SpringNode {
39-
constructor(private n: LsStereoTypedNode, children: SpringNode[], parent?: SpringNode) {
40-
super(children, parent);
41-
}
6+
export class StereotypedNode {
7+
constructor(private n: LsStereoTypedNode, public children: StereotypedNode[], protected parent?: StereotypedNode) {}
428

439
getTreeItem(savedState?: TreeItemCollapsibleState): TreeItem {
44-
const item = super.getTreeItem(savedState);
45-
item.label = this.n.attributes.text;
46-
item.iconPath = this.computeIcon();
10+
const defaultState = savedState !== undefined ? savedState : TreeItemCollapsibleState.Collapsed;
11+
const item = new TreeItem(this.label, Array.isArray(this.children) && this.children.length ? defaultState : TreeItemCollapsibleState.None);
12+
item.iconPath = new ThemeIcon(this.n.attributes.icon);
13+
item.id = this.nodeId;
4714

4815
// Add context value if reference attribute exists
4916
if (this.n.attributes.reference) {
5017
item.contextValue = "stereotypedNodeWithReference";
5118
}
5219

53-
if (this.n.attributes.icon === 'project') {
20+
if (this.projectId) {
5421
item.contextValue = "project";
5522
}
5623

57-
5824
if (this.n.attributes.location) {
5925
const location = this.n.attributes.location as Location;
6026
item.command = {
@@ -68,24 +34,20 @@ export class StereotypedNode extends SpringNode {
6834
return item;
6935
}
7036

71-
getProjectId(): string {
72-
return this.n.attributes.projectId || this.n.attributes.text;
37+
get projectId(): string {
38+
return this.n.attributes.projectId;
7339
}
7440

75-
getNodeId(): string {
41+
get nodeId(): string {
7642
return this.n.attributes.nodeId || this.n.attributes.text;
7743
}
7844

79-
protected getNodeText(): string {
45+
get label(): string {
8046
return this.n.attributes.text || '';
8147
}
8248

83-
getReferenceValue(): any {
84-
return this.n.attributes.reference;
85-
}
86-
87-
computeIcon() {
88-
return new ThemeIcon(this.n.attributes.icon);
49+
get referenceValue(): ls.Location | undefined {
50+
return this.n.attributes.reference as ls.Location;
8951
}
9052

9153
}

vscode-extensions/vscode-spring-boot/lib/explorer/structure-tree-manager.ts

Lines changed: 71 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,35 @@
1-
import { commands, EventEmitter, Event, ExtensionContext, window, Memento, Uri, QuickPickItem } from "vscode";
2-
import { SpringNode, StereotypedNode } from "./nodes";
1+
import { commands, EventEmitter, Event, ExtensionContext, window, Memento, QuickPickItem } from "vscode";
2+
import { StereotypedNode } from "./nodes";
33
import { ExtensionAPI } from "../api";
4-
import * as ls from 'vscode-languageserver-protocol';
5-
64

75
const SPRING_STRUCTURE_CMD = "sts/spring-boot/structure";
86

7+
interface StructureCommandParams {
8+
updateMetadata: boolean;
9+
groups?: Record<string, string[]>;
10+
affectedProjects?: string[];
11+
}
12+
913
export class StructureManager {
1014

11-
private _rootElements: Thenable<SpringNode[]>
12-
private _onDidChange: EventEmitter<SpringNode | undefined> = new EventEmitter<SpringNode | undefined>();
15+
private _rootElementsRequest: Thenable<StereotypedNode[]>
16+
private _rootElements: StereotypedNode[] = [];
17+
private _onDidChange = new EventEmitter<undefined | StereotypedNode | StereotypedNode[]>();
1318
private workspaceState: Memento;
1419

1520
constructor(context: ExtensionContext, api: ExtensionAPI) {
1621
this.workspaceState = context.workspaceState;
1722
context.subscriptions.push(commands.registerCommand("vscode-spring-boot.structure.refresh", () => this.refresh(true)));
18-
context.subscriptions.push(commands.registerCommand("vscode-spring-boot.structure.openReference", (node) => {
19-
const reference = node?.getReferenceValue() as ls.Location;;
23+
context.subscriptions.push(commands.registerCommand("vscode-spring-boot.structure.openReference", (node: StereotypedNode) => {
24+
const reference = node?.referenceValue;
2025
if (reference) {
2126
const location = api.client.protocol2CodeConverter.asLocation(reference)
2227
window.showTextDocument(location.uri, { selection: location.range });
2328
}
2429
}));
2530

2631
context.subscriptions.push(commands.registerCommand("vscode-spring-boot.structure.grouping", async (node: StereotypedNode) => {
27-
const projectName = node.getProjectId();
32+
const projectName = node.projectId;
2833
const groups = await commands.executeCommand<Groups>("sts/spring-boot/structure/groups", projectName);
2934
const initialGroups: string[] | undefined = this.getVisibleGroups(projectName);
3035
const items = (groups?.groups || []).map(g => ({
@@ -45,38 +50,81 @@ export class StructureManager {
4550
}
4651
}));
4752

48-
context.subscriptions.push(api.getSpringIndex().onSpringIndexUpdated(e => this.refresh(false)));
53+
context.subscriptions.push(api.getSpringIndex().onSpringIndexUpdated(indexUpdateDetails => this.refresh(false, indexUpdateDetails.affectedProjects)));
4954

5055
}
5156

52-
get rootElements(): Thenable<SpringNode[]> {
53-
return this._rootElements;
57+
get rootElements(): Thenable<StereotypedNode[]> {
58+
return this._rootElementsRequest;
5459
}
5560

56-
refresh(updateMetadata: boolean): void {
57-
this._rootElements = commands.executeCommand(SPRING_STRUCTURE_CMD,
58-
{
59-
"updateMetadata" : updateMetadata,
60-
"groups" : this.getGroupings()
61-
}).then(json => {
61+
// Serves 2 purposes: non UI trigerred refresh as a result of the index update and a UI trigerred refresh
62+
// The UI trigerred refresh needs to preceed with an event fired such that tree view would kick off a new promise getting all new root elements and would show progress while promise is being resolved.
63+
// The index update typically would have a list of projects for which index has changed then the refresh can be silent with letting the tree know about new data once it is computed
64+
// If the index update event doesn't have a list of project then this is an edge case for which we'd show the preogress and treat it like UI trigerred refresh
65+
refresh(updateMetadata: boolean, affectedProjects?: string[]): void {
66+
const isPartialLoad = !!(affectedProjects && affectedProjects.length);
67+
// Notify the tree to get the children to trigger "loading" bar in the view???
68+
const params = {
69+
updateMetadata,
70+
affectedProjects,
71+
groups: this.getGroupings(),
72+
} as StructureCommandParams;
73+
this._rootElementsRequest = commands.executeCommand(SPRING_STRUCTURE_CMD, params).then(json => {
6274
const nodes = this.parseArray(json);
63-
this._onDidChange.fire(undefined);
64-
return nodes;
75+
if (isPartialLoad) {
76+
const newNodes = [] as StereotypedNode[];
77+
const nodesMap = {} as Record<string, StereotypedNode>;
78+
affectedProjects.forEach(projectName => nodesMap[projectName] = nodes.find(n => n.projectId === projectName));
79+
// merge old and newly fetched stereotype root nodes
80+
let onlyMutations = true;
81+
this._rootElements.forEach(n => {
82+
if (nodesMap.hasOwnProperty(n.projectId)) {
83+
const newN = nodesMap[n.projectId];
84+
delete nodesMap[n.projectId];
85+
if (newN) {
86+
newNodes.push(newN);
87+
} else {
88+
// element removed
89+
onlyMutations = false;
90+
}
91+
} else {
92+
newNodes.push(n);
93+
}
94+
});
95+
if (Object.values(nodesMap).length) {
96+
// elements added
97+
onlyMutations = false;
98+
Object.values(nodesMap).filter(n => !!n).forEach(n => newNodes.push(n));
99+
}
100+
this._rootElements = newNodes;
101+
// TODO: Partial tree refresh didn't wowrk for restbucks it remains either without children or without the full text label
102+
// (test with `spring-restbucks` project in a workspace with other boot projects, i.e. demo, spring-petclinic)
103+
this._onDidChange.fire(/*onlyMutations ? nodes : */undefined);
104+
} else {
105+
this._rootElements = nodes;
106+
// No need to fire another event to update the UI since there is an event fired before referesh is trigerred to reference the new promise
107+
}
108+
return this._rootElements;
65109
});
110+
if (!isPartialLoad) {
111+
// Fire an event for full reload to have a progress bar while the promise above is resolved
112+
this._onDidChange.fire(undefined);
113+
}
66114
}
67115

68-
private parseNode(json: any, parent?: SpringNode): SpringNode | undefined {
116+
private parseNode(json: any, parent?: StereotypedNode): StereotypedNode | undefined {
69117
const node = new StereotypedNode(json as LsStereoTypedNode, [], parent);
70118
// Parse children after creating the node so we can pass it as parent
71119
node.children.push(...this.parseArray(json.children, node));
72120
return node;
73121
}
74122

75-
private parseArray(json: any, parent?: SpringNode): SpringNode[] {
123+
private parseArray(json: any, parent?: StereotypedNode): StereotypedNode[] {
76124
return Array.isArray(json) ? (json as []).map(j => this.parseNode(j, parent)).filter(e => !!e) : [];
77125
}
78126

79-
public get onDidChange(): Event<SpringNode | undefined> {
127+
public get onDidChange(): Event<undefined | StereotypedNode | StereotypedNode[]> {
80128
return this._onDidChange.event;
81129
}
82130

0 commit comments

Comments
 (0)