Skip to content

Commit bf0a7e1

Browse files
committed
feat: Add support for debugger Smart Step Into action via DAP
`StepInTargets` Fixes #1431 Signed-off-by: azerr <azerr@redhat.com>
1 parent 8f470d0 commit bf0a7e1

9 files changed

Lines changed: 543 additions & 1 deletion

File tree

docs/dap/DAPSupport.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ Current state of [Requests](https://microsoft.github.io/debug-adapter-protocol//
6464
*[StackTrace](https://microsoft.github.io/debug-adapter-protocol//specification.html#Requests_StackTrace).
6565
*[StepBack](https://microsoft.github.io/debug-adapter-protocol//specification.html#Requests_StepBack).
6666
*[StepIn](https://microsoft.github.io/debug-adapter-protocol//specification.html#Requests_StepIn).
67-
* [StepInTargets](https://microsoft.github.io/debug-adapter-protocol//specification.html#Requests_StepInTargets).
67+
* [StepInTargets](https://microsoft.github.io/debug-adapter-protocol//specification.html#Requests_StepInTargets).
6868
*[StepOut](https://microsoft.github.io/debug-adapter-protocol//specification.html#Requests_StepOut).
6969
*[Terminate](https://microsoft.github.io/debug-adapter-protocol//specification.html#Requests_Terminate).
7070
*[TerminateThreads](https://microsoft.github.io/debug-adapter-protocol//specification.html#Requests_TerminateThreads).

docs/dap/UserGuide.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,34 @@ Take a sample JavaScript file containing an error:
8383
In this example, no breakpoints are defined.
8484
However, when you start the DAP server, it stops at the line with the line error:
8585

86+
### Smart Step Into
87+
88+
[Smart Step Into](https://www.jetbrains.com/help/idea/stepping-through-the-program.html#smart-step-into) is available
89+
if the DAP server supports [StepInTargets](https://microsoft.github.io/debug-adapter-protocol//specification.html#Requests_StepInTargets).
90+
91+
When you're stopped at a breakpoint on a line with multiple function calls, press **Shift+F7** (or select **Smart Step Into** from the debug menu).
92+
A popup will appear showing all available functions you can step into.
93+
Use the arrow keys to navigate between functions, and press **Enter** to step into the selected one.
94+
You can also click directly on a function name in the editor.
95+
96+
For example, on this line:
97+
```python
98+
result = multiply(add(x, y), subtract(x, y))
99+
```
100+
101+
Smart Step Into will let you choose between `multiply`, `add`, and `subtract`.
102+
103+
The feature also handles duplicate function names on the same line correctly. For instance:
104+
```python
105+
result = add(1, 2) + add(3, 4)
106+
```
107+
108+
Each `add` call will be highlighted separately, allowing you to step into the specific occurrence you want.
109+
110+
Here's a demo with [Python Debugpy](./user-defined-dap/python-debugpy.md) which supports [StepInTargets](https://microsoft.github.io/debug-adapter-protocol//specification.html#Requests_StepInTargets):
111+
112+
![DAP Smart Step Into](./images/DAP_SmartStepIntoDemo.gif)
113+
86114
![DAP exception breakpoint / Syntax error](./images/DAP_exception_breakpoint_sample_stop.png)
87115

88116
This happens because `Caught Exceptions` is selected.
62.3 KB
Loading

src/main/java/com/redhat/devtools/lsp4ij/dap/DAPDebugProcess.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import com.intellij.xdebugger.evaluation.XDebuggerEditorsProvider;
3636
import com.intellij.xdebugger.frame.XStackFrame;
3737
import com.intellij.xdebugger.frame.XSuspendContext;
38+
import com.intellij.xdebugger.stepping.XSmartStepIntoHandler;
3839
import com.intellij.xdebugger.ui.XDebugTabLayouter;
3940
import com.redhat.devtools.lsp4ij.dap.breakpoints.DAPBreakpointHandlerBase;
4041
import com.redhat.devtools.lsp4ij.dap.breakpoints.DAPExceptionBreakpointsPanel;
@@ -47,6 +48,8 @@
4748
import com.redhat.devtools.lsp4ij.dap.disassembly.DAPAlternativeSourceHandler;
4849
import com.redhat.devtools.lsp4ij.dap.disassembly.DisassemblyFile;
4950
import com.redhat.devtools.lsp4ij.dap.disassembly.breakpoints.DisassemblyBreakpointHandlerBase;
51+
import com.redhat.devtools.lsp4ij.dap.stepping.DAPSmartStepIntoHandler;
52+
import com.redhat.devtools.lsp4ij.dap.stepping.DAPStepIntoVariant;
5053
import com.redhat.devtools.lsp4ij.dap.threads.ThreadsPanel;
5154
import com.redhat.devtools.lsp4ij.internal.CancellationSupport;
5255
import com.redhat.devtools.lsp4ij.internal.CompletableFutures;
@@ -86,6 +89,9 @@ public class DAPDebugProcess extends XDebugProcess implements Disposable {
8689
private final @NotNull DAPAlternativeSourceHandler alternativeSourceHandler;
8790
private final long sessionId;
8891

92+
// Smart Step Into
93+
private DAPSmartStepIntoHandler smartStepIntoHandler;
94+
8995
private @Nullable CompletableFuture<Void> connectToServerFuture;
9096
private Status status;
9197
private Supplier<TransportStreams> streamsSupplier;
@@ -317,6 +323,14 @@ public void startStepInto(@Nullable XSuspendContext context) {
317323
}
318324
}
319325

326+
@Override
327+
public @Nullable XSmartStepIntoHandler<DAPStepIntoVariant> getSmartStepIntoHandler() {
328+
if (smartStepIntoHandler == null) {
329+
smartStepIntoHandler = new DAPSmartStepIntoHandler(getSession());
330+
}
331+
return smartStepIntoHandler;
332+
}
333+
320334
private @Nullable SteppingGranularity getSteppingGranularity(@NotNull DAPClient client) {
321335
return client.canDisassemble() && getAlternativeSourceHandler()
322336
.getAlternativeSourceKindState().getValue() ? SteppingGranularity.INSTRUCTION : null;

src/main/java/com/redhat/devtools/lsp4ij/dap/client/DAPClient.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,15 +563,29 @@ public void stepOut(int threadId, SteppingGranularity granularity) {
563563
}
564564

565565
public void stepIn(int threadId, SteppingGranularity granularity) {
566+
stepIn(threadId, null, granularity);
567+
}
568+
569+
public void stepIn(int threadId, @Nullable Integer targetId, @Nullable SteppingGranularity granularity) {
566570
if (debugProtocolServer == null) {
567571
return;
568572
}
569573
StepInArguments args = new StepInArguments();
570574
args.setThreadId(threadId);
575+
args.setTargetId(targetId);
571576
args.setGranularity(granularity);
572577
debugProtocolServer.stepIn(args);
573578
}
574579

580+
public CompletableFuture<StepInTargetsResponse> stepInTargets(int frameId) {
581+
if (debugProtocolServer == null) {
582+
return CompletableFuture.completedFuture(null);
583+
}
584+
StepInTargetsArguments args = new StepInTargetsArguments();
585+
args.setFrameId(frameId);
586+
return debugProtocolServer.stepInTargets(args);
587+
}
588+
575589
public CompletableFuture<EvaluateResponse> evaluate(@NotNull String expression,
576590
@Nullable Integer frameId,
577591
@NotNull String context) {
@@ -680,6 +694,15 @@ public boolean isSupportsEvaluateForHovers() {
680694
return Boolean.TRUE.equals(getCapabilities().getSupportsEvaluateForHovers());
681695
}
682696

697+
/**
698+
* Returns true if the debug adapter supports the 'stepInTargets' request and false otherwise.
699+
*
700+
* @return true if the debug adapter supports the 'stepInTargets' request and false otherwise.
701+
*/
702+
public boolean isSupportsStepInTargetsRequest() {
703+
return Boolean.TRUE.equals(getCapabilities().getSupportsStepInTargetsRequest());
704+
}
705+
683706
public boolean canDisassemble() {
684707
return Boolean.TRUE.equals(getCapabilities().getSupportsDisassembleRequest());
685708
}
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2026 Red Hat, Inc.
3+
* Distributed under license by Red Hat, Inc. All rights reserved.
4+
* This program is made available under the terms of the
5+
* Eclipse Public License v2.0 which accompanies this distribution,
6+
* and is available at https://www.eclipse.org/legal/epl-v20.html
7+
*
8+
* Contributors:
9+
* Red Hat, Inc. - initial API and implementation
10+
******************************************************************************/
11+
package com.redhat.devtools.lsp4ij.dap.stepping;
12+
13+
import com.intellij.openapi.editor.Document;
14+
import com.intellij.openapi.progress.ProcessCanceledException;
15+
import com.intellij.xdebugger.XDebugSession;
16+
import com.intellij.xdebugger.XSourcePosition;
17+
import com.intellij.xdebugger.frame.XSuspendContext;
18+
import com.intellij.xdebugger.stepping.XSmartStepIntoHandler;
19+
import com.redhat.devtools.lsp4ij.LSPIJUtils;
20+
import com.redhat.devtools.lsp4ij.dap.client.DAPClient;
21+
import com.redhat.devtools.lsp4ij.dap.client.DAPStackFrame;
22+
import com.redhat.devtools.lsp4ij.dap.client.DAPSuspendContext;
23+
import com.redhat.devtools.lsp4ij.internal.CompletableFutures;
24+
import org.eclipse.lsp4j.debug.StepInTarget;
25+
import org.eclipse.lsp4j.debug.StepInTargetsResponse;
26+
import org.jetbrains.annotations.NotNull;
27+
import org.jetbrains.annotations.Nullable;
28+
import org.jetbrains.concurrency.AsyncPromise;
29+
import org.jetbrains.concurrency.Promise;
30+
import org.slf4j.Logger;
31+
import org.slf4j.LoggerFactory;
32+
33+
import java.util.ArrayList;
34+
import java.util.Collections;
35+
import java.util.List;
36+
import java.util.Map;
37+
import java.util.concurrent.CompletableFuture;
38+
import java.util.concurrent.ExecutionException;
39+
40+
/**
41+
* Handles "Smart Step Into" functionality for DAP debug sessions.
42+
* When multiple function calls exist on a single line, this handler allows the user
43+
* to choose which function to step into via the DAP stepInTargets request.
44+
*/
45+
public class DAPSmartStepIntoHandler extends XSmartStepIntoHandler<DAPStepIntoVariant> {
46+
47+
private static final Logger LOGGER = LoggerFactory.getLogger(DAPSmartStepIntoHandler.class);
48+
49+
private final @NotNull XDebugSession session;
50+
51+
/**
52+
* Creates a new Smart Step Into handler.
53+
*
54+
* @param session the debug session
55+
*/
56+
public DAPSmartStepIntoHandler(@NotNull XDebugSession session) {
57+
this.session = session;
58+
}
59+
60+
@Override
61+
public @NotNull List<DAPStepIntoVariant> computeSmartStepVariants(@NotNull XSourcePosition position) {
62+
CompletableFuture<List<DAPStepIntoVariant>> future = getStepInTargetsFuture(position);
63+
64+
try {
65+
CompletableFutures.waitUntilDone(future, (com.intellij.psi.PsiFile) null, 5000);
66+
} catch (ProcessCanceledException e) {
67+
throw e;
68+
} catch (ExecutionException e) {
69+
LOGGER.warn("Error getting smart step variants", e);
70+
return Collections.emptyList();
71+
} catch (java.util.concurrent.TimeoutException e) {
72+
return Collections.emptyList();
73+
}
74+
75+
List<DAPStepIntoVariant> result = future.getNow(null);
76+
return result != null ? result : Collections.emptyList();
77+
}
78+
79+
/**
80+
* Helper method to get the CompletableFuture for step-in targets.
81+
* Extracted to avoid code duplication between sync and async versions.
82+
*/
83+
private CompletableFuture<List<DAPStepIntoVariant>> getStepInTargetsFuture(@NotNull XSourcePosition position) {
84+
XSuspendContext suspendContext = session.getSuspendContext();
85+
if (!(suspendContext instanceof DAPSuspendContext dapContext)) {
86+
return CompletableFuture.completedFuture(Collections.emptyList());
87+
}
88+
89+
var activeStack = dapContext.getActiveExecutionStack();
90+
if (activeStack == null) {
91+
return CompletableFuture.completedFuture(Collections.emptyList());
92+
}
93+
94+
var topFrame = activeStack.getTopFrame();
95+
if (!(topFrame instanceof DAPStackFrame dapFrame)) {
96+
return CompletableFuture.completedFuture(Collections.emptyList());
97+
}
98+
99+
DAPClient client = dapFrame.getClient();
100+
101+
// Check capability
102+
if (!client.isSupportsStepInTargetsRequest()) {
103+
return CompletableFuture.completedFuture(Collections.emptyList());
104+
}
105+
106+
int frameId = dapFrame.getFrameId();
107+
CompletableFuture<StepInTargetsResponse> responseFuture = client.stepInTargets(frameId);
108+
109+
// Transform the response into variants
110+
Document document = LSPIJUtils.getDocument(position.getFile());
111+
int zeroBasedLine = position.getLine();
112+
113+
return responseFuture.handle((response, throwable) -> {
114+
if (throwable != null) {
115+
LOGGER.error("Error while fetching step-in targets from DAP server", throwable);
116+
return Collections.emptyList();
117+
}
118+
119+
if (response == null || response.getTargets() == null || response.getTargets().length == 0) {
120+
return Collections.emptyList();
121+
}
122+
123+
List<DAPStepIntoVariant> variants = new ArrayList<>();
124+
125+
// Track how many times we've seen each function name to handle duplicates
126+
Map<String, Integer> functionNameCounts = new java.util.HashMap<>();
127+
128+
for (StepInTarget target : response.getTargets()) {
129+
// Extract function name from label to count occurrences
130+
String label = target.getLabel();
131+
String functionName = label;
132+
int parenIndex = label.indexOf('(');
133+
if (parenIndex > 0) {
134+
functionName = label.substring(0, parenIndex);
135+
}
136+
137+
// Get the occurrence index for this function name
138+
int occurrenceIndex = functionNameCounts.getOrDefault(functionName, 0);
139+
functionNameCounts.put(functionName, occurrenceIndex + 1);
140+
141+
DAPStepIntoVariant variant = new DAPStepIntoVariant(target, document, zeroBasedLine, occurrenceIndex);
142+
variants.add(variant);
143+
}
144+
return variants;
145+
});
146+
}
147+
148+
@Override
149+
public @NotNull Promise<List<DAPStepIntoVariant>> computeSmartStepVariantsAsync(
150+
@NotNull XSourcePosition position) {
151+
152+
// Get the CompletableFuture and wrap it in an AsyncPromise
153+
CompletableFuture<List<DAPStepIntoVariant>> future = getStepInTargetsFuture(position);
154+
155+
AsyncPromise<List<DAPStepIntoVariant>> promise = new AsyncPromise<>();
156+
future.whenComplete((result, throwable) -> {
157+
if (throwable != null) {
158+
promise.setResult(Collections.emptyList());
159+
} else {
160+
promise.setResult(result != null ? result : Collections.emptyList());
161+
}
162+
});
163+
164+
return promise;
165+
}
166+
167+
@Override
168+
public void startStepInto(@NotNull DAPStepIntoVariant variant, @Nullable XSuspendContext context) {
169+
if (!(context instanceof DAPSuspendContext dapContext)) {
170+
return;
171+
}
172+
173+
Integer threadId = dapContext.getThreadId();
174+
if (threadId == null) {
175+
return;
176+
}
177+
178+
var activeStack = dapContext.getActiveExecutionStack();
179+
if (activeStack == null) {
180+
return;
181+
}
182+
183+
var topFrame = activeStack.getTopFrame();
184+
if (!(topFrame instanceof DAPStackFrame dapFrame)) {
185+
return;
186+
}
187+
188+
DAPClient client = dapFrame.getClient();
189+
190+
// Get stepping granularity (null for normal stepping, INSTRUCTION for disassembly)
191+
var granularity = getSteppingGranularity(client);
192+
193+
// Execute stepIn with targetId
194+
Integer targetId = variant.getTargetId();
195+
client.stepIn(threadId, targetId, granularity);
196+
}
197+
198+
@Override
199+
public void stepIntoEmpty(XDebugSession session) {
200+
// Fallback to regular step into when no variants available
201+
session.stepInto();
202+
}
203+
204+
@Override
205+
public @Nullable String getPopupTitle(@NotNull XSourcePosition position) {
206+
return "Choose Method to Step Into";
207+
}
208+
209+
/**
210+
* Gets the stepping granularity for the current session.
211+
* Returns INSTRUCTION granularity when disassembly mode is active, null otherwise.
212+
*
213+
* @param client the DAP client
214+
* @return the stepping granularity, or null for default (source line) granularity
215+
*/
216+
private @Nullable org.eclipse.lsp4j.debug.SteppingGranularity getSteppingGranularity(@NotNull DAPClient client) {
217+
// Note: For Smart Step Into, we typically use source-level granularity
218+
// Disassembly-level stepping with target selection is rarely needed
219+
// If needed in the future, this could access DAPDebugProcess.getAlternativeSourceHandler()
220+
return null;
221+
}
222+
}

0 commit comments

Comments
 (0)