From f3a4788405c2d30219eeb4287d171e2471427e9a Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Wed, 15 Oct 2025 15:08:23 -0700 Subject: [PATCH 1/8] Eclipse Structure View (initial work) Signed-off-by: BoykoAlex --- .../icons/stereotypes/bell.svg | 1 + .../icons/stereotypes/bracket.svg | 1 + .../stereotypes/callhierarchy-incoming.svg | 1 + .../icons/stereotypes/circuit-board.svg | 1 + .../icons/stereotypes/coffee.svg | 1 + .../icons/stereotypes/database.svg | 1 + .../debug-breakpoint-data-unverified.svg | 1 + .../icons/stereotypes/debug-disconnect.svg | 1 + .../icons/stereotypes/file-binary.svg | 1 + .../icons/stereotypes/gear.svg | 1 + .../icons/stereotypes/globe.svg | 1 + .../icons/stereotypes/layers.svg | 1 + .../icons/stereotypes/library.svg | 1 + .../icons/stereotypes/lightbulb.svg | 1 + .../icons/stereotypes/link.svg | 1 + .../icons/stereotypes/package.svg | 1 + .../icons/stereotypes/project.svg | 1 + .../icons/stereotypes/record.svg | 1 + .../icons/stereotypes/search.svg | 1 + .../icons/stereotypes/symbol-class.svg | 1 + .../icons/stereotypes/symbol-field.svg | 1 + .../icons/stereotypes/symbol-interface.svg | 1 + .../icons/stereotypes/symbol-method.svg | 1 + .../icons/stereotypes/symbol-property.svg | 1 + .../icons/stereotypes/symbol-value.svg | 1 + .../icons/stereotypes/target.svg | 1 + .../icons/stereotypes/text-size.svg | 1 + .../icons/stereotypes/verified.svg | 1 + .../icons/stereotypes/zap.svg | 1 + .../plugin.xml | 12 +++ .../boot/ls/BootLanguageServerPlugin.java | 33 ++++++- .../tooling/boot/ls/BootLsState.java | 46 +++++++++ .../DelegatingStreamConnectionProvider.java | 14 ++- .../boot/ls/views/LogicalStructureView.java | 98 +++++++++++++++++++ .../tooling/boot/ls/views/RefreshAction.java | 32 ++++++ .../tooling/boot/ls/views/StereotypeNode.java | 64 ++++++++++++ .../boot/ls/views/StructureClient.java | 61 ++++++++++++ .../views/StructureTreeContentProvider.java | 52 ++++++++++ .../ls/views/StructureTreeLabelProvider.java | 46 +++++++++ .../jdt/ls/commons/BootProjectTracker.java | 43 ++++---- .../boot/java/commands/JsonNodeHandler.java | 20 +++- 41 files changed, 529 insertions(+), 21 deletions(-) create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/bell.svg create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/bracket.svg create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/callhierarchy-incoming.svg create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/circuit-board.svg create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/coffee.svg create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/database.svg create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/debug-breakpoint-data-unverified.svg create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/debug-disconnect.svg create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/file-binary.svg create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/gear.svg create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/globe.svg create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/layers.svg create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/library.svg create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/lightbulb.svg create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/link.svg create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/package.svg create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/project.svg create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/record.svg create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/search.svg create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/symbol-class.svg create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/symbol-field.svg create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/symbol-interface.svg create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/symbol-method.svg create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/symbol-property.svg create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/symbol-value.svg create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/target.svg create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/text-size.svg create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/verified.svg create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/zap.svg create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/BootLsState.java create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/LogicalStructureView.java create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/RefreshAction.java create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StereotypeNode.java create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StructureClient.java create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StructureTreeContentProvider.java create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StructureTreeLabelProvider.java diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/bell.svg b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/bell.svg new file mode 100644 index 0000000000..3c125639a9 --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/bell.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/bracket.svg b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/bracket.svg new file mode 100644 index 0000000000..af267ca1d4 --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/bracket.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/callhierarchy-incoming.svg b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/callhierarchy-incoming.svg new file mode 100644 index 0000000000..c235778bc6 --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/callhierarchy-incoming.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/circuit-board.svg b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/circuit-board.svg new file mode 100644 index 0000000000..cc10d5d390 --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/circuit-board.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/coffee.svg b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/coffee.svg new file mode 100644 index 0000000000..4640e3ac3e --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/coffee.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/database.svg b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/database.svg new file mode 100644 index 0000000000..6b8df4c5ba --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/database.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/debug-breakpoint-data-unverified.svg b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/debug-breakpoint-data-unverified.svg new file mode 100644 index 0000000000..4fbd73edf9 --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/debug-breakpoint-data-unverified.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/debug-disconnect.svg b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/debug-disconnect.svg new file mode 100644 index 0000000000..bb050c2717 --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/debug-disconnect.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/file-binary.svg b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/file-binary.svg new file mode 100644 index 0000000000..00649d2d6b --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/file-binary.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/gear.svg b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/gear.svg new file mode 100644 index 0000000000..8ee3ec48ab --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/gear.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/globe.svg b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/globe.svg new file mode 100644 index 0000000000..4699fb5419 --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/layers.svg b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/layers.svg new file mode 100644 index 0000000000..f67ac65f33 --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/layers.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/library.svg b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/library.svg new file mode 100644 index 0000000000..bedd9ee1d3 --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/library.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/lightbulb.svg b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/lightbulb.svg new file mode 100644 index 0000000000..37df948008 --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/lightbulb.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/link.svg b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/link.svg new file mode 100644 index 0000000000..46bf4d5340 --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/package.svg b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/package.svg new file mode 100644 index 0000000000..ee991f892d --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/package.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/project.svg b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/project.svg new file mode 100644 index 0000000000..5aca8548e5 --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/project.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/record.svg b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/record.svg new file mode 100644 index 0000000000..abebeac9c1 --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/record.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/search.svg b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/search.svg new file mode 100644 index 0000000000..c6e88e26f9 --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/symbol-class.svg b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/symbol-class.svg new file mode 100644 index 0000000000..d837c94d3c --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/symbol-class.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/symbol-field.svg b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/symbol-field.svg new file mode 100644 index 0000000000..a344cf007f --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/symbol-field.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/symbol-interface.svg b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/symbol-interface.svg new file mode 100644 index 0000000000..cef462e73f --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/symbol-interface.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/symbol-method.svg b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/symbol-method.svg new file mode 100644 index 0000000000..15066578a9 --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/symbol-method.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/symbol-property.svg b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/symbol-property.svg new file mode 100644 index 0000000000..e5222f9f1c --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/symbol-property.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/symbol-value.svg b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/symbol-value.svg new file mode 100644 index 0000000000..c7839cc169 --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/symbol-value.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/target.svg b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/target.svg new file mode 100644 index 0000000000..e6aa0c249c --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/target.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/text-size.svg b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/text-size.svg new file mode 100644 index 0000000000..7a0c63ffc2 --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/text-size.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/verified.svg b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/verified.svg new file mode 100644 index 0000000000..8235c09907 --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/verified.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/zap.svg b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/zap.svg new file mode 100644 index 0000000000..ea695e85bd --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/icons/stereotypes/zap.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/plugin.xml b/eclipse-language-servers/org.springframework.tooling.boot.ls/plugin.xml index 20765430a3..ef018ee22c 100644 --- a/eclipse-language-servers/org.springframework.tooling.boot.ls/plugin.xml +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/plugin.xml @@ -928,5 +928,17 @@ --> + + + + diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/BootLanguageServerPlugin.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/BootLanguageServerPlugin.java index fa1315de84..9936c069ef 100644 --- a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/BootLanguageServerPlugin.java +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/BootLanguageServerPlugin.java @@ -12,12 +12,17 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Enumeration; import java.util.List; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.preferences.IEclipsePreferences; import org.eclipse.core.runtime.preferences.InstanceScope; +import org.eclipse.jdt.internal.ui.JavaPluginImages; import org.eclipse.jface.bindings.Binding; import org.eclipse.jface.resource.ImageRegistry; +import org.eclipse.swt.graphics.Image; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.keys.IBindingService; import org.eclipse.ui.plugin.AbstractUIPlugin; @@ -33,6 +38,8 @@ */ public class BootLanguageServerPlugin extends AbstractUIPlugin { + private static final String STEREOTYPE_IMG_PREFIX = "stereotype-"; + public static final String SPRING_ICON = "SPRING_ICON"; public static String PLUGIN_ID = "org.springframework.tooling.boot.ls"; @@ -43,6 +50,8 @@ public class BootLanguageServerPlugin extends AbstractUIPlugin { private static BootLanguageServerPlugin plugin; public static final String BOOT_LS_DEFINITION_ID = "org.eclipse.languageserver.languages.springboot"; + + private BootLsState lsState = new BootLsState(); public BootLanguageServerPlugin() { // Empty @@ -51,7 +60,7 @@ public BootLanguageServerPlugin() { public static IEclipsePreferences getPreferences() { return InstanceScope.INSTANCE.getNode(PLUGIN_ID); } - + @Override public void start(BundleContext context) throws Exception { plugin = this; @@ -112,11 +121,31 @@ public void run() { } } + @SuppressWarnings("restriction") @Override protected void initializeImageRegistry(ImageRegistry reg) { super.initializeImageRegistry(reg); reg.put(SPRING_ICON, imageDescriptorFromPlugin(PLUGIN_ID, "icons/spring_obj.gif")); + + // Add setereotype icons to the registry + Enumeration paths = getBundle().getEntryPaths("icons/stereotypes"); + while(paths.hasMoreElements()) { + String relativePath = paths.nextElement(); + IPath p = new Path(relativePath); + if (p.getFileExtension().equals("svg")) { + String fileName = p.lastSegment(); + String name = fileName.substring(0, fileName.length() - 4); + reg.put(STEREOTYPE_IMG_PREFIX + name, JavaPluginImages.createImageDescriptor(getBundle(), p, false)); + } + } + } + + public BootLsState getLsState() { + return lsState; + } + + public Image getStereotypeImage(String name) { + return getImageRegistry().get(STEREOTYPE_IMG_PREFIX + name); } - } diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/BootLsState.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/BootLsState.java new file mode 100644 index 0000000000..16362fd0b3 --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/BootLsState.java @@ -0,0 +1,46 @@ +package org.springframework.tooling.boot.ls; + +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +import org.eclipse.core.runtime.ListenerList; + +public class BootLsState { + + private enum State { + INITIALIZED, + INDEXED, + STOPPED + } + + private AtomicReference state = new AtomicReference<>(State.STOPPED); + private ListenerList> listeners = new ListenerList<>(); + + public boolean isIndexed() { + return state.get() == State.INDEXED; + } + + void indexed() { + state.set(State.INDEXED); + listeners.forEach(l -> l.accept(this)); + } + + void initialized() { + state.set(State.INITIALIZED); + listeners.forEach(l -> l.accept(this)); + } + + void stopped() { + state.set(State.STOPPED); + listeners.forEach(l -> l.accept(this)); + } + + public void addStateChangedListener(Consumer l) { + listeners.add(l); + } + + public void removeStateChangedListener(Consumer l) { + listeners.remove(l); + } + +} diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/DelegatingStreamConnectionProvider.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/DelegatingStreamConnectionProvider.java index 204e2f139c..ce3a3cbf1f 100644 --- a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/DelegatingStreamConnectionProvider.java +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/DelegatingStreamConnectionProvider.java @@ -36,6 +36,7 @@ import org.eclipse.lsp4j.ExecuteCommandParams; import org.eclipse.lsp4j.InitializeResult; import org.eclipse.lsp4j.jsonrpc.messages.Message; +import org.eclipse.lsp4j.jsonrpc.messages.NotificationMessage; import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage; import org.eclipse.lsp4j.services.LanguageServer; import org.eclipse.ui.PlatformUI; @@ -127,6 +128,7 @@ public InputStream getErrorStream() { @Override public void stop() { + BootLanguageServerPlugin.getDefault().getLsState().stopped(); IProxyService proxyService = PlatformUI.getWorkbench().getService(IProxyService.class); if (proxyService != null) { proxyService.removeProxyChangeListener(proxySettingsListener); @@ -171,7 +173,7 @@ public void handleMessage(Message message, LanguageServer languageServer, URI ro //Add remote boot apps listener RemoteBootAppsDataHolder.getDefault().getRemoteApps().addListener(remoteAppsListener); - + if (isCopilotInstalled()) { // Enable Copilot features if the Copilot plugin is installed languageServer.getWorkspaceService().executeCommand(new ExecuteCommandParams( @@ -179,8 +181,18 @@ public void handleMessage(Message message, LanguageServer languageServer, URI ro List.of(true) )); } + + BootLanguageServerPlugin.getDefault().getLsState().initialized(); + } + } else if (message instanceof NotificationMessage) { + NotificationMessage notification = (NotificationMessage) message; + // Handle spring/index/updated notification + if ("spring/index/updated".equals(notification.getMethod())) { + // Emit event to the Flux + BootLanguageServerPlugin.getDefault().getLsState().indexed(); } } + } private boolean isCopilotInstalled() { diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/LogicalStructureView.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/LogicalStructureView.java new file mode 100644 index 0000000000..96797de595 --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/LogicalStructureView.java @@ -0,0 +1,98 @@ +/******************************************************************************* + * 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.tooling.boot.ls.views; + +import java.util.Collections; +import java.util.function.Consumer; + +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.lsp4e.ui.UI; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.part.ViewPart; +import org.springframework.tooling.boot.ls.BootLanguageServerPlugin; +import org.springframework.tooling.boot.ls.BootLsState; + + +/** + * Traditional Eclipse view for displaying logical structure as a tree. + * + * @author Alex Boyko + */ +public class LogicalStructureView extends ViewPart { + + public static final String ID = "org.springframework.tooling.boot.ls.views.LogicalStructureView"; + + private TreeViewer treeViewer; + + final private StructureClient client = new StructureClient(); + + private Consumer lsStateListener = state -> { + if (state.isIndexed()) { + fetchStructure(false); + } else { + UI.getDisplay().asyncExec(() -> treeViewer.setInput(null)); + } + }; + + void fetchStructure(boolean updateMetadata) { + client.fetch(updateMetadata).thenAccept(nodes -> { + UI.getDisplay().asyncExec(() -> { + Object[] expanded = treeViewer.getExpandedElements(); + treeViewer.setInput(nodes); + treeViewer.setExpandedElements(expanded); + }); + }); + } + + @Override + public void createPartControl(Composite parent) { + treeViewer = new TreeViewer(parent, SWT.SINGLE | SWT.H_SCROLL | SWT.V_SCROLL); + + // Set up content provider + treeViewer.setContentProvider(new StructureTreeContentProvider()); + + // Set up label provider + treeViewer.setLabelProvider(new StructureTreeLabelProvider()); + + // Set initial input - placeholder data + treeViewer.setInput(Collections.emptyList()); + + BootLsState lsState = BootLanguageServerPlugin.getDefault().getLsState(); + + if (lsState.isIndexed()) { + client.fetch(false).thenAccept(nodes -> { + UI.getDisplay().asyncExec(() -> { + treeViewer.setInput(nodes); + }); + }); + } + + lsState.addStateChangedListener(lsStateListener); + + treeViewer.getControl().addDisposeListener(e -> lsState.removeStateChangedListener(lsStateListener)); + + // Make the viewer available for selection + getSite().setSelectionProvider(treeViewer); + + initActions(); + } + + private void initActions() { + getViewSite().getActionBars().getToolBarManager().add(new RefreshAction(this)); + } + + @Override + public void setFocus() { + treeViewer.getControl().setFocus(); + } +} + diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/RefreshAction.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/RefreshAction.java new file mode 100644 index 0000000000..b5fd6816bc --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/RefreshAction.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * 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.tooling.boot.ls.views; + +import org.eclipse.jdt.internal.ui.JavaPluginImages; +import org.eclipse.jface.action.Action; + +class RefreshAction extends Action { + + private final LogicalStructureView logicalStructureView; + + @SuppressWarnings("restriction") + RefreshAction(LogicalStructureView logicalStructureView) { + super("Refresh"); + this.logicalStructureView = logicalStructureView; + setToolTipText("Refresh Structure"); + JavaPluginImages.setLocalImageDescriptors(this, "refresh.svg"); + } + + @Override + public void run() { + this.logicalStructureView.fetchStructure(true); + } +} \ No newline at end of file diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StereotypeNode.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StereotypeNode.java new file mode 100644 index 0000000000..4c30ad48e3 --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StereotypeNode.java @@ -0,0 +1,64 @@ +/******************************************************************************* + * 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.tooling.boot.ls.views; + +import java.util.Map; + + +/** + * Represents a node in the logical structure tree. + * Placeholder implementation for demonstration purposes. + * + * @author Alex Boyko + */ +record StereotypeNode(StereotypeNode[] children, Map attributes) { + + private static final String PROJECT_ID = "projectId"; + + private static final String LOCATION = "location"; + + private static final String ICON = "icon"; + private static final String TEXT = "text"; + + private static final String NODE_ID = "nodeId"; + + public String getText() { + return (String) attributes.get(TEXT); + } + + public String getId() { + return (String) attributes.get(NODE_ID); + } + + public String getIcon() { + return (String) attributes.get(ICON); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof StereotypeNode) { + return getId().equals(((StereotypeNode) obj).getId()); + } + return false; + } + + @Override + public int hashCode() { + return getId().hashCode(); + } + + @Override + public String toString() { + return getId(); + } + +} + diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StructureClient.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StructureClient.java new file mode 100644 index 0000000000..e1f349c03b --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StructureClient.java @@ -0,0 +1,61 @@ +package org.springframework.tooling.boot.ls.views; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; + +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.lsp4e.LanguageServers; +import org.eclipse.lsp4e.LanguageServers.LanguageServerProjectExecutor; +import org.eclipse.lsp4j.ExecuteCommandParams; +import org.eclipse.lsp4j.ServerCapabilities; +import org.springframework.tooling.jdt.ls.commons.BootProjectTracker; + +import com.google.common.reflect.TypeToken; +import com.google.gson.Gson; +import com.google.gson.JsonElement; + +class StructureClient { + + private static final String FETCH_SPRING_BOOT_STRUCTURE = "sts/spring-boot/structure"; + private static final Predicate WS_STRUCTURE_CMD_CAP = capabilities -> capabilities.getExecuteCommandProvider().getCommands().contains(FETCH_SPRING_BOOT_STRUCTURE); + + @SuppressWarnings({ "restriction", "serial" }) + CompletableFuture> fetch(boolean updateMetadata) { + List allSpringProjects = BootProjectTracker.streamSpringProjects().toList(); + if (!allSpringProjects.isEmpty()) { + StructureParameter param = new StructureParameter(updateMetadata, null); + LanguageServerProjectExecutor lss = LanguageServers.forProject(allSpringProjects.get(0).getProject()).withFilter(WS_STRUCTURE_CMD_CAP).excludeInactive(); + List> res = lss.computeAll(ls -> ls.getWorkspaceService().executeCommand(new ExecuteCommandParams(FETCH_SPRING_BOOT_STRUCTURE, List.of(param)))); + final List nodes = Collections.synchronizedList(new ArrayList<>()); + final Gson gson = new Gson(); + for (CompletableFuture<@Nullable Object> f : res) { + f.thenAccept(o -> { + JsonElement json = null; + if (o instanceof List) { + json = gson.toJsonTree(o); + } else if (o instanceof JsonElement) { + json = (JsonElement) o; + } + if (json != null) { + List n = gson.fromJson(json, new TypeToken>() {}.getType()); + if (n != null) { + nodes.addAll(n); + } + } + }); + } + return CompletableFuture.allOf(res.toArray(new CompletableFuture[res.size()])).thenApply(v -> nodes); + } + return CompletableFuture.completedFuture(Collections.emptyList()); + } + + + private record StructureParameter(boolean updateMetadata, Map> groupings) {} + +} diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StructureTreeContentProvider.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StructureTreeContentProvider.java new file mode 100644 index 0000000000..60edb9b78c --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StructureTreeContentProvider.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * 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.tooling.boot.ls.views; + +import java.util.List; + +import org.eclipse.jface.viewers.ITreeContentProvider; + +/** + * Content provider for the logical structure tree view. + * Provides placeholder content for demonstration purposes. + * + * @author Alex Boyko + */ +public class StructureTreeContentProvider implements ITreeContentProvider { + + @Override + public Object[] getElements(Object input) { + if (input instanceof List) { + return ((List) input).toArray(); + } + return getChildren(input); + } + + @Override + public Object[] getChildren(Object parentElement) { + if (parentElement instanceof StereotypeNode) { + StereotypeNode node = (StereotypeNode) parentElement; + return node.children(); + } + return new Object[0]; + } + + @Override + public Object getParent(Object element) { + return null; + } + + @Override + public boolean hasChildren(Object element) { + return getChildren(element).length > 0; + } +} + diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StructureTreeLabelProvider.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StructureTreeLabelProvider.java new file mode 100644 index 0000000000..44e53c2a91 --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StructureTreeLabelProvider.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * 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.tooling.boot.ls.views; + +import org.eclipse.jface.viewers.LabelProvider; +import org.eclipse.swt.graphics.Image; +import org.springframework.tooling.boot.ls.BootLanguageServerPlugin; + +/** + * Label provider for the logical structure tree view. + * Provides text labels for tree elements. + * + * @author Alex Boyko + */ +public class StructureTreeLabelProvider extends LabelProvider { + + @Override + public String getText(Object element) { + if (element instanceof StereotypeNode) { + return ((StereotypeNode) element).getText(); + } + return super.getText(element); + } + + @Override + public Image getImage(Object element) { + if (element instanceof StereotypeNode) { + String descriptor = ((StereotypeNode) element).getIcon(); + if (descriptor != null && !descriptor.isBlank()) { + return BootLanguageServerPlugin.getDefault().getStereotypeImage(descriptor); + } + } + return super.getImage(element); + } + + +} + diff --git a/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/src/org/springframework/tooling/jdt/ls/commons/BootProjectTracker.java b/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/src/org/springframework/tooling/jdt/ls/commons/BootProjectTracker.java index 82b97ba902..02e8fb4a37 100644 --- a/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/src/org/springframework/tooling/jdt/ls/commons/BootProjectTracker.java +++ b/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/src/org/springframework/tooling/jdt/ls/commons/BootProjectTracker.java @@ -11,12 +11,14 @@ package org.springframework.tooling.jdt.ls.commons; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.function.Consumer; +import java.util.stream.Stream; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.ResourcesPlugin; @@ -55,14 +57,18 @@ public void classpathChanged(IJavaProject jp) { } private void processProject(IJavaProject jp) { - if (isSpringProject(jp)) { - if (springProjects.add(jp)) { - fireEvent(); - } - } else { - if (springProjects.remove(jp)) { - fireEvent(); + try { + if (isSpringProject(jp)) { + if (springProjects.add(jp)) { + fireEvent(); + } + } else { + if (springProjects.remove(jp)) { + fireEvent(); + } } + } catch (JavaModelException e) { + logger.log(e); } } @@ -84,23 +90,26 @@ public void removeListener(Consumer> l) { listeners.remove(l); } - private boolean isSpringProject(IJavaProject jp) { + private static boolean isSpringProject(IJavaProject jp) throws JavaModelException { if (jp.exists()) { - try { - IClasspathEntry[] classpath = jp.getResolvedClasspath(true); - //Look for a 'spring-core' jar or project entry - for (IClasspathEntry e : classpath) { - if (isBootJar(e) || isBootProject(e)) { - return true; - } + IClasspathEntry[] classpath = jp.getResolvedClasspath(true); + // Look for a 'spring-core' jar or project entry + for (IClasspathEntry e : classpath) { + if (isBootJar(e) || isBootProject(e)) { + return true; } - } catch (JavaModelException e) { - logger.log(e); } } return false; } + public static Stream streamSpringProjects() { + return Arrays.stream(ResourcesPlugin.getWorkspace().getRoot().getProjects()) + .filter(p -> p != null && p.isAccessible()) + .map(JavaCore::create) + .filter(jp -> jp != null && jp.exists()); + } + private static boolean isBootProject(IClasspathEntry e) { if (e.getEntryKind()==IClasspathEntry.CPE_PROJECT) { IPath path = e.getPath(); 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 a45dfd9196..7c1b11e61f 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 @@ -58,6 +58,8 @@ public class JsonNodeHandler implements NodeHandler labels; @@ -127,6 +129,7 @@ public void handleApplication(A application) { .withAttribute(ICON, StereotypeIcons.getIcon(StereotypeIcons.APPLICATION_KEY)) .withAttribute(PROJECT_ID, project.getElementName()) ; + assignNodeId(root, null); } @Override @@ -201,11 +204,26 @@ public void postGroup() { private void addChild(Consumer consumer) { this.current = addChildFoo(consumer); } - + + private static void assignNodeId(Node n, Node p) { + String textId = n.attributes.containsKey(TEXT) ? (String) n.attributes.get(TEXT) : ""; + + Location location = (Location) n.attributes.get(LOCATION); + String locationId = location == null ? "" : "%s:%d:%d".formatted(location.getUri(), location.getRange().getStart().getLine(), location.getRange().getStart().getCharacter()); + + String referenceId = n.attributes.containsKey(REFERENCE) ? (String) n.attributes.get(REFERENCE) : ""; + + String nodeSpecificId = "%s|%s|%s".formatted(textId, locationId, referenceId).replaceAll("\\|+$", ""); + + n.attributes.put(NODE_ID, p != null && p.attributes.containsKey(NODE_ID) ? "%s//%s".formatted(p.attributes.get(NODE_ID), nodeSpecificId) : nodeSpecificId); + } + private Node addChildFoo(Consumer consumer) { var node = new Node(this.current); consumer.accept(node); + + assignNodeId(node, current); this.current.children.add(node); From d254d24402d7698f49b0b12828c455f2e4d7e1ad Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Mon, 20 Oct 2025 10:12:57 -0700 Subject: [PATCH 2/8] Grouping action for the whole tree Signed-off-by: BoykoAlex --- .../tooling/boot/ls/views/GroupingAction.java | 37 ++++ .../tooling/boot/ls/views/GroupingDialog.java | 164 +++++++++++++++++ .../boot/ls/views/GroupingDialogModel.java | 170 ++++++++++++++++++ .../boot/ls/views/GroupingRepository.java | 75 ++++++++ .../boot/ls/views/LogicalStructureView.java | 53 ++++-- .../tooling/boot/ls/views/StereotypeNode.java | 30 +--- .../ls/views/StereotypeNodeDeserializer.java | 56 ++++++ .../boot/ls/views/StructureClient.java | 58 ++++-- .../ls/views/StructureTreeLabelProvider.java | 4 +- .../jdt/ls/commons/BootProjectTracker.java | 8 +- .../java/commands/SpringIndexCommands.java | 2 +- 11 files changed, 607 insertions(+), 50 deletions(-) create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/GroupingAction.java create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/GroupingDialog.java create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/GroupingDialogModel.java create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/GroupingRepository.java create mode 100644 eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StereotypeNodeDeserializer.java diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/GroupingAction.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/GroupingAction.java new file mode 100644 index 0000000000..500dc8711e --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/GroupingAction.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * 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.tooling.boot.ls.views; + +import org.eclipse.jdt.internal.ui.JavaPluginImages; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.lsp4e.ui.UI; + +class GroupingAction extends Action { + + private LogicalStructureView structureView; + + @SuppressWarnings("restriction") + public GroupingAction(LogicalStructureView structureView) { + super("Grouping...", JavaPluginImages.DESC_ELCL_FILTER); + this.structureView = structureView; + } + + @Override + public void run() { + GroupingDialog dialog = new GroupingDialog(UI.getActiveShell(), structureView::fetchGroups, structureView::getGroupings); + if (dialog.open() == IDialogConstants.OK_ID) { + structureView.setGroupings(dialog.getResult()); + structureView.fetchStructure(false); + } + } + +} diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/GroupingDialog.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/GroupingDialog.java new file mode 100644 index 0000000000..6c0015defb --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/GroupingDialog.java @@ -0,0 +1,164 @@ +/******************************************************************************* + * 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.tooling.boot.ls.views; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +import org.eclipse.jface.dialogs.IDialogSettings; +import org.eclipse.jface.dialogs.TrayDialog; +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.jface.viewers.ICheckStateProvider; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.LabelProvider; +import org.eclipse.lsp4e.ui.UI; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.dialogs.ContainerCheckedTreeViewer; +import org.osgi.framework.FrameworkUtil; +import org.springframework.tooling.boot.ls.views.GroupingDialogModel.GroupItem; +import org.springframework.tooling.boot.ls.views.GroupingDialogModel.ProjectItem; +import org.springframework.tooling.boot.ls.views.GroupingDialogModel.TreeItem; +import org.springframework.tooling.boot.ls.views.StructureClient.Groups; + +public class GroupingDialog extends TrayDialog { + + private GroupingDialogModel model; + + protected GroupingDialog(Shell parentShell, Supplier>> client, Supplier>> groupings) { + super(parentShell); + this.model = new GroupingDialogModel(client, groupings); + } + + @Override + protected Control createDialogArea(Composite parent) { + Composite composite = (Composite) super.createDialogArea(parent); + + ContainerCheckedTreeViewer viewer = new ContainerCheckedTreeViewer(composite, SWT.SINGLE); + + viewer.setCheckStateProvider(new ICheckStateProvider() { + + @Override + public boolean isGrayed(Object element) { + if (element instanceof TreeItem) { + return ((TreeItem) element).getChecked() == null; + } + return false; + } + + @Override + public boolean isChecked(Object element) { + if (element instanceof TreeItem) { + Boolean checked = ((TreeItem) element).getChecked(); + return checked == null || Boolean.TRUE.equals(checked); + } + return false; + } + }); + + viewer.setContentProvider(new ITreeContentProvider() { + + @Override + public Object[] getElements(Object input) { + if (input instanceof GroupingDialogModel) { + return ((GroupingDialogModel) input).getLiveSet().getValue().toArray(); + } + return new Object[0]; + } + + @Override + public Object[] getChildren(Object p) { + if (p instanceof ProjectItem) { + return ((ProjectItem) p).getGroups().toArray(); + } + return new Object[0]; + } + + @Override + public Object getParent(Object e) { + if (e instanceof GroupItem) { + return ((GroupItem) e).getProjectItem(); + } + return null; + } + + @Override + public boolean hasChildren(Object e) { + if (e instanceof ProjectItem) { + return !((ProjectItem) e).getGroups().isEmpty(); + } + + return false; + } + + }); + + viewer.setLabelProvider(new LabelProvider() { + + @Override + public String getText(Object e) { + return e instanceof TreeItem ? ((TreeItem) e).getLabel() : null; + } + + }); + + viewer.addCheckStateListener(e -> { + Object o = e.getElement(); + if (o instanceof TreeItem) { + ((TreeItem) o).setChecked(e.getChecked()); + } + }); + + viewer.setInput(model); + + model.getLiveSet().addListener((e, v) -> { + UI.getDisplay().asyncExec(viewer::refresh); + }); + + model.getLoaded().addListener((e, v) -> { + UI.getDisplay().asyncExec(viewer::expandAll); + }); + + model.load(); + + viewer.getControl().setLayoutData(GridDataFactory.fillDefaults().grab(true, true).create()); + + return composite; + } + + @Override + protected IDialogSettings getDialogBoundsSettings() { + IDialogSettings settings = PlatformUI + .getDialogSettingsProvider(FrameworkUtil.getBundle(getClass())) + .getDialogSettings(); + String dialogSettingsId = getClass().getName(); + IDialogSettings section = settings.getSection(dialogSettingsId); + if (section == null) { + section = settings.addNewSection(dialogSettingsId); + } + return section; + } + + @Override + protected boolean isResizable() { + return true; + } + + Map> getResult() { + return model.getResult(); + } + +} diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/GroupingDialogModel.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/GroupingDialogModel.java new file mode 100644 index 0000000000..5ced4a4a8a --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/GroupingDialogModel.java @@ -0,0 +1,170 @@ +/******************************************************************************* + * 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.tooling.boot.ls.views; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +import org.springframework.tooling.boot.ls.views.StructureClient.Groups; +import org.springsource.ide.eclipse.commons.livexp.core.LiveSetVariable; +import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; + +class GroupingDialogModel { + + interface TreeItem { + Boolean getChecked(); + String getLabel(); + void setChecked(boolean checked); + } + + static class ProjectItem implements TreeItem { + + private final String name; + private final List groups; + + public ProjectItem(String name) { + this.name = name; + this.groups = new ArrayList<>(); + } + + public Boolean getChecked() { + if (groups.isEmpty()) { + return true; + } + boolean checked = groups.get(0).checked; + for (GroupItem g : groups) { + if (g.checked != checked) { + return null; + } + } + return checked; + } + + public GroupItem addGroup(String id, String label) { + GroupItem groupItem = new GroupItem(this, id, label, false); + groups.add(groupItem); + return groupItem; + } + + public String getLabel() { + return name; + } + + public List getGroups() { + return groups; + } + + void apply(List checkedGroups) { + groups.forEach(g -> g.checked = checkedGroups == null || checkedGroups.contains(g.id)); + } + + List extract() { + List checkedGroups = groups.stream().filter(g -> g.checked).map(g -> g.id).toList(); + return checkedGroups.size() == groups.size() ? null : checkedGroups; + } + + @Override + public void setChecked(boolean checked) { + groups.forEach(g -> g.setChecked(checked)); + } + + } + + static class GroupItem implements TreeItem { + + private final ProjectItem projectItem; + private final String id; + private final String label; + private boolean checked; + + private GroupItem(ProjectItem projectItem, String id, String label, boolean checked) { + this.projectItem = projectItem; + this.id = id; + this.label = label; + this.checked = checked; + } + + public void setChecked(boolean c) { + this.checked = c; + } + + public Boolean getChecked() { + return this.checked; + } + + public String getId() { + return id; + } + + public String getLabel() { + return label; + } + + ProjectItem getProjectItem() { + return projectItem; + } + + } + + private final Supplier>> client; + + private final LiveSetVariable projectItems; + + private final Supplier>> groupings; + + private final LiveVariable loaded; + + public GroupingDialogModel(Supplier>> client, Supplier>> groupings) { + this.client = client; + this.groupings = groupings; + this.projectItems = new LiveSetVariable<>(); + this.loaded = new LiveVariable<>(false); + } + + void load() { + loaded.setValue(false); + Map> groupingsMap = groupings.get(); + client.get().thenApply(allGroups -> { + return allGroups.stream().map(groups -> { + String projectName = groups.projectName(); + ProjectItem projectItem = new ProjectItem(projectName); + groups.groups().stream().forEach(g -> projectItem.addGroup(g.identifier(), g.displayName())); + projectItem.apply(groupingsMap.get(projectName)); + return projectItem; + }).toList(); + }).thenAccept(items -> { + projectItems.replaceAll(items); + loaded.setValue(true); + }); + } + + LiveSetVariable getLiveSet() { + return projectItems; + } + + LiveVariable getLoaded() { + return loaded; + } + + public Map> getResult() { + // Cannot use `Collectors.toMap()` due to NPE with null values + Map> res = new HashMap<>(); + for (ProjectItem p : projectItems.getValue()) { + res.put(p.getLabel(), p.extract()); + } + return res; + } + +} diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/GroupingRepository.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/GroupingRepository.java new file mode 100644 index 0000000000..a78b242890 --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/GroupingRepository.java @@ -0,0 +1,75 @@ +/******************************************************************************* + * 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.tooling.boot.ls.views; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ProjectScope; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.preferences.IEclipsePreferences; +import org.osgi.service.prefs.BackingStoreException; +import org.springframework.tooling.boot.ls.BootLanguageServerPlugin; +import org.springframework.tooling.jdt.ls.commons.BootProjectTracker; + +class GroupingRepository { + + private static final String KEY = "stereotype-structure-grouping"; + + private List getGrouping(IProject project) { + IEclipsePreferences prefNode = new ProjectScope(project).getNode(BootLanguageServerPlugin.getDefault().getBundle().getSymbolicName()); + String s = prefNode.get(KEY, null); + return s == null ? null : Arrays.asList(s.split("\\|")); + } + + private void setGrouping(IProject project, List grouping) { + IEclipsePreferences prefNode = new ProjectScope(project).getNode(BootLanguageServerPlugin.getDefault().getBundle().getSymbolicName()); + if (grouping == null) { + prefNode.remove(KEY); + } else { + prefNode.put(KEY, String.join("|", grouping.toArray(new String[grouping.size()]))); + } + try { + prefNode.flush(); + } catch (BackingStoreException e) { + BootLanguageServerPlugin.getDefault().getLog().error("Failed to stote stereotype structure grouping settings", e); + } + } + + Map> getWorkspaceGroupings() { + Map> workspaceGroupings = new HashMap<>(); + BootProjectTracker.streamSpringProjects().forEach(p -> { + List g = getGrouping(p.getProject()); + if (g != null) { + workspaceGroupings.put(p.getElementName(), g); + } + }); + return workspaceGroupings; + } + + void saveWorkspaceGroupings(Map> groupings) { + IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); + for (Entry> e : groupings.entrySet()) { + String n = e.getKey(); + List g = e.getValue(); + IProject project = root.getProject(n); + if (project != null) { + setGrouping(project, g); + } + } + } + +} diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/LogicalStructureView.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/LogicalStructureView.java index 96797de595..6aa45ea622 100644 --- a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/LogicalStructureView.java +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/LogicalStructureView.java @@ -11,15 +11,24 @@ package org.springframework.tooling.boot.ls.views; import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; +import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.lsp4e.LSPEclipseUtils; import org.eclipse.lsp4e.ui.UI; +import org.eclipse.lsp4j.Location; import org.eclipse.swt.SWT; import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.IActionBars; import org.eclipse.ui.part.ViewPart; import org.springframework.tooling.boot.ls.BootLanguageServerPlugin; import org.springframework.tooling.boot.ls.BootLsState; +import org.springframework.tooling.boot.ls.views.StructureClient.Groups; +import org.springframework.tooling.boot.ls.views.StructureClient.StructureParameter; /** @@ -33,7 +42,9 @@ public class LogicalStructureView extends ViewPart { private TreeViewer treeViewer; - final private StructureClient client = new StructureClient(); + final private StructureClient structureClient = new StructureClient(); + + final private GroupingRepository groupingRepository = new GroupingRepository(); private Consumer lsStateListener = state -> { if (state.isIndexed()) { @@ -44,7 +55,7 @@ public class LogicalStructureView extends ViewPart { }; void fetchStructure(boolean updateMetadata) { - client.fetch(updateMetadata).thenAccept(nodes -> { + structureClient.fetchStructure(new StructureParameter(updateMetadata, getGroupings())).thenAccept(nodes -> { UI.getDisplay().asyncExec(() -> { Object[] expanded = treeViewer.getExpandedElements(); treeViewer.setInput(nodes); @@ -52,7 +63,12 @@ void fetchStructure(boolean updateMetadata) { }); }); } + + CompletableFuture> fetchGroups() { + return structureClient.fetchGroups(); + } + @SuppressWarnings("restriction") @Override public void createPartControl(Composite parent) { treeViewer = new TreeViewer(parent, SWT.SINGLE | SWT.H_SCROLL | SWT.V_SCROLL); @@ -69,30 +85,47 @@ public void createPartControl(Composite parent) { BootLsState lsState = BootLanguageServerPlugin.getDefault().getLsState(); if (lsState.isIndexed()) { - client.fetch(false).thenAccept(nodes -> { - UI.getDisplay().asyncExec(() -> { - treeViewer.setInput(nodes); - }); - }); + fetchStructure(false); } lsState.addStateChangedListener(lsStateListener); treeViewer.getControl().addDisposeListener(e -> lsState.removeStateChangedListener(lsStateListener)); + treeViewer.addDoubleClickListener(e -> { + Object o = ((IStructuredSelection) e.getSelection()).getFirstElement(); + if (o instanceof StereotypeNode) { + StereotypeNode n = (StereotypeNode) o; + Location l = n.location(); + if (l != null) { + LSPEclipseUtils.openInEditor(l); + } + } + }); + // Make the viewer available for selection getSite().setSelectionProvider(treeViewer); - initActions(); + initActions(getViewSite().getActionBars()); } - private void initActions() { - getViewSite().getActionBars().getToolBarManager().add(new RefreshAction(this)); + private void initActions(IActionBars actionBars) { + actionBars.getToolBarManager().add(new GroupingAction(this)); + actionBars.getToolBarManager().add(new RefreshAction(this)); } @Override public void setFocus() { treeViewer.getControl().setFocus(); } + + void setGroupings(Map> groupings) { + groupingRepository.saveWorkspaceGroupings(groupings); + } + + Map> getGroupings() { + return groupingRepository.getWorkspaceGroupings(); + } + } diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StereotypeNode.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StereotypeNode.java index 4c30ad48e3..3ec1863d36 100644 --- a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StereotypeNode.java +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StereotypeNode.java @@ -12,6 +12,7 @@ import java.util.Map; +import org.eclipse.lsp4j.Location; /** * Represents a node in the logical structure tree. @@ -19,45 +20,24 @@ * * @author Alex Boyko */ -record StereotypeNode(StereotypeNode[] children, Map attributes) { - - private static final String PROJECT_ID = "projectId"; - - private static final String LOCATION = "location"; - - private static final String ICON = "icon"; - private static final String TEXT = "text"; - - private static final String NODE_ID = "nodeId"; - - public String getText() { - return (String) attributes.get(TEXT); - } - - public String getId() { - return (String) attributes.get(NODE_ID); - } - - public String getIcon() { - return (String) attributes.get(ICON); - } +record StereotypeNode(String id, String text, String icon, Location location, String reference, Map attributes, StereotypeNode[] children) { @Override public boolean equals(Object obj) { if (obj instanceof StereotypeNode) { - return getId().equals(((StereotypeNode) obj).getId()); + return id().equals(((StereotypeNode) obj).id()); } return false; } @Override public int hashCode() { - return getId().hashCode(); + return id().hashCode(); } @Override public String toString() { - return getId(); + return text; } } diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StereotypeNodeDeserializer.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StereotypeNodeDeserializer.java new file mode 100644 index 0000000000..f325cc6720 --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StereotypeNodeDeserializer.java @@ -0,0 +1,56 @@ +/******************************************************************************* + * 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.tooling.boot.ls.views; + +import java.lang.reflect.Type; +import java.util.Map; + +import org.eclipse.lsp4j.Location; + +import com.google.common.reflect.TypeToken; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +@SuppressWarnings({ "restriction", "serial" }) +public class StereotypeNodeDeserializer implements com.google.gson.JsonDeserializer { + + private static final String LOCATION = "location"; + private static final String ICON = "icon"; + private static final String TEXT = "text"; + private static final String CHILDREN = "children"; + private static final String REFERENCE = "reference"; + + private static final String NODE_ID = "nodeId"; + + + @Override + public StereotypeNode deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException { + JsonObject object = json.getAsJsonObject(); + JsonObject attributes = object.getAsJsonObject("attributes"); + return new StereotypeNode( + extractString(attributes, NODE_ID), + extractString(attributes, TEXT), + extractString(attributes, ICON), + context.deserialize(attributes.get(LOCATION), Location.class), + extractString(attributes, REFERENCE), + context.deserialize(attributes, new TypeToken>(){}.getType()), + context.deserialize(object.get(CHILDREN), StereotypeNode[].class) + ); + } + + private static String extractString(JsonObject object, String property) { + JsonElement e = object.get(property); + return e != null && e.isJsonPrimitive() ? e.getAsString() : null; + } + +} diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StructureClient.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StructureClient.java index e1f349c03b..8989ceec45 100644 --- a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StructureClient.java +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StructureClient.java @@ -1,10 +1,11 @@ package org.springframework.tooling.boot.ls.views; import java.util.ArrayList; -import java.util.Collection; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.function.Predicate; @@ -18,22 +19,27 @@ import com.google.common.reflect.TypeToken; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; +@SuppressWarnings({ "restriction", "serial" }) class StructureClient { + record Groups (String projectName, List groups) {} + record Group (String identifier, String displayName) {} + record StructureParameter(boolean updateMetadata, Map> groups) {} + private static final String FETCH_SPRING_BOOT_STRUCTURE = "sts/spring-boot/structure"; + private static final String FETCH_STRUCTURE_GROUPS = "sts/spring-boot/structure/groups"; + private static final Predicate WS_STRUCTURE_CMD_CAP = capabilities -> capabilities.getExecuteCommandProvider().getCommands().contains(FETCH_SPRING_BOOT_STRUCTURE); + private static final Predicate WS_GROUPS_CMD_CAP = capabilities -> capabilities.getExecuteCommandProvider().getCommands().contains(FETCH_STRUCTURE_GROUPS); - @SuppressWarnings({ "restriction", "serial" }) - CompletableFuture> fetch(boolean updateMetadata) { - List allSpringProjects = BootProjectTracker.streamSpringProjects().toList(); - if (!allSpringProjects.isEmpty()) { - StructureParameter param = new StructureParameter(updateMetadata, null); - LanguageServerProjectExecutor lss = LanguageServers.forProject(allSpringProjects.get(0).getProject()).withFilter(WS_STRUCTURE_CMD_CAP).excludeInactive(); + CompletableFuture> fetchStructure(StructureParameter param) { + return getExecutor(WS_STRUCTURE_CMD_CAP).map(lss -> { List> res = lss.computeAll(ls -> ls.getWorkspaceService().executeCommand(new ExecuteCommandParams(FETCH_SPRING_BOOT_STRUCTURE, List.of(param)))); final List nodes = Collections.synchronizedList(new ArrayList<>()); - final Gson gson = new Gson(); + final Gson gson = new GsonBuilder().registerTypeAdapter(StereotypeNode.class, new StereotypeNodeDeserializer()).create(); for (CompletableFuture<@Nullable Object> f : res) { f.thenAccept(o -> { JsonElement json = null; @@ -51,11 +57,41 @@ CompletableFuture> fetch(boolean updateMetadata) { }); } return CompletableFuture.allOf(res.toArray(new CompletableFuture[res.size()])).thenApply(v -> nodes); - } - return CompletableFuture.completedFuture(Collections.emptyList()); + }).orElse( CompletableFuture.completedFuture(List.of())); + } + + CompletableFuture> fetchGroups() { + return getExecutor(WS_GROUPS_CMD_CAP).map(lss -> { + List> res = lss.computeAll(ls -> ls.getWorkspaceService().executeCommand(new ExecuteCommandParams(FETCH_STRUCTURE_GROUPS, List.of()))); + final List groups = Collections.synchronizedList(new ArrayList<>()); + final Gson gson = new GsonBuilder().create(); + for (CompletableFuture<@Nullable Object> f : res) { + f.thenAccept(o -> { + JsonElement json = null; + if (o instanceof List) { + json = gson.toJsonTree(o); + } else if (o instanceof JsonElement) { + json = (JsonElement) o; + } + if (json != null) { + Groups[] g = gson.fromJson(json, Groups[].class); + if (g != null) { + groups.addAll(Arrays.asList(g)); + } + } + }); + } + return CompletableFuture.allOf(res.toArray(new CompletableFuture[res.size()])).thenApply(v -> groups); + }).orElse(CompletableFuture.completedFuture(List.of())); } + private Optional getExecutor(Predicate capabilityFilter) { + List allSpringProjects = BootProjectTracker.streamSpringProjects().toList(); + if (!allSpringProjects.isEmpty()) { + return Optional.of(LanguageServers.forProject(allSpringProjects.get(0).getProject()).withFilter(capabilityFilter).excludeInactive()); + } + return Optional.empty(); + } - private record StructureParameter(boolean updateMetadata, Map> groupings) {} } diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StructureTreeLabelProvider.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StructureTreeLabelProvider.java index 44e53c2a91..da7aa70f12 100644 --- a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StructureTreeLabelProvider.java +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StructureTreeLabelProvider.java @@ -25,7 +25,7 @@ public class StructureTreeLabelProvider extends LabelProvider { @Override public String getText(Object element) { if (element instanceof StereotypeNode) { - return ((StereotypeNode) element).getText(); + return ((StereotypeNode) element).text(); } return super.getText(element); } @@ -33,7 +33,7 @@ public String getText(Object element) { @Override public Image getImage(Object element) { if (element instanceof StereotypeNode) { - String descriptor = ((StereotypeNode) element).getIcon(); + String descriptor = ((StereotypeNode) element).icon(); if (descriptor != null && !descriptor.isBlank()) { return BootLanguageServerPlugin.getDefault().getStereotypeImage(descriptor); } diff --git a/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/src/org/springframework/tooling/jdt/ls/commons/BootProjectTracker.java b/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/src/org/springframework/tooling/jdt/ls/commons/BootProjectTracker.java index 02e8fb4a37..0dbffbed85 100644 --- a/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/src/org/springframework/tooling/jdt/ls/commons/BootProjectTracker.java +++ b/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/src/org/springframework/tooling/jdt/ls/commons/BootProjectTracker.java @@ -107,7 +107,13 @@ public static Stream streamSpringProjects() { return Arrays.stream(ResourcesPlugin.getWorkspace().getRoot().getProjects()) .filter(p -> p != null && p.isAccessible()) .map(JavaCore::create) - .filter(jp -> jp != null && jp.exists()); + .filter(jp -> { + try { + return jp != null && jp.exists() && isSpringProject(jp); + } catch (JavaModelException e) { + return false; + } + }); } private static boolean isBootProject(IClasspathEntry e) { 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 9d4b2c962c..d95624eab7 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 @@ -78,7 +78,7 @@ public SpringIndexCommands(SimpleLanguageServer server, SpringMetamodelIndex spr return projectFinder.all().stream().filter(p -> projectName.equals(p.getElementName())).findFirst().map(this::getGroups).orElseThrow(); } } - return projectFinder.all().stream().map(this::getGroups); + return projectFinder.all().stream().map(this::getGroups).toList(); })); } From 35209e8ca18074b609f660529c81acc1b0ba1d99 Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Tue, 21 Oct 2025 10:16:05 -0700 Subject: [PATCH 3/8] Open JAR resource (reference) command - WIP Signed-off-by: BoykoAlex --- .../boot/ls/views/LogicalStructureView.java | 3 + .../tooling/boot/ls/views/StereotypeNode.java | 2 +- .../ls/views/StereotypeNodeDeserializer.java | 2 +- .../plugin.xml | 18 +++ .../commands/OpenJarEntryInEditor.java | 126 ++++++++++++++++++ .../commands/OpenJavaElementInEditor.java | 4 +- .../boot/java/commands/JsonNodeHandler.java | 25 ++-- .../boot/java/links/EclipseSourceLinks.java | 28 ++++ .../lib/explorer/structure-tree-manager.ts | 10 +- 9 files changed, 204 insertions(+), 14 deletions(-) create mode 100644 eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/commands/OpenJarEntryInEditor.java diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/LogicalStructureView.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/LogicalStructureView.java index 6aa45ea622..baf63ffa62 100644 --- a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/LogicalStructureView.java +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/LogicalStructureView.java @@ -97,6 +97,9 @@ public void createPartControl(Composite parent) { if (o instanceof StereotypeNode) { StereotypeNode n = (StereotypeNode) o; Location l = n.location(); + if (l == null) { + l = n.reference(); + } if (l != null) { LSPEclipseUtils.openInEditor(l); } diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StereotypeNode.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StereotypeNode.java index 3ec1863d36..fa833cd392 100644 --- a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StereotypeNode.java +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StereotypeNode.java @@ -20,7 +20,7 @@ * * @author Alex Boyko */ -record StereotypeNode(String id, String text, String icon, Location location, String reference, Map attributes, StereotypeNode[] children) { +record StereotypeNode(String id, String text, String icon, Location location, Location reference, Map attributes, StereotypeNode[] children) { @Override public boolean equals(Object obj) { diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StereotypeNodeDeserializer.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StereotypeNodeDeserializer.java index f325cc6720..c4abb02c74 100644 --- a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StereotypeNodeDeserializer.java +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/views/StereotypeNodeDeserializer.java @@ -42,7 +42,7 @@ public StereotypeNode deserialize(JsonElement json, Type type, JsonDeserializati extractString(attributes, TEXT), extractString(attributes, ICON), context.deserialize(attributes.get(LOCATION), Location.class), - extractString(attributes, REFERENCE), + context.deserialize(attributes.get(REFERENCE), Location.class), context.deserialize(attributes, new TypeToken>(){}.getType()), context.deserialize(object.get(CHILDREN), StereotypeNode[].class) ); diff --git a/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/plugin.xml b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/plugin.xml index 0ab41f8369..8dcb1abdc8 100644 --- a/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/plugin.xml +++ b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/plugin.xml @@ -103,6 +103,10 @@ class="org.springframework.tooling.ls.eclipse.commons.commands.ExplainwithAi" commandId="vscode-spring-boot.query.explain"> + + @@ -177,6 +181,20 @@ typeId="org.eclipse.lsp4e.commandParameterType"> + + + + + + diff --git a/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/commands/OpenJarEntryInEditor.java b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/commands/OpenJarEntryInEditor.java new file mode 100644 index 0000000000..e65994c92f --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/commands/OpenJarEntryInEditor.java @@ -0,0 +1,126 @@ +/******************************************************************************* + * 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.tooling.ls.eclipse.commons.commands; + +import java.net.URI; +import java.util.Optional; + +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; +import org.eclipse.core.commands.IHandler; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.Status; +import org.eclipse.jdt.core.IClassFile; +import org.eclipse.jdt.core.IJarEntryResource; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IPackageFragment; +import org.eclipse.jdt.core.IPackageFragmentRoot; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.internal.ui.javaeditor.EditorUtility; +import org.eclipse.ui.PartInitException; +import org.springframework.tooling.ls.eclipse.commons.LanguageServerCommonsActivator; + +@SuppressWarnings("restriction") +public class OpenJarEntryInEditor extends AbstractHandler implements IHandler { + + private static final String JAR_URI_PARAM = "jarUri"; + + record JarUri(IPath jar, IPath path) {} + + private static JarUri createJarUri(URI uri) { + if (!"jar".equals(uri.getScheme())) { + throw new IllegalArgumentException(); + } + String s = uri.getSchemeSpecificPart(); + int idx = s.indexOf('!'); + if (idx <= 0) { + throw new IllegalArgumentException(); + } + return new JarUri(new Path(URI.create(s.substring(0, idx)).getPath()), new Path(s.substring(idx + 1))); + } + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + String projectName = event.getParameter(OpenJavaElementInEditor.PROJECT_NAME); + String jarUriStr = event.getParameter(JAR_URI_PARAM); + + IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(projectName); + + if (project != null && jarUriStr != null) { + URI uri = URI.create(jarUriStr); + IJavaProject javaProject = JavaCore.create(project); + if (javaProject != null) { + JarUri jarUri = createJarUri(uri); + findJavaObj(javaProject, jarUri).ifPresent(s -> { + try { + EditorUtility.openInEditor(s, true); + } catch (PartInitException e) { + LanguageServerCommonsActivator.getInstance().getLog().log(e.getStatus()); + } + }); + } else { + LanguageServerCommonsActivator.getInstance().getLog().log(new Status(IStatus.WARNING, + LanguageServerCommonsActivator.PLUGIN_ID, "Cannot find project: " + projectName)); + } + } + + return null; + } + + private static Optional findJavaObj(IJavaProject j, JarUri uri) { + try { + for (IPackageFragmentRoot fr : j.getAllPackageFragmentRoots()) { + if (uri.jar().equals(fr.getPath())) { + for (Object o : fr.getNonJavaResources()) { + if (o instanceof IJarEntryResource je) { + return findJarEntry(je, uri.path()); + } + } + if ("class".equals(uri.path().getFileExtension())) { + String packageName = uri.path().removeLastSegments(1).toString().replace(IPath.SEPARATOR, '.'); + IPackageFragment pkg = fr.getPackageFragment(packageName); + if (pkg != null) { + IClassFile cf = pkg.getClassFile(uri.path().lastSegment()); + if (cf != null) { + return Optional.of(cf); + } + } + } + + } + } + } catch (JavaModelException e) { + LanguageServerCommonsActivator.getInstance().getLog().log(e.getStatus()); + } + return Optional.empty(); + } + + private static Optional findJarEntry(IJarEntryResource r, IPath p) { + if (r.getFullPath().equals(p)) { + return Optional.of(r); + } else if (r.getFullPath().isPrefixOf(p)) { + for (IJarEntryResource c : r.getChildren()) { + Optional opt = findJarEntry(c, p); + if (opt.isPresent()) { + return opt; + } + } + } + return Optional.empty(); + } + +} diff --git a/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/commands/OpenJavaElementInEditor.java b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/commands/OpenJavaElementInEditor.java index 9017a3e350..77fa40dfc4 100644 --- a/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/commands/OpenJavaElementInEditor.java +++ b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/commands/OpenJavaElementInEditor.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018, 2019 Pivotal, Inc. + * Copyright (c) 2018, 2025 Pivotal, 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 @@ -35,7 +35,7 @@ public class OpenJavaElementInEditor extends AbstractHandler { private static final String BINDING_KEY = "bindingKey"; - private static final String PROJECT_NAME = "projectName"; + static final String PROJECT_NAME = "projectName"; @Override public Object execute(ExecutionEvent event) throws ExecutionException { 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 7c1b11e61f..792ab5e747 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 @@ -29,16 +29,20 @@ import org.eclipse.lsp4j.DocumentSymbol; import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.Range; import org.jmolecules.stereotype.api.Stereotype; import org.jmolecules.stereotype.catalog.StereotypeCatalog; import org.jmolecules.stereotype.tooling.LabelProvider; 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.links.EclipseSourceLinks; 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; import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.languageserver.util.LspClient; +import org.springframework.ide.vscode.commons.languageserver.util.LspClient.Client; import org.springframework.ide.vscode.commons.protocol.spring.SymbolElement; import com.google.gson.Gson; @@ -94,30 +98,35 @@ public void handleStereotype(Stereotype stereotype, NodeContext context) { var definition = catalog.getDefinition(stereotype); var sources = definition.getSources(); - String reference = null; + Location reference = null; for (Object source : sources) { if (source instanceof URL) { URL url = (URL) source; + 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); + reference = new Location(url.toURI().toASCIIString(), new Range()); + if ("jar".equals(url.getProtocol())) { + if (LspClient.currentClient() == Client.ECLIPSE) { + reference.setUri(EclipseSourceLinks.eclipseIntroUriForJarEntry(project.getElementName(), url.toURI()).toASCIIString()); + } else { + reference.setUri(url.toURI().toASCIIString().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(); + reference = (Location) source; } } - final String referenceUri = reference; + final Location referenceLocation = reference; addChild(node -> node .withAttribute(TEXT, labels.getStereotypeLabel(stereotype)) .withAttribute(ICON, StereotypeIcons.getIcon(stereotype)) .withAttribute(HOVER, "defined in: " + sources.toString()) - .withAttribute(REFERENCE, referenceUri) + .withAttribute(REFERENCE, referenceLocation) ); } } @@ -211,7 +220,7 @@ private static void assignNodeId(Node n, Node p) { Location location = (Location) n.attributes.get(LOCATION); String locationId = location == null ? "" : "%s:%d:%d".formatted(location.getUri(), location.getRange().getStart().getLine(), location.getRange().getStart().getCharacter()); - String referenceId = n.attributes.containsKey(REFERENCE) ? (String) n.attributes.get(REFERENCE) : ""; + String referenceId = n.attributes.containsKey(REFERENCE) ? ((Location) n.attributes.get(REFERENCE)).getUri() : ""; String nodeSpecificId = "%s|%s|%s".formatted(textId, locationId, referenceId).replaceAll("\\|+$", ""); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/EclipseSourceLinks.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/EclipseSourceLinks.java index f91f40b573..a1c594f21b 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/EclipseSourceLinks.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/EclipseSourceLinks.java @@ -44,6 +44,9 @@ public class EclipseSourceLinks implements SourceLinks { private static final String RESOURCE_COMMAND = "org.springframework.tooling.ls.eclipse.commons.commands.OpenResourceInEditor"; private static final String PATH = "path"; + private static final String JAR_ENTRY_COMMAND = "org.springframework.tooling.ls.eclipse.commons.commands.OpenJarEntryInEditor"; + private static final String JAR_URI_PARAM = "jarUri"; + private static final Logger log = LoggerFactory.getLogger(EclipseSourceLinks.class); private JavaProjectFinder projectFinder; @@ -130,5 +133,30 @@ public static URI eclipseIntroUri(IJavaProject project, IMember member) { } return null; } + + public static URI eclipseIntroUriForJarEntry(String projectName, URI jarEntryUri) { + try { + StringBuilder paramBuilder = new StringBuilder(JAR_ENTRY_COMMAND); + + paramBuilder.append(PARAMETERS_START); + paramBuilder.append(JAR_URI_PARAM); + paramBuilder.append(EQUALS); + paramBuilder.append(jarEntryUri.toString()); + + paramBuilder.append(PARAMETERS_SEPARATOR); + paramBuilder.append(PROJECT_NAME_PARAMETER_ID); + paramBuilder.append(EQUALS); + paramBuilder.append(projectName); + + paramBuilder.append(PARAMETERS_END); + + StringBuilder urlBuilder = new StringBuilder(URL_PREFIX); + urlBuilder.append(URLEncoder.encode(paramBuilder.toString(), "UTF8")); + return URI.create(urlBuilder.toString()); + } catch (UnsupportedEncodingException e) { + log.error("{}", e); + } + return null; + } } 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 263c106a40..1abcdeae1c 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,8 @@ import { commands, EventEmitter, Event, ExtensionContext, window, Memento, Uri, QuickPickItem } from "vscode"; import { SpringNode, StereotypedNode } from "./nodes"; +import { ExtensionAPI } from "../api"; +import * as ls from 'vscode-languageserver-protocol'; + const SPRING_STRUCTURE_CMD = "sts/spring-boot/structure"; @@ -9,14 +12,14 @@ export class StructureManager { private _onDidChange: EventEmitter = new EventEmitter(); private workspaceState: Memento; - constructor(context: ExtensionContext) { + constructor(context: ExtensionContext, api: ExtensionAPI) { 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) { - commands.executeCommand('vscode.open', Uri.parse(reference)); + commands.executeCommand('vscode.open', api.client.protocol2CodeConverter.asLocation(reference as ls.Location)); } } })); @@ -42,6 +45,9 @@ export class StructureManager { this.refresh(false); } })); + + context.subscriptions.push(api.getSpringIndex().onSpringIndexUpdated(e => this.refresh(false))); + } get rootElements(): Thenable { From dbabc6d16932d48aa126c9c52106ab690c5ae527 Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Tue, 21 Oct 2025 14:27:24 -0700 Subject: [PATCH 4/8] Navigate to Reference Signed-off-by: BoykoAlex --- .../ide/vscode/boot/app/CommandsConfig.java | 6 +- .../commands/JMoleculesStructureView.java | 7 +- .../boot/java/commands/JsonNodeHandler.java | 26 +-- .../ide/vscode/boot/java/commands/Misc.java | 3 +- .../java/commands/ModulithStructureView.java | 7 +- .../java/commands/SpringIndexCommands.java | 9 +- .../boot/java/links/AbstractSourceLinks.java | 200 +++--------------- .../boot/java/links/AtomSourceLinks.java | 6 +- .../boot/java/links/EclipseSourceLinks.java | 10 +- .../java/links/JavaServerSourceLinks.java | 17 +- .../boot/java/links/LegacySourceLinks.java | 198 +++++++++++++++++ .../boot/java/links/SourceLinkFactory.java | 8 +- .../vscode/boot/java/links/SourceLinks.java | 12 +- .../boot/java/links/VSCodeSourceLinks.java | 4 +- 14 files changed, 305 insertions(+), 208 deletions(-) create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/LegacySourceLinks.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 e736737d68..a8be38e767 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 @@ -16,6 +16,7 @@ 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.links.SourceLinks; import org.springframework.ide.vscode.boot.java.stereotypes.StereotypeCatalogRegistry; import org.springframework.ide.vscode.boot.modulith.ModulithService; import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; @@ -30,8 +31,9 @@ public class CommandsConfig { @Bean SpringIndexCommands springIndexCommands(SimpleLanguageServer server, JavaProjectFinder projectFinder, - SpringMetamodelIndex symbolIndex, ModulithService modulithService, StereotypeCatalogRegistry stereotypeCatalogRegistry) { - return new SpringIndexCommands(server, symbolIndex, modulithService, projectFinder, stereotypeCatalogRegistry); + SpringMetamodelIndex symbolIndex, ModulithService modulithService, StereotypeCatalogRegistry stereotypeCatalogRegistry, + SourceLinks sourceLinks) { + return new SpringIndexCommands(server, symbolIndex, modulithService, projectFinder, stereotypeCatalogRegistry, sourceLinks); } @Bean 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 c10ce4139d..b1487e55b9 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 @@ -19,6 +19,7 @@ import org.jmolecules.stereotype.tooling.ProjectTree; import org.jmolecules.stereotype.tooling.SimpleLabelProvider; import org.springframework.ide.vscode.boot.java.commands.JsonNodeHandler.Node; +import org.springframework.ide.vscode.boot.java.links.SourceLinks; import org.springframework.ide.vscode.boot.java.stereotypes.IndexBasedStereotypeFactory; import org.springframework.ide.vscode.boot.java.stereotypes.StereotypeClassElement; import org.springframework.ide.vscode.boot.java.stereotypes.StereotypeMethodElement; @@ -29,10 +30,12 @@ public class JMoleculesStructureView { private final AbstractStereotypeCatalog catalog; private final CachedSpringMetamodelIndex springIndex; + private final SourceLinks sourceLinks; - public JMoleculesStructureView(AbstractStereotypeCatalog catalog, CachedSpringMetamodelIndex springIndex) { + public JMoleculesStructureView(AbstractStereotypeCatalog catalog, CachedSpringMetamodelIndex springIndex, SourceLinks sourceLinks) { this.catalog = catalog; this.springIndex = springIndex; + this.sourceLinks = sourceLinks; } public Node createTree(IJavaProject project, IndexBasedStereotypeFactory factory, Collection selectedGroups) { @@ -56,7 +59,7 @@ public Node createTree(IJavaProject project, IndexBasedStereotypeFactory factory }; // create json nodes to display the structure in a nice way - var jsonHandler = new JsonNodeHandler(labelProvider, consumer, springIndex, catalog, project); + var jsonHandler = new JsonNodeHandler(labelProvider, consumer, springIndex, sourceLinks, catalog, project); // create the project tree and apply all the groupers from the project // TODO: in the future, we need to trim this grouper arrays down to what is selected on the UI 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 792ab5e747..47fe6933c3 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,6 +16,7 @@ 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,13 +37,13 @@ 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.links.EclipseSourceLinks; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.boot.java.links.SourceLinks; 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; import org.springframework.ide.vscode.commons.java.IJavaProject; -import org.springframework.ide.vscode.commons.languageserver.util.LspClient; -import org.springframework.ide.vscode.commons.languageserver.util.LspClient.Client; import org.springframework.ide.vscode.commons.protocol.spring.SymbolElement; import com.google.gson.Gson; @@ -53,6 +54,8 @@ * @author Martin Lippert */ public class JsonNodeHandler implements NodeHandler { + + private static final Logger log = LoggerFactory.getLogger(JsonNodeHandler.class); private static final String PROJECT_ID = "projectId"; @@ -69,16 +72,18 @@ public class JsonNodeHandler implements NodeHandler labels; private final BiConsumer customHandler; private final CachedSpringMetamodelIndex springIndex; + private final SourceLinks sourceLinks; private final IJavaProject project; private Node current; private StereotypeCatalog catalog; public JsonNodeHandler(LabelProvider labels, BiConsumer customHandler, - CachedSpringMetamodelIndex springIndex, StereotypeCatalog catalog, IJavaProject project) { + CachedSpringMetamodelIndex springIndex, SourceLinks sourceLinks, StereotypeCatalog catalog, IJavaProject project) { this.labels = labels; this.springIndex = springIndex; this.customHandler = customHandler; + this.sourceLinks = sourceLinks; this.project = project; this.root = new Node(null); @@ -104,16 +109,13 @@ public void handleStereotype(Stereotype stereotype, NodeContext context) { URL url = (URL) source; try { - reference = new Location(url.toURI().toASCIIString(), new Range()); - if ("jar".equals(url.getProtocol())) { - if (LspClient.currentClient() == Client.ECLIPSE) { - reference.setUri(EclipseSourceLinks.eclipseIntroUriForJarEntry(project.getElementName(), url.toURI()).toASCIIString()); - } else { - reference.setUri(url.toURI().toASCIIString().replaceFirst(Misc.JAR_URL_PROTOCOL_PREFIX, Misc.BOOT_LS_URL_PRTOCOL_PREFIX)); - } + URI uri = url.toURI(); + reference = new Location(uri.toASCIIString(), new Range()); + if (Misc.JAR.equals(uri.getScheme())) { + sourceLinks.sourceLinkForJarEntry(project, uri).map(u -> u.toASCIIString()).ifPresent(reference::setUri); } } catch (URISyntaxException e) { - // something went wrong + log.error("", e); } } else if (source instanceof Location) { 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 3571029fa8..af5da84760 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 @@ -24,7 +24,8 @@ public class Misc { public static final String BOOT_LS_URL_PRTOCOL_PREFIX = "spring-boot-ls:"; - public static final String JAR_URL_PROTOCOL_PREFIX = "jar:"; + public static final String JAR = "jar"; + public static final String JAR_URL_PROTOCOL_PREFIX = JAR + ":"; private static final String STS_FETCH_JAR_CONTENT = "sts/jar/fetch-content"; 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 38b31af626..97b6e1db9e 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 @@ -19,6 +19,7 @@ import org.jmolecules.stereotype.tooling.ProjectTree; import org.springframework.ide.vscode.boot.java.commands.ApplicationModulesStructureProvider.SimpleApplicationModulesStructureProvider; import org.springframework.ide.vscode.boot.java.commands.JsonNodeHandler.Node; +import org.springframework.ide.vscode.boot.java.links.SourceLinks; import org.springframework.ide.vscode.boot.java.stereotypes.IndexBasedStereotypeFactory; import org.springframework.ide.vscode.boot.modulith.AppModules; import org.springframework.ide.vscode.boot.modulith.ModulithService; @@ -29,10 +30,12 @@ public class ModulithStructureView { private final AbstractStereotypeCatalog catalog; private final CachedSpringMetamodelIndex springIndex; private final ModulithService modulithService; + private SourceLinks sourceLinks; - public ModulithStructureView(AbstractStereotypeCatalog catalog, CachedSpringMetamodelIndex springIndex, ModulithService modulithService) { + public ModulithStructureView(AbstractStereotypeCatalog catalog, CachedSpringMetamodelIndex springIndex, SourceLinks sourceLinks, ModulithService modulithService) { this.catalog = catalog; this.springIndex = springIndex; + this.sourceLinks = sourceLinks; this.modulithService = modulithService; } @@ -57,7 +60,7 @@ public Node createTree(IJavaProject project, IndexBasedStereotypeFactory factory }; // create json nodes to display the structure in a nice way - var jsonHandler = new JsonNodeHandler(labelProvider, consumer, springIndex, catalog, project); + var jsonHandler = new JsonNodeHandler(labelProvider, consumer, springIndex, sourceLinks, catalog, project); // create the project tree and apply all the groupers from the project // TODO: in the future, we need to trim this grouper arrays down to what is selected on the UI 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 d95624eab7..82614f4c92 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 @@ -23,6 +23,7 @@ import org.slf4j.LoggerFactory; import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; import org.springframework.ide.vscode.boot.java.commands.JsonNodeHandler.Node; +import org.springframework.ide.vscode.boot.java.links.SourceLinks; import org.springframework.ide.vscode.boot.java.stereotypes.IndexBasedStereotypeFactory; import org.springframework.ide.vscode.boot.java.stereotypes.StereotypeCatalogRegistry; import org.springframework.ide.vscode.boot.modulith.ModulithService; @@ -45,12 +46,14 @@ public class SpringIndexCommands { private final ModulithService modulithService; private final StereotypeCatalogRegistry stereotypeCatalogRegistry; + private final SourceLinks sourceLinks; public SpringIndexCommands(SimpleLanguageServer server, SpringMetamodelIndex springIndex, ModulithService modulithService, - JavaProjectFinder projectFinder, StereotypeCatalogRegistry stereotypeCatalogRegistry) { + JavaProjectFinder projectFinder, StereotypeCatalogRegistry stereotypeCatalogRegistry, SourceLinks sourceLinks) { this.modulithService = modulithService; this.stereotypeCatalogRegistry = stereotypeCatalogRegistry; + this.sourceLinks = sourceLinks; server.onCommand(SPRING_STRUCTURE_CMD, params -> server.getAsync().invoke(() -> { StructureCommandArgs args = StructureCommandArgs.parseFrom(params); @@ -112,10 +115,10 @@ private Node nodeFrom(IJavaProject project, CachedSpringMetamodelIndex springInd } if (ModulithService.isModulithDependentProject(project) && StructureViewUtil.hasModulithStructureViewEnabled()) { - return new ModulithStructureView(catalog, springIndex, modulithService).createTree(project, factory, selectedGroups); + return new ModulithStructureView(catalog, springIndex, sourceLinks, modulithService).createTree(project, factory, selectedGroups); } else { - return new JMoleculesStructureView(catalog, springIndex).createTree(project, factory, selectedGroups); + return new JMoleculesStructureView(catalog, springIndex, sourceLinks).createTree(project, factory, selectedGroups); } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/AbstractSourceLinks.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/AbstractSourceLinks.java index 107cc146ae..47c6b9f13d 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/AbstractSourceLinks.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/AbstractSourceLinks.java @@ -1,198 +1,50 @@ /******************************************************************************* - * Copyright (c) 2018, 2019 Pivotal, Inc. + * 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: - * Pivotal, Inc. - initial API and implementation + * Broadcom, Inc. - initial API and implementation *******************************************************************************/ package org.springframework.ide.vscode.boot.java.links; import java.io.File; +import java.io.IOException; +import java.net.JarURLConnection; import java.net.URI; -import java.net.URISyntaxException; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Optional; -import java.util.Stack; -import org.eclipse.jdt.core.dom.ASTVisitor; -import org.eclipse.jdt.core.dom.AbstractTypeDeclaration; -import org.eclipse.jdt.core.dom.AnnotationTypeDeclaration; -import org.eclipse.jdt.core.dom.CompilationUnit; -import org.eclipse.jdt.core.dom.EnumDeclaration; -import org.eclipse.jdt.core.dom.TypeDeclaration; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; -import org.springframework.ide.vscode.commons.java.IClasspath; -import org.springframework.ide.vscode.commons.java.IClasspathUtil; -import org.springframework.ide.vscode.commons.java.IJavaModuleData; +import org.eclipse.core.runtime.Assert; +import org.springframework.ide.vscode.boot.java.commands.Misc; import org.springframework.ide.vscode.commons.java.IJavaProject; -import org.springframework.ide.vscode.commons.javadoc.TypeUrlProviderFromContainerUrl; -import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; -import org.springframework.ide.vscode.commons.util.text.Region; -/** - * Base logic for {@link SourceLinks} independent of any client - * - * @author Alex Boyko - * - */ public abstract class AbstractSourceLinks implements SourceLinks { - private static final Logger log = LoggerFactory.getLogger(AbstractSourceLinks.class); - - private CompilationUnitCache cuCache; - - private JavaProjectFinder projectFinder; - - protected AbstractSourceLinks(CompilationUnitCache cuCache, JavaProjectFinder projectFinder) { - this.cuCache = cuCache; - this.projectFinder = projectFinder; - } - - @Override - public Optional sourceLinkUrlForFQName(IJavaProject project, String fqName) { - Optional url = project == null ? Optional.empty() : getSourceLinkUrlForFQName(project, fqName); - if (!url.isPresent()) { - for (IJavaProject jp : projectFinder.all()) { - if (jp != project) { - url = getSourceLinkUrlForFQName(jp, fqName); - if (url.isPresent()) { - break; - } - } - } - } - return url; - } - - private Optional getSourceLinkUrlForFQName(IJavaProject project, String fqName) { - IJavaModuleData classpathResource = project.getIndex().findClasspathResourceContainer(fqName); - if (classpathResource != null) { - File file = classpathResource.getContainer(); - if (file.isDirectory()) { - return javaSourceLinkUrl(project, fqName, classpathResource); - } else { - return jarSourceLinkUrl(project, fqName, classpathResource); - } - } - return Optional.empty(); - } - - @Override - public Optional sourceLinkUrlForClasspathResource(String path) { - return SourceLinks.sourceLinkUrlForClasspathResource(this, projectFinder, path); - } - - private Optional javaSourceLinkUrl(IJavaProject project, String fqName, IJavaModuleData folderModuleData) { - IClasspath classpath = project.getClasspath(); - return SourceLinks.sourceFromSourceFolder(fqName, classpath) - .map(sourcePath -> javaSourceLinkUrl(project, sourcePath, fqName)); - } - - private String javaSourceLinkUrl(IJavaProject project, Path sourcePath, String fqName) { - Optional linkOptional = sourceLinkForResourcePath(sourcePath); - if (linkOptional.isPresent()) { - Optional positionLink = findCU(project, sourcePath.toUri()).map(cu -> positionLink(cu, fqName)); - return positionLink.isPresent() ? linkOptional.get() + positionLink.get() : linkOptional.get(); - } - return null; - } - - abstract protected String positionLink(CompilationUnit cu, String fqName); - - private Optional findCU(IJavaProject project, URI uri) { - return cuCache == null ? Optional.empty() : cuCache.withCompilationUnit(project, uri, compilationUnit -> Optional.ofNullable(compilationUnit)); - } - - abstract protected Optional jarLinkUrl(IJavaProject project, String fqName, IJavaModuleData jarModuleData); - - private Optional jarSourceLinkUrl(IJavaProject project, String fqName, IJavaModuleData jarModuleData) { - return jarLinkUrl(project, fqName, jarModuleData).map(sourceUrl -> { - Optional positionLink = findCUForFQNameFromJar(project, jarModuleData, fqName).map(cu -> positionLink(cu, fqName)); - return positionLink.isPresent() ? sourceUrl + positionLink.get() : sourceUrl; - }); - } - - private Optional findCUForFQNameFromJar(IJavaProject project, IJavaModuleData jarModuleData, String fqName) { - return IClasspathUtil.sourceContainer(project.getClasspath(), jarModuleData.getContainer()).map(url -> { - try { - return TypeUrlProviderFromContainerUrl.JAR_SOURCE_URL_PROVIDER.url(url, fqName, jarModuleData.getModule()); - } catch (Exception e) { - log.warn("Failed to determine source URL from url={} fqName={}", url, fqName, e); - return null; - } - }).map(sourceUrl -> { - try { - return sourceUrl.toURI(); - } catch (URISyntaxException e) { - throw new IllegalStateException(e); + public Optional sourceLinkForJarEntry(IJavaProject contextProject, URI uri) { + Assert.isTrue(Misc.JAR.equals(uri.getScheme())); + try { + JarURLConnection c = (JarURLConnection) uri.toURL().openConnection(); + Path jarPath = Paths.get(c.getJarFile().getName()); + if (!Files.exists(jarPath)) { + // URL for CF resources looks like + // jar:file:/home/vcap/app/lib/gs-rest-service-complete.jar!/hello/MyService.class + String entryName = c.getEntryName(); + if (entryName.endsWith(CLASS)) { + String fqName = entryName.substring(0, entryName.length() - CLASS.length()).replace(File.separator, + "."); + return sourceLinkUrlForFQName(null, fqName).map(s -> URI.create(s)); + } + return Optional.empty(); } - }).map(sourcePath -> findCU(project, sourcePath).orElse(null)); - } - - protected Region findTypeRegion(CompilationUnit cu, String fqName) { - if (cu == null) { - return null; - } - int[] values = new int[] {0, -1}; - int lastDotIndex = fqName.lastIndexOf('.'); - String packageName = fqName.substring(0, lastDotIndex); - String typeName = fqName.substring(lastDotIndex + 1); - if (packageName.equals(cu.getPackage().getName().getFullyQualifiedName())) { - Stack visitedType = new Stack<>(); - cu.accept(new ASTVisitor() { - - private boolean visitDeclaration(AbstractTypeDeclaration node) { - visitedType.push(node.getName().getIdentifier()); - if (values[1] < 0) { - if (String.join("$", visitedType.toArray(new String[visitedType.size()])).equals(typeName)) { - values[0] = node.getName().getStartPosition(); - values[1] = node.getName().getLength(); - } - } - return values[1] < 0; - } - - @Override - public boolean visit(TypeDeclaration node) { - return visitDeclaration(node); - } - - @Override - public boolean visit(AnnotationTypeDeclaration node) { - return visitDeclaration(node); - } - - @Override - public boolean visit(EnumDeclaration node) { - return visitDeclaration(node); - } - - @Override - public void endVisit(EnumDeclaration node) { - visitedType.pop(); - super.endVisit(node); - } - - @Override - public void endVisit(AnnotationTypeDeclaration node) { - visitedType.pop(); - super.endVisit(node); - } - - @Override - public void endVisit(TypeDeclaration node) { - visitedType.pop(); - super.endVisit(node); - } - - }); + } catch (IOException e) { + log.error("", e); } - return values[1] < 0 ? null : new Region(values[0], values[1]); + return Optional.of(uri); } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/AtomSourceLinks.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/AtomSourceLinks.java index ea413e3c2f..badd8254ae 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/AtomSourceLinks.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/AtomSourceLinks.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018, 2019 Pivotal, Inc. + * Copyright (c) 2018, 2025 Pivotal, 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 @@ -33,9 +33,9 @@ * @author Alex Boyko * */ -public class AtomSourceLinks extends AbstractSourceLinks { +public class AtomSourceLinks extends LegacySourceLinks { - private static Supplier LOG = Suppliers.memoize(() -> LoggerFactory.getLogger(AbstractSourceLinks.class)); + private static Supplier LOG = Suppliers.memoize(() -> LoggerFactory.getLogger(LegacySourceLinks.class)); public AtomSourceLinks(CompilationUnitCache cuCache, JavaProjectFinder projectFinder) { super(cuCache, projectFinder); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/EclipseSourceLinks.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/EclipseSourceLinks.java index a1c594f21b..2f1153a184 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/EclipseSourceLinks.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/EclipseSourceLinks.java @@ -29,7 +29,7 @@ * @author Alex Boyko * */ -public class EclipseSourceLinks implements SourceLinks { +public class EclipseSourceLinks extends AbstractSourceLinks { private static final String URL_PREFIX = "http://org.eclipse.ui.intro/execute?command="; private static final String EQUALS = "="; @@ -134,7 +134,7 @@ public static URI eclipseIntroUri(IJavaProject project, IMember member) { return null; } - public static URI eclipseIntroUriForJarEntry(String projectName, URI jarEntryUri) { + static URI eclipseIntroUriForJarEntry(String projectName, URI jarEntryUri) { try { StringBuilder paramBuilder = new StringBuilder(JAR_ENTRY_COMMAND); @@ -159,4 +159,10 @@ public static URI eclipseIntroUriForJarEntry(String projectName, URI jarEntryUri return null; } + @Override + public Optional sourceLinkForJarEntry(IJavaProject contextProject, URI uri) { + return super.sourceLinkForJarEntry(contextProject, uri) + .map(u -> eclipseIntroUriForJarEntry(contextProject.getElementName(), u)); + } + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/JavaServerSourceLinks.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/JavaServerSourceLinks.java index c6c871b32e..b897fdf86a 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/JavaServerSourceLinks.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/JavaServerSourceLinks.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018, 2023 Pivotal, Inc. + * Copyright (c) 2018, 2025 Pivotal, 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 @@ -10,6 +10,7 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.links; +import java.net.URI; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.nio.file.Path; @@ -19,12 +20,15 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import org.springframework.ide.vscode.boot.java.commands.Misc; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.languageserver.util.LspClient; +import org.springframework.ide.vscode.commons.languageserver.util.LspClient.Client; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; import org.springframework.ide.vscode.commons.protocol.java.JavaDataParams; -public class JavaServerSourceLinks implements SourceLinks { +public class JavaServerSourceLinks extends AbstractSourceLinks { private SimpleLanguageServer server; private JavaProjectFinder projectFinder; @@ -66,4 +70,13 @@ public Optional sourceLinkForResourcePath(Path path) { return Optional.ofNullable(path).map(p -> p.toUri().toASCIIString()); } + @Override + public Optional sourceLinkForJarEntry(IJavaProject contextProject, URI uri) { + // Ideally client should be asked for a URI for a JAR entry that it can deal with. + // It feels a bit too much adding this message to STS Client at the moment hence we check if the client is Eclipse here + return super.sourceLinkForJarEntry(contextProject, uri).map(u -> LspClient.currentClient() == Client.ECLIPSE + ? EclipseSourceLinks.eclipseIntroUriForJarEntry(contextProject.getElementName(), uri) + : URI.create(uri.toString().replace(Misc.JAR_URL_PROTOCOL_PREFIX, Misc.BOOT_LS_URL_PRTOCOL_PREFIX))); + } + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/LegacySourceLinks.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/LegacySourceLinks.java new file mode 100644 index 0000000000..64f64859e3 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/LegacySourceLinks.java @@ -0,0 +1,198 @@ +/******************************************************************************* + * Copyright (c) 2018, 2025 Pivotal, 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: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.links; + +import java.io.File; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.Stack; + +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.AbstractTypeDeclaration; +import org.eclipse.jdt.core.dom.AnnotationTypeDeclaration; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.EnumDeclaration; +import org.eclipse.jdt.core.dom.TypeDeclaration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; +import org.springframework.ide.vscode.commons.java.IClasspath; +import org.springframework.ide.vscode.commons.java.IClasspathUtil; +import org.springframework.ide.vscode.commons.java.IJavaModuleData; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.javadoc.TypeUrlProviderFromContainerUrl; +import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.util.text.Region; + +/** + * Base logic for {@link SourceLinks} independent of any client + * + * @author Alex Boyko + * + */ +public abstract class LegacySourceLinks extends AbstractSourceLinks { + + private static final Logger log = LoggerFactory.getLogger(LegacySourceLinks.class); + + private CompilationUnitCache cuCache; + + private JavaProjectFinder projectFinder; + + protected LegacySourceLinks(CompilationUnitCache cuCache, JavaProjectFinder projectFinder) { + this.cuCache = cuCache; + this.projectFinder = projectFinder; + } + + @Override + public Optional sourceLinkUrlForFQName(IJavaProject project, String fqName) { + Optional url = project == null ? Optional.empty() : getSourceLinkUrlForFQName(project, fqName); + if (!url.isPresent()) { + for (IJavaProject jp : projectFinder.all()) { + if (jp != project) { + url = getSourceLinkUrlForFQName(jp, fqName); + if (url.isPresent()) { + break; + } + } + } + } + return url; + } + + private Optional getSourceLinkUrlForFQName(IJavaProject project, String fqName) { + IJavaModuleData classpathResource = project.getIndex().findClasspathResourceContainer(fqName); + if (classpathResource != null) { + File file = classpathResource.getContainer(); + if (file.isDirectory()) { + return javaSourceLinkUrl(project, fqName, classpathResource); + } else { + return jarSourceLinkUrl(project, fqName, classpathResource); + } + } + return Optional.empty(); + } + + @Override + public Optional sourceLinkUrlForClasspathResource(String path) { + return SourceLinks.sourceLinkUrlForClasspathResource(this, projectFinder, path); + } + + private Optional javaSourceLinkUrl(IJavaProject project, String fqName, IJavaModuleData folderModuleData) { + IClasspath classpath = project.getClasspath(); + return SourceLinks.sourceFromSourceFolder(fqName, classpath) + .map(sourcePath -> javaSourceLinkUrl(project, sourcePath, fqName)); + } + + private String javaSourceLinkUrl(IJavaProject project, Path sourcePath, String fqName) { + Optional linkOptional = sourceLinkForResourcePath(sourcePath); + if (linkOptional.isPresent()) { + Optional positionLink = findCU(project, sourcePath.toUri()).map(cu -> positionLink(cu, fqName)); + return positionLink.isPresent() ? linkOptional.get() + positionLink.get() : linkOptional.get(); + } + return null; + } + + abstract protected String positionLink(CompilationUnit cu, String fqName); + + private Optional findCU(IJavaProject project, URI uri) { + return cuCache == null ? Optional.empty() : cuCache.withCompilationUnit(project, uri, compilationUnit -> Optional.ofNullable(compilationUnit)); + } + + abstract protected Optional jarLinkUrl(IJavaProject project, String fqName, IJavaModuleData jarModuleData); + + private Optional jarSourceLinkUrl(IJavaProject project, String fqName, IJavaModuleData jarModuleData) { + return jarLinkUrl(project, fqName, jarModuleData).map(sourceUrl -> { + Optional positionLink = findCUForFQNameFromJar(project, jarModuleData, fqName).map(cu -> positionLink(cu, fqName)); + return positionLink.isPresent() ? sourceUrl + positionLink.get() : sourceUrl; + }); + } + + private Optional findCUForFQNameFromJar(IJavaProject project, IJavaModuleData jarModuleData, String fqName) { + return IClasspathUtil.sourceContainer(project.getClasspath(), jarModuleData.getContainer()).map(url -> { + try { + return TypeUrlProviderFromContainerUrl.JAR_SOURCE_URL_PROVIDER.url(url, fqName, jarModuleData.getModule()); + } catch (Exception e) { + log.warn("Failed to determine source URL from url={} fqName={}", url, fqName, e); + return null; + } + }).map(sourceUrl -> { + try { + return sourceUrl.toURI(); + } catch (URISyntaxException e) { + throw new IllegalStateException(e); + } + }).map(sourcePath -> findCU(project, sourcePath).orElse(null)); + } + + protected Region findTypeRegion(CompilationUnit cu, String fqName) { + if (cu == null) { + return null; + } + int[] values = new int[] {0, -1}; + int lastDotIndex = fqName.lastIndexOf('.'); + String packageName = fqName.substring(0, lastDotIndex); + String typeName = fqName.substring(lastDotIndex + 1); + if (packageName.equals(cu.getPackage().getName().getFullyQualifiedName())) { + Stack visitedType = new Stack<>(); + cu.accept(new ASTVisitor() { + + private boolean visitDeclaration(AbstractTypeDeclaration node) { + visitedType.push(node.getName().getIdentifier()); + if (values[1] < 0) { + if (String.join("$", visitedType.toArray(new String[visitedType.size()])).equals(typeName)) { + values[0] = node.getName().getStartPosition(); + values[1] = node.getName().getLength(); + } + } + return values[1] < 0; + } + + @Override + public boolean visit(TypeDeclaration node) { + return visitDeclaration(node); + } + + @Override + public boolean visit(AnnotationTypeDeclaration node) { + return visitDeclaration(node); + } + + @Override + public boolean visit(EnumDeclaration node) { + return visitDeclaration(node); + } + + @Override + public void endVisit(EnumDeclaration node) { + visitedType.pop(); + super.endVisit(node); + } + + @Override + public void endVisit(AnnotationTypeDeclaration node) { + visitedType.pop(); + super.endVisit(node); + } + + @Override + public void endVisit(TypeDeclaration node) { + visitedType.pop(); + super.endVisit(node); + } + + }); + } + return values[1] < 0 ? null : new Region(values[0], values[1]); + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/SourceLinkFactory.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/SourceLinkFactory.java index 25a712f419..3876226c12 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/SourceLinkFactory.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/SourceLinkFactory.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018, 2019 Pivotal, Inc. + * Copyright (c) 2018, 2025 Pivotal, 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 @@ -10,6 +10,7 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.links; +import java.net.URI; import java.nio.file.Path; import java.util.Optional; @@ -44,6 +45,11 @@ public Optional sourceLinkForResourcePath(Path path) { return Optional.empty(); } + @Override + public Optional sourceLinkForJarEntry(IJavaProject contextProject, URI uri) { + return Optional.empty(); + } + }; /** diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/SourceLinks.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/SourceLinks.java index 1ba84543d8..38c7685e92 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/SourceLinks.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/SourceLinks.java @@ -26,6 +26,7 @@ import org.eclipse.lsp4j.TextDocumentIdentifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.boot.java.commands.Misc; import org.springframework.ide.vscode.commons.java.IClasspath; import org.springframework.ide.vscode.commons.java.IClasspathUtil; import org.springframework.ide.vscode.commons.java.IJavaModuleData; @@ -132,7 +133,7 @@ public static Optional sourceLinkUrlForClasspathResource(SourceLinks sou // URL for CF resources looks like jar:file:/home/vcap/app/lib/gs-rest-service-complete.jar!/hello/MyService.class // The above doesn't wotk with "filePath.toUri().toURL()" URL url = new URL(path.substring(0, idx)); - if (url.getProtocol().equals("jar")) { + if (url.getProtocol().equals(Misc.JAR)) { URLConnection connection = url.openConnection(); if (connection instanceof JarURLConnection) { JarURLConnection jarConnection = (JarURLConnection) connection; @@ -192,5 +193,12 @@ public static Optional sourceLinkUrlForClasspathResource(SourceLinks sou * @return the link URL optional */ Optional sourceLinkForResourcePath(Path path); - + + /** + * From URI produces a source link compatible with the client + * @param contextProject + * @param uri + * @return + */ + Optional sourceLinkForJarEntry(IJavaProject contextProject, URI uri); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/VSCodeSourceLinks.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/VSCodeSourceLinks.java index ef1dbede70..365490255d 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/VSCodeSourceLinks.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/VSCodeSourceLinks.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018, 2019 Pivotal, Inc. + * Copyright (c) 2018, 2025 Pivotal, 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 @@ -26,7 +26,7 @@ * @author Alex Boyko * */ -public class VSCodeSourceLinks extends AbstractSourceLinks { +public class VSCodeSourceLinks extends LegacySourceLinks { public VSCodeSourceLinks(CompilationUnitCache cuCache, JavaProjectFinder projectFinder) { super(cuCache, projectFinder); From f111cdd60c087f80f63aef14f10284858e0428a7 Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Tue, 21 Oct 2025 16:29:05 -0700 Subject: [PATCH 5/8] Adopt VSCode to location. Eclipse open any JAR support Signed-off-by: BoykoAlex --- .../commands/OpenJarEntryInEditor.java | 76 ++++++++++++++++++- .../boot/java/commands/JsonNodeHandler.java | 3 +- .../IndexBasedStereotypeFactory.java | 8 +- .../vscode-spring-boot/lib/Main.ts | 10 +-- .../lib/explorer/structure-tree-manager.ts | 9 +-- 5 files changed, 89 insertions(+), 17 deletions(-) diff --git a/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/commands/OpenJarEntryInEditor.java b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/commands/OpenJarEntryInEditor.java index e65994c92f..597082dbc5 100644 --- a/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/commands/OpenJarEntryInEditor.java +++ b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/commands/OpenJarEntryInEditor.java @@ -10,6 +10,9 @@ *******************************************************************************/ package org.springframework.tooling.ls.eclipse.commons.commands; +import java.io.IOException; +import java.io.InputStream; +import java.net.JarURLConnection; import java.net.URI; import java.util.Optional; @@ -18,7 +21,9 @@ import org.eclipse.core.commands.ExecutionException; import org.eclipse.core.commands.IHandler; import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IStorage; import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Path; @@ -65,13 +70,78 @@ public Object execute(ExecutionEvent event) throws ExecutionException { IJavaProject javaProject = JavaCore.create(project); if (javaProject != null) { JarUri jarUri = createJarUri(uri); - findJavaObj(javaProject, jarUri).ifPresent(s -> { + Object inputElement = findJavaObj(javaProject, jarUri).orElseGet(() -> { try { - EditorUtility.openInEditor(s, true); + JarURLConnection c = (JarURLConnection) uri.toURL().openConnection(); + return new IStorage() { + + @Override + public T getAdapter(Class adapter) { + if (URI.class.equals(adapter)) { + return (T) uri; + } + return null; + } + + @Override + public InputStream getContents() throws CoreException { + try { + return c.getInputStream(); + } catch (IOException e) { + throw new CoreException(new Status(IStatus.ERROR, + LanguageServerCommonsActivator.PLUGIN_ID, "Cannot load JAR entry", e)); + } + } + + @Override + public IPath getFullPath() { + return new Path(jarUriStr); + } + + @Override + public String getName() { + return new Path(c.getEntryName()).lastSegment(); + } + + @Override + public boolean isReadOnly() { + return true; + } + + @Override + public int hashCode() { + return uri.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof IStorage s) { + return uri.equals(s.getAdapter(URI.class)); + } + return false; + } + + @Override + public String toString() { + return jarUriStr; + } + + + + }; + } catch (IOException e) { + LanguageServerCommonsActivator.getInstance().getLog().log(new Status(IStatus.ERROR, + LanguageServerCommonsActivator.PLUGIN_ID, "Cannot load JAR entry: " + uri)); + return null; + } + }); + if (inputElement != null) { + try { + EditorUtility.openInEditor(inputElement, true); } catch (PartInitException e) { LanguageServerCommonsActivator.getInstance().getLog().log(e.getStatus()); } - }); + } } else { LanguageServerCommonsActivator.getInstance().getLog().log(new Status(IStatus.WARNING, LanguageServerCommonsActivator.PLUGIN_ID, "Cannot find project: " + projectName)); 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 47fe6933c3..ea5898cf76 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 @@ -30,6 +30,7 @@ import org.eclipse.lsp4j.DocumentSymbol; import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.Range; import org.jmolecules.stereotype.api.Stereotype; import org.jmolecules.stereotype.catalog.StereotypeCatalog; @@ -110,7 +111,7 @@ public void handleStereotype(Stereotype stereotype, NodeContext context) { try { URI uri = url.toURI(); - reference = new Location(uri.toASCIIString(), new Range()); + reference = new Location(uri.toASCIIString(), new Range(new Position(0,0), new Position(0,0))); if (Misc.JAR.equals(uri.getScheme())) { sourceLinks.sourceLinkForJarEntry(project, uri).map(u -> u.toASCIIString()).ifPresent(reference::setUri); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/IndexBasedStereotypeFactory.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/IndexBasedStereotypeFactory.java index dac69e6df7..df9e4c6453 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/IndexBasedStereotypeFactory.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/IndexBasedStereotypeFactory.java @@ -127,8 +127,12 @@ private static boolean doesImplement(StereotypeClassElement type, String fqn) { } private StereotypePackageElement findPackageFor(StereotypeClassElement type) { - String packageName = type.getType().substring(0, type.getType().lastIndexOf('.')); - return springIndex.findPackageNode(packageName, this.project.getElementName()); + int index = type.getType().lastIndexOf('.'); + if (index >= 0) { + String packageName = type.getType().substring(0, index); + return springIndex.findPackageNode(packageName, this.project.getElementName()); + } + return null; } private void registerStereotype(StereotypeDefinitionElement element) { diff --git a/vscode-extensions/vscode-spring-boot/lib/Main.ts b/vscode-extensions/vscode-spring-boot/lib/Main.ts index 4dece1d3e9..f1d48cf615 100644 --- a/vscode-extensions/vscode-spring-boot/lib/Main.ts +++ b/vscode-extensions/vscode-spring-boot/lib/Main.ts @@ -160,10 +160,6 @@ export function activate(context: ExtensionContext): Thenable { return commons.activate(options, context).then(client => { - // 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 registerClasspathService(client); @@ -201,8 +197,10 @@ export function activate(context: ExtensionContext): Thenable { const api = new ApiManager(client).api - context.subscriptions.push(api.getSpringIndex().onSpringIndexUpdated(e => structureManager.refresh(false))); - + // Activation of structure explorer + const structureManager = new StructureManager(context, api); + new ExplorerTreeProvider(structureManager).createTreeView(context, 'explorer.spring'); + return api; }); } 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 1abcdeae1c..7533c926a3 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,11 +16,10 @@ export class StructureManager { 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) { - commands.executeCommand('vscode.open', api.client.protocol2CodeConverter.asLocation(reference as ls.Location)); - } + const reference = node?.getReferenceValue() as ls.Location;; + if (reference) { + const location = api.client.protocol2CodeConverter.asLocation(reference) + window.showTextDocument(location.uri, { selection: location.range }); } })); From 6d6740cc871a3587af58c6413c60f19bec0e49ba Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Wed, 22 Oct 2025 10:51:16 -0700 Subject: [PATCH 6/8] Cleanup Signed-off-by: BoykoAlex --- .../commands/OpenJarEntryInEditor.java | 47 +++++++------------ 1 file changed, 18 insertions(+), 29 deletions(-) diff --git a/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/commands/OpenJarEntryInEditor.java b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/commands/OpenJarEntryInEditor.java index 597082dbc5..17d482517c 100644 --- a/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/commands/OpenJarEntryInEditor.java +++ b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/commands/OpenJarEntryInEditor.java @@ -44,20 +44,6 @@ public class OpenJarEntryInEditor extends AbstractHandler implements IHandler { private static final String JAR_URI_PARAM = "jarUri"; - record JarUri(IPath jar, IPath path) {} - - private static JarUri createJarUri(URI uri) { - if (!"jar".equals(uri.getScheme())) { - throw new IllegalArgumentException(); - } - String s = uri.getSchemeSpecificPart(); - int idx = s.indexOf('!'); - if (idx <= 0) { - throw new IllegalArgumentException(); - } - return new JarUri(new Path(URI.create(s.substring(0, idx)).getPath()), new Path(s.substring(idx + 1))); - } - @Override public Object execute(ExecutionEvent event) throws ExecutionException { String projectName = event.getParameter(OpenJavaElementInEditor.PROJECT_NAME); @@ -69,12 +55,12 @@ public Object execute(ExecutionEvent event) throws ExecutionException { URI uri = URI.create(jarUriStr); IJavaProject javaProject = JavaCore.create(project); if (javaProject != null) { - JarUri jarUri = createJarUri(uri); - Object inputElement = findJavaObj(javaProject, jarUri).orElseGet(() -> { - try { - JarURLConnection c = (JarURLConnection) uri.toURL().openConnection(); + try { + final JarURLConnection c = (JarURLConnection) uri.toURL().openConnection(); + Object inputElement = findJavaObj(javaProject, c).orElseGet(() -> { return new IStorage() { + @SuppressWarnings("unchecked") @Override public T getAdapter(Class adapter) { if (URI.class.equals(adapter)) { @@ -129,11 +115,6 @@ public String toString() { }; - } catch (IOException e) { - LanguageServerCommonsActivator.getInstance().getLog().log(new Status(IStatus.ERROR, - LanguageServerCommonsActivator.PLUGIN_ID, "Cannot load JAR entry: " + uri)); - return null; - } }); if (inputElement != null) { try { @@ -142,6 +123,11 @@ public String toString() { LanguageServerCommonsActivator.getInstance().getLog().log(e.getStatus()); } } + } catch (IOException e) { + LanguageServerCommonsActivator.getInstance().getLog().log(new Status(IStatus.ERROR, + LanguageServerCommonsActivator.PLUGIN_ID, "Cannot load JAR entry: " + uri)); + return null; + } } else { LanguageServerCommonsActivator.getInstance().getLog().log(new Status(IStatus.WARNING, LanguageServerCommonsActivator.PLUGIN_ID, "Cannot find project: " + projectName)); @@ -151,20 +137,23 @@ public String toString() { return null; } - private static Optional findJavaObj(IJavaProject j, JarUri uri) { + private static Optional findJavaObj(IJavaProject j, JarURLConnection c) { try { + IPath jarPath = new Path(c.getJarFileURL().getPath()); + // jar entry name doesn't start with '/' but the JarEntryResource full path does start from '/' + IPath entryPath = new Path("/" + c.getEntryName()); for (IPackageFragmentRoot fr : j.getAllPackageFragmentRoots()) { - if (uri.jar().equals(fr.getPath())) { + if (jarPath.equals(fr.getPath())) { for (Object o : fr.getNonJavaResources()) { if (o instanceof IJarEntryResource je) { - return findJarEntry(je, uri.path()); + return findJarEntry(je, entryPath); } } - if ("class".equals(uri.path().getFileExtension())) { - String packageName = uri.path().removeLastSegments(1).toString().replace(IPath.SEPARATOR, '.'); + if ("class".equals(entryPath.getFileExtension())) { + String packageName = entryPath.removeLastSegments(1).toString().replace(IPath.SEPARATOR, '.'); IPackageFragment pkg = fr.getPackageFragment(packageName); if (pkg != null) { - IClassFile cf = pkg.getClassFile(uri.path().lastSegment()); + IClassFile cf = pkg.getClassFile(entryPath.lastSegment()); if (cf != null) { return Optional.of(cf); } From a4132d7fde2d0923423452f98f0ee84baeab3d4f Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Wed, 22 Oct 2025 11:14:16 -0700 Subject: [PATCH 7/8] Typo in the id Signed-off-by: BoykoAlex --- .../ide/vscode/boot/java/commands/JsonNodeHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ea5898cf76..1e104b4a46 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 @@ -227,7 +227,7 @@ private static void assignNodeId(Node n, Node p) { String nodeSpecificId = "%s|%s|%s".formatted(textId, locationId, referenceId).replaceAll("\\|+$", ""); - n.attributes.put(NODE_ID, p != null && p.attributes.containsKey(NODE_ID) ? "%s//%s".formatted(p.attributes.get(NODE_ID), nodeSpecificId) : nodeSpecificId); + n.attributes.put(NODE_ID, p != null && p.attributes.containsKey(NODE_ID) ? "%s/%s".formatted(p.attributes.get(NODE_ID), nodeSpecificId) : nodeSpecificId); } private Node addChildFoo(Consumer consumer) { From dc2e53c0af0a397ffaea8cb7fd291541c20df5f4 Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Wed, 22 Oct 2025 12:39:40 -0700 Subject: [PATCH 8/8] Cleanup Signed-off-by: BoykoAlex --- .../vscode-spring-boot/lib/explorer/nodes.ts | 29 +------------------ 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/vscode-extensions/vscode-spring-boot/lib/explorer/nodes.ts b/vscode-extensions/vscode-spring-boot/lib/explorer/nodes.ts index b0c0dfc1c5..eb5cb4538f 100644 --- a/vscode-extensions/vscode-spring-boot/lib/explorer/nodes.ts +++ b/vscode-extensions/vscode-spring-boot/lib/explorer/nodes.ts @@ -73,19 +73,7 @@ export class StereotypedNode extends SpringNode { } getNodeId(): string { - // Create a unique identifier based on node attributes, excluding icon - // Include parent path in the computation for better uniqueness - const textId = this.n.attributes.text || ''; - const locationId = this.n.attributes.location ? - `${this.n.attributes.location.uri}:${this.n.attributes.location.range.start.line}:${this.n.attributes.location.range.start.character}` : ''; - const referenceId = this.n.attributes.reference ? String(this.n.attributes.reference) : ''; - - // Build the node-specific part of the ID (without icon) - const nodeSpecificId = `${textId}|${locationId}|${referenceId}`.replace(/\|+$/, ''); // Remove trailing separators - - // Include parent path for better uniqueness - const parentPath = this.getParentPath(); - return parentPath ? `${parentPath}/${nodeSpecificId}` : nodeSpecificId; + return this.n.attributes.nodeId || this.n.attributes.text; } protected getNodeText(): string { @@ -98,21 +86,6 @@ export class StereotypedNode extends SpringNode { computeIcon() { return new ThemeIcon(this.n.attributes.icon); -/* switch (this.n.attributes.icon) { - retur - case "fa-named-interface": // specify the case - return new ThemeIcon("symbol-interface"); - case "fa-package": - return new ThemeIcon("symbol-constant"); - case "fa-stereotype": - return new ThemeIcon("mention"); - case "fa-application": - return new ThemeIcon("folder"); - case "fa-method": - return new ThemeIcon("symbol-method"); - default: - return new ThemeIcon("symbol-class"); - } */ } }