diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java index 63d39c44ca44b..39481c4302fd3 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java @@ -118,6 +118,7 @@ import io.quarkus.qute.Expression; import io.quarkus.qute.Expression.VirtualMethodPart; import io.quarkus.qute.Identifiers; +import io.quarkus.qute.JavaElementUriBuilder; import io.quarkus.qute.LoopSectionHelper; import io.quarkus.qute.NamespaceResolver; import io.quarkus.qute.ParameterDeclaration; @@ -129,6 +130,7 @@ import io.quarkus.qute.SectionHelperFactory; import io.quarkus.qute.SetSectionHelper; import io.quarkus.qute.Template; +import io.quarkus.qute.TemplateContents; import io.quarkus.qute.TemplateData; import io.quarkus.qute.TemplateException; import io.quarkus.qute.TemplateExtension; @@ -2627,6 +2629,7 @@ void collecTemplateContents(BeanArchiveIndexBuildItem index, List annotationClass) { + return JavaElementUriBuilder + .builder(target.toString()) + .setAnnotation(annotationClass.getName()) + .build(); + } + @BuildStep @Record(value = STATIC_INIT) void initialize(BuildProducer syntheticBeans, QuteRecorder recorder, diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java index eba378fb719c2..3fb2a0e90c1be 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java @@ -380,7 +380,7 @@ private Optional locate(String path) { LOGGER.debugf("Locate template contents for %s", path); TemplateInfo template = templates.get(path); if (template != null && template.hasContent()) { - return getTemplateLocation(template.content, path); + return getTemplateLocation(template, path); } // Try path with suffixes @@ -391,7 +391,7 @@ private Optional locate(String path) { } template = templates.get(pathWithSuffix); if (template != null && template.hasContent()) { - return getTemplateLocation(template.content, pathWithSuffix); + return getTemplateLocation(template, pathWithSuffix); } } @@ -421,8 +421,10 @@ private Optional locate(String path) { return Optional.empty(); } - private Optional getTemplateLocation(String content, String pathWithSuffix) { - return Optional.of(new StringTemplateLocation(content, Optional.ofNullable(createVariant(pathWithSuffix)))); + private Optional getTemplateLocation(TemplateInfo templateInfo, String pathWithSuffix) { + String content = templateInfo.content; + URI source = templateInfo.parseSource(); + return Optional.of(new StringTemplateLocation(content, Optional.ofNullable(createVariant(pathWithSuffix)), source)); } private Optional getTemplateLocation(URL resource, String templatePath, String path) { diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/JavaElementUriBuilder.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/JavaElementUriBuilder.java new file mode 100644 index 0000000000000..02d886c89fa86 --- /dev/null +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/JavaElementUriBuilder.java @@ -0,0 +1,139 @@ +package io.quarkus.qute; + +import java.net.URI; + +/** + * Builder for Qute-specific URIs that reference a Java element + * (class, method, or annotation) from a template. + *

+ * These URIs have the format: + * + *

+ * qute-java://<fully-qualified-class-name>[#method][@annotation]
+ * 
+ * + * Examples: + *
    + *
  • Class-level annotation: qute-java://com.acme.Bean@io.quarkus.qute.TemplateContents
  • + *
  • Method-level annotation: qute-java://com.acme.Bean#process@io.quarkus.qute.TemplateContents
  • + *
+ *

+ * + *

+ * This builder is used to construct such URIs in a type-safe way and to provide + * utility methods to identify and parse them. It is aligned with + * {@link io.quarkus.qute.debug.client.JavaSourceLocationArguments#javaElementUri}. + *

+ */ +public class JavaElementUriBuilder { + + /** Scheme used for Qute Java URIs. */ + public static final String QUTE_JAVA_SCHEME = "qute-java"; + + /** Prefix for Qute Java URIs. */ + public static final String QUTE_JAVA_URI_PREFIX = QUTE_JAVA_SCHEME + "://"; + + private final String typeName; + private String method; + private String annotation; + + private JavaElementUriBuilder(String typeName) { + this.typeName = typeName; + } + + /** + * Returns the fully qualified Java class name for this URI. + * + * @return the class name + */ + public String getTypeName() { + return typeName; + } + + /** + * Returns the Java method name (nullable if not specified). + * + * @return the method name or {@code null} + */ + public String getMethod() { + return method; + } + + /** + * Sets the Java method name. + * + * @param method the method name to set + * @return this builder + */ + public JavaElementUriBuilder setMethod(String method) { + this.method = method; + return this; + } + + /** + * Returns the fully qualified Java annotation name (nullable if not specified). + * + * @return the annotation name or {@code null} + */ + public String getAnnotation() { + return annotation; + } + + /** + * Sets the fully qualified Java annotation name. + * + * @param annotation the annotation name to set + * @return this builder + */ + public JavaElementUriBuilder setAnnotation(String annotation) { + this.annotation = annotation; + return this; + } + + /** + * Creates a new builder for the given Java class name. + * + * @param typeName fully qualified Java class name + * @return a new {@link JavaElementUriBuilder} + */ + public static JavaElementUriBuilder builder(String typeName) { + return new JavaElementUriBuilder(typeName); + } + + /** + * Builds the Qute Java URI representing the element. + * + * @return a {@link URI} for the Java element + */ + public URI build() { + StringBuilder uri = new StringBuilder(QUTE_JAVA_URI_PREFIX); + uri.append(typeName); + if (method != null) { + uri.append("#").append(method); + } + if (annotation != null) { + uri.append("@").append(annotation); + } + return URI.create(uri.toString()); + } + + /** + * Returns true if the given URI uses the qute-java scheme. + * + * @param uri the URI to check + * @return {@code true} if this URI is a Qute Java element URI + */ + public static boolean isJavaUri(URI uri) { + return uri != null && QUTE_JAVA_SCHEME.equals(uri.getScheme()); + } + + /** + * Returns true if the given string starts with the qute-java URI prefix. + * + * @param uri the URI string to check + * @return {@code true} if this string is a Qute Java element URI + */ + public static boolean isJavaUri(String uri) { + return uri != null && uri.startsWith(QUTE_JAVA_URI_PREFIX); + } +} diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/StringTemplateLocation.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/StringTemplateLocation.java index a64bbb03d2dec..f6b9cd3462e84 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/StringTemplateLocation.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/StringTemplateLocation.java @@ -1,6 +1,7 @@ package io.quarkus.qute; import java.io.Reader; +import java.net.URI; import java.util.Objects; import java.util.Optional; @@ -11,14 +12,20 @@ public class StringTemplateLocation implements TemplateLocation { private final String content; private final Optional variant; + private final URI source; public StringTemplateLocation(String content) { this(content, Optional.empty()); } public StringTemplateLocation(String content, Optional variant) { + this(content, variant, null); + } + + public StringTemplateLocation(String content, Optional variant, URI source) { this.content = Objects.requireNonNull(content); this.variant = Objects.requireNonNull(variant); + this.source = source; } @Override @@ -31,4 +38,12 @@ public Optional getVariant() { return variant; } + @Override + public Optional getSource() { + if (source != null) { + return Optional.of(source); + } + return Optional.empty(); + } + } diff --git a/independent-projects/qute/debug/README.md b/independent-projects/qute/debug/README.md index 3188bc45db718..f5c02097a394e 100644 --- a/independent-projects/qute/debug/README.md +++ b/independent-projects/qute/debug/README.md @@ -1,6 +1,6 @@ # Qute Debugger -Qute Debugger allows you to **debug Qute templates using breakpoints** in any IDE or editor that supports the [Debug Adapter Protocol (DAP)](https://microsoft.github.io/debug-adapter-protocol/). +Since Quarkus **3.29**, Qute Debugger allows you to **debug Qute templates using breakpoints** in any IDE or editor that supports the [Debug Adapter Protocol (DAP)](https://microsoft.github.io/debug-adapter-protocol/). It works seamlessly with: @@ -138,6 +138,12 @@ Pause only when certain conditions are met. ![Conditional Breakpoint](./images/ConditionalBreakpoint.png) +### Hover + +Hover + +![Hover](./images/Hover.png) + ### Expression Evaluation Evaluate expressions on the fly while debugging templates. @@ -156,12 +162,6 @@ Inspect the current context and variables in real-time. ![Variables](./images/Variables.png) -## Feature Summary +### Degugging in Java file -| Feature | Supported | -|------------------------|-----------| -| Simple Breakpoints | ✅ | -| Conditional Breakpoints| ✅ | -| Expression Evaluation | ✅ | -| Code Completion | ✅ | -| Variable Inspection | ✅ | +Since Quarkus **3.31** ... diff --git a/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/Debugger.java b/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/Debugger.java index 7a54d599bce96..ad2227061538e 100644 --- a/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/Debugger.java +++ b/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/Debugger.java @@ -15,10 +15,12 @@ import org.eclipse.lsp4j.debug.Thread; import org.eclipse.lsp4j.debug.Variable; +import io.quarkus.qute.debug.client.JavaSourceResolver; + /** * Qute debugger API. */ -public interface Debugger { +public interface Debugger extends JavaSourceResolver { /** * Returns the current state of the remote debugger for a given thread. diff --git a/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/adapter/DebugServerAdapter.java b/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/adapter/DebugServerAdapter.java index 54ac0a4702e90..2145e608d656b 100644 --- a/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/adapter/DebugServerAdapter.java +++ b/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/adapter/DebugServerAdapter.java @@ -48,6 +48,7 @@ import io.quarkus.qute.debug.StoppedEvent; import io.quarkus.qute.debug.ThreadEvent; import io.quarkus.qute.debug.agent.DebuggeeAgent; +import io.quarkus.qute.debug.client.JavaSourceResolver; /** * Debug Adapter Protocol (DAP) server implementation for Qute debugging. @@ -130,6 +131,9 @@ public CompletableFuture initialize(InitializeRequestArguments arg public void connect(IDebugProtocolClient client) { this.client = client; this.agent.setEnabled(true); + if (client instanceof JavaSourceResolver javaFileInfoProvider) { + ((DebuggeeAgent) agent).setJavaSourceResolver(javaFileInfoProvider); + } } /** diff --git a/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/adapter/RegisterDebugServerAdapter.java b/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/adapter/RegisterDebugServerAdapter.java index a50ccf3e9266e..a258a248763b4 100644 --- a/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/adapter/RegisterDebugServerAdapter.java +++ b/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/adapter/RegisterDebugServerAdapter.java @@ -10,13 +10,13 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import org.eclipse.lsp4j.debug.launch.DSPLauncher; -import org.eclipse.lsp4j.debug.services.IDebugProtocolClient; import org.eclipse.lsp4j.jsonrpc.Launcher; +import org.eclipse.lsp4j.jsonrpc.debug.DebugLauncher; import io.quarkus.qute.Engine; import io.quarkus.qute.EngineBuilder.EngineListener; import io.quarkus.qute.debug.agent.DebuggeeAgent; +import io.quarkus.qute.debug.client.QuteDebugProtocolClient; import io.quarkus.qute.trace.TemplateEvent; import io.quarkus.qute.trace.TraceListener; @@ -279,8 +279,8 @@ private void setupLauncher(Socket client, boolean suspend) throws IOException { launcherFuture.cancel(true); } - Launcher launcher = DSPLauncher.createServerLauncher(server, client.getInputStream(), - client.getOutputStream(), executor, null); + Launcher launcher = DebugLauncher.createLauncher(server, QuteDebugProtocolClient.class, + client.getInputStream(), client.getOutputStream(), executor, null); var clientProxy = launcher.getRemoteProxy(); server.connect(clientProxy); diff --git a/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/agent/DebuggeeAgent.java b/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/agent/DebuggeeAgent.java index 6fbcc60125080..40d2b3dbd8362 100644 --- a/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/agent/DebuggeeAgent.java +++ b/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/agent/DebuggeeAgent.java @@ -41,6 +41,9 @@ import io.quarkus.qute.debug.agent.source.SourceReferenceRegistry; import io.quarkus.qute.debug.agent.source.SourceTemplateRegistry; import io.quarkus.qute.debug.agent.variables.VariablesRegistry; +import io.quarkus.qute.debug.client.JavaSourceLocationArguments; +import io.quarkus.qute.debug.client.JavaSourceLocationResponse; +import io.quarkus.qute.debug.client.JavaSourceResolver; import io.quarkus.qute.trace.ResolveEvent; import io.quarkus.qute.trace.TemplateEvent; @@ -103,6 +106,8 @@ public class DebuggeeAgent implements Debugger { /** Indicates whether the debugging agent is enabled. */ private boolean enabled; + private JavaSourceResolver javaSourceResolver; + /** * Creates a new {@link DebuggeeAgent} instance. */ @@ -472,7 +477,11 @@ public VariablesRegistry getVariablesRegistry() { public SourceTemplateRegistry getSourceTemplateRegistry(Engine engine) { return this.sourceTemplateRegistry.computeIfAbsent(engine, - k -> new SourceTemplateRegistry(breakpointsRegistry, sourceReferenceRegistry, engine)); + k -> new SourceTemplateRegistry(breakpointsRegistry, sourceReferenceRegistry, this, engine)); + } + + public void setJavaSourceResolver(JavaSourceResolver javaSourceResolver) { + this.javaSourceResolver = javaSourceResolver; } @Override @@ -516,4 +525,12 @@ public void reset() { this.sourceTemplateRegistry.clear(); this.sourceReferenceRegistry.reset(); } + + @Override + public CompletableFuture resolveJavaSource(JavaSourceLocationArguments params) { + if (javaSourceResolver != null) { + return javaSourceResolver.resolveJavaSource(params); + } + return CompletableFuture.completedFuture(null); + } } diff --git a/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/agent/frames/RemoteStackFrame.java b/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/agent/frames/RemoteStackFrame.java index 3b2b2ea24f648..c6b64798fc94b 100644 --- a/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/agent/frames/RemoteStackFrame.java +++ b/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/agent/frames/RemoteStackFrame.java @@ -87,11 +87,11 @@ public RemoteStackFrame(ResolveEvent event, RemoteStackFrame previousFrame, int line = event.getTemplateNode().getOrigin().getLine(); super.setId(id); super.setName(event.getTemplateNode().toString()); - super.setLine(line); this.templateId = event.getTemplateNode().getOrigin().getTemplateId(); super.setSource( sourceTemplateRegistry.getSource(templateId, previousFrame != null ? previousFrame.getSource() : null)); + super.setLine(line + (getSource() != null ? getSource().getStartLine() : 0)); } /** @return the template ID associated with this frame */ diff --git a/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/agent/source/FileSource.java b/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/agent/source/FileSource.java index a44686bbe7c5d..cd4548a69797f 100644 --- a/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/agent/source/FileSource.java +++ b/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/agent/source/FileSource.java @@ -21,14 +21,18 @@ */ public class FileSource extends RemoteSource { + public FileSource(URI uri, String templateId) { + this(uri, templateId, 0); + } + /** * Creates a new {@link FileSource} for a Qute template located on the local filesystem. * * @param uri the URI of the template file (must use the {@code file:} scheme) * @param templateId the Qute template identifier */ - public FileSource(URI uri, String templateId) { - super(uri, templateId); + protected FileSource(URI uri, String templateId, int startLine) { + super(uri, templateId, startLine); // Initialize the DAP "path" field so the client can open the file. try { diff --git a/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/agent/source/JavaFileSource.java b/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/agent/source/JavaFileSource.java new file mode 100644 index 0000000000000..c2c2e9180ebe5 --- /dev/null +++ b/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/agent/source/JavaFileSource.java @@ -0,0 +1,11 @@ +package io.quarkus.qute.debug.agent.source; + +import java.net.URI; + +public class JavaFileSource extends FileSource { + + public JavaFileSource(URI uri, String templateId, int startLine) { + super(uri, templateId, startLine); + } + +} diff --git a/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/agent/source/RemoteSource.java b/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/agent/source/RemoteSource.java index 24bdffab7a9f5..4ca2c70aa5041 100644 --- a/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/agent/source/RemoteSource.java +++ b/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/agent/source/RemoteSource.java @@ -35,16 +35,22 @@ public abstract class RemoteSource extends Source { */ private final transient String templateId; + private final int startLine; + + public RemoteSource(URI uri, String templateId) { + this(uri, templateId, 0); + } + /** * Creates a new remote source associated with the given template. * * @param uri the URI of the template source, or {@code null} if not applicable * @param templateId the template ID used by the Qute engine (never {@code null}) */ - public RemoteSource(URI uri, String templateId) { + public RemoteSource(URI uri, String templateId, int startLine) { this.uri = uri; this.templateId = templateId; - + this.startLine = startLine; // Initialize the DAP "name" field for display purposes in the client. // If the URI is known, extract the filename; otherwise, use the templateId. super.setName(uri != null ? computeName(uri) : templateId); @@ -82,6 +88,10 @@ public String getTemplateId() { return templateId; } + public int getStartLine() { + return startLine; + } + /** * Utility method that extracts the file name from a URI or path string. *

diff --git a/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/agent/source/SourceTemplateRegistry.java b/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/agent/source/SourceTemplateRegistry.java index a0336304b05d7..dfc44e89c49c3 100644 --- a/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/agent/source/SourceTemplateRegistry.java +++ b/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/agent/source/SourceTemplateRegistry.java @@ -10,29 +10,39 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; import org.eclipse.lsp4j.debug.Source; import io.quarkus.qute.Engine; +import io.quarkus.qute.JavaElementUriBuilder; import io.quarkus.qute.debug.agent.breakpoints.BreakpointsRegistry; +import io.quarkus.qute.debug.client.JavaSourceLocationArguments; +import io.quarkus.qute.debug.client.JavaSourceLocationResponse; +import io.quarkus.qute.debug.client.JavaSourceResolver; /** - * Registry responsible for resolving and managing mappings between Qute template IDs - * and their corresponding {@link RemoteSource} instances. + * Registry responsible for resolving and managing mappings between Qute + * template IDs and their corresponding {@link RemoteSource} instances. *

- * This class plays a key role in the Qute debugger by allowing the Debug Adapter Protocol (DAP) - * to locate and serve the correct source file for a given template, whether it originates - * from a local filesystem or a JAR. + * This class plays a key role in the Qute debugger by allowing the Debug + * Adapter Protocol (DAP) to locate and serve the correct source file for a + * given template, whether it originates from a local filesystem or a JAR. *

* *

- * The registry maintains a cache of resolved template IDs to avoid redundant lookups - * and supports flexible base paths and file extensions to locate template files. + * The registry maintains a cache of resolved template IDs to avoid redundant + * lookups and supports flexible base paths and file extensions to locate + * template files. *

*/ public class SourceTemplateRegistry { + private static final String JAR_SCHEME = "jar"; + private final Map templateIdToSource = new HashMap<>(); private final Engine engine; @@ -42,6 +52,8 @@ public class SourceTemplateRegistry { private final BreakpointsRegistry breakpointsRegistry; private final SourceReferenceRegistry sourceReferenceRegistry; + private final JavaSourceResolver javaSourceResolver; + /** * Creates a registry with default base paths and file extensions. *
    @@ -63,12 +75,11 @@ public class SourceTemplateRegistry { *
*/ public SourceTemplateRegistry(BreakpointsRegistry breakpointsRegistry, - SourceReferenceRegistry sourceReferenceRegistry, - Engine engine) { - this(breakpointsRegistry, sourceReferenceRegistry, engine, + SourceReferenceRegistry sourceReferenceRegistry, JavaSourceResolver javaFileInfoProvider, Engine engine) { + this(breakpointsRegistry, sourceReferenceRegistry, javaFileInfoProvider, engine, List.of("src/main/resources/templates/", "templates/", "content/"), - List.of(".qute", ".html", ".qute.html", ".yaml", ".qute.yaml", - ".yml", ".qute.yml", ".txt", ".qute.txt", ".md", ".qute.md")); + List.of(".qute", ".html", ".qute.html", ".yaml", ".qute.yaml", ".yml", ".qute.yml", ".txt", ".qute.txt", + ".md", ".qute.md")); } /** @@ -77,16 +88,17 @@ public SourceTemplateRegistry(BreakpointsRegistry breakpointsRegistry, * @param breakpointsRegistry registry managing active breakpoints * @param sourceReferenceRegistry registry responsible for DAP source references * @param engine the Qute template engine instance - * @param basePaths list of possible base directories where templates might be located - * @param fileExtensions list of supported file extensions for template files + * @param basePaths list of possible base directories where + * templates might be located + * @param fileExtensions list of supported file extensions for template + * files */ public SourceTemplateRegistry(BreakpointsRegistry breakpointsRegistry, - SourceReferenceRegistry sourceReferenceRegistry, - Engine engine, - List basePaths, - List fileExtensions) { + SourceReferenceRegistry sourceReferenceRegistry, JavaSourceResolver javaFileInfoProvider, Engine engine, + List basePaths, List fileExtensions) { this.breakpointsRegistry = breakpointsRegistry; this.sourceReferenceRegistry = sourceReferenceRegistry; + this.javaSourceResolver = javaFileInfoProvider; this.engine = engine; this.basePaths = basePaths; this.fileExtensions = fileExtensions; @@ -95,8 +107,8 @@ public SourceTemplateRegistry(BreakpointsRegistry breakpointsRegistry, /** * Attempts to resolve a {@link RemoteSource} for a given Qute template ID. *

- * This method first checks the internal cache. If the source is not yet registered, - * it attempts to resolve it from: + * This method first checks the internal cache. If the source is not yet + * registered, it attempts to resolve it from: *

    *
  • The {@link Engine} via {@link Engine#locate(String)}.
  • *
  • Previously known sources (from breakpoints or cached URIs).
  • @@ -104,8 +116,10 @@ public SourceTemplateRegistry(BreakpointsRegistry breakpointsRegistry, *

    * * @param templateId the Qute template identifier - * @param previousSource the previously known source, used to infer relative paths (optional) - * @return the resolved {@link RemoteSource}, or {@code null} if none could be found + * @param previousSource the previously known source, used to infer relative + * paths (optional) + * @return the resolved {@link RemoteSource}, or {@code null} if none could be + * found */ public RemoteSource getSource(String templateId, Source previousSource) { RemoteSource source = templateIdToSource.get(templateId); @@ -115,10 +129,40 @@ public RemoteSource getSource(String templateId, Source previousSource) { URI sourceUri = getSourceUriFromEngine(templateId, this.engine); if (sourceUri == null) { - sourceUri = getGuessedSourceUri(templateId, previousSource); + if (JavaElementUriBuilder.isJavaUri(templateId)) { + // Template id defines a qute-java// uri + // ex: + // qute-java://org.acme.quarkus.sample.HelloResource$Hello@io.quarkus.qute.TemplateContents + sourceUri = URI.create(templateId); + } else { + sourceUri = getGuessedSourceUri(templateId, previousSource); + } } if (sourceUri != null) { + if (JavaElementUriBuilder.isJavaUri(sourceUri)) { + // ex: + // qute-java://org.acme.quarkus.sample.HelloResource$Hello@io.quarkus.qute.TemplateContents + JavaSourceLocationArguments args = parse(sourceUri.toString()); + try { + JavaSourceLocationResponse response = javaSourceResolver.resolveJavaSource(args).get(2000, + TimeUnit.MILLISECONDS); + if (response != null) { + URI javaSourceUri = URI.create(response.getJavaFileUri()); + int startLine = response.getStartLine(); + source = new JavaFileSource(javaSourceUri, templateId, startLine); + templateIdToSource.put(templateId, source); + return source; + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (ExecutionException e) { + e.printStackTrace(); + } catch (TimeoutException e) { + e.printStackTrace(); + } + return null; + } source = createSource(sourceUri, templateId); templateIdToSource.put(templateId, source); return source; @@ -127,15 +171,68 @@ public RemoteSource getSource(String templateId, Source previousSource) { return null; } + private static JavaSourceLocationArguments parse(String javaElementUri) { + JavaSourceLocationArguments args = new JavaSourceLocationArguments(); + args.setJavaElementUri(javaElementUri); + + char[] chars = javaElementUri.toCharArray(); + int i = JavaElementUriBuilder.QUTE_JAVA_URI_PREFIX.length(); + int len = chars.length; + + StringBuilder type = new StringBuilder(); + StringBuilder method = new StringBuilder(); + StringBuilder annotation = new StringBuilder(); + + // 0 = reading type + // 1 = reading method (after '#') + // 2 = reading annotation (after '@') + int mode = 0; + + for (; i < len; i++) { + char c = chars[i]; + + switch (mode) { + case 0: // reading type + if (c == '#') { + mode = 1; + } else if (c == '@') { + mode = 2; + } else { + type.append(c); + } + break; + + case 1: // reading method + if (c == '@') { + mode = 2; + } else { + method.append(c); + } + break; + + case 2: // reading annotation + annotation.append(c); + break; + } + } + + args.setTypeName(type.length() == 0 ? null : type.toString()); + args.setMethod(method.length() == 0 ? null : method.toString()); + args.setAnnotation(annotation.length() == 0 ? null : annotation.toString()); + + return args; + } + /** * Creates a {@link RemoteSource} depending on the URI scheme. *
      - *
    • If the URI scheme is "jar", creates a {@link JarSource} and registers a source reference.
    • + *
    • If the URI scheme is "jar", creates a {@link JarSource} and registers a + * source reference.
    • *
    • Otherwise, creates a {@link FileSource}.
    • *
    */ private RemoteSource createSource(URI sourceUri, String templateId) { - if ("jar".equals(sourceUri.getScheme())) { + if (JAR_SCHEME.equals(sourceUri.getScheme())) { return new JarSource(sourceUri, templateId, sourceReferenceRegistry); } return new FileSource(sourceUri, templateId); @@ -158,8 +255,8 @@ private static URI getSourceUriFromEngine(String templateId, Engine engine) { } /** - * Tries to infer the {@link URI} of a template based on its ID by checking known - * source URIs, typical base paths, and possible file extensions. + * Tries to infer the {@link URI} of a template based on its ID by checking + * known source URIs, typical base paths, and possible file extensions. *

    * This method is used as a fallback when the Qute engine cannot resolve a * template ID to a physical location. @@ -167,7 +264,8 @@ private static URI getSourceUriFromEngine(String templateId, Engine engine) { * *

      *
    • Searches in the registered breakpoint URIs and cached RemoteSources.
    • - *
    • Supports multiple base paths (e.g. "templates/", "META-INF/resources/").
    • + *
    • Supports multiple base paths (e.g. "templates/", + * "META-INF/resources/").
    • *
    • Supports common file extensions (.html, .qute, .qute.html, etc.).
    • *
    * @@ -180,9 +278,7 @@ private static URI getSourceUriFromEngine(String templateId, Engine engine) { private URI getGuessedSourceUri(String templateId, Source previousSource) { Set knownUris = new HashSet<>(breakpointsRegistry.getSourceUris()); - knownUris.addAll(templateIdToSource.values().stream() - .map(RemoteSource::getUri) - .filter(Objects::nonNull) + knownUris.addAll(templateIdToSource.values().stream().map(RemoteSource::getUri).filter(Objects::nonNull) .collect(Collectors.toSet())); if (knownUris.isEmpty()) { @@ -237,8 +333,8 @@ public List getFileExtensions() { } /** - * Converts a {@link Source} object into a normalized {@link URI}. - * Supports fallback for Windows paths and relative URIs. + * Converts a {@link Source} object into a normalized {@link URI}. Supports + * fallback for Windows paths and relative URIs. */ public static URI toUri(Source source) { String path = source.getPath(); @@ -257,7 +353,8 @@ public static URI toUri(Source source) { } /** - * Normalizes file URIs (e.g. ensures consistent casing of Windows drive letters). + * Normalizes file URIs (e.g. ensures consistent casing of Windows drive + * letters). */ private static URI normalize(URI uri) { if ("file".equalsIgnoreCase(uri.getScheme())) { diff --git a/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/client/JavaSourceLocationArguments.java b/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/client/JavaSourceLocationArguments.java new file mode 100644 index 0000000000000..44ae454acb9cd --- /dev/null +++ b/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/client/JavaSourceLocationArguments.java @@ -0,0 +1,116 @@ +package io.quarkus.qute.debug.client; + +/** + * Arguments describing a Java element referenced from a Qute template. + *

    + * Sent by the Debug Adapter via {@link JavaSourceResolver#resolveJavaSource} + * to the client in order to locate the corresponding Java source file and position. + *

    + * + *

    Example of a qute-java URI

    + * + *
    + * qute-java://com.acme.Bean#process@io.quarkus.qute.TemplateContents
    + * 
    + * + *

    + * Interpretation: + *

    + *
      + *
    • javaElementUri = "qute-java://com.acme.Bean#process@io.quarkus.qute.TemplateContents"
    • + *
    • typeName = "com.acme.Bean"
    • + *
    • method = "process" (optional; if null, the annotation is applied on the class)
    • + *
    • annotation = "io.quarkus.qute.TemplateContents"
    • + *
    + */ +public class JavaSourceLocationArguments { + + /** The qute-java URI used to locate the Java element from a template. */ + private String javaElementUri; + + /** Fully qualified Java class, interface name (e.g., "com.acme.Bean"). */ + private String typeName; + + /** + * Java method name. + *

    + * Optional: if {@code null}, the annotation is applied to the class itself. + *

    + */ + private String method; + + /** Fully qualified Java annotation name (typically "io.quarkus.qute.TemplateContents"). */ + private String annotation; + + /** + * Returns the qute-java URI used to locate the Java element. + * + * @return the URI string + */ + public String getJavaElementUri() { + return javaElementUri; + } + + /** + * Sets the qute-java URI used to locate the Java element. + * + * @param javaElementUri the URI string to set + */ + public void setJavaElementUri(String javaElementUri) { + this.javaElementUri = javaElementUri; + } + + /** + * Returns the fully qualified Java class, interface name. + * + * @return the class name + */ + public String getTypeName() { + return typeName; + } + + /** + * Sets the fully qualified Java class, interface name. + * + * @param typeName the class, interface name to set + */ + public void setTypeName(String typeName) { + this.typeName = typeName; + } + + /** + * Returns the Java method name. + * + * @return the method name, or {@code null} if the annotation applies to the class + */ + public String getMethod() { + return method; + } + + /** + * Sets the Java method name. + * + * @param method the method name to set, or {@code null} if the annotation applies to the class + */ + public void setMethod(String method) { + this.method = method; + } + + /** + * Returns the fully qualified Java annotation name. + * + * @return the annotation name (e.g., "io.quarkus.qute.TemplateContents") + */ + public String getAnnotation() { + return annotation; + } + + /** + * Sets the fully qualified Java annotation name. + * + * @param annotation the annotation name to set + */ + public void setAnnotation(String annotation) { + this.annotation = annotation; + } +} diff --git a/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/client/JavaSourceLocationResponse.java b/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/client/JavaSourceLocationResponse.java new file mode 100644 index 0000000000000..5a45265f52c06 --- /dev/null +++ b/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/client/JavaSourceLocationResponse.java @@ -0,0 +1,71 @@ +package io.quarkus.qute.debug.client; + +/** + * Response containing the location of a Java method, class, or template content + * referenced from a Qute template. + *

    + * Typically returned by {@link JavaSourceResolver#resolveJavaSource}. + *

    + * + *

    Example

    + * + *
    + * qute-java://org.acme.quarkus.sample.HelloResource$Hello@io.quarkus.qute.TemplateContents
    + * → javaFileUri = "file:///project/src/main/java/org/acme/quarkus/sample/HelloResource.java"
    + * → startLine = 16
    + * 
    + * + *

    + * The {@code startLine} represents the line where the template content or the Java + * element declaration starts. Lines are 1-based in this API (first line = 1). + *

    + */ +public class JavaSourceLocationResponse { + + /** URI of the Java source file containing the resolved element. */ + private String javaFileUri; + + /** + * Start line of the Java element or template content in the file. + *

    + * 1-based index of the line where the element or template content starts. + *

    + */ + private int startLine; + + /** + * Returns the URI of the Java source file containing the resolved element. + * + * @return the file URI + */ + public String getJavaFileUri() { + return javaFileUri; + } + + /** + * Sets the URI of the Java source file containing the resolved element. + * + * @param javaFileUri the file URI to set + */ + public void setJavaFileUri(String javaFileUri) { + this.javaFileUri = javaFileUri; + } + + /** + * Returns the start line of the Java element or template content in the source file. + * + * @return 1-based start line of the element or template content + */ + public int getStartLine() { + return startLine; + } + + /** + * Sets the start line of the Java element or template content in the source file. + * + * @param startLine 1-based start line of the element or template content + */ + public void setStartLine(int startLine) { + this.startLine = startLine; + } +} diff --git a/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/client/JavaSourceResolver.java b/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/client/JavaSourceResolver.java new file mode 100644 index 0000000000000..b3065d62cd582 --- /dev/null +++ b/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/client/JavaSourceResolver.java @@ -0,0 +1,92 @@ +package io.quarkus.qute.debug.client; + +import java.util.concurrent.CompletableFuture; + +import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; + +/** + * Resolver for Java source locations referenced from Qute templates. + *

    + * Implementations of this interface provide a way to resolve a Qute template reference + * (via a {@code qute-java://} URI) to the corresponding Java source file and template position. + *

    + * + *

    + * This resolver is used by the Debug Adapter Protocol (DAP) **only to support breakpoints** + * on Java methods or classes related to Qute templates. + *

    + * + *

    Example with a Qute template in a JAX-RS resource

    + * + *
    + * package org.acme.quarkus.sample;
    + *
    + * import jakarta.ws.rs.GET;
    + * import jakarta.ws.rs.Path;
    + * import jakarta.ws.rs.QueryParam;
    + * import jakarta.ws.rs.Produces;
    + * import jakarta.ws.rs.core.MediaType;
    + *
    + * import io.quarkus.qute.TemplateContents;
    + * import io.quarkus.qute.TemplateInstance;
    + *
    + * @Path("hello")
    + * public class HelloResource {
    + *
    + *     @TemplateContents("""
    + *             <p>Hello {name ?: "Qute"}</p>!
    + *             """)
    + *     record Hello(String name) implements TemplateInstance {
    + *     }
    + *
    + *     @GET
    + *     @Produces(MediaType.TEXT_PLAIN)
    + *     public TemplateInstance get(@QueryParam("name") String name) {
    + *         return new Hello(name);
    + *     }
    + * }
    + * 
    + * + *

    + * Corresponding {@code qute-java://} URI for the record: + *

    + * + *
    + * qute-java://org.acme.quarkus.sample.HelloResource$Hello@io.quarkus.qute.TemplateContents
    + * 
    + * + *

    + * Parsed into {@link JavaSourceLocationArguments}: + *

    + *
      + *
    • unresolvedUri = "qute-java://org.acme.quarkus.sample.HelloResource$Hello@io.quarkus.qute.TemplateContents"
    • + *
    • typeName = "org.acme.quarkus.sample.HelloResource$Hello"
    • + *
    • method = null (the annotation is on the record class)
    • + *
    • annotation = "io.quarkus.qute.TemplateContents"
    • + *
    + * + *

    + * Example resulting {@link JavaSourceLocationResponse}: + *

    + *
      + *
    • javaFileUri = "file:///path/to/project/src/main/java/org/acme/quarkus/sample/HelloResource.java"
    • + *
    • startLine = 16 (line of the text block content of the TemplateContents annotation)
    • + *
    + */ +public interface JavaSourceResolver { + + /** + * Resolves the Java method or class referenced from a Qute template for the purpose of setting breakpoints. + *

    + * The {@link JavaSourceLocationArguments} contains the parsed information from + * a {@code qute-java://} URI. The method returns a {@link CompletableFuture} that + * completes with a {@link JavaSourceLocationResponse} containing the Java file URI + * and the start line of the template content (or the method/class if applicable). + *

    + * + * @param args the arguments describing the Java element to resolve + * @return a future completing with the resolved Java source location + */ + @JsonRequest("qute/resolveJavaSource") + CompletableFuture resolveJavaSource(JavaSourceLocationArguments args); +} diff --git a/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/client/QuteDebugProtocolClient.java b/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/client/QuteDebugProtocolClient.java new file mode 100644 index 0000000000000..90234540cd397 --- /dev/null +++ b/independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/client/QuteDebugProtocolClient.java @@ -0,0 +1,24 @@ +package io.quarkus.qute.debug.client; + +import org.eclipse.lsp4j.debug.services.IDebugProtocolClient; + +/** + * Combined Debug Protocol client interface for Qute templates. + *

    + * Extends the standard {@link IDebugProtocolClient} to include + * the {@link JavaSourceResolver} functionality for resolving + * Java sources referenced from Qute templates via {@code qute-java://} URIs. + *

    + * + *

    + * Implementations of this interface can: + *

      + *
    • Receive standard DAP events and messages.
    • + *
    • Resolve Java source locations corresponding to Qute templates, + * enabling breakpoints in template-related Java code.
    • + *
    + *

    + */ +public interface QuteDebugProtocolClient extends IDebugProtocolClient, JavaSourceResolver { + +} \ No newline at end of file diff --git a/independent-projects/qute/debug/src/test/java/io/quarkus/qute/debug/client/DAPClient.java b/independent-projects/qute/debug/src/test/java/io/quarkus/qute/debug/client/DAPClient.java index 27c8fe6399c1c..2d47449fcf255 100644 --- a/independent-projects/qute/debug/src/test/java/io/quarkus/qute/debug/client/DAPClient.java +++ b/independent-projects/qute/debug/src/test/java/io/quarkus/qute/debug/client/DAPClient.java @@ -39,7 +39,6 @@ import org.eclipse.lsp4j.debug.Variable; import org.eclipse.lsp4j.debug.VariablesArguments; import org.eclipse.lsp4j.debug.launch.DSPLauncher; -import org.eclipse.lsp4j.debug.services.IDebugProtocolClient; import org.eclipse.lsp4j.debug.services.IDebugProtocolServer; import org.eclipse.lsp4j.jsonrpc.Launcher; import org.eclipse.lsp4j.jsonrpc.MessageConsumer; @@ -50,7 +49,7 @@ import io.quarkus.qute.debug.DebuggerState; import io.quarkus.qute.debug.client.TransportStreams.SocketTransportStreams; -public class DAPClient implements IDebugProtocolClient, Debugger { +public class DAPClient implements QuteDebugProtocolClient, Debugger { private IDebugProtocolServer debugProtocolServer; private Future debugProtocolFuture; @@ -58,6 +57,7 @@ public class DAPClient implements IDebugProtocolClient, Debugger { private final CompletableFuture capabilitiesFuture = new CompletableFuture<>(); private final CompletableFuture initialized = new CompletableFuture<>(); private boolean enabled; + private JavaSourceResolver javaFileInfoProvider; public CompletableFuture connectToServer(int port) { ServerTrace serverTrace = ServerTrace.getDefaultValue(); @@ -363,4 +363,15 @@ private T getResult(CompletableFuture future) { } } + public void setJavaFileInfoProvider(JavaSourceResolver javaFileInfoProvider) { + this.javaFileInfoProvider = javaFileInfoProvider; + } + + @Override + public CompletableFuture resolveJavaSource(JavaSourceLocationArguments params) { + if (javaFileInfoProvider != null) { + return javaFileInfoProvider.resolveJavaSource(params); + } + return CompletableFuture.completedFuture(null); + } }