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..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; @@ -97,16 +96,13 @@ 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 + 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) { @@ -123,39 +119,7 @@ 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 new file mode 100644 index 0000000000..3571029fa8 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/Misc.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * 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 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; + + +public class Misc { + + 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) { + // 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 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 valid URL parameter."); + }); + }); + } + +} diff --git a/vscode-extensions/vscode-spring-boot/lib/Main.ts b/vscode-extensions/vscode-spring-boot/lib/Main.ts index 59e58811b3..4dece1d3e9 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,13 @@ 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:...` (load JAR content via Boot LS) + context.subscriptions.push(workspace.registerTextDocumentContentProvider('spring-boot-ls', new (class implements TextDocumentContentProvider { + provideTextDocumentContent(uri: Uri) { + return commands.executeCommand('sts/jar/fetch-content', 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..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 @@ -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, Uri, QuickPickItem } from "vscode"; import { SpringNode, StereotypedNode } from "./nodes"; -import { ExplorerTreeProvider } from "./explorer-tree-provider"; const SPRING_STRUCTURE_CMD = "sts/spring-boot/structure"; @@ -17,8 +16,7 @@ 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); + commands.executeCommand('vscode.open', Uri.parse(reference)); } } }));