Skip to content

Commit 47fb5d7

Browse files
committed
Qute debugging support inside Java file
Signed-off-by: azerr <azerr@redhat.com>
1 parent 50c9111 commit 47fb5d7

File tree

7 files changed

+523
-8
lines changed

7 files changed

+523
-8
lines changed

qute.jdt/com.redhat.qute.jdt/META-INF/MANIFEST.MF

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Export-Package: com.redhat.qute.commons,
2525
com.redhat.qute.commons.jaxrs,
2626
com.redhat.qute.commons.usertags,
2727
com.redhat.qute.jdt,
28+
com.redhat.qute.jdt.debug,
2829
com.redhat.qute.jdt.internal.java;x-friends:="com.redhat.qute.jdt.test",
2930
com.redhat.qute.jdt.internal.ls;x-friends:="com.redhat.qute.jdt.test",
3031
com.redhat.qute.jdt.internal.template;x-friends:="com.redhat.qute.jdt.test",

qute.jdt/com.redhat.qute.jdt/plugin.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@
3737
</delegateCommandHandler>
3838
</extension>
3939

40+
<!-- Delegate command handler for Qute debugging -->
41+
<extension point="org.eclipse.jdt.ls.core.delegateCommandHandler">
42+
<delegateCommandHandler class="com.redhat.qute.jdt.internal.ls.QuteSupportForDebugDelegateCommandHandler">
43+
<command id="qute/debug/resolveJavaSource"/>
44+
</delegateCommandHandler>
45+
</extension>
46+
4047
<!-- =========== Qute core & Quarkus Integration =========== -->
4148

