Skip to content

Commit 663f987

Browse files
committed
feat: Attach Qute debugger with inlay hint.
Signed-off-by: azerr <[email protected]>
1 parent 1e84f4d commit 663f987

File tree

5 files changed

+190
-33
lines changed

5 files changed

+190
-33
lines changed

src/main/java/com/redhat/devtools/intellij/quarkus/run/AttachDebuggerExecutionListener.java

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import org.jetbrains.annotations.NotNull;
2424
import org.jetbrains.annotations.Nullable;
2525

26+
import static com.redhat.devtools.intellij.quarkus.run.AttachDebuggerProcessListener.isDebuggerAutoAttach;
27+
2628
/**
2729
* Execution listener singleton which tracks any process starting to add in debug mode
2830
* an instance of {@link AttachDebuggerProcessListener} to the process handler
@@ -45,26 +47,29 @@ public void processStarting(@NotNull String executorId,
4547
// Debug mode...
4648
RunnerAndConfigurationSettings settings = env.getRunnerAndConfigurationSettings();
4749
if (settings.getConfiguration() instanceof QuarkusRunConfiguration) {
48-
// The execution has been done by debugging a Quarkus run configuration (Gradle / Maven)
49-
// add a AttachDebuggerProcessListener to track
50-
// 'Listening for transport dt_socket at address: $PORT' message and starts
51-
// the remote debugger with the given port $PORT
52-
handler.addProcessListener(new AttachDebuggerProcessListener(project, env, getDebugPort(handler)));
50+
if (!isDebuggerAutoAttach()) {
51+
// The execution has been done by debugging a Quarkus run configuration (Gradle / Maven)
52+
// add a AttachDebuggerProcessListener to track
53+
// 'Listening for transport dt_socket at address: $PORT' message and starts
54+
// the remote debugger with the given port $PORT
55+
handler.addProcessListener(new AttachDebuggerProcessListener(project, env, getDebugPort(handler)));
56+
}
5357
}
5458
}
5559

5660
/**
5761
* Returns the port declared in teh command line with -Ddebug= and null otherwise.
62+
*
5863
* @param handler the process handler.
5964
* @return the port declared in teh command line with -Ddebug= and null otherwise.
6065
*/
6166
private @Nullable Integer getDebugPort(@NotNull ProcessHandler handler) {
6267
if (handler instanceof BaseOSProcessHandler osProcessHandler) {
6368
String commandLine = osProcessHandler.getCommandLine();
6469
int startIndex = commandLine.indexOf("-Ddebug=");
65-
if(startIndex != -1) {
70+
if (startIndex != -1) {
6671
StringBuilder port = new StringBuilder();
67-
for (int i = startIndex+"-Ddebug=".length(); i < commandLine.length(); i++) {
72+
for (int i = startIndex + "-Ddebug=".length(); i < commandLine.length(); i++) {
6873
char c = commandLine.charAt(i);
6974
if (Character.isDigit(c)) {
7075
port.append(c);

src/main/java/com/redhat/devtools/intellij/quarkus/run/AttachDebuggerProcessListener.java

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.intellij.execution.executors.DefaultDebugExecutor;
2020
import com.intellij.execution.process.ProcessEvent;
2121
import com.intellij.execution.process.ProcessListener;
22+
import com.intellij.execution.process.ProcessOutputType;
2223
import com.intellij.execution.remote.RemoteConfiguration;
2324
import com.intellij.execution.remote.RemoteConfigurationType;
2425
import com.intellij.execution.runners.ExecutionEnvironment;
@@ -30,6 +31,7 @@
3031
import com.intellij.openapi.project.Project;
3132
import com.intellij.openapi.ui.Messages;
3233
import com.intellij.openapi.util.Key;
34+
import com.intellij.openapi.util.registry.Registry;
3335
import com.redhat.devtools.intellij.qute.run.QuteConfigurationType;
3436
import com.redhat.devtools.intellij.qute.run.QuteRunConfiguration;
3537
import org.jetbrains.annotations.NotNull;
@@ -41,14 +43,15 @@
4143
import java.net.ConnectException;
4244
import java.net.Socket;
4345
import java.nio.charset.StandardCharsets;
46+
import java.util.MissingResourceException;
4447

4548
import static com.redhat.devtools.intellij.quarkus.run.QuarkusRunConfiguration.QUARKUS_CONFIGURATION;
4649

4750
/**
4851
* ProcessListener which tracks the message Listening for transport dt_socket at address: $PORT' to start the
4952
* remote debugger of the given port $PORT.
5053
*/
51-
class AttachDebuggerProcessListener implements ProcessListener {
54+
public class AttachDebuggerProcessListener implements ProcessListener {
5255

5356
private final static Logger LOGGER = LoggerFactory.getLogger(AttachDebuggerProcessListener.class);
5457

@@ -73,30 +76,32 @@ class AttachDebuggerProcessListener implements ProcessListener {
7376

7477
@Override
7578
public void onTextAvailable(@NotNull ProcessEvent event, @NotNull Key outputType) {
76-
String message = event.getText();
77-
if (!connected && debugPort != null && message.startsWith(LISTENING_FOR_TRANSPORT_DT_SOCKET_AT_ADDRESS + debugPort)) {
78-
connected = true;
79-
ProgressManager.getInstance().run(new Task.Backgroundable(project, QUARKUS_CONFIGURATION, false) {
80-
@Override
81-
public void run(@NotNull ProgressIndicator indicator) {
82-
String name = env.getRunProfile().getName();
83-
createRemoteConfiguration(indicator, debugPort, name);
79+
if (ProcessOutputType.isStdout(outputType)) {
80+
String message = event.getText();
81+
if (!connected && debugPort != null && message.startsWith(LISTENING_FOR_TRANSPORT_DT_SOCKET_AT_ADDRESS + debugPort)) {
82+
connected = true;
83+
ProgressManager.getInstance().run(new Task.Backgroundable(project, QUARKUS_CONFIGURATION, true) {
84+
@Override
85+
public void run(@NotNull ProgressIndicator indicator) {
86+
String name = env.getRunProfile().getName();
87+
createRemoteConfiguration(indicator, debugPort, name);
88+
}
89+
});
90+
} else if (!quteConnected && message.startsWith(QUTE_LISTENING_ON_PORT)) {
91+
quteConnected = true;
92+
Integer quteDebugPort = getQuteDebugPort(message);
93+
if (quteDebugPort == null) {
94+
LOGGER.error("Cannot extract Qute debug port from the given message: {}", message);
95+
return;
8496
}
85-
});
86-
} else if (!quteConnected && message.startsWith(QUTE_LISTENING_ON_PORT)) {
87-
quteConnected = true;
88-
Integer quteDebugPort = getQuteDebugPort(message);
89-
if (quteDebugPort == null) {
90-
LOGGER.error("Cannot extract Qute debug port from the given message: {}", message);
91-
return;
97+
ProgressManager.getInstance().run(new Task.Backgroundable(project, QUARKUS_CONFIGURATION, true) {
98+
@Override
99+
public void run(@NotNull ProgressIndicator indicator) {
100+
String name = env.getRunProfile().getName();
101+
createQuteConfiguration(indicator, quteDebugPort, name);
102+
}
103+
});
92104
}
93-
ProgressManager.getInstance().run(new Task.Backgroundable(project, QUARKUS_CONFIGURATION, false) {
94-
@Override
95-
public void run(@NotNull ProgressIndicator indicator) {
96-
String name = env.getRunProfile().getName();
97-
createQuteConfiguration(indicator, quteDebugPort, name);
98-
}
99-
});
100105
}
101106
}
102107

@@ -131,9 +136,19 @@ private void createRemoteConfiguration(@NotNull ProgressIndicator indicator, int
131136
}
132137

133138
private void createQuteConfiguration(@NotNull ProgressIndicator indicator, int port, String name) {
139+
createQuteConfiguration(port, name, project, indicator, true);
140+
}
141+
142+
public static void createQuteConfiguration(int port,
143+
@NotNull String name,
144+
@NotNull Project project,
145+
@NotNull ProgressIndicator indicator,
146+
boolean waitForPortAvailable) {
134147
indicator.setText("Connecting Qute debugger to port " + port);
135148
try {
136-
waitForPortAvailable(port, indicator);
149+
if (waitForPortAvailable) {
150+
waitForPortAvailable(port, indicator);
151+
}
137152
RunnerAndConfigurationSettings settings = RunManager.getInstance(project).createConfiguration(name + " (Qute)", QuteConfigurationType.class);
138153
QuteRunConfiguration quteConfiguration = (QuteRunConfiguration) settings.getConfiguration();
139154
quteConfiguration.setAttachPort(Integer.toString(port));
@@ -145,7 +160,15 @@ private void createQuteConfiguration(@NotNull ProgressIndicator indicator, int p
145160
}
146161
}
147162

148-
private void waitForPortAvailable(int port, ProgressIndicator monitor) throws IOException {
163+
public static boolean isDebuggerAutoAttach() {
164+
try {
165+
return Registry.is("debugger.auto.attach.from.any.console");
166+
} catch (MissingResourceException e) {
167+
return false;
168+
}
169+
}
170+
171+
private static void waitForPortAvailable(int port, ProgressIndicator monitor) throws IOException {
149172
long start = System.currentTimeMillis();
150173
while (System.currentTimeMillis() - start < 120_000 && !monitor.isCanceled()) {
151174
try (Socket socket = new Socket("localhost", port)) {

src/main/java/com/redhat/devtools/intellij/quarkus/run/QuarkusRunConfigurationManager.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
*******************************************************************************/
1414
package com.redhat.devtools.intellij.quarkus.run;
1515

16-
import com.intellij.execution.ExecutionListener;
1716
import com.intellij.execution.ExecutionManager;
1817
import com.intellij.execution.RunManager;
1918
import com.intellij.execution.RunnerAndConfigurationSettings;
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Copyright 2000-2025 JetBrains s.r.o. and contributors.
2+
// Use of this source code is governed by the Apache 2.0 license.
3+
4+
package com.redhat.devtools.intellij.qute.run;
5+
6+
import com.intellij.codeInsight.hints.presentation.InlayPresentation;
7+
import com.intellij.codeInsight.hints.presentation.PresentationFactory;
8+
import com.intellij.codeInsight.hints.presentation.PresentationRenderer;
9+
import com.intellij.execution.filters.ConsoleFilterProvider;
10+
import com.intellij.execution.filters.Filter;
11+
import com.intellij.execution.impl.InlayProvider;
12+
import com.intellij.openapi.application.ApplicationManager;
13+
import com.intellij.openapi.application.ModalityState;
14+
import com.intellij.openapi.editor.Editor;
15+
import com.intellij.openapi.editor.EditorCustomElementRenderer;
16+
import com.intellij.openapi.progress.EmptyProgressIndicator;
17+
import com.intellij.openapi.project.Project;
18+
import org.jetbrains.annotations.NotNull;
19+
import org.jetbrains.annotations.Nullable;
20+
21+
import java.util.Arrays;
22+
23+
import static com.redhat.devtools.intellij.quarkus.run.AttachDebuggerProcessListener.createQuteConfiguration;
24+
import static com.redhat.devtools.intellij.quarkus.run.AttachDebuggerProcessListener.isDebuggerAutoAttach;
25+
26+
/**
27+
* QuteDebuggerConsoleFilterProvider
28+
* <p>
29+
* Copy-pasted and adapted from IntelliJ Community's JavaDebuggerConsoleFilterProvider
30+
* for the Qute debugger.
31+
* <p>
32+
* Features:
33+
* 1. Detects console lines indicating that the Qute debugger server is listening on a port.
34+
* 2. Provides a clickable inlay "Attach Qute debugger" to attach the debugger automatically.
35+
* 3. Can trigger automatic attachment if the registry key
36+
* 'debugger.auto.attach.from.any.console' is enabled.
37+
*
38+
* @see <a href="https://github.com/JetBrains/intellij-community/blob/master/java/execution/impl/src/com/intellij/execution/impl/JavaDebuggerConsoleFilterProvider.java">JavaDebuggerConsoleFilterProvider.java</a>
39+
*/
40+
public final class QuteDebuggerConsoleFilterProvider implements ConsoleFilterProvider {
41+
42+
/**
43+
* Provides default filters for this project.
44+
*/
45+
@Override
46+
public Filter @NotNull [] getDefaultFilters(@NotNull Project project) {
47+
return new Filter[]{new QuteDebuggerAttachFilter(project)};
48+
}
49+
50+
/**
51+
* Detects the Qute server port from a console line.
52+
* Example line: "Qute debugger server listening on port 5005"
53+
*
54+
* @param line a console line
55+
* @return the detected port or null if the line does not match
56+
*/
57+
public static @Nullable Integer getConnectionMatcher(String line) {
58+
if (line.contains("Qute debugger server listening on port ")) {
59+
String port = line.substring("Qute debugger server listening on port ".length()).trim();
60+
try {
61+
return Integer.parseInt(port);
62+
} catch (Exception e) {
63+
// Should never occur
64+
return null;
65+
}
66+
}
67+
return null;
68+
}
69+
70+
/**
71+
* Filter applied to each console line to detect the Qute port and create an inlay.
72+
*/
73+
private static class QuteDebuggerAttachFilter implements Filter {
74+
@NotNull Project myProject;
75+
76+
private QuteDebuggerAttachFilter(@NotNull Project project) {
77+
this.myProject = project;
78+
}
79+
80+
@Override
81+
public @Nullable Result applyFilter(@NotNull String line, int entireLength) {
82+
Integer port = getConnectionMatcher(line);
83+
if (port == null) {
84+
return null; // No port detected → nothing to do
85+
}
86+
87+
// Automatic attachment if registry is enabled and debugger not already attached
88+
if (isDebuggerAutoAttach()) {
89+
ApplicationManager.getApplication().invokeLater(
90+
() -> createQuteConfiguration(port, "", myProject, new EmptyProgressIndicator(), true),
91+
ModalityState.any());
92+
}
93+
94+
int start = entireLength - line.length();
95+
96+
// Return a Result with:
97+
// 1) an AttachInlayResult for the clickable inlay
98+
// 2) an empty ResultItem to "trick" CompositeFilter into proper behavior
99+
return new Result(Arrays.asList(
100+
new AttachInlayResult(start, start + line.length() - 1, port),
101+
new ResultItem(0, 0, null)
102+
));
103+
}
104+
}
105+
106+
/**
107+
* Custom ResultItem that creates a clickable inlay for attaching the Qute debugger.
108+
*/
109+
private static class AttachInlayResult extends Filter.ResultItem implements InlayProvider {
110+
private final Integer port;
111+
112+
AttachInlayResult(int highlightStartOffset, int highlightEndOffset, Integer port) {
113+
super(highlightStartOffset, highlightEndOffset, null);
114+
this.port = port;
115+
}
116+
117+
@Override
118+
public EditorCustomElementRenderer createInlayRenderer(Editor editor) {
119+
PresentationFactory factory = new PresentationFactory(editor);
120+
InlayPresentation presentation = factory.referenceOnHover(
121+
factory.roundWithBackground(factory.smallText("Attach Qute debugger")),
122+
(event, point) -> {
123+
// On click → attach Qute debugger
124+
createQuteConfiguration(port, "", editor.getProject(), new EmptyProgressIndicator(), true);
125+
});
126+
return new PresentationRenderer(presentation);
127+
}
128+
}
129+
}

src/main/resources/META-INF/lsp4ij-qute.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@
115115
implementation="com.redhat.devtools.intellij.qute.run.QuteConfigurationType"/>
116116
<xdebugger.breakpointType
117117
implementation="com.redhat.devtools.intellij.qute.run.QuteBreakpointType"/>
118+
<consoleFilterProvider implementation="com.redhat.devtools.intellij.qute.run.QuteDebuggerConsoleFilterProvider"/>
118119
</extensions>
119120

120121
<extensions defaultExtensionNs="com.redhat.devtools.lsp4ij">

0 commit comments

Comments
 (0)