From 2eef65c815b4a0ee3ce9d51dbce264c2ef0b7c85 Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Thu, 2 Oct 2025 16:03:05 -0700 Subject: [PATCH 1/5] Open default stereotype definition JSON Signed-off-by: BoykoAlex --- .../ide/vscode/boot/app/CommandsConfig.java | 10 ++++-- .../boot/java/commands/JsonNodeHandler.java | 30 +++++++++------- .../ide/vscode/boot/java/commands/Misc.java | 36 +++++++++++++++++++ .../ProjectBasedCatalogSource.java | 10 ++++++ .../vscode-spring-boot/lib/Main.ts | 13 ++++++- .../lib/explorer/structure-tree-manager.ts | 15 +++++--- 6 files changed, 94 insertions(+), 20 deletions(-) create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/Misc.java diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/CommandsConfig.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/CommandsConfig.java index 6956ee5c51..e736737d68 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/CommandsConfig.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/CommandsConfig.java @@ -13,6 +13,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; +import org.springframework.ide.vscode.boot.java.commands.Misc; import org.springframework.ide.vscode.boot.java.commands.SpringIndexCommands; import org.springframework.ide.vscode.boot.java.commands.WorkspaceBootExecutableProjects; import org.springframework.ide.vscode.boot.java.stereotypes.StereotypeCatalogRegistry; @@ -31,6 +32,11 @@ public class CommandsConfig { SpringIndexCommands springIndexCommands(SimpleLanguageServer server, JavaProjectFinder projectFinder, SpringMetamodelIndex symbolIndex, ModulithService modulithService, StereotypeCatalogRegistry stereotypeCatalogRegistry) { return new SpringIndexCommands(server, symbolIndex, modulithService, projectFinder, stereotypeCatalogRegistry); - } - + } + + @Bean + Misc misc(SimpleLanguageServer server) { + return new Misc(server); + } + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/JsonNodeHandler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/JsonNodeHandler.java index 6233353cea..4d55c4ae51 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/JsonNodeHandler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/JsonNodeHandler.java @@ -36,6 +36,7 @@ import org.jmolecules.stereotype.tooling.MethodNodeContext; import org.jmolecules.stereotype.tooling.NodeContext; import org.jmolecules.stereotype.tooling.NodeHandler; +import org.springframework.ide.vscode.boot.java.stereotypes.ProjectBasedCatalogSource; import org.springframework.ide.vscode.boot.java.stereotypes.StereotypeClassElement; import org.springframework.ide.vscode.boot.java.stereotypes.StereotypeMethodElement; import org.springframework.ide.vscode.boot.java.stereotypes.StereotypePackageElement; @@ -97,17 +98,22 @@ public void handleStereotype(Stereotype stereotype, NodeContext context) { for (Object source : sources) { if (source instanceof URL) { URL url = (URL) source; - if (url.getProtocol().equals("jar")) { - reference = convertUrlToJdtUri((URL) source, project.getElementName()); - } - else if (url.getProtocol().equals("file")) { - try { - reference = url.toURI().toASCIIString(); - } - catch (URISyntaxException e) { - // something went wrong - } - } + reference = ProjectBasedCatalogSource.getDefaultStereotypePath(url) + .map(p -> "spring-boot-ls://resource" + p) + .orElseGet(() -> { + if (url.getProtocol().equals("jar")) { + return convertUrlToJdtUri((URL) source, project.getElementName()); + } else if (url.getProtocol().equals("file")) { + try { + return url.toURI().toASCIIString(); + } + catch (URISyntaxException e) { + // something went wrong + } + } + return null; + }); + } else if (source instanceof Location) { reference = ((Location) source).getUri(); @@ -123,7 +129,7 @@ else if (source instanceof Location) { ); } } - + private String convertUrlToJdtUri(URL url, String projectName) { try { URI uri = url.toURI(); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/Misc.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/Misc.java new file mode 100644 index 0000000000..0c3b9d55fa --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/Misc.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2025 Broadcom, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.commands; + +import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; + +import com.google.gson.JsonElement; + +import io.micrometer.core.instrument.util.IOUtils; + +public class Misc { + + private static final String STS_FETCH_CONTENT = "sts/resource/fetch-content"; + + public Misc(SimpleLanguageServer server) { + server.onCommand(STS_FETCH_CONTENT, params -> { + return server.getAsync().invoke(() -> { + if (params.getArguments().size() == 1) { + Object o = params.getArguments().get(0); + String r = o instanceof JsonElement ? ((JsonElement) o).getAsString() : o.toString(); + return IOUtils.toString(getClass().getResourceAsStream(r)); + } + throw new IllegalArgumentException("The command must have one parameter."); + }); + }); + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/ProjectBasedCatalogSource.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/ProjectBasedCatalogSource.java index 7e46d30c39..768c95a848 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/ProjectBasedCatalogSource.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/ProjectBasedCatalogSource.java @@ -16,6 +16,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Optional; import java.util.jar.JarFile; import java.util.stream.Stream; import java.util.zip.ZipEntry; @@ -41,6 +42,15 @@ public class ProjectBasedCatalogSource implements CatalogSource { public ProjectBasedCatalogSource(IJavaProject project) { this.project = project; } + + public static Optional getDefaultStereotypePath(URL url) { + for (String p : DEFAULT_STEREOTYPE_DEFINITIONS) { + if (url.equals(ProjectBasedCatalogSource.class.getResource(p))) { + return Optional.of(p); + } + } + return Optional.empty(); + } @Override public Stream getSources() { diff --git a/vscode-extensions/vscode-spring-boot/lib/Main.ts b/vscode-extensions/vscode-spring-boot/lib/Main.ts index 59e58811b3..836876202d 100644 --- a/vscode-extensions/vscode-spring-boot/lib/Main.ts +++ b/vscode-extensions/vscode-spring-boot/lib/Main.ts @@ -8,7 +8,8 @@ import { ExtensionContext, Uri, lm, - TreeItemCollapsibleState + TreeItemCollapsibleState, + TextDocumentContentProvider } from 'vscode'; import * as commons from '@pivotal-tools/commons-vscode'; @@ -191,6 +192,16 @@ export function activate(context: ExtensionContext): Thenable { context.subscriptions.push(commands.registerCommand('vscode-spring-boot.agent.apply', applyLspEdit)); + // Register content loader for URIs of type `spring-boot-ls://resource/...` (load boot ls classpath resources) + context.subscriptions.push(workspace.registerTextDocumentContentProvider('spring-boot-ls', new (class implements TextDocumentContentProvider { + provideTextDocumentContent(uri: Uri) { + if (uri.authority === 'resource') { + return commands.executeCommand('sts/resource/fetch-content', uri.fsPath); + } + throw new Error(`Unsupported uri: ${uri.toString()}`); + } + })())); + const api = new ApiManager(client).api context.subscriptions.push(api.getSpringIndex().onSpringIndexUpdated(e => structureManager.refresh(false))); 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 74b157e113..c6c1bd3e9b 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,6 +1,5 @@ -import { commands, EventEmitter, Event, ExtensionContext, Disposable, window, TreeItemCollapsibleState, TreeItem, QuickPickItem, QuickPickOptions, Memento, workspace } from "vscode"; +import { commands, EventEmitter, Event, ExtensionContext, window, Memento } from "vscode"; import { SpringNode, StereotypedNode } from "./nodes"; -import { ExplorerTreeProvider } from "./explorer-tree-provider"; const SPRING_STRUCTURE_CMD = "sts/spring-boot/structure"; @@ -17,14 +16,20 @@ export class StructureManager { 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); + const uri = Uri.parse(reference); + if (uri.scheme === 'jdt') { + // Reference is a specific URL that should be passed to java.open.file command + commands.executeCommand('java.open.file', reference); + } else { + // Reference is a specific URL that should be passed to vscode.open command + commands.executeCommand('vscode.open', uri); + } } } })); context.subscriptions.push(commands.registerCommand("vscode-spring-boot.structure.grouping", async (node: StereotypedNode) => { - const projectName = node.getProjectId(); + 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 => ({ From a70ac81bfdd3a708a81d0a19b1149357d013067b Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Thu, 2 Oct 2025 16:42:50 -0700 Subject: [PATCH 2/5] Fix issues Signed-off-by: BoykoAlex --- .../vscode-spring-boot/lib/explorer/structure-tree-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c6c1bd3e9b..b6f12852d4 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,4 +1,4 @@ -import { commands, EventEmitter, Event, ExtensionContext, window, Memento } from "vscode"; +import { commands, EventEmitter, Event, ExtensionContext, window, Memento, Uri, QuickPickItem } from "vscode"; import { SpringNode, StereotypedNode } from "./nodes"; const SPRING_STRUCTURE_CMD = "sts/spring-boot/structure"; From 3a8ce2b62ecd30ef37f77751486a768a6591701e Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Fri, 3 Oct 2025 11:55:24 -0700 Subject: [PATCH 3/5] Support fetching JAR url content via spring-boot-ls uri Signed-off-by: BoykoAlex --- .../boot/java/commands/JsonNodeHandler.java | 58 +++---------------- .../ide/vscode/boot/java/commands/Misc.java | 26 +++++++-- .../ProjectBasedCatalogSource.java | 10 ---- .../vscode-spring-boot/lib/Main.ts | 7 +-- 4 files changed, 30 insertions(+), 71 deletions(-) diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/JsonNodeHandler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/JsonNodeHandler.java index 4d55c4ae51..a45dfd9196 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/JsonNodeHandler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/JsonNodeHandler.java @@ -16,7 +16,6 @@ package org.springframework.ide.vscode.boot.java.commands; -import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; @@ -36,7 +35,6 @@ import org.jmolecules.stereotype.tooling.MethodNodeContext; import org.jmolecules.stereotype.tooling.NodeContext; import org.jmolecules.stereotype.tooling.NodeHandler; -import org.springframework.ide.vscode.boot.java.stereotypes.ProjectBasedCatalogSource; import org.springframework.ide.vscode.boot.java.stereotypes.StereotypeClassElement; import org.springframework.ide.vscode.boot.java.stereotypes.StereotypeMethodElement; import org.springframework.ide.vscode.boot.java.stereotypes.StereotypePackageElement; @@ -98,22 +96,14 @@ public void handleStereotype(Stereotype stereotype, NodeContext context) { for (Object source : sources) { if (source instanceof URL) { URL url = (URL) source; - reference = ProjectBasedCatalogSource.getDefaultStereotypePath(url) - .map(p -> "spring-boot-ls://resource" + p) - .orElseGet(() -> { - if (url.getProtocol().equals("jar")) { - return convertUrlToJdtUri((URL) source, project.getElementName()); - } else if (url.getProtocol().equals("file")) { - try { - return url.toURI().toASCIIString(); - } - catch (URISyntaxException e) { - // something went wrong - } - } - return null; - }); - + try { + reference = url.toURI().toASCIIString(); + if (reference.startsWith(Misc.JAR_URL_PROTOCOL_PREFIX)) { + reference = reference.replaceFirst(Misc.JAR_URL_PROTOCOL_PREFIX, Misc.BOOT_LS_URL_PRTOCOL_PREFIX); + } + } catch (URISyntaxException e) { + // something went wrong + } } else if (source instanceof Location) { reference = ((Location) source).getUri(); @@ -130,38 +120,6 @@ else if (source instanceof Location) { } } - private String convertUrlToJdtUri(URL url, String projectName) { - try { - URI uri = url.toURI(); - - // Extract the scheme-specific part (everything after "jar:") - String schemeSpecificPart = uri.getSchemeSpecificPart(); - - // Split on "!/" to separate jar file path from internal path - String[] parts = schemeSpecificPart.split("!/", 2); - - if (parts.length != 2) { - return null; - } - - String jarFilePath = parts[0]; - String internalPath = parts[1]; - - // Remove "file:" prefix from jar file path if present - if (jarFilePath.startsWith("file:")) { - jarFilePath = jarFilePath.substring(5); - } - - String jarFileName = jarFilePath.substring(jarFilePath.lastIndexOf('/') + 1); - - // Construct the JDT URI with just the jar file name - return "jdt://contents/" + jarFileName + "/" + internalPath + "?=" + projectName; - - } catch (URISyntaxException e) { - return null; - } - } - @Override public void handleApplication(A application) { this.root diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/Misc.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/Misc.java index 0c3b9d55fa..3571029fa8 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/Misc.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/Misc.java @@ -10,25 +10,39 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.commands; +import java.io.InputStream; +import java.net.URI; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; + import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; +import org.springframework.ide.vscode.commons.util.IOUtil; import com.google.gson.JsonElement; -import io.micrometer.core.instrument.util.IOUtils; public class Misc { - private static final String STS_FETCH_CONTENT = "sts/resource/fetch-content"; + public static final String BOOT_LS_URL_PRTOCOL_PREFIX = "spring-boot-ls:"; + public static final String JAR_URL_PROTOCOL_PREFIX = "jar:"; + + private static final String STS_FETCH_JAR_CONTENT = "sts/jar/fetch-content"; public Misc(SimpleLanguageServer server) { - server.onCommand(STS_FETCH_CONTENT, params -> { + // Fetch JAR content via a special protocol `spring-boot-ls` as we don't want to handle all JARs in VSCode + server.onCommand(STS_FETCH_JAR_CONTENT, params -> { return server.getAsync().invoke(() -> { if (params.getArguments().size() == 1) { Object o = params.getArguments().get(0); - String r = o instanceof JsonElement ? ((JsonElement) o).getAsString() : o.toString(); - return IOUtils.toString(getClass().getResourceAsStream(r)); + String s = o instanceof JsonElement ? ((JsonElement) o).getAsString() : o.toString(); + if (s.startsWith(BOOT_LS_URL_PRTOCOL_PREFIX)) { + s = s.replaceFirst(BOOT_LS_URL_PRTOCOL_PREFIX, JAR_URL_PROTOCOL_PREFIX); + URI uri = URI.create(URLDecoder.decode(s, StandardCharsets.UTF_8)); + // Java has support for JAR URLs + return IOUtil.toString((InputStream) uri.toURL().getContent()); + } } - throw new IllegalArgumentException("The command must have one parameter."); + throw new IllegalArgumentException("The command must have one valid URL parameter."); }); }); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/ProjectBasedCatalogSource.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/ProjectBasedCatalogSource.java index 768c95a848..7e46d30c39 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/ProjectBasedCatalogSource.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/ProjectBasedCatalogSource.java @@ -16,7 +16,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.Optional; import java.util.jar.JarFile; import java.util.stream.Stream; import java.util.zip.ZipEntry; @@ -42,15 +41,6 @@ public class ProjectBasedCatalogSource implements CatalogSource { public ProjectBasedCatalogSource(IJavaProject project) { this.project = project; } - - public static Optional getDefaultStereotypePath(URL url) { - for (String p : DEFAULT_STEREOTYPE_DEFINITIONS) { - if (url.equals(ProjectBasedCatalogSource.class.getResource(p))) { - return Optional.of(p); - } - } - return Optional.empty(); - } @Override public Stream getSources() { diff --git a/vscode-extensions/vscode-spring-boot/lib/Main.ts b/vscode-extensions/vscode-spring-boot/lib/Main.ts index 836876202d..4dece1d3e9 100644 --- a/vscode-extensions/vscode-spring-boot/lib/Main.ts +++ b/vscode-extensions/vscode-spring-boot/lib/Main.ts @@ -192,13 +192,10 @@ export function activate(context: ExtensionContext): Thenable { context.subscriptions.push(commands.registerCommand('vscode-spring-boot.agent.apply', applyLspEdit)); - // Register content loader for URIs of type `spring-boot-ls://resource/...` (load boot ls classpath resources) + // Register content loader for URIs of type `spring-boot-ls:...` (load JAR content via Boot LS) context.subscriptions.push(workspace.registerTextDocumentContentProvider('spring-boot-ls', new (class implements TextDocumentContentProvider { provideTextDocumentContent(uri: Uri) { - if (uri.authority === 'resource') { - return commands.executeCommand('sts/resource/fetch-content', uri.fsPath); - } - throw new Error(`Unsupported uri: ${uri.toString()}`); + return commands.executeCommand('sts/jar/fetch-content', uri.toString()); } })())); From 31b5102019e15a38f272951ff311a3b13f5f829a Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Fri, 3 Oct 2025 11:55:44 -0700 Subject: [PATCH 4/5] Missing change Signed-off-by: BoykoAlex --- .../lib/explorer/structure-tree-manager.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) 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 b6f12852d4..e6df760fdb 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 @@ -16,14 +16,7 @@ export class StructureManager { if (node && node.getReferenceValue) { const reference = node.getReferenceValue(); if (reference) { - const uri = Uri.parse(reference); - if (uri.scheme === 'jdt') { - // Reference is a specific URL that should be passed to java.open.file command - commands.executeCommand('java.open.file', reference); - } else { - // Reference is a specific URL that should be passed to vscode.open command - commands.executeCommand('vscode.open', uri); - } + commands.executeCommand('vscode.open', Uri.parse(reference)); } } })); From 15f3b4286b4a995b982575a4d710be535154cfc8 Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Fri, 3 Oct 2025 12:00:53 -0700 Subject: [PATCH 5/5] Use project id rather than node id Signed-off-by: BoykoAlex --- .../vscode-spring-boot/lib/explorer/structure-tree-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e6df760fdb..263c106a40 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 @@ -22,7 +22,7 @@ export class StructureManager { })); context.subscriptions.push(commands.registerCommand("vscode-spring-boot.structure.grouping", async (node: StereotypedNode) => { - const projectName = node.getNodeId(); + const projectName = node.getProjectId(); const groups = await commands.executeCommand("sts/spring-boot/structure/groups", projectName); const initialGroups: string[] | undefined = this.getVisibleGroups(projectName); const items = (groups?.groups || []).map(g => ({