4249
<!-- Template root path providers for Qute core (src/main/resources/templates) -->
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2026 Red Hat Inc. and others.
3+
* All rights reserved. This program and the accompanying materials
4+
* which accompanies this distribution, and is available at
5+
* http://www.eclipse.org/legal/epl-v20.html
6+
*
7+
* SPDX-License-Identifier: EPL-2.0
8+
*******************************************************************************/
9+
package com.redhat.qute.jdt;
10+
11+
import java.util.logging.Level;
12+
import java.util.logging.Logger;
13+
14+
import org.eclipse.core.resources.IProject;
15+
import org.eclipse.core.resources.IWorkspaceRoot;
16+
import org.eclipse.core.resources.ResourcesPlugin;
17+
import org.eclipse.core.runtime.IProgressMonitor;
18+
import org.eclipse.jdt.core.IAnnotation;
19+
import org.eclipse.jdt.core.ICompilationUnit;
20+
import org.eclipse.jdt.core.IJavaProject;
21+
import org.eclipse.jdt.core.IMethod;
22+
import org.eclipse.jdt.core.ISourceRange;
23+
import org.eclipse.jdt.core.IType;
24+
import org.eclipse.jdt.core.JavaCore;
25+
import org.eclipse.jdt.core.JavaModelException;
26+
import org.eclipse.jdt.core.dom.AST;
27+
import org.eclipse.jdt.core.dom.ASTParser;
28+
import org.eclipse.jdt.core.dom.CompilationUnit;
29+
30+
import com.redhat.qute.jdt.debug.JavaSourceLocationArguments;
31+
import com.redhat.qute.jdt.debug.JavaSourceLocationResponse;
32+
import com.redhat.qute.jdt.utils.IJDTUtils;
33+
34+
/**
35+
* JDT-side implementation for resolving Java source locations referenced from
36+
* Qute templates.
37+
*
38+
* Supports: - type resolution - method resolution - annotation-based template
39+
* resolution (@TemplateContents) - text blocks (""" """)
40+
*/
41+
public class QuteSupportForDebug {
42+
43+
private static final Logger LOGGER = Logger.getLogger(QuteSupportForDebug.class.getName());
44+
45+
private static final QuteSupportForDebug INSTANCE = new QuteSupportForDebug();
46+
47+
public static QuteSupportForDebug getInstance() {
48+
return INSTANCE;
49+
}
50+
51+
public JavaSourceLocationResponse resolveJavaSource(JavaSourceLocationArguments args, IJDTUtils utils,
52+
IProgressMonitor monitor) {
53+
54+
try {
55+
56+
IType type = findType(args.getTypeName(), monitor);
57+
if (type == null) {
58+
return null;
59+
}
60+
61+
ICompilationUnit cu = type.getCompilationUnit();
62+
if (cu == null) {
63+
return null;
64+
}
65+
66+
String javaFileUri = utils.toUri(cu);
67+
68+
// 1️⃣ Annotation-based template (highest priority)
69+
if (args.getAnnotation() != null) {
70+
IAnnotation annotation = findAnnotation(type, args.getAnnotation());
71+
72+
if (annotation != null) {
73+
int offset = computeTemplateContentOffset(cu, annotation);
74+
int startLine = computeLine(cu, offset);
75+
return new JavaSourceLocationResponse(javaFileUri, startLine);
76+
}
77+
}
78+
79+
// 2️⃣ Method-based resolution
80+
if (args.getMethod() != null) {
81+
IMethod method = findMethod(type, args.getMethod());
82+
if (method != null) {
83+
int offset = method.getNameRange().getOffset();
84+
int startLine = computeLine(cu, offset);
85+
return new JavaSourceLocationResponse(javaFileUri, startLine);
86+
}
87+
}
88+
return null;
89+
90+
} catch (Exception e) {
91+
LOGGER.log(Level.SEVERE, "Failed to resolve Java source location", e);
92+
}
93+
94+
return null;
95+
}
96+
97+
/* ---------------------------------------------------------------------- */
98+
/* Resolution helpers */
99+
/* ---------------------------------------------------------------------- */
100+
101+
private static IType findType(String className, IProgressMonitor monitor) {
102+
try {
103+
IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
104+
105+
for (IProject project : root.getProjects()) {
106+
if (!project.isOpen() || !project.hasNature(JavaCore.NATURE_ID)) {
107+
continue;
108+
}
109+
110+
IJavaProject javaProject = JavaCore.create(project);
111+
112+
IType type = javaProject.findType(className, monitor);
113+
if (type != null) {
114+
return type;
115+
}
116+
117+
// Fallback java.lang
118+
if (!className.contains(".")) {
119+
type = javaProject.findType("java.lang." + className, monitor);
120+
if (type != null) {
121+
return type;
122+
}
123+
}
124+
}
125+
} catch (Exception e) {
126+
LOGGER.log(Level.SEVERE, "Error while finding type '" + className + "'", e);
127+
}
128+
return null;
129+
}
130+
131+
private static IMethod findMethod(IType type, String methodName) throws JavaModelException {
132+
133+
for (IMethod method : type.getMethods()) {
134+
if (method.getElementName().equals(methodName)) {
135+
return method;
136+
}
137+
}
138+
return null;
139+
}
140+
141+
private static IAnnotation findAnnotation(IType type, String annotationFqn) throws JavaModelException {
142+
143+
String simpleName = simpleName(annotationFqn);
144+
145+
for (IAnnotation ann : type.getAnnotations()) {
146+
if (ann.getElementName().equals(simpleName)) {
147+
return ann;
148+
}
149+
}
150+
return null;
151+
}
152+
153+
private static String simpleName(String fqn) {
154+
int idx = fqn.lastIndexOf('.');
155+
return idx == -1 ? fqn : fqn.substring(idx + 1);
156+
}
157+
158+
/* ---------------------------------------------------------------------- */
159+
/* Offset & line computation */
160+
/* ---------------------------------------------------------------------- */
161+
162+
private static int computeTemplateContentOffset(ICompilationUnit cu, IAnnotation annotation)
163+
throws JavaModelException {
164+
165+
ISourceRange range = annotation.getSourceRange();
166+
String source = cu.getSource();
167+
168+
int start = range.getOffset();
169+
int end = start + range.getLength();
170+
171+
String annotationSource = source.substring(start, end);
172+
173+
int paren = annotationSource.indexOf('(');
174+
if (paren == -1) {
175+
return start;
176+
}
177+
178+
int offset = start + paren + 1;
179+
180+
// Skip whitespace
181+
while (offset < source.length() && Character.isWhitespace(source.charAt(offset))) {
182+
offset++;
183+
}
184+
185+
// Text block
186+
if (source.startsWith("\"\"\"", offset)) {
187+
return offset + 3;
188+
}
189+
190+
// Classic string
191+
if (source.charAt(offset) == '"') {
192+
return offset + 1;
193+
}
194+
195+
return offset;
196+
}
197+
198+
private static int computeLine(ICompilationUnit cu, int offset) throws JavaModelException {
199+
200+
CompilationUnit ast = parse(cu);
201+
return ast.getLineNumber(offset) - 1; // DAP is 0-based
202+
}
203+
204+
private static CompilationUnit parse(ICompilationUnit cu) {
205+
ASTParser parser = ASTParser.newParser(AST.JLS_Latest);
206+
parser.setSource(cu);
207+
parser.setResolveBindings(false);
208+
return (CompilationUnit) parser.createAST(null);
209+
}
210+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package com.redhat.qute.jdt.debug;
2+
3+
/**
4+
* Arguments describing a Java element referenced from a Qute template.
5+
* <p>
6+
* Sent by the Debug Adapter via {@link JavaSourceResolver#resolveJavaSource}
7+
* to the client in order to locate the corresponding Java source file and position.
8+
* </p>
9+
*
10+
* <h2>Example of a qute-java URI</h2>
11+
*
12+
* <pre>
13+
* qute-java://com.acme.Bean#process@io.quarkus.qute.TemplateContents
14+
* </pre>
15+
*
16+
* <p>
17+
* Interpretation:
18+
* </p>
19+
* <ul>
20+
* <li>javaElementUri = "qute-java://com.acme.Bean#process@io.quarkus.qute.TemplateContents"</li>
21+
* <li>typeName = "com.acme.Bean"</li>
22+
* <li>method = "process" (optional; if null, the annotation is applied on the class)</li>
23+
* <li>annotation = "io.quarkus.qute.TemplateContents"</li>
24+
* </ul>
25+
*/
26+
public class JavaSourceLocationArguments {
27+
28+
/** The qute-java URI used to locate the Java element from a template. */
29+
private String javaElementUri;
30+
31+
/** Fully qualified Java class, interface name (e.g., "com.acme.Bean"). */
32+
private String typeName;
33+
34+
/**
35+
* Java method name.
36+
* <p>
37+
* Optional: if {@code null}, the annotation is applied to the class itself.
38+
* </p>
39+
*/
40+
private String method;
41+
42+
/** Fully qualified Java annotation name (typically "io.quarkus.qute.TemplateContents"). */
43+
private String annotation;
44+
45+
/**
46+
* Returns the qute-java URI used to locate the Java element.
47+
*
48+
* @return the URI string
49+
*/
50+
public String getJavaElementUri() {
51+
return javaElementUri;
52+
}
53+
54+
/**
55+
* Sets the qute-java URI used to locate the Java element.
56+
*
57+
* @param javaElementUri the URI string to set
58+
*/
59+
public void setJavaElementUri(String javaElementUri) {
60+
this.javaElementUri = javaElementUri;
61+
}
62+
63+
/**
64+
* Returns the fully qualified Java class, interface name.
65+
*
66+
* @return the class name
67+
*/
68+
public String getTypeName() {
69+
return typeName;
70+
}
71+
72+
/**
73+
* Sets the fully qualified Java class, interface name.
74+
*
75+
* @param typeName the class, interface name to set
76+
*/
77+
public void setTypeName(String typeName) {
78+
this.typeName = typeName;
79+
}
80+
81+
/**
82+
* Returns the Java method name.
83+
*
84+
* @return the method name, or {@code null} if the annotation applies to the class
85+
*/
86+
public String getMethod() {
87+
return method;
88+
}
89+
90+
/**
91+
* Sets the Java method name.
92+
*
93+
* @param method the method name to set, or {@code null} if the annotation applies to the class
94+
*/
95+
public void setMethod(String method) {
96+
this.method = method;
97+
}
98+
99+
/**
100+
* Returns the fully qualified Java annotation name.
101+
*
102+
* @return the annotation name (e.g., "io.quarkus.qute.TemplateContents")
103+
*/
104+
public String getAnnotation() {
105+
return annotation;
106+
}
107+
108+
/**
109+
* Sets the fully qualified Java annotation name.
110+
*
111+
* @param annotation the annotation name to set
112+
*/
113+
public void setAnnotation(String annotation) {
114+
this.annotation = annotation;
115+
}
116+
}

0 commit comments

Comments
 (0)