Skip to content

Commit f145459

Browse files
committed
Evaluate Qute expression in the render Thread with Qute debugger
Signed-off-by: azerr <[email protected]>
1 parent 2c2f42e commit f145459

File tree

2 files changed

+220
-115
lines changed

2 files changed

+220
-115
lines changed

independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/agent/RemoteStackFrame.java

Lines changed: 103 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -24,96 +24,80 @@
2424
import io.quarkus.qute.trace.ResolveEvent;
2525

2626
/**
27-
* Represents a single stack frame in the Qute debugging process.
27+
* Represents a single Qute stack frame in the debugging process.
28+
*
2829
* <p>
29-
* A {@link RemoteStackFrame} corresponds to the evaluation of a
30-
* {@link TemplateNode} at runtime. It stores contextual information such as the
31-
* variables in scope, the template being executed, and the current execution
32-
* state.
30+
* A {@link RemoteStackFrame} corresponds to the evaluation of a {@link TemplateNode}
31+
* during the rendering of a Qute template. It encapsulates the current execution context,
32+
* including variables, scopes, and the source template being processed.
3333
* </p>
3434
*
3535
* <p>
36-
* It extends {@link StackFrame} from the Debug Adapter Protocol (DAP), allowing
37-
* integration with remote debugging clients.
36+
* This class integrates with the Debug Adapter Protocol (DAP) through
37+
* {@link org.eclipse.lsp4j.debug.StackFrame}, enabling external debuggers (like VSCode or IntelliJ)
38+
* to display Qute stack frames, inspect variables, and evaluate expressions.
3839
* </p>
3940
*/
4041
public class RemoteStackFrame extends StackFrame {
4142

42-
/**
43-
* Represents an empty array of stack frames.
44-
*/
43+
/** Represents an empty array of stack frames. */
4544
public static final StackFrame[] EMPTY_STACK_FRAMES = new StackFrame[0];
4645

47-
/**
48-
* Counter used to assign a unique ID to each frame.
49-
*/
46+
/** Counter used to assign a unique ID to each frame. */
5047
private static final AtomicInteger frameIdCounter = new AtomicInteger();
5148

52-
/**
53-
* The previous frame in the call stack, or {@code null} if this is the first
54-
* frame.
55-
*/
49+
/** The previous frame in the call stack, or {@code null} if this is the first frame. */
5650
private final transient RemoteStackFrame previousFrame;
5751

58-
/**
59-
* The ID of the template currently being executed.
60-
*/
52+
/** The ID of the template currently being executed. */
6153
private final transient String templateId;
6254

63-
/**
64-
* Registry of variables used in this stack frame.
65-
*/
55+
/** Registry of variables used in this stack frame. */
6656
private final transient VariablesRegistry variablesRegistry;
6757

68-
/**
69-
* Lazily created list of available scopes (locals, globals, namespaces).
70-
*/
58+
/** Lazily created list of available scopes (locals, globals, namespaces). */
7159
private transient Collection<RemoteScope> scopes;
7260

73-
/**
74-
* The resolve event associated with this frame, containing runtime context.
75-
*/
61+
/** The resolve event associated with this frame, containing runtime context. */
7662
private final transient ResolveEvent event;
7763

64+
/** The remote thread that owns this frame, responsible for executing evaluations. */
65+
private final transient RemoteThread remoteThread;
66+
7867
/**
7968
* Creates a new {@link RemoteStackFrame}.
8069
*
81-
* @param event the resolve event describing the current
82-
* execution
70+
* @param event the resolve event describing the current execution
8371
* @param previousFrame the previous stack frame, may be {@code null}
8472
* @param sourceTemplateRegistry registry for mapping templates to debug sources
85-
* @param variablesRegistry the registry for managing variables
73+
* @param variablesRegistry registry for managing variables
74+
* @param remoteThread the owning remote thread
8675
*/
8776
public RemoteStackFrame(ResolveEvent event, RemoteStackFrame previousFrame,
88-
SourceTemplateRegistry sourceTemplateRegistry, VariablesRegistry variablesRegistry) {
77+
SourceTemplateRegistry sourceTemplateRegistry, VariablesRegistry variablesRegistry,
78+
RemoteThread remoteThread) {
8979
this.event = event;
9080
this.previousFrame = previousFrame;
9181
this.variablesRegistry = variablesRegistry;
82+
this.remoteThread = remoteThread;
83+
9284
int id = frameIdCounter.incrementAndGet();
9385
int line = event.getTemplateNode().getOrigin().getLine();
9486
super.setId(id);
9587
super.setName(event.getTemplateNode().toString());
9688
super.setLine(line);
89+
9790
this.templateId = event.getTemplateNode().getOrigin().getTemplateId();
9891
super.setSource(
99-
sourceTemplateRegistry.getSource(templateId,
100-
previousFrame != null ? previousFrame.getSource() : null));
92+
sourceTemplateRegistry.getSource(templateId, previousFrame != null ? previousFrame.getSource() : null));
10193
}
10294

103-
/**
104-
* Returns the template ID associated with this frame.
105-
*
106-
* @return the template ID
107-
*/
95+
/** @return the template ID associated with this frame */
10896
public String getTemplateId() {
10997
return templateId;
11098
}
11199

112-
/**
113-
* Returns the template Uri associated with this frame and null otherwise.
114-
*
115-
* @return the template Uri associated with this frame and null otherwise.
116-
*/
100+
/** @return the template URI associated with this frame, or {@code null} if unavailable */
117101
public URI getTemplateUri() {
118102
var source = getSource();
119103
return source != null ? source.getUri() : null;
@@ -124,11 +108,7 @@ public RemoteSource getSource() {
124108
return (RemoteSource) super.getSource();
125109
}
126110

127-
/**
128-
* Returns the previous stack frame, or {@code null} if none exists.
129-
*
130-
* @return the previous {@link RemoteStackFrame}
131-
*/
111+
/** @return the previous stack frame, or {@code null} if none exists */
132112
public RemoteStackFrame getPrevious() {
133113
return previousFrame;
134114
}
@@ -138,13 +118,12 @@ public RemoteStackFrame getPrevious() {
138118
* <p>
139119
* Scopes include:
140120
* <ul>
141-
* <li>Locals (variables in the current template context)</li>
142-
* <li>Globals (global variables accessible in Qute)</li>
143-
* <li>Namespace resolvers (custom resolvers for Qute templates)</li>
121+
* <li>Locals variables specific to the current template</li>
122+
* <li>Globals — shared Qute global variables</li>
123+
* <li>Namespace resolvers — registered resolvers for {@code namespace:expression}</li>
144124
* </ul>
145-
* </p>
146125
*
147-
* @return the collection of {@link RemoteScope}
126+
* @return a collection of {@link RemoteScope}
148127
*/
149128
public Collection<RemoteScope> getScopes() {
150129
if (scopes == null) {
@@ -153,32 +132,24 @@ public Collection<RemoteScope> getScopes() {
153132
return scopes;
154133
}
155134

156-
/**
157-
* Creates the list of scopes for this frame.
158-
*
159-
* @return a collection of {@link RemoteScope}
160-
*/
161135
private Collection<RemoteScope> createScopes() {
162136
Collection<RemoteScope> scopes = new ArrayList<>();
163-
// Locals scope
164137
scopes.add(new LocalsScope(event.getContext(), this, variablesRegistry));
165-
// Global scope
166138
scopes.add(new GlobalsScope(event.getContext(), this, variablesRegistry));
167-
// Namespace resolvers scope
168139
scopes.add(new NamespaceResolversScope(event.getEngine(), this, variablesRegistry));
169140
return scopes;
170141
}
171142

172143
/**
173-
* Evaluates an expression in the current frame context.
144+
* Evaluates an arbitrary Qute expression in the current frame context.
174145
* <p>
175-
* If the expression contains conditional operators, it is parsed and evaluated
176-
* as a conditional expression. Otherwise, it is treated as a simple Qute
177-
* expression.
146+
* If the expression looks like a conditional (e.g. {@code user.age > 18}),
147+
* it is parsed and evaluated as a conditional expression. Otherwise, it is
148+
* evaluated as a simple Qute value expression.
178149
* </p>
179150
*
180-
* @param expression the expression to evaluate
181-
* @return a {@link CompletableFuture} containing the result of the evaluation
151+
* @param expression the Qute expression or condition to evaluate
152+
* @return a {@link CompletableFuture} resolving to the evaluation result
182153
*/
183154
public CompletableFuture<Object> evaluate(String expression) {
184155
if (isConditionExpression(expression)) {
@@ -188,42 +159,81 @@ public CompletableFuture<Object> evaluate(String expression) {
188159
} catch (Exception e) {
189160
return CompletableFuture.failedFuture(e);
190161
}
191-
// Evaluate condition expression without ignoring syntax expression
192-
return evaluateCondition(ifNode, false);
162+
// Run the condition evaluation in the render thread
163+
return evaluateConditionInRenderThread(ifNode, false);
193164
}
194-
// Evaluate simple expression
195-
return event.getContext().evaluate(expression).toCompletableFuture();
165+
return evaluateExpressionInRenderThread(expression);
196166
}
197167

198168
/**
199-
* Determines if a given expression should be treated as a conditional
200-
* expression.
169+
* Evaluates a Qute expression inside the render thread.
201170
*
202-
* @param expression the expression to test
203-
* @return {@code true} if the expression contains conditional operators,
204-
* {@code false} otherwise
171+
* <p>
172+
* Qute expressions (like {@code uri:Todos.index}) must be evaluated inside the
173+
* original rendering thread to ensure CDI {@code @RequestScoped} contexts are
174+
* active. Evaluating them elsewhere may cause
175+
* {@code ContextNotActiveException}.
176+
* </p>
177+
*/
178+
private CompletableFuture<Object> evaluateExpressionInRenderThread(String expression) {
179+
return remoteThread.evaluateInRenderThread(() -> event.getContext().evaluate(expression).toCompletableFuture());
180+
}
181+
182+
/**
183+
* Checks if an expression contains conditional operators and should be
184+
* interpreted as a condition.
205185
*/
206186
private static boolean isConditionExpression(String expression) {
207-
return expression.contains("!") || expression.contains(">") || expression.contains("gt")
208-
|| expression.contains(">=") || expression.contains(" ge") || expression.contains("<")
209-
|| expression.contains(" lt") || expression.contains("<=") || expression.contains(" le")
210-
|| expression.contains(" eq") || expression.contains("==") || expression.contains(" is")
211-
|| expression.contains("!=") || expression.contains(" ne") || expression.contains("&&")
212-
|| expression.contains(" and") || expression.contains("||") || expression.contains(" or");
187+
return expression.contains("!") || expression.contains(">") || expression.contains("==")
188+
|| expression.contains("<") || expression.contains("&&") || expression.contains("||")
189+
|| expression.contains(" eq") || expression.contains(" ne")
190+
|| expression.contains(" gt") || expression.contains(" lt")
191+
|| expression.contains(" ge") || expression.contains(" le")
192+
|| expression.contains(" and") || expression.contains(" or")
193+
|| expression.contains(" is");
213194
}
214195

215196
/**
216-
* Evaluates a parsed conditional expression.
197+
* Evaluates a parsed conditional expression within the render thread context.
198+
*
199+
* <p>
200+
* This is used for conditional breakpoints: before suspending execution,
201+
* the condition must be evaluated safely inside the render thread.
202+
* </p>
203+
*
204+
* <p>
205+
* Calling {@code evaluateConditionInRenderThread()} from a suspended state
206+
* ensures the evaluation is scheduled on the render thread asynchronously
207+
* (via {@link RemoteThread#evaluateInRenderThread(java.util.concurrent.Callable)}),
208+
* avoiding deadlocks or premature resumption.
209+
* </p>
217210
*
218211
* @param ifNode the parsed {@link TemplateNode} representing the condition
219-
* @param ignoreError whether to ignore evaluation errors and return
220-
* {@code false}
221-
* @return a {@link CompletableFuture} containing {@code true} or {@code false}
212+
* @param ignoreError whether to ignore evaluation errors and return {@code false}
213+
* @return a future resolving to {@code true} or {@code false}
214+
*/
215+
public CompletableFuture<Object> evaluateConditionInRenderThread(TemplateNode ifNode, boolean ignoreError) {
216+
return remoteThread.evaluateInRenderThread(() -> evaluateCondition(ifNode, ignoreError));
217+
}
218+
219+
/**
220+
* Evaluates the given Qute {@code if} node in the current context.
221+
*
222+
* <p>
223+
* This method converts the Qute {@link TemplateNode} result into a boolean
224+
* value. If evaluation fails and {@code ignoreError} is {@code true}, it
225+
* returns {@code false} instead of throwing an exception.
226+
* </p>
227+
*
228+
* <p>
229+
* This method runs synchronously inside the render thread. It is typically
230+
* called by {@link #evaluateConditionInRenderThread(TemplateNode, boolean)}.
231+
* </p>
222232
*/
223233
public CompletableFuture<Object> evaluateCondition(TemplateNode ifNode, boolean ignoreError) {
224234
try {
225-
return ifNode.resolve(event.getContext())//
226-
.toCompletableFuture()//
235+
return ifNode.resolve(event.getContext())
236+
.toCompletableFuture()
227237
.handle((result, error) -> {
228238
if (error != null) {
229239
if (ignoreError) {
@@ -239,32 +249,18 @@ public CompletableFuture<Object> evaluateCondition(TemplateNode ifNode, boolean
239249
}
240250
}
241251

242-
/**
243-
* Returns the current Qute engine.
244-
*
245-
* @return the {@link Engine}
246-
*/
252+
/** @return the current Qute engine */
247253
public Engine getEngine() {
248254
return event.getEngine();
249255
}
250256

251-
/**
252-
* Returns the {@link ResolveEvent} associated with this frame.
253-
*
254-
* @return the {@link ResolveEvent}
255-
*/
257+
/** @return the current {@link ResolveEvent} associated with this frame */
256258
ResolveEvent getEvent() {
257259
return event;
258260
}
259261

260-
/**
261-
* Creates a new evaluation context for the given base object.
262-
*
263-
* @param base the base object
264-
* @return a new {@link EvalContext}
265-
*/
262+
/** Creates a new {@link EvalContext} for the given base object. */
266263
public EvalContext createEvalContext(Object base) {
267264
return new DebuggerEvalContext(base, this);
268265
}
269-
270266
}

0 commit comments

Comments
 (0)