diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/JMoleculesStructureView.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/JMoleculesStructureView.java index 5b18ca89a1..c10ce4139d 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/JMoleculesStructureView.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/JMoleculesStructureView.java @@ -10,6 +10,7 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.commands; +import java.util.Collection; import java.util.List; import java.util.function.BiConsumer; @@ -34,7 +35,7 @@ public JMoleculesStructureView(AbstractStereotypeCatalog catalog, CachedSpringMe this.springIndex = springIndex; } - public Node createTree(IJavaProject project, IndexBasedStereotypeFactory factory, List selectedGroups) { + public Node createTree(IJavaProject project, IndexBasedStereotypeFactory factory, Collection selectedGroups) { StereotypePackageElement mainApplicationPackage = StructureViewUtil.identifyMainApplicationPackage(project, springIndex); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/ModulithStructureView.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/ModulithStructureView.java index e1c860c2e7..38b31af626 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/ModulithStructureView.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/ModulithStructureView.java @@ -10,6 +10,7 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.commands; +import java.util.Collection; import java.util.List; import java.util.function.BiConsumer; @@ -35,7 +36,7 @@ public ModulithStructureView(AbstractStereotypeCatalog catalog, CachedSpringMeta this.modulithService = modulithService; } - public Node createTree(IJavaProject project, IndexBasedStereotypeFactory factory, List selectedGroups) { + public Node createTree(IJavaProject project, IndexBasedStereotypeFactory factory, Collection selectedGroups) { var adapter = new ModulithStereotypeFactoryAdapter(factory); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/SpringIndexCommands.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/SpringIndexCommands.java index b5019d46e4..9d4b2c962c 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/SpringIndexCommands.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/SpringIndexCommands.java @@ -10,9 +10,12 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.commands; +import java.util.Collection; import java.util.Comparator; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; import org.eclipse.lsp4j.ExecuteCommandParams; @@ -27,10 +30,11 @@ import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; -import com.google.gson.JsonArray; +import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; +import com.google.gson.reflect.TypeToken; public class SpringIndexCommands { @@ -54,15 +58,27 @@ public SpringIndexCommands(SimpleLanguageServer server, SpringMetamodelIndex spr CachedSpringMetamodelIndex cachedIndex = new CachedSpringMetamodelIndex(springIndex); return projectFinder.all().stream() .sorted(Comparator.comparing(IJavaProject::getElementName)) - .map(project -> nodeFrom(project, cachedIndex, args.updateMetadata, args.selectedGroups)) + .map(project -> nodeFrom(project, cachedIndex, args.updateMetadata, + args.selectedGroups == null ? null : args.selectedGroups.get(project.getElementName()))) .filter(Objects::nonNull) .collect(Collectors.toList()); })); server.onCommand(SPRING_STRUCTURE_GROUPS_CMD, params -> server.getAsync().invoke(() -> { - return projectFinder.all().stream() - .map(project -> getGroups(project)) - .toList(); + if (params.getArguments().size() == 1) { + Object o = params.getArguments().get(0); + String name = null; + if (o instanceof JsonElement) { + name = ((JsonElement) o).getAsString(); + } else if (o instanceof String) { + name = (String) o; + } + if (name != null) { + final String projectName = name; + return projectFinder.all().stream().filter(p -> projectName.equals(p.getElementName())).findFirst().map(this::getGroups).orElseThrow(); + } + } + return projectFinder.all().stream().map(this::getGroups); })); } @@ -76,7 +92,7 @@ private Groups getGroups(IJavaProject project) { return new Groups(project.getElementName(), groups); } - private Node nodeFrom(IJavaProject project, CachedSpringMetamodelIndex springIndex, boolean updateMetadata, List selectedGroups) { + private Node nodeFrom(IJavaProject project, CachedSpringMetamodelIndex springIndex, boolean updateMetadata, Collection selectedGroups) { log.info("create structural view tree information for project: " + project.getElementName()); if (updateMetadata) { @@ -103,11 +119,11 @@ private Node nodeFrom(IJavaProject project, CachedSpringMetamodelIndex springInd } } - private static record StructureCommandArgs(boolean updateMetadata, List selectedGroups) { + private static record StructureCommandArgs(boolean updateMetadata, Map> selectedGroups) { public static StructureCommandArgs parseFrom(ExecuteCommandParams params) { boolean updateMetadata = false; - List selectedGroups = null; + Map> selectedGroups = null; List arguments = params.getArguments(); if (arguments != null && arguments.size() == 1) { @@ -119,11 +135,8 @@ public static StructureCommandArgs parseFrom(ExecuteCommandParams params) { updateMetadata = jsonElement != null && jsonElement instanceof JsonPrimitive ? jsonElement.getAsBoolean() : false; JsonElement groupsElement = paramObject.get("groups"); - if (groupsElement instanceof JsonArray && ((JsonArray) groupsElement).size() > 0) { - JsonArray groupsArray = (JsonArray) groupsElement; - selectedGroups = groupsArray.asList().stream() - .map(groupEntry -> groupEntry.getAsString()) - .toList(); + if (groupsElement != null) { + selectedGroups = new Gson().fromJson(groupsElement, new TypeToken>>() {}); } } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/StructureViewUtil.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/StructureViewUtil.java index 885dea429e..6733c22717 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/StructureViewUtil.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/StructureViewUtil.java @@ -11,6 +11,7 @@ package org.springframework.ide.vscode.boot.java.commands; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.function.Function; @@ -36,7 +37,7 @@ public class StructureViewUtil { - public static List identifyGroupers(AbstractStereotypeCatalog catalog, List selectedGroups) { + public static List identifyGroupers(AbstractStereotypeCatalog catalog, Collection selectedGroups) { // List allGroupsWithSpecificOrder = Arrays.asList( // new String[] {"architecture"}, diff --git a/vscode-extensions/vscode-spring-boot/lib/Main.ts b/vscode-extensions/vscode-spring-boot/lib/Main.ts index e47f141c47..59e58811b3 100644 --- a/vscode-extensions/vscode-spring-boot/lib/Main.ts +++ b/vscode-extensions/vscode-spring-boot/lib/Main.ts @@ -28,8 +28,8 @@ import * as springBootAgent from './copilot/springBootAgent'; import { applyLspEdit } from "./copilot/guideApply"; import { isLlmApiReady } from "./copilot/util"; import CopilotRequest, { logger } from "./copilot/copilotRequest"; -import { ExplorerTreeProvider } from "./explorer/explorer-tree-provider"; import { StructureManager } from "./explorer/structure-tree-manager"; +import { ExplorerTreeProvider } from "./explorer/explorer-tree-provider"; const PROPERTIES_LANGUAGE_ID = "spring-boot-properties"; const YAML_LANGUAGE_ID = "spring-boot-properties-yaml"; @@ -159,58 +159,9 @@ export function activate(context: ExtensionContext): Thenable { return commons.activate(options, context).then(client => { - // Spring structure tree in the Explorer view - /* - Requires the following code to be added in the `package.json` to - 1. Declare view: - "views": { - "explorer": [ - { - "id": "explorer.spring", - "name": "Spring", - "when": "java:serverMode || workbenchState==empty", - "contextualTitle": "Spring", - "icon": "resources/logo.png" - } - ] - }, - - 2. Menu item (toolbar action) on the explorer view delegating to the command - "view/title": [ - { - "command": "vscode-spring-boot.structure.refresh", - "when": "view == explorer.spring", - "group": "navigation@5" - } - ], - - */ - const structureManager = new StructureManager(); - const explorerTreeProvider = new ExplorerTreeProvider(structureManager); - const treeView = window.createTreeView('explorer.spring', { treeDataProvider: explorerTreeProvider, showCollapseAll: true }); - - // Track expansion/collapse events to preserve state across refreshes - context.subscriptions.push(treeView.onDidExpandElement(e => { - const nodeId = e.element.getNodeId(); - explorerTreeProvider.setExpansionState(nodeId, TreeItemCollapsibleState.Expanded); - })); - - context.subscriptions.push(treeView.onDidCollapseElement(e => { - const nodeId = e.element.getNodeId(); - explorerTreeProvider.setExpansionState(nodeId, TreeItemCollapsibleState.Collapsed); - })); - - context.subscriptions.push(treeView); - context.subscriptions.push(commands.registerCommand("vscode-spring-boot.structure.refresh", () => structureManager.refresh(true))); - context.subscriptions.push(commands.registerCommand("vscode-spring-boot.structure.openReference", (node) => { - if (node && node.getReferenceValue) { - const reference = node.getReferenceValue(); - if (reference) { - // Reference is a specific URL that should be passed to java.open.file command - commands.executeCommand('java.open.file', reference); - } - } - })); + // Activation of structure explorer + const structureManager = new StructureManager(context); + new ExplorerTreeProvider(structureManager).createTreeView(context, 'explorer.spring'); context.subscriptions.push(commands.registerCommand('vscode-spring-boot.ls.start', () => client.start().then(() => { // Boot LS is fully started diff --git a/vscode-extensions/vscode-spring-boot/lib/copilot/copilotRequest.ts b/vscode-extensions/vscode-spring-boot/lib/copilot/copilotRequest.ts index 280047378b..fed18a0f6d 100644 --- a/vscode-extensions/vscode-spring-boot/lib/copilot/copilotRequest.ts +++ b/vscode-extensions/vscode-spring-boot/lib/copilot/copilotRequest.ts @@ -41,7 +41,6 @@ export default class CopilotRequest { let response: string = ''; messages.push(...message); try { - messages.forEach(m => logger.info(m.content)); response = await this.sendRequest(messages, modelOptions, cancellationToken); answer += response; logger.info(`Response: \n`, response); diff --git a/vscode-extensions/vscode-spring-boot/lib/explorer/explorer-tree-provider.ts b/vscode-extensions/vscode-spring-boot/lib/explorer/explorer-tree-provider.ts index 907c933556..421d6e04cf 100644 --- a/vscode-extensions/vscode-spring-boot/lib/explorer/explorer-tree-provider.ts +++ b/vscode-extensions/vscode-spring-boot/lib/explorer/explorer-tree-provider.ts @@ -1,4 +1,4 @@ -import { Event, EventEmitter, ProviderResult, TreeDataProvider, TreeItem, TreeItemCollapsibleState } from "vscode"; +import { Event, EventEmitter, ExtensionContext, ProviderResult, TreeDataProvider, TreeItem, TreeItemCollapsibleState, window } from "vscode"; import { StructureManager } from "./structure-tree-manager"; import { SpringNode } from "./nodes"; @@ -17,6 +17,24 @@ export class ExplorerTreeProvider implements TreeDataProvider { }); } + createTreeView(context: ExtensionContext, viewId: string) { + const treeView = window.createTreeView(viewId, { treeDataProvider: this, showCollapseAll: true }); + + // Track expansion/collapse events to preserve state across refreshes + context.subscriptions.push(treeView.onDidExpandElement(e => { + const nodeId = e.element.getNodeId(); + this.setExpansionState(nodeId, TreeItemCollapsibleState.Expanded); + })); + + context.subscriptions.push(treeView.onDidCollapseElement(e => { + const nodeId = e.element.getNodeId(); + this.setExpansionState(nodeId, TreeItemCollapsibleState.Collapsed); + })); + + context.subscriptions.push(treeView); + return treeView + } + getTreeItem(element: SpringNode): TreeItem | Thenable { const nodeId = element.getNodeId(); const savedState = this.expansionStates.get(nodeId); @@ -35,11 +53,11 @@ export class ExplorerTreeProvider implements TreeDataProvider { } - getExpansionState(nodeId: string): TreeItemCollapsibleState | undefined { + private getExpansionState(nodeId: string): TreeItemCollapsibleState | undefined { return this.expansionStates.get(nodeId); } - setExpansionState(nodeId: string, state: TreeItemCollapsibleState): void { + private setExpansionState(nodeId: string, state: TreeItemCollapsibleState): void { this.expansionStates.set(nodeId, state); } diff --git a/vscode-extensions/vscode-spring-boot/lib/explorer/nodes.ts b/vscode-extensions/vscode-spring-boot/lib/explorer/nodes.ts index 6b492ec64d..a05f382629 100644 --- a/vscode-extensions/vscode-spring-boot/lib/explorer/nodes.ts +++ b/vscode-extensions/vscode-spring-boot/lib/explorer/nodes.ts @@ -3,7 +3,7 @@ import { Location } from "vscode-languageclient"; import { LsStereoTypedNode } from "./structure-tree-manager"; export class SpringNode { - constructor(public children: SpringNode[], private parent?: SpringNode) {} + constructor(public children: SpringNode[], protected parent?: SpringNode) {} getTreeItem(savedState?: TreeItemCollapsibleState): TreeItem { const defaultState = savedState !== undefined ? savedState : TreeItemCollapsibleState.Collapsed; @@ -39,7 +39,7 @@ export class StereotypedNode extends SpringNode { constructor(private n: LsStereoTypedNode, children: SpringNode[], parent?: SpringNode) { super(children, parent); } - + getTreeItem(savedState?: TreeItemCollapsibleState): TreeItem { const item = super.getTreeItem(savedState); item.label = this.n.attributes.text; @@ -49,10 +49,14 @@ export class StereotypedNode extends SpringNode { if (this.n.attributes.reference) { item.contextValue = "stereotypedNodeWithReference"; } + + if (this.n.attributes.icon === 'project') { + item.contextValue = "project"; + } + if (this.n.attributes.location) { const location = this.n.attributes.location as Location; - // Hard-coded range. Not present... likely not serialized correctly. item.command = { command: "vscode.open", title: "Navigate", diff --git a/vscode-extensions/vscode-spring-boot/lib/explorer/structure-tree-manager.ts b/vscode-extensions/vscode-spring-boot/lib/explorer/structure-tree-manager.ts index afa7d5079c..0d016ac110 100644 --- a/vscode-extensions/vscode-spring-boot/lib/explorer/structure-tree-manager.ts +++ b/vscode-extensions/vscode-spring-boot/lib/explorer/structure-tree-manager.ts @@ -1,5 +1,6 @@ -import { commands, EventEmitter, Event } from "vscode"; +import { commands, EventEmitter, Event, ExtensionContext, Disposable, window, TreeItemCollapsibleState, TreeItem, QuickPickItem, QuickPickOptions, Memento, workspace } from "vscode"; import { SpringNode, StereotypedNode } from "./nodes"; +import { ExplorerTreeProvider } from "./explorer-tree-provider"; const SPRING_STRUCTURE_CMD = "sts/spring-boot/structure"; @@ -7,6 +8,43 @@ export class StructureManager { private _rootElements: Thenable private _onDidChange: EventEmitter = new EventEmitter(); + private workspaceState: Memento; + + constructor(context: ExtensionContext) { + this.workspaceState = context.workspaceState; + context.subscriptions.push(commands.registerCommand("vscode-spring-boot.structure.refresh", () => this.refresh(true))); + context.subscriptions.push(commands.registerCommand("vscode-spring-boot.structure.openReference", (node) => { + if (node && node.getReferenceValue) { + const reference = node.getReferenceValue(); + if (reference) { + // Reference is a specific URL that should be passed to java.open.file command + commands.executeCommand('java.open.file', reference); + } + } + })); + + context.subscriptions.push(commands.registerCommand("vscode-spring-boot.structure.grouping", async (node: StereotypedNode) => { + const projectName = node.getNodeId(); + const groups = await commands.executeCommand("sts/spring-boot/structure/groups", projectName); + const initialGroups: string[] | undefined = this.getVisibleGroups(projectName); + const items = (groups?.groups || []).map(g => ({ + label: g.displayName, + group: g, + description: g.identifier, + picked: initialGroups ? initialGroups.includes(g.identifier) : true + } as GroupQuickPickItem)); + const selectedGroupItems = await window.showQuickPick(items, { + canPickMany: true, + ignoreFocusOut: true, + title: `Select groups to show/hide for project ${projectName}`, + placeHolder: 'Select groups to show/hide' + }); + if (selectedGroupItems) { + await this.setVisibleGroups(projectName, items.length === selectedGroupItems.length ? undefined : selectedGroupItems.map(i => i.group.identifier)); + this.refresh(false); + } + })); + } get rootElements(): Thenable { return this._rootElements; @@ -16,8 +54,7 @@ export class StructureManager { this._rootElements = commands.executeCommand(SPRING_STRUCTURE_CMD, { "updateMetadata" : updateMetadata, - "groups" : [ - ] + "groups" : this.getGroupings() }).then(json => { const nodes = this.parseArray(json); this._onDidChange.fire(undefined); @@ -40,9 +77,48 @@ export class StructureManager { return this._onDidChange.event; } + private getVisibleGroups(projectName: string): string[] | undefined { + const groupings = this.getGroupings(); + return groupings ? groupings[projectName] : undefined; + } + + private getGroupings(): Record | undefined { + return this.workspaceState.get>(`vscode-spring-boot.structure.group`, undefined); + } + + private async setVisibleGroups(projectName: string, groups: string[] | undefined): Promise { + let groupings = this.getGroupings(); + if (groupings) { + if (groups) { + groupings[projectName] = groups; + } else { + delete groupings[projectName]; + } + } else { + if (groups) { + groupings = { [projectName]: groups }; + } + } + await this.workspaceState.update(`vscode-spring-boot.structure.group`, groupings); + } + } export interface LsStereoTypedNode { readonly attributes: Record; readonly children: LsStereoTypedNode[]; -} \ No newline at end of file +} + +interface Group { + identifier: string; + displayName: string; +} + +interface Groups { + projectName: string; + groups?: Group[]; +} + +interface GroupQuickPickItem extends QuickPickItem { + group: Group; +} diff --git a/vscode-extensions/vscode-spring-boot/package.json b/vscode-extensions/vscode-spring-boot/package.json index bdc85d5368..98a07f8918 100644 --- a/vscode-extensions/vscode-spring-boot/package.json +++ b/vscode-extensions/vscode-spring-boot/package.json @@ -209,6 +209,11 @@ "command": "vscode-spring-boot.structure.openReference", "when": "view == explorer.spring && viewItem == stereotypedNodeWithReference", "group": "inline" + }, + { + "command": "vscode-spring-boot.structure.grouping", + "when": "view == explorer.spring && viewItem == project", + "group": "inline" } ] }, @@ -309,6 +314,12 @@ "title": "Open Reference", "category": "Spring Boot", "icon": "$(link-external)" + }, + { + "command": "vscode-spring-boot.structure.grouping", + "title": "Select Groups to Show/Hide", + "category": "Spring Boot", + "icon": "$(checklist)" } ], "configuration": [