Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -34,7 +35,7 @@ public JMoleculesStructureView(AbstractStereotypeCatalog catalog, CachedSpringMe
this.springIndex = springIndex;
}

public Node createTree(IJavaProject project, IndexBasedStereotypeFactory factory, List<String> selectedGroups) {
public Node createTree(IJavaProject project, IndexBasedStereotypeFactory factory, Collection<String> selectedGroups) {

StereotypePackageElement mainApplicationPackage = StructureViewUtil.identifyMainApplicationPackage(project, springIndex);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -35,7 +36,7 @@ public ModulithStructureView(AbstractStereotypeCatalog catalog, CachedSpringMeta
this.modulithService = modulithService;
}

public Node createTree(IJavaProject project, IndexBasedStereotypeFactory factory, List<String> selectedGroups) {
public Node createTree(IJavaProject project, IndexBasedStereotypeFactory factory, Collection<String> selectedGroups) {

var adapter = new ModulithStereotypeFactoryAdapter(factory);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {

Expand All @@ -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);
}));
}

Expand All @@ -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<String> selectedGroups) {
private Node nodeFrom(IJavaProject project, CachedSpringMetamodelIndex springIndex, boolean updateMetadata, Collection<String> selectedGroups) {
log.info("create structural view tree information for project: " + project.getElementName());

if (updateMetadata) {
Expand All @@ -103,11 +119,11 @@ private Node nodeFrom(IJavaProject project, CachedSpringMetamodelIndex springInd
}
}

private static record StructureCommandArgs(boolean updateMetadata, List<String> selectedGroups) {
private static record StructureCommandArgs(boolean updateMetadata, Map<String, Set<String>> selectedGroups) {

public static StructureCommandArgs parseFrom(ExecuteCommandParams params) {
boolean updateMetadata = false;
List<String> selectedGroups = null;
Map<String, Set<String>> selectedGroups = null;

List<Object> arguments = params.getArguments();
if (arguments != null && arguments.size() == 1) {
Expand All @@ -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<Map<String, Set<String>>>() {});
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gson#fromJson requires a Type for generic deserialization. Pass the Type from the TypeToken using .getType(), otherwise this code will not compile and will not deserialize correctly.

Suggested change
selectedGroups = new Gson().fromJson(groupsElement, new TypeToken<Map<String, Set<String>>>() {});
selectedGroups = new Gson().fromJson(groupsElement, new TypeToken<Map<String, Set<String>>>() {}.getType());

Copilot uses AI. Check for mistakes.
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -36,7 +37,7 @@

public class StructureViewUtil {

public static List<String[]> identifyGroupers(AbstractStereotypeCatalog catalog, List<String> selectedGroups) {
public static List<String[]> identifyGroupers(AbstractStereotypeCatalog catalog, Collection<String> selectedGroups) {

// List<String[]> allGroupsWithSpecificOrder = Arrays.asList(
// new String[] {"architecture"},
Expand Down
57 changes: 4 additions & 53 deletions vscode-extensions/vscode-spring-boot/lib/Main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -159,58 +159,9 @@ export function activate(context: ExtensionContext): Thenable<ExtensionAPI> {

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -17,6 +17,24 @@ export class ExplorerTreeProvider implements TreeDataProvider<SpringNode> {
});
}

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<TreeItem> {
const nodeId = element.getNodeId();
const savedState = this.expansionStates.get(nodeId);
Expand All @@ -35,11 +53,11 @@ export class ExplorerTreeProvider implements TreeDataProvider<SpringNode> {
}


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);
}

Expand Down
10 changes: 7 additions & 3 deletions vscode-extensions/vscode-spring-boot/lib/explorer/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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",
Expand Down
Loading