Skip to content

Commit 56ea4b2

Browse files
committed
Open JAR resource (reference) command - WIP
1 parent 333874b commit 56ea4b2

File tree

9 files changed

+204
-14
lines changed

9 files changed

+204
-14
lines changed

eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/LogicalStructureView.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ public void createPartControl(Composite parent) {
9797
if (o instanceof StereotypeNode) {
9898
StereotypeNode n = (StereotypeNode) o;
9999
Location l = n.location();
100+
if (l == null) {
101+
l = n.reference();
102+
}
100103
if (l != null) {
101104
LSPEclipseUtils.openInEditor(l);
102105
}

eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StereotypeNode.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
*
2121
* @author Alex Boyko
2222
*/
23-
record StereotypeNode(String id, String text, String icon, Location location, String reference, Map<String, Object> attributes, StereotypeNode[] children) {
23+
record StereotypeNode(String id, String text, String icon, Location location, Location reference, Map<String, Object> attributes, StereotypeNode[] children) {
2424

2525
@Override
2626
public boolean equals(Object obj) {

eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StereotypeNodeDeserializer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public StereotypeNode deserialize(JsonElement json, Type type, JsonDeserializati
4242
extractString(attributes, TEXT),
4343
extractString(attributes, ICON),
4444
context.deserialize(attributes.get(LOCATION), Location.class),
45-
extractString(attributes, REFERENCE),
45+
context.deserialize(attributes.get(REFERENCE), Location.class),
4646
context.deserialize(attributes, new TypeToken<Map<String, Object>>(){}.getType()),
4747
context.deserialize(object.get(CHILDREN), StereotypeNode[].class)
4848
);

eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/plugin.xml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@
103103
class="org.springframework.tooling.ls.eclipse.commons.commands.ExplainwithAi"
104104
commandId="vscode-spring-boot.query.explain">
105105
</handler>
106+
<handler
107+
class="org.springframework.tooling.ls.eclipse.commons.commands.OpenJarEntryInEditor"
108+
commandId="org.springframework.tooling.ls.eclipse.commons.commands.OpenJarEntryInEditor">
109+
</handler>
106110
</extension>
107111
<extension
108112
point="org.eclipse.ui.commands">
@@ -177,6 +181,20 @@
177181
typeId="org.eclipse.lsp4e.commandParameterType">
178182
</commandParameter>
179183
</command>
184+
<command
185+
id="org.springframework.tooling.ls.eclipse.commons.commands.OpenJarEntryInEditor"
186+
name="Open Jar Entry in Editor">
187+
<commandParameter
188+
id="jarUri"
189+
name="jarUri"
190+
optional="false">
191+
</commandParameter>
192+
<commandParameter
193+
id="projectName"
194+
name="projectName"
195+
optional="false">
196+
</commandParameter>
197+
</command>
180198
</extension>
181199
<extension
182200
point="org.eclipse.e4.ui.css.swt.theme">
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Broadcom, Inc.
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v1.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-v10.html
7+
*
8+
* Contributors:
9+
* Broadcom, Inc. - initial API and implementation
10+
*******************************************************************************/
11+
package org.springframework.tooling.ls.eclipse.commons.commands;
12+
13+
import java.net.URI;
14+
import java.util.Optional;
15+
16+
import org.eclipse.core.commands.AbstractHandler;
17+
import org.eclipse.core.commands.ExecutionEvent;
18+
import org.eclipse.core.commands.ExecutionException;
19+
import org.eclipse.core.commands.IHandler;
20+
import org.eclipse.core.resources.IProject;
21+
import org.eclipse.core.resources.ResourcesPlugin;
22+
import org.eclipse.core.runtime.IPath;
23+
import org.eclipse.core.runtime.IStatus;
24+
import org.eclipse.core.runtime.Path;
25+
import org.eclipse.core.runtime.Status;
26+
import org.eclipse.jdt.core.IClassFile;
27+
import org.eclipse.jdt.core.IJarEntryResource;
28+
import org.eclipse.jdt.core.IJavaProject;
29+
import org.eclipse.jdt.core.IPackageFragment;
30+
import org.eclipse.jdt.core.IPackageFragmentRoot;
31+
import org.eclipse.jdt.core.JavaCore;
32+
import org.eclipse.jdt.core.JavaModelException;
33+
import org.eclipse.jdt.internal.ui.javaeditor.EditorUtility;
34+
import org.eclipse.ui.PartInitException;
35+
import org.springframework.tooling.ls.eclipse.commons.LanguageServerCommonsActivator;
36+
37+
@SuppressWarnings("restriction")
38+
public class OpenJarEntryInEditor extends AbstractHandler implements IHandler {
39+
40+
private static final String JAR_URI_PARAM = "jarUri";
41+
42+
record JarUri(IPath jar, IPath path) {}
43+
44+
private static JarUri createJarUri(URI uri) {
45+
if (!"jar".equals(uri.getScheme())) {
46+
throw new IllegalArgumentException();
47+
}
48+
String s = uri.getSchemeSpecificPart();
49+
int idx = s.indexOf('!');
50+
if (idx <= 0) {
51+
throw new IllegalArgumentException();
52+
}
53+
return new JarUri(new Path(URI.create(s.substring(0, idx)).getPath()), new Path(s.substring(idx + 1)));
54+
}
55+
56+
@Override
57+
public Object execute(ExecutionEvent event) throws ExecutionException {
58+
String projectName = event.getParameter(OpenJavaElementInEditor.PROJECT_NAME);
59+
String jarUriStr = event.getParameter(JAR_URI_PARAM);
60+
61+
IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(projectName);
62+
63+
if (project != null && jarUriStr != null) {
64+
URI uri = URI.create(jarUriStr);
65+
IJavaProject javaProject = JavaCore.create(project);
66+
if (javaProject != null) {
67+
JarUri jarUri = createJarUri(uri);
68+
findJavaObj(javaProject, jarUri).ifPresent(s -> {
69+
try {
70+
EditorUtility.openInEditor(s, true);
71+
} catch (PartInitException e) {
72+
LanguageServerCommonsActivator.getInstance().getLog().log(e.getStatus());
73+
}
74+
});
75+
} else {
76+
LanguageServerCommonsActivator.getInstance().getLog().log(new Status(IStatus.WARNING,
77+
LanguageServerCommonsActivator.PLUGIN_ID, "Cannot find project: " + projectName));
78+
}
79+
}
80+
81+
return null;
82+
}
83+
84+
private static Optional<Object> findJavaObj(IJavaProject j, JarUri uri) {
85+
try {
86+
for (IPackageFragmentRoot fr : j.getAllPackageFragmentRoots()) {
87+
if (uri.jar().equals(fr.getPath())) {
88+
for (Object o : fr.getNonJavaResources()) {
89+
if (o instanceof IJarEntryResource je) {
90+
return findJarEntry(je, uri.path());
91+
}
92+
}
93+
if ("class".equals(uri.path().getFileExtension())) {
94+
String packageName = uri.path().removeLastSegments(1).toString().replace(IPath.SEPARATOR, '.');
95+
IPackageFragment pkg = fr.getPackageFragment(packageName);
96+
if (pkg != null) {
97+
IClassFile cf = pkg.getClassFile(uri.path().lastSegment());
98+
if (cf != null) {
99+
return Optional.of(cf);
100+
}
101+
}
102+
}
103+
104+
}
105+
}
106+
} catch (JavaModelException e) {
107+
LanguageServerCommonsActivator.getInstance().getLog().log(e.getStatus());
108+
}
109+
return Optional.empty();
110+
}
111+
112+
private static Optional<Object> findJarEntry(IJarEntryResource r, IPath p) {
113+
if (r.getFullPath().equals(p)) {
114+
return Optional.of(r);
115+
} else if (r.getFullPath().isPrefixOf(p)) {
116+
for (IJarEntryResource c : r.getChildren()) {
117+
Optional<Object> opt = findJarEntry(c, p);
118+
if (opt.isPresent()) {
119+
return opt;
120+
}
121+
}
122+
}
123+
return Optional.empty();
124+
}
125+
126+
}

eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/commands/OpenJavaElementInEditor.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2018, 2019 Pivotal, Inc.
2+
* Copyright (c) 2018, 2025 Pivotal, Inc.
33
* All rights reserved. This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v1.0
55
* which accompanies this distribution, and is available at
@@ -35,7 +35,7 @@
3535
public class OpenJavaElementInEditor extends AbstractHandler {
3636

3737
private static final String BINDING_KEY = "bindingKey";
38-
private static final String PROJECT_NAME = "projectName";
38+
static final String PROJECT_NAME = "projectName";
3939

4040
@Override
4141
public Object execute(ExecutionEvent event) throws ExecutionException {

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

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,20 @@
2929

3030
import org.eclipse.lsp4j.DocumentSymbol;
3131
import org.eclipse.lsp4j.Location;
32+
import org.eclipse.lsp4j.Range;
3233
import org.jmolecules.stereotype.api.Stereotype;
3334
import org.jmolecules.stereotype.catalog.StereotypeCatalog;
3435
import org.jmolecules.stereotype.tooling.LabelProvider;
3536
import org.jmolecules.stereotype.tooling.MethodNodeContext;
3637
import org.jmolecules.stereotype.tooling.NodeContext;
3738
import org.jmolecules.stereotype.tooling.NodeHandler;
39+
import org.springframework.ide.vscode.boot.java.links.EclipseSourceLinks;
3840
import org.springframework.ide.vscode.boot.java.stereotypes.StereotypeClassElement;
3941
import org.springframework.ide.vscode.boot.java.stereotypes.StereotypeMethodElement;
4042
import org.springframework.ide.vscode.boot.java.stereotypes.StereotypePackageElement;
4143
import org.springframework.ide.vscode.commons.java.IJavaProject;
44+
import org.springframework.ide.vscode.commons.languageserver.util.LspClient;
45+
import org.springframework.ide.vscode.commons.languageserver.util.LspClient.Client;
4246
import org.springframework.ide.vscode.commons.protocol.spring.SymbolElement;
4347

4448
import com.google.gson.Gson;
@@ -94,30 +98,35 @@ public void handleStereotype(Stereotype stereotype, NodeContext context) {
9498
var definition = catalog.getDefinition(stereotype);
9599
var sources = definition.getSources();
96100

97-
String reference = null;
101+
Location reference = null;
98102
for (Object source : sources) {
99103
if (source instanceof URL) {
100104
URL url = (URL) source;
105+
101106
try {
102-
reference = url.toURI().toASCIIString();
103-
if (reference.startsWith(Misc.JAR_URL_PROTOCOL_PREFIX)) {
104-
reference = reference.replaceFirst(Misc.JAR_URL_PROTOCOL_PREFIX, Misc.BOOT_LS_URL_PRTOCOL_PREFIX);
107+
reference = new Location(url.toURI().toASCIIString(), new Range());
108+
if ("jar".equals(url.getProtocol())) {
109+
if (LspClient.currentClient() == Client.ECLIPSE) {
110+
reference.setUri(EclipseSourceLinks.eclipseIntroUriForJarEntry(project.getElementName(), url.toURI()).toASCIIString());
111+
} else {
112+
reference.setUri(url.toURI().toASCIIString().replaceFirst(Misc.JAR_URL_PROTOCOL_PREFIX, Misc.BOOT_LS_URL_PRTOCOL_PREFIX));
113+
}
105114
}
106115
} catch (URISyntaxException e) {
107116
// something went wrong
108117
}
109118
}
110119
else if (source instanceof Location) {
111-
reference = ((Location) source).getUri();
120+
reference = (Location) source;
112121
}
113122
}
114123

115-
final String referenceUri = reference;
124+
final Location referenceLocation = reference;
116125
addChild(node -> node
117126
.withAttribute(TEXT, labels.getStereotypeLabel(stereotype))
118127
.withAttribute(ICON, StereotypeIcons.getIcon(stereotype))
119128
.withAttribute(HOVER, "defined in: " + sources.toString())
120-
.withAttribute(REFERENCE, referenceUri)
129+
.withAttribute(REFERENCE, referenceLocation)
121130
);
122131
}
123132
}
@@ -211,7 +220,7 @@ private static void assignNodeId(Node n, Node p) {
211220
Location location = (Location) n.attributes.get(LOCATION);
212221
String locationId = location == null ? "" : "%s:%d:%d".formatted(location.getUri(), location.getRange().getStart().getLine(), location.getRange().getStart().getCharacter());
213222

214-
String referenceId = n.attributes.containsKey(REFERENCE) ? (String) n.attributes.get(REFERENCE) : "";
223+
String referenceId = n.attributes.containsKey(REFERENCE) ? ((Location) n.attributes.get(REFERENCE)).getUri() : "";
215224

216225
String nodeSpecificId = "%s|%s|%s".formatted(textId, locationId, referenceId).replaceAll("\\|+$", "");
217226

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ public class EclipseSourceLinks implements SourceLinks {
4444
private static final String RESOURCE_COMMAND = "org.springframework.tooling.ls.eclipse.commons.commands.OpenResourceInEditor";
4545
private static final String PATH = "path";
4646

47+
private static final String JAR_ENTRY_COMMAND = "org.springframework.tooling.ls.eclipse.commons.commands.OpenJarEntryInEditor";
48+
private static final String JAR_URI_PARAM = "jarUri";
49+
4750
private static final Logger log = LoggerFactory.getLogger(EclipseSourceLinks.class);
4851

4952
private JavaProjectFinder projectFinder;
@@ -130,5 +133,30 @@ public static URI eclipseIntroUri(IJavaProject project, IMember member) {
130133
}
131134
return null;
132135
}
136+
137+
public static URI eclipseIntroUriForJarEntry(String projectName, URI jarEntryUri) {
138+
try {
139+
StringBuilder paramBuilder = new StringBuilder(JAR_ENTRY_COMMAND);
140+
141+
paramBuilder.append(PARAMETERS_START);
142+
paramBuilder.append(JAR_URI_PARAM);
143+
paramBuilder.append(EQUALS);
144+
paramBuilder.append(jarEntryUri.toString());
145+
146+
paramBuilder.append(PARAMETERS_SEPARATOR);
147+
paramBuilder.append(PROJECT_NAME_PARAMETER_ID);
148+
paramBuilder.append(EQUALS);
149+
paramBuilder.append(projectName);
150+
151+
paramBuilder.append(PARAMETERS_END);
152+
153+
StringBuilder urlBuilder = new StringBuilder(URL_PREFIX);
154+
urlBuilder.append(URLEncoder.encode(paramBuilder.toString(), "UTF8"));
155+
return URI.create(urlBuilder.toString());
156+
} catch (UnsupportedEncodingException e) {
157+
log.error("{}", e);
158+
}
159+
return null;
160+
}
133161

134162
}

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

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

47
const SPRING_STRUCTURE_CMD = "sts/spring-boot/structure";
58

@@ -9,14 +12,14 @@ export class StructureManager {
912
private _onDidChange: EventEmitter<SpringNode | undefined> = new EventEmitter<SpringNode | undefined>();
1013
private workspaceState: Memento;
1114

12-
constructor(context: ExtensionContext) {
15+
constructor(context: ExtensionContext, api: ExtensionAPI) {
1316
this.workspaceState = context.workspaceState;
1417
context.subscriptions.push(commands.registerCommand("vscode-spring-boot.structure.refresh", () => this.refresh(true)));
1518
context.subscriptions.push(commands.registerCommand("vscode-spring-boot.structure.openReference", (node) => {
1619
if (node && node.getReferenceValue) {
1720
const reference = node.getReferenceValue();
1821
if (reference) {
19-
commands.executeCommand('vscode.open', Uri.parse(reference));
22+
commands.executeCommand('vscode.open', api.client.protocol2CodeConverter.asLocation(reference as ls.Location));
2023
}
2124
}
2225
}));
@@ -42,6 +45,9 @@ export class StructureManager {
4245
this.refresh(false);
4346
}
4447
}));
48+
49+
context.subscriptions.push(api.getSpringIndex().onSpringIndexUpdated(e => this.refresh(false)));
50+
4551
}
4652

4753
get rootElements(): Thenable<SpringNode[]> {

0 commit comments

Comments
 (0)