Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
46d59c8
feat(ide): IntelliJ-style Java authoring — New → Java scaffolding + f…
iliyan-velichkov Jun 23, 2026
880a01b
fix(engine-java-lsp): raise JDT.LS heap (configurable) + make CreateJ…
iliyan-velichkov Jun 23, 2026
fb31f74
fix(ide): New→Java reveals the created file in the tree; Repository e…
iliyan-velichkov Jun 23, 2026
744f4b8
feat(editor): cross-file Go to Definition/References + better Java co…
iliyan-velichkov Jun 23, 2026
08cc829
feat(editor): cross-file Java rename — references, and the type's own…
iliyan-velichkov Jun 23, 2026
ae1e546
feat(editor): more IntelliJ-style Java — outline, folding, inlay hint…
iliyan-velichkov Jun 23, 2026
1dcc9a3
fix(editor): references code preview, first-Ctrl+Space completion, ke…
iliyan-velichkov Jun 23, 2026
29175bc
fix(editor): make Java quick-fixes & source actions work; refresh tre…
iliyan-velichkov Jun 23, 2026
f4c9858
fix(editor): correct & sync Java type rename (stale error, old name i…
iliyan-velichkov Jun 23, 2026
acca89e
perf+ux(editor): cap completion results; reveal renamed file in the tree
iliyan-velichkov Jun 23, 2026
e84ebf9
feat(editor): surface refactor assists via the lightbulb (assign para…
iliyan-velichkov Jun 23, 2026
e768c87
perf(editor): cut Java LSP load — code-action triggerKind, no referen…
iliyan-velichkov Jun 23, 2026
7d4717d
fix(ide): move New→Java to a top-level "Java" menu (unbreak New scrol…
iliyan-velichkov Jun 23, 2026
a9a2be0
perf(java-lsp): persist JDT.LS index across restarts; pre-warm classp…
iliyan-velichkov Jun 23, 2026
3610039
feat(editor): Java quick wins — postfix completion, organize-imports …
iliyan-velichkov Jun 24, 2026
70c0a1c
feat(editor): Java semantic highlighting via JDT.LS semantic tokens
iliyan-velichkov Jun 24, 2026
1eaebd5
feat(ide,java-lsp): server REST facade + workspace Go to Symbol view …
iliyan-velichkov Jun 24, 2026
5463bfd
feat(ide): Call & Type Hierarchy panel (Ctrl+Alt+H / Ctrl+H)
iliyan-velichkov Jun 24, 2026
698b2a7
fix(ide): remove Go to Symbol view; fix Call/Type Hierarchy lazy-load…
iliyan-velichkov Jun 24, 2026
57b6c31
fix(java-lsp): hierarchy endpoint 500 — Jackson 2 JsonNode vs Spring …
iliyan-velichkov Jun 24, 2026
777ddd0
ci(codeql): exclude generated java-lsp-client.js bundle from analysis
iliyan-velichkov Jun 24, 2026
09a1d04
refactor(tests): drive cascading context menu with Selenide APIs, not…
iliyan-velichkov Jun 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ jobs:
build-mode: ${{ matrix.build-mode }}
config: |
name: "Comprehensive Multi-language CodeQL Config"
# Exclude generated/bundled artifacts: java-lsp-client.js is the esbuild output of
# java-lsp-client.ts (the .ts source IS analysed) and embeds vendored
# vscode-languageserver protocol/types; scanning the bundle only yields noise from that
# third-party generated code.
paths-ignore:
- 'components/ui/editor-monaco/src/main/resources/META-INF/dirigible/editor-monaco/js/java-lsp-client.js'
query-filters:
- exclude:
id: java/path-injection
Expand Down
4 changes: 4 additions & 0 deletions components/group/group-ide/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@
<groupId>org.eclipse.dirigible</groupId>
<artifactId>dirigible-components-ui-view-problems</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.dirigible</groupId>
<artifactId>dirigible-components-ui-view-java-hierarchy</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.dirigible</groupId>
<artifactId>dirigible-components-ui-view-properties</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/*
* Copyright (c) 2010-2026 Eclipse Dirigible contributors
*
* All rights reserved. This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v20.html
*
* SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.dirigible.components.ide.lsp.java.endpoint;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import jakarta.annotation.security.RolesAllowed;
import org.eclipse.dirigible.components.base.endpoint.BaseEndpoint;
import org.eclipse.dirigible.components.ide.lsp.java.process.JdtLsInstance;
import org.eclipse.dirigible.components.ide.lsp.java.process.JdtLsManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

import java.security.Principal;
import java.util.concurrent.TimeUnit;

/**
* HTTP facade over a workspace's JDT Language Server for IDE views that live outside the Monaco
* editor iframe (Call Hierarchy, Type Hierarchy). Those views cannot reuse the editor's per-iframe
* LSP WebSocket, so each request here drives the shared {@link JdtLsInstance} directly.
*
* <p>
* Request and response bodies are handled as raw JSON strings, parsed/produced with this module's
* own Jackson 2 {@link ObjectMapper}. Spring Boot 4's default HTTP message converter is Jackson 3
* ({@code tools.jackson}), which cannot bind to Jackson 2 {@code JsonNode} types — so the
* controller boundary deliberately stays on {@code String} and never exposes a {@code JsonNode}
* parameter or return value.
*
* <p>
* URIs travel as the browser's virtual form ({@code file:///workspace/<ws>/...}); the bridge
* translates to/from the real on-disk paths, so responses come back virtual and are usable by the
* views as-is.
*/
@RestController
@RequestMapping(BaseEndpoint.PREFIX_ENDPOINT_IDE + "java-lsp")
@RolesAllowed({"ADMINISTRATOR", "DEVELOPER"})
@ConditionalOnProperty(name = "java.lsp.enabled", havingValue = "true", matchIfMissing = true)
public class JavaLspQueryEndpoint {

private static final Logger logger = LoggerFactory.getLogger(JavaLspQueryEndpoint.class);

private static final long REQUEST_TIMEOUT_SECONDS = 60;

private final JdtLsManager manager;
private final ObjectMapper mapper = new ObjectMapper();

public JavaLspQueryEndpoint(JdtLsManager manager) {
this.manager = manager;
}

/** Resolves the call-hierarchy item at a position ({@code textDocument/prepareCallHierarchy}). */
@PostMapping("/call-hierarchy/prepare")
public ResponseEntity<String> prepareCallHierarchy(@RequestBody String body, Principal principal) {
JsonNode b = parse(body);
return json(request(principal, workspaceOf(b), "textDocument/prepareCallHierarchy", positionParams(b)));
}

/** Callers of the given call-hierarchy item ({@code callHierarchy/incomingCalls}). */
@PostMapping("/call-hierarchy/incoming")
public ResponseEntity<String> incomingCalls(@RequestBody String body, Principal principal) {
JsonNode b = parse(body);
return json(request(principal, workspaceOf(b), "callHierarchy/incomingCalls", itemParams(b)));
}

/** Callees of the given call-hierarchy item ({@code callHierarchy/outgoingCalls}). */
@PostMapping("/call-hierarchy/outgoing")
public ResponseEntity<String> outgoingCalls(@RequestBody String body, Principal principal) {
JsonNode b = parse(body);
return json(request(principal, workspaceOf(b), "callHierarchy/outgoingCalls", itemParams(b)));
}

/** Resolves the type-hierarchy item at a position ({@code textDocument/prepareTypeHierarchy}). */
@PostMapping("/type-hierarchy/prepare")
public ResponseEntity<String> prepareTypeHierarchy(@RequestBody String body, Principal principal) {
JsonNode b = parse(body);
return json(request(principal, workspaceOf(b), "textDocument/prepareTypeHierarchy", positionParams(b)));
}

/** Supertypes of the given type-hierarchy item ({@code typeHierarchy/supertypes}). */
@PostMapping("/type-hierarchy/supertypes")
public ResponseEntity<String> supertypes(@RequestBody String body, Principal principal) {
JsonNode b = parse(body);
return json(request(principal, workspaceOf(b), "typeHierarchy/supertypes", itemParams(b)));
}

/** Subtypes of the given type-hierarchy item ({@code typeHierarchy/subtypes}). */
@PostMapping("/type-hierarchy/subtypes")
public ResponseEntity<String> subtypes(@RequestBody String body, Principal principal) {
JsonNode b = parse(body);
return json(request(principal, workspaceOf(b), "typeHierarchy/subtypes", itemParams(b)));
}

// -------------------------------------------------------------------------

private JsonNode parse(String body) {
try {
return mapper.readTree(body);
} catch (Exception e) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid JSON body", e);
}
}

private static ResponseEntity<String> json(JsonNode node) {
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(node.toString());
}

private JsonNode request(Principal principal, String workspace, String method, JsonNode params) {
if (workspace == null || workspace.isBlank()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Missing 'workspace'");
}
String username = principal != null ? principal.getName() : "anonymous";
try {
JdtLsInstance instance = manager.getOrStart(username, workspace);
instance.ensureInitialized()
.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
JsonNode response = instance.sendRequest(method, mapper.writeValueAsString(params))
.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
return response.path("result");
} catch (IllegalStateException notReady) {
// JDT.LS still starting up or not bundled in this build.
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, notReady.getMessage(), notReady);
} catch (InterruptedException ie) {
Thread.currentThread()
.interrupt();
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "Interrupted", ie);
} catch (Exception e) {
logger.warn("[java-lsp] {} failed for workspace {}", method, workspace, e);
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
throw new ResponseStatusException(HttpStatus.BAD_GATEWAY, "JDT.LS request failed", e);
}
}

private static String workspaceOf(JsonNode body) {
return body.path("workspace")
.asText(null);
}

/** Builds {@code {textDocument:{uri}, position:{line,character}}} from the request body. */
private ObjectNode positionParams(JsonNode body) {
ObjectNode params = mapper.createObjectNode();
params.putObject("textDocument")
.put("uri", body.path("uri")
.asText());
params.putObject("position")
.put("line", body.path("line")
.asInt())
.put("character", body.path("character")
.asInt());
return params;
}

/** Builds {@code {item: <hierarchy item>}} from the request body's {@code item} field. */
private ObjectNode itemParams(JsonNode body) {
ObjectNode params = mapper.createObjectNode();
params.set("item", body.path("item"));
return params;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,14 @@ public synchronized CompletableFuture<Void> ensureInitialized() {
// root matches what sendToProcess uses for virtual→real URI translation.
String escapedUri = realRoot.replace("\\", "\\\\")
.replace("\"", "\\\"");
// Advertise call/type hierarchy so JDT.LS serves them to the REST facade (JavaLspQueryEndpoint)
// even when no browser editor is open. The editor client advertises the same set, so the
// capabilities survive an editor-driven re-initialize of the shared process.
String capabilities = "{\"workspace\":{\"executeCommand\":{\"dynamicRegistration\":false}}," + "\"textDocument\":{"
+ "\"callHierarchy\":{\"dynamicRegistration\":false}," + "\"typeHierarchy\":{\"dynamicRegistration\":false}}}";
String initParams = "{\"processId\":" + pid + "," + "\"clientInfo\":{\"name\":\"dirigible-java-debug\"}," + "\"rootUri\":\""
+ escapedUri + "\"," + "\"workspaceFolders\":[{\"uri\":\"" + escapedUri + "\",\"name\":\"workspace\"}],"
+ "\"capabilities\":{\"workspace\":{\"executeCommand\":{\"dynamicRegistration\":false}}},"
+ "\"initializationOptions\":{}}";
+ "\"capabilities\":" + capabilities + "," + "\"initializationOptions\":{}}";

CompletableFuture<JsonNode> initResponse = sendRequest("initialize", initParams);
CompletableFuture<Void> result = new CompletableFuture<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand Down Expand Up @@ -112,6 +114,15 @@ public void run(ApplicationArguments args) {
ensureInstalled();
available = true;
logger.info("[java-lsp] JDT.LS is ready at {}", jdtlsHome);
// Materialise the platform classpath now (it is cached on disk), so the first Java
// file the user opens does not pay the multi-second extraction on its critical path.
try {
int entries = classPathIndex.classPathEntries()
.size();
logger.info("[java-lsp] Pre-warmed compile classpath: {} entries", entries);
} catch (Exception warmEx) {
logger.warn("[java-lsp] Classpath pre-warm failed (will materialise lazily on first use)", warmEx);
}
} catch (Exception e) {
logger.warn("[java-lsp] JDT.LS is not available: {}. Java language support will be disabled.", e.getMessage());
}
Expand Down Expand Up @@ -197,11 +208,25 @@ private JdtLsInstance startInstance(String username, String workspace) throws Ex
Path dataDir = jdtlsHome.resolve("data")
.resolve(username)
.resolve(workspace);
// Wipe stale workspace index so JDT.LS re-imports projects with the current .classpath.
// Without this, a cached Eclipse workspace that references deleted JAR paths (e.g. from
// a previous temp-dir extraction) causes unresolved-import errors on every restart.
deleteDirectory(dataDir);
Files.createDirectories(dataDir);
// Reuse the JDT.LS index across restarts so the (expensive) re-index of the platform classpath
// is not paid every time. The cached index only goes stale when the classpath itself changes
// (ClassPathIndex extracts to a stable dir and re-extracts only on a new build), so wipe the
// index only when a classpath fingerprint marker is missing or no longer matches.
Path fingerprintMarker = dataDir.resolveSibling(dataDir.getFileName() + ".classpath-fp");
String fingerprint = classpathFingerprint();
boolean indexValid = !fingerprint.isEmpty() && Files.isDirectory(dataDir) && Files.exists(fingerprintMarker)
&& fingerprint.equals(readFingerprint(fingerprintMarker));
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
if (indexValid) {
logger.info("[java-lsp] Reusing cached JDT.LS index for {}/{}", sanitize(username), sanitize(workspace));
Comment thread
iliyan-velichkov marked this conversation as resolved.
Dismissed
Comment thread
iliyan-velichkov marked this conversation as resolved.
Dismissed
} else {
logger.info("[java-lsp] Building JDT.LS index for {}/{} (first run or classpath changed)", sanitize(username),
Comment thread
iliyan-velichkov marked this conversation as resolved.
Dismissed
sanitize(workspace));
Comment thread
iliyan-velichkov marked this conversation as resolved.
Dismissed
deleteDirectory(dataDir);
Files.createDirectories(dataDir);
if (!fingerprint.isEmpty()) {
Files.writeString(fingerprintMarker, fingerprint);
}
}

String launcherJar = findLauncherJar();
String configDir = resolveConfigDir();
Expand Down Expand Up @@ -331,7 +356,7 @@ private List<String> buildCommand(String launcherJar, String configDir, String d
cmd.add("-Dlog.protocol=true");
cmd.add("-Dlog.level=ALL");
cmd.add("-noverify");
cmd.add("-Xmx512m");
cmd.add("-Xmx" + DirigibleConfig.JAVA_LSP_MAX_HEAP.getStringValue());
cmd.add("-jar");
cmd.add(launcherJar);
cmd.add("-configuration");
Expand All @@ -357,6 +382,34 @@ private static Path workspaceRoot(String username, String workspace) {
.normalize();
}

/**
* SHA-256 over the sorted classpath entry paths; identifies whether a cached index is still valid.
*/
private String classpathFingerprint() {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
classPathIndex.classPathEntries()
.stream()
.map(Path::toString)
.sorted()
.forEach(entry -> digest.update(entry.getBytes(StandardCharsets.UTF_8)));
return HexFormat.of()
.formatHex(digest.digest());
} catch (Exception e) {
logger.warn("[java-lsp] Could not compute classpath fingerprint; index will be rebuilt", e);
return "";
}
}

private static String readFingerprint(Path marker) {
try {
return Files.readString(marker)
.trim();
} catch (IOException e) {
return "";
}
}

private static void deleteDirectory(Path dir) throws IOException {
if (!Files.exists(dir)) {
return;
Expand Down
6 changes: 6 additions & 0 deletions components/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@
<module>ui/view-console</module>
<module>ui/view-preview</module>
<module>ui/view-problems</module>
<module>ui/view-java-hierarchy</module>
<module>ui/view-properties</module>
<module>ui/view-git</module>
<module>ui/view-terminal</module>
Expand Down Expand Up @@ -941,6 +942,11 @@
<artifactId>dirigible-components-ui-view-problems</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.dirigible</groupId>
<artifactId>dirigible-components-ui-view-java-hierarchy</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.dirigible</groupId>
<artifactId>dirigible-components-ui-view-properties</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<script type="text/javascript" src="/services/web/platform-core/ui/platform/layout-hub.js"></script>
<script type="text/javascript" src="/services/web/platform-core/ui/platform/status-bar-hub.js"></script>
<script type="text/javascript" src="/services/web/platform-core/ui/platform/theming-hub.js"></script>
<script type="text/javascript" src="/services/web/platform-core/ui/platform/dialog-hub.js"></script>
<script type="text/javascript" src="/services/web/service-workspace/workspace-hub.js"></script>
<script type="text/javascript" src="/webjars/monaco-editor/min/vs/loader.js"></script>
<script type="text/javascript" src="js/assignmentsParser.js"></script>
Expand Down
Loading
Loading