Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/dap/DAPSupport.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Current state of [Requests](https://microsoft.github.io/debug-adapter-protocol//
* ✅ [StackTrace](https://microsoft.github.io/debug-adapter-protocol//specification.html#Requests_StackTrace).
* ❌ [StepBack](https://microsoft.github.io/debug-adapter-protocol//specification.html#Requests_StepBack).
* ✅ [StepIn](https://microsoft.github.io/debug-adapter-protocol//specification.html#Requests_StepIn).
* [StepInTargets](https://microsoft.github.io/debug-adapter-protocol//specification.html#Requests_StepInTargets).
* [StepInTargets](https://microsoft.github.io/debug-adapter-protocol//specification.html#Requests_StepInTargets).
* ✅ [StepOut](https://microsoft.github.io/debug-adapter-protocol//specification.html#Requests_StepOut).
* ✅ [Terminate](https://microsoft.github.io/debug-adapter-protocol//specification.html#Requests_Terminate).
* ❌ [TerminateThreads](https://microsoft.github.io/debug-adapter-protocol//specification.html#Requests_TerminateThreads).
Expand Down
28 changes: 28 additions & 0 deletions docs/dap/UserGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,34 @@ Take a sample JavaScript file containing an error:
In this example, no breakpoints are defined.
However, when you start the DAP server, it stops at the line with the line error:

### Smart Step Into

[Smart Step Into](https://www.jetbrains.com/help/idea/stepping-through-the-program.html#smart-step-into) is available
if the DAP server supports [StepInTargets](https://microsoft.github.io/debug-adapter-protocol//specification.html#Requests_StepInTargets).

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).
A popup will appear showing all available functions you can step into.
Use the arrow keys to navigate between functions, and press **Enter** to step into the selected one.
You can also click directly on a function name in the editor.

For example, on this line:
```python
result = multiply(add(x, y), subtract(x, y))
```

Smart Step Into will let you choose between `multiply`, `add`, and `subtract`.

The feature also handles duplicate function names on the same line correctly. For instance:
```python
result = add(1, 2) + add(3, 4)
```

Each `add` call will be highlighted separately, allowing you to step into the specific occurrence you want.

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):

![DAP Smart Step Into](./images/DAP_SmartStepIntoDemo.gif)

![DAP exception breakpoint / Syntax error](./images/DAP_exception_breakpoint_sample_stop.png)

This happens because `Caught Exceptions` is selected.
Expand Down
Binary file added docs/dap/images/DAP_SmartStepIntoDemo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions src/main/java/com/redhat/devtools/lsp4ij/dap/DAPDebugProcess.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import com.intellij.xdebugger.evaluation.XDebuggerEditorsProvider;
import com.intellij.xdebugger.frame.XStackFrame;
import com.intellij.xdebugger.frame.XSuspendContext;
import com.intellij.xdebugger.stepping.XSmartStepIntoHandler;
import com.intellij.xdebugger.ui.XDebugTabLayouter;
import com.redhat.devtools.lsp4ij.dap.breakpoints.DAPBreakpointHandlerBase;
import com.redhat.devtools.lsp4ij.dap.breakpoints.DAPExceptionBreakpointsPanel;
Expand All @@ -47,6 +48,8 @@
import com.redhat.devtools.lsp4ij.dap.disassembly.DAPAlternativeSourceHandler;
import com.redhat.devtools.lsp4ij.dap.disassembly.DisassemblyFile;
import com.redhat.devtools.lsp4ij.dap.disassembly.breakpoints.DisassemblyBreakpointHandlerBase;
import com.redhat.devtools.lsp4ij.dap.stepping.DAPSmartStepIntoHandler;
import com.redhat.devtools.lsp4ij.dap.stepping.DAPStepIntoVariant;
import com.redhat.devtools.lsp4ij.dap.threads.ThreadsPanel;
import com.redhat.devtools.lsp4ij.internal.CancellationSupport;
import com.redhat.devtools.lsp4ij.internal.CompletableFutures;
Expand Down Expand Up @@ -86,6 +89,9 @@ public class DAPDebugProcess extends XDebugProcess implements Disposable {
private final @NotNull DAPAlternativeSourceHandler alternativeSourceHandler;
private final long sessionId;

// Smart Step Into
private DAPSmartStepIntoHandler smartStepIntoHandler;

private @Nullable CompletableFuture<Void> connectToServerFuture;
private Status status;
private Supplier<TransportStreams> streamsSupplier;
Expand Down Expand Up @@ -317,6 +323,14 @@ public void startStepInto(@Nullable XSuspendContext context) {
}
}

@Override
public @Nullable XSmartStepIntoHandler<DAPStepIntoVariant> getSmartStepIntoHandler() {
if (smartStepIntoHandler == null) {
smartStepIntoHandler = new DAPSmartStepIntoHandler(getSession());
}
return smartStepIntoHandler;
}

private @Nullable SteppingGranularity getSteppingGranularity(@NotNull DAPClient client) {
return client.canDisassemble() && getAlternativeSourceHandler()
.getAlternativeSourceKindState().getValue() ? SteppingGranularity.INSTRUCTION : null;
Expand Down
23 changes: 23 additions & 0 deletions src/main/java/com/redhat/devtools/lsp4ij/dap/client/DAPClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -563,15 +563,29 @@ public void stepOut(int threadId, SteppingGranularity granularity) {
}

public void stepIn(int threadId, SteppingGranularity granularity) {
stepIn(threadId, null, granularity);
}

public void stepIn(int threadId, @Nullable Integer targetId, @Nullable SteppingGranularity granularity) {
if (debugProtocolServer == null) {
return;
}
StepInArguments args = new StepInArguments();
args.setThreadId(threadId);
args.setTargetId(targetId);
args.setGranularity(granularity);
debugProtocolServer.stepIn(args);
}

public CompletableFuture<StepInTargetsResponse> stepInTargets(int frameId) {
if (debugProtocolServer == null) {
return CompletableFuture.completedFuture(null);
}
StepInTargetsArguments args = new StepInTargetsArguments();
args.setFrameId(frameId);
return debugProtocolServer.stepInTargets(args);
}

public CompletableFuture<EvaluateResponse> evaluate(@NotNull String expression,
@Nullable Integer frameId,
@NotNull String context) {
Expand Down Expand Up @@ -680,6 +694,15 @@ public boolean isSupportsEvaluateForHovers() {
return Boolean.TRUE.equals(getCapabilities().getSupportsEvaluateForHovers());
}

/**
* Returns true if the debug adapter supports the 'stepInTargets' request and false otherwise.
*
* @return true if the debug adapter supports the 'stepInTargets' request and false otherwise.
*/
public boolean isSupportsStepInTargetsRequest() {
return Boolean.TRUE.equals(getCapabilities().getSupportsStepInTargetsRequest());
}

public boolean canDisassemble() {
return Boolean.TRUE.equals(getCapabilities().getSupportsDisassembleRequest());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
/*******************************************************************************
* Copyright (c) 2026 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at https://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package com.redhat.devtools.lsp4ij.dap.stepping;

import com.intellij.openapi.editor.Document;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.xdebugger.XDebugSession;
import com.intellij.xdebugger.XSourcePosition;
import com.intellij.xdebugger.frame.XSuspendContext;
import com.intellij.xdebugger.stepping.XSmartStepIntoHandler;
import com.redhat.devtools.lsp4ij.LSPIJUtils;
import com.redhat.devtools.lsp4ij.dap.client.DAPClient;
import com.redhat.devtools.lsp4ij.dap.client.DAPStackFrame;
import com.redhat.devtools.lsp4ij.dap.client.DAPSuspendContext;
import com.redhat.devtools.lsp4ij.internal.CompletableFutures;
import org.eclipse.lsp4j.debug.StepInTarget;
import org.eclipse.lsp4j.debug.StepInTargetsResponse;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.concurrency.AsyncPromise;
import org.jetbrains.concurrency.Promise;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

/**
* Handles "Smart Step Into" functionality for DAP debug sessions.
* When multiple function calls exist on a single line, this handler allows the user
* to choose which function to step into via the DAP stepInTargets request.
*/
public class DAPSmartStepIntoHandler extends XSmartStepIntoHandler<DAPStepIntoVariant> {

private static final Logger LOGGER = LoggerFactory.getLogger(DAPSmartStepIntoHandler.class);

private final @NotNull XDebugSession session;

/**
* Creates a new Smart Step Into handler.
*
* @param session the debug session
*/
public DAPSmartStepIntoHandler(@NotNull XDebugSession session) {
this.session = session;
}

@Override
public @NotNull List<DAPStepIntoVariant> computeSmartStepVariants(@NotNull XSourcePosition position) {
CompletableFuture<List<DAPStepIntoVariant>> future = getStepInTargetsFuture(position);

try {
CompletableFutures.waitUntilDone(future, (com.intellij.psi.PsiFile) null, 5000);
} catch (ProcessCanceledException e) {
throw e;
} catch (ExecutionException e) {
LOGGER.warn("Error getting smart step variants", e);
return Collections.emptyList();
} catch (java.util.concurrent.TimeoutException e) {
return Collections.emptyList();
}

List<DAPStepIntoVariant> result = future.getNow(null);
return result != null ? result : Collections.emptyList();
}

/**
* Helper method to get the CompletableFuture for step-in targets.
* Extracted to avoid code duplication between sync and async versions.
*/
private CompletableFuture<List<DAPStepIntoVariant>> getStepInTargetsFuture(@NotNull XSourcePosition position) {
XSuspendContext suspendContext = session.getSuspendContext();
if (!(suspendContext instanceof DAPSuspendContext dapContext)) {
return CompletableFuture.completedFuture(Collections.emptyList());
}

var activeStack = dapContext.getActiveExecutionStack();
if (activeStack == null) {
return CompletableFuture.completedFuture(Collections.emptyList());
}

var topFrame = activeStack.getTopFrame();
if (!(topFrame instanceof DAPStackFrame dapFrame)) {
return CompletableFuture.completedFuture(Collections.emptyList());
}

DAPClient client = dapFrame.getClient();

// Check capability
if (!client.isSupportsStepInTargetsRequest()) {
return CompletableFuture.completedFuture(Collections.emptyList());
}

int frameId = dapFrame.getFrameId();
CompletableFuture<StepInTargetsResponse> responseFuture = client.stepInTargets(frameId);

// Transform the response into variants
Document document = LSPIJUtils.getDocument(position.getFile());
int zeroBasedLine = position.getLine();

return responseFuture.handle((response, throwable) -> {
if (throwable != null) {
LOGGER.error("Error while fetching step-in targets from DAP server", throwable);
return Collections.emptyList();
}

if (response == null || response.getTargets() == null || response.getTargets().length == 0) {
return Collections.emptyList();
}

List<DAPStepIntoVariant> variants = new ArrayList<>();

// Track how many times we've seen each function name to handle duplicates
Map<String, Integer> functionNameCounts = new java.util.HashMap<>();

for (StepInTarget target : response.getTargets()) {
// Extract function name from label to count occurrences
String label = target.getLabel();
String functionName = label;
int parenIndex = label.indexOf('(');
if (parenIndex > 0) {
functionName = label.substring(0, parenIndex);
}

// Get the occurrence index for this function name
int occurrenceIndex = functionNameCounts.getOrDefault(functionName, 0);
functionNameCounts.put(functionName, occurrenceIndex + 1);

DAPStepIntoVariant variant = new DAPStepIntoVariant(target, document, zeroBasedLine, occurrenceIndex);
variants.add(variant);
}
return variants;
});
}

@Override
public @NotNull Promise<List<DAPStepIntoVariant>> computeSmartStepVariantsAsync(
@NotNull XSourcePosition position) {

// Get the CompletableFuture and wrap it in an AsyncPromise
CompletableFuture<List<DAPStepIntoVariant>> future = getStepInTargetsFuture(position);

AsyncPromise<List<DAPStepIntoVariant>> promise = new AsyncPromise<>();
future.whenComplete((result, throwable) -> {
if (throwable != null) {
promise.setResult(Collections.emptyList());
} else {
promise.setResult(result != null ? result : Collections.emptyList());
}
});

return promise;
}

@Override
public void startStepInto(@NotNull DAPStepIntoVariant variant, @Nullable XSuspendContext context) {
if (!(context instanceof DAPSuspendContext dapContext)) {
return;
}

Integer threadId = dapContext.getThreadId();
if (threadId == null) {
return;
}

var activeStack = dapContext.getActiveExecutionStack();
if (activeStack == null) {
return;
}

var topFrame = activeStack.getTopFrame();
if (!(topFrame instanceof DAPStackFrame dapFrame)) {
return;
}

DAPClient client = dapFrame.getClient();

// Get stepping granularity (null for normal stepping, INSTRUCTION for disassembly)
var granularity = getSteppingGranularity(client);

// Execute stepIn with targetId
Integer targetId = variant.getTargetId();
client.stepIn(threadId, targetId, granularity);
}

@Override
public void stepIntoEmpty(XDebugSession session) {
// Fallback to regular step into when no variants available
session.stepInto();
}

@Override
public @Nullable String getPopupTitle(@NotNull XSourcePosition position) {
return "Choose Method to Step Into";
}

/**
* Gets the stepping granularity for the current session.
* Returns INSTRUCTION granularity when disassembly mode is active, null otherwise.
*
* @param client the DAP client
* @return the stepping granularity, or null for default (source line) granularity
*/
private @Nullable org.eclipse.lsp4j.debug.SteppingGranularity getSteppingGranularity(@NotNull DAPClient client) {
// Note: For Smart Step Into, we typically use source-level granularity
// Disassembly-level stepping with target selection is rarely needed
// If needed in the future, this could access DAPDebugProcess.getAlternativeSourceHandler()
return null;
}
}
Loading
Loading