Skip to content

Add support for R breakpoints#11407

Merged
lionel- merged 31 commits intomainfrom
feature/breakpoints
Jan 22, 2026
Merged

Add support for R breakpoints#11407
lionel- merged 31 commits intomainfrom
feature/breakpoints

Conversation

@lionel-
Copy link
Copy Markdown
Contributor

@lionel- lionel- commented Jan 16, 2026

Branched from #10815

Addresses #1766
Addresses #11402

Backend PR: posit-dev/ark#1003

This PR implements the frontend side of breakpoint support for R in Positron. The backend now injects browser() calls at parse time, and verifies breakpoints during evaluation. This PR adds the infrastructure needed to:

Maintain a permanent DAP session so breakpoints can be communicated to Ark

  • Properly distinguish between "foreground" debug sessions (user is actively debugging) and "background" debug sessions (DAP connected for breakpoint management). This is captured by a context key.
  • Use that new notion to determine:
    • Which kind of console history to use (regular history or debug history).
    • Whether to focus the debug pane when a debug session starts (background Ark session should not focus)
    • Whether to show the welcome view in the debug pane (background sessions should not cause the welcome view to disappear)

Notify Ark backend of active breakpoints in a timely manner

  • Because Ark does its own invalidation of breakpoints, we need the frontend to send breakpoints "on save" unconditionally (bypassing the internal check for invalidated breakpoints).
  • Send breakpoints to the debug adapter before Cmd+Enter execution in dirty documents (since sendBreakpointsOnAllSaves only triggers on save)

Permanent DAP session

Previously, the DAP session was only connected when a browser REPL was active. The R extension would send a start_debug notification to request Positron to connect with a DAP client. This is no longer the case: the DAP is now expected to always be connected so that the backend can receive notifications about breakpoint state at all time. This allows R to inject breakpoints in executed or sourced code, without requiring the user to enable a debug session first.

The start_debug and stop_debug messages sent via the Jupyter comm are now hints for showing/hiding the debug toolbar rather than session lifecycle events. When R enters the debugger, Ark sends start_debug; when it exits, it sends stop_debug. The DAP connection remains active throughout.

DapComm refactoring

DapComm now uses a static factory pattern and manages reconnections automatically:

  • Created via DapComm.create() which sets up the comm and configuration.
  • connect() and disconnect() control the debug session lifecycle.
  • Automatically reconnects when the debug session terminates (e.g., user disconnects via toolbar).
  • The start_debug message now calls setSuppressDebugToolbar(false) instead of starting a new session.
  • The stop_debug message suppresses the toolbar again. It is debounced by 100ms (via new Debounced utility) to prevent toolbar flickering when stepping through code.

Multi-session

In case of multiple console sessions, only the DAP of the foreground session is connected. If all DAP sessions remained connected, then the verification status of breakpoints could be very surprising. A breakpoint verified in a background session but not the foreground session with which the user is working would appear as verified in the UI, even though there is no way the foreground session can break there.

To manage this, the R extension now manages LSP and DAP together through unified activateServices() and deactivateServices() methods (previously activateLsp/deactivateLsp). Both services are activated/deactivated in parallel through a shared queue, ensuring proper ordering during session switches.

Foreground vs background debug sessions

With a permanent DAP connection, we need to distinguish between "foreground" debug sessions (user is actively debugging) and "background" sessions (DAP connected for breakpoint management only). This distinction affects several behaviors.

suppressDebugToolbar as an indicator of background debug sessions

The main indication that there is an active foreground debug session is that the debug toolbar is shown, i.e. the suppressDebugToolbar property of the session is not set.

In VS Code, it is only possible to start a session in background mode. I made Positron-only changes to allow extensions to flip the debug toolbar on and off. When R enters in a browser() state, a notification is sent to "unsuppress" the debug toolbar, i.e. bring the debug session to the foreground.

New extension API:

export function setSuppressDebugToolbar(session: DebugSession, suppress: boolean): void;

Allows extensions to toggle toolbar suppression on a running debug session. The R extension uses this to show the toolbar when entering the debugger and hide it when exiting.

Context key and helpers

  • CONTEXT_DEBUG_TOOLBAR_SUPPRESSED context key: true when all active debug sessions have suppressDebugToolbar set, which means the debug toolbar is not shown.
  • isForegroundDebugSession() helper: returns true when a debug session is active AND the toolbar is not suppressed.
  • getForegroundDebugState() helper: returns the debug state string only for foreground sessions, otherwise 'inactive'.

Key distinction:

  • User preferences like debug.toolBarLocation: "docked" do NOT affect the context key.
  • Extension suppression via suppressDebugToolbar DOES affect the context key.

Console history selection

The console maintains separate history navigators for debug vs normal sessions. Previously determined by CONTEXT_DEBUG_STATE, which didn't account for background sessions with suppressed debug toolbars.

Now uses new isForegroundDebugSession() helper for selecting the history navigator in consoleInput.tsx, and new getForegroundDebugState() for tagging history entries in languageInputHistory.ts and sessionInputHistory.ts. This ensures that only foreground debug sessions cause commands to be logged in the debug-only history.

Debug pane focus

When a debug session starts, VS Code normally focuses the debug pane. Background sessions (with suppressDebugToolbar: true) should not cause this focus change. The debugUx value computation now accounts for session suppression.

Welcome view visibility

The debug welcome view should appear when there's no "foreground" debug session. Previously it would hide whenever any debug session was active, including background ones. We now check whether all sessions are suppressed and show the welcome view in that case.

Timely breakpoint notifications

First of all, I regret having implementing this here, as this adds significant churn to this PR. This should have been implemented in another PR. Sorry for the extra cognitive load that implies for the review. Given the timeline, I would prefer to keep this here so I don't have to reconsider how verification works across frontend and backend.

The goal here is to avoid stepping through a stale view of an editor. To prevent this, VS Code has an internal mechanism of breakpoint invalidation. Basically if a breakpoint is on a line that was moved up or down, the breakpoint gets invalidated and a breakpoint updated is sent to the backend.

This invalidation is incomplete because if you change code below a breakpoint, it will not get invalidated even though the file and source have changed and might lead to stale stepping. Since The Ark DAP lives alongside an LSP, we can do better: Take document-change notifications as an indication that the file is now stale and breakpoints are invalid. This approach works well but required changes to ensure that we receive breakpoint notifications event when VS Code think there is no need.

Furthermore, it's confusing for users having to save files so that breakpoint verification may work. Since we now have code execution gestures as a way of injecting/verifying breakpoints, I found that the UX was poor because of the need to save.

Two new mechanisms solve these:

  1. The sendBreakpointsOnAllSaves debugger capability (described below) ensures Ark receives breakpoints on every file save, even when VS Code thinks breakpoints haven't been invalidated.

  2. When executing code via Cmd+Enter in a dirty document, breakpoints haven't been sent since the last save. We now send them right before execution:

     if (textFileService.isDirty(documentUri)) {
         await debugService.sendBreakpoints(documentUri, true);
    }

    VS Code is not equipped by default to verify breakpoints in dirty documents though, so to make it respond to our verification notifications some changes were needed.

Two new debugger contribution properties allow extensions to opt into these behaviors.

sendBreakpointsOnAllSaves

VS Code normally only sends SetBreakpoints DAP events on file save when breakpoint decorations have changed position. This optimization prevents unnecessary DAP traffic but doesn't work for Ark.

When sendBreakpointsOnAllSaves: true, the somethingChanged check in breakpointEditorContribution.ts is bypassed, ensuring breakpoints are sent on every save.

This required:

  • Adding shouldSendBreakpointsOnAllSaves(languageId) method to IAdapterManager, which checks if any debugger interested in the language has the capability enabled
  • Modifying breakpointEditorContribution.ts to call this method and bypass the early return when the capability is set

verifyBreakpointsInDirtyDocuments

VS Code marks breakpoints as unverified when a file has unsaved changes, assuming unsaved changes invalidate breakpoint locations. However, Ark handles source modifications internally and can verify breakpoints even in dirty documents because:

  • It receives SetBreakpoints with sourceModified: true on every save
  • It receives SetBreakpoints when code is executed in dirty documents
  • It tracks source state and updates breakpoint locations accordingly

When verifyBreakpointsInDirtyDocuments: true:

  • The Breakpoint.verified getter trusts the adapter's verification status even when the file is dirty
  • The breakpoint hover message defers to the adapter's message (or none) rather than showing the hardcoded "file is modified, please restart debug session" warning

To implement verifyBreakpointsInDirtyDocuments, the Breakpoint class needs to check whether the debugger supports this capability. This required changing VS Code files with the following fenced adjustments:

  • Adding IDebugService to Breakpoint, DebugModel, and DebugStorage constructors
  • Adding shouldVerifyBreakpointsInDirtyDocuments(uri) method to IAdapterManager, which checks if any debugger interested in the file's language has the capability enabled
  • Modifying the verified and message getters in Breakpoint to call this method

R extension changes

The positron-r extension's debugger contribution now includes:

{
    "type": "ark",
    "label": "R Debugger",
    "languages": ["r"],
    "supportsUiLaunch": false,
    "sendBreakpointsOnAllSaves": true,
    "verifyBreakpointsInDirtyDocuments": true
}

And enables breakpoints for R files:

"breakpoints": [
    { "language": "r" }
]

The debug welcome view message is also updated from "limited debugging support" to "first-class debugging support".

Release Notes

New Features

  • R gains support for breakpoints (ark: Debugger: Breakpoint support #1766). In scripts, breakpoints are enabled after evaluation or sourcing. In packages, you need to install the latest version of pkgload (e.g. with pak::pak("r-lib/pkgload") and call load_all() to activate breakpoints.

Bug Fixes

QA notes

See also notes in linked PR
posit-dev/ark#1003

The main additional things to watch out for have to do with the background debug session that is now permanently connected to the frontend (should not affect existing UX related to console history or debug pane in any way), and with the way we validate (verify), invalidate, and revalidate breakpoints.

  • Dirty documents: Editing a file should immediately invalidate all breakpoints (see also notes in linked PR). It should be possible to verify breakpoints by executing code in the dirty document with and without saving the file. Including new breakpoints that were added after editing the document.
  • Console history: Console commands emitted while a foreground debug session is ongoing should go to debug history. Those emitted while a background debug session is connected should go to normal history.
  • Ctrl+R, Cmd+Up are now sensitive to whether a (foreground) debug session is active.
  • Debug pane focus: Starting a new R console session should not focus the debug pane.
  • Welcome view of the debug pane: Should show when no foreground debug session are ongoing, even with background debug session connected.
  • Toolbar visibility: Should only appear when R is in browser REPL, i.e. an active foreground debug session. When the debug session is in the background, the debug toolbar should not be shown.
  • UI flickering The debug toolbar and debug pane should not flicker when stepping through code (100ms debounce on stop_debug).
  • Session switching: Breakpoint state preserved per session. Switching sessions back and forth should restore state.
  • Disconnect/reconnect: Disconnecting via toolbar (Shift-F5) should auto-reconnect the DAP session (i.e. breakpoints are managed and can be verified/hit).

@:all

@github-actions
Copy link
Copy Markdown

github-actions bot commented Jan 16, 2026

E2E Tests 🚀
This PR will run tests tagged with: @:all

readme  valid tags

@lionel- lionel- requested a review from DavisVaughan January 16, 2026 17:24
@lionel-
Copy link
Copy Markdown
Contributor Author

lionel- commented Jan 16, 2026

Full suite running at https://github.com/posit-dev/positron/actions/runs/21079125210

In previous runs I noticed some failures due to, it seems, Ark not being properly pulled from the linked PR. Logs showed that it was downloaded from the branch by positron-r, but the test behaviour indicated an older Ark, so not sure what's going on.

The following do pass checks locally:

Screenshot 2026-01-16 at 18 32 36

@lionel- lionel- force-pushed the feature/breakpoints branch from 948bff1 to b9ff940 Compare January 16, 2026 21:22
DavisVaughan
DavisVaughan previously approved these changes Jan 21, 2026
@@ -373,27 +373,25 @@ export type CommBackendMessage =
* Disposing the `DapComm` automatically disposes of the nested `Comm`.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What pulls up the welcome screen?

Image

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the welcome view but not the streamlined one. I suggested in #10769 (review) to look up the language of the active console when there is no opened editor, to avoid this.

Comment thread src/vs/workbench/contrib/positronConsole/browser/components/consoleInput.tsx Outdated
Comment thread src/vs/workbench/contrib/debug/test/common/mockDebug.ts
Comment thread extensions/positron-r/package.nls.json Outdated

this._comm = comm;
this._port = serverPort;
async connect() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should all the methods be static?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope the methods are stateful and use this

Comment thread extensions/positron-supervisor/src/DapComm.ts
Comment on lines +620 to +626
// Check if the debugger wants breakpoints sent on all saves
const languageId = model.getLanguageId();
const shouldSendAnyway = this.debugService.getAdapterManager()
.shouldSendBreakpointsOnAllSaves(languageId);

if (!somethingChanged && !shouldSendAnyway) {
// --- End Positron ---
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something smells funny to me about the fact that this method is onModelDecorationsChanged() and nowhere in the file is "saving" referenced except for this added code. Is shouldSendBreakpointsOnAllSaves() really the right name? Does it have something more general to do with "model decorations" instead? I don't know enough to say whether this is definitely right or wrong.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is the right name and right place. I've updated the comment, is this clearer?

		// Some debuggers need breakpoints re-sent on every file save, even if line
		// numbers haven't changed. If so, we proceed to call `updateBreakpoints()`
		// which queues this URI to send breakpoints on the next save.

Comment thread src/vs/workbench/contrib/debug/browser/debugToolBar.ts Outdated
Comment thread src/vs/workbench/contrib/debug/browser/debugToolBar.ts Outdated
@lionel- lionel- force-pushed the feature/execute-code-location branch from 7c8a166 to 01d7c39 Compare January 22, 2026 13:01
@lionel- lionel- force-pushed the feature/breakpoints branch from b9ff940 to 50a1f3c Compare January 22, 2026 13:01
Base automatically changed from feature/execute-code-location to main January 22, 2026 17:50
@lionel- lionel- dismissed DavisVaughan’s stale review January 22, 2026 17:50

The base branch was changed.

@lionel- lionel- force-pushed the feature/breakpoints branch from 50a1f3c to 6885cab Compare January 22, 2026 18:06
@lionel-
Copy link
Copy Markdown
Contributor Author

lionel- commented Jan 22, 2026

Full suite with released Ark running at https://github.com/posit-dev/positron/actions/runs/21260187796

@lionel- lionel- merged commit a24c7aa into main Jan 22, 2026
37 checks passed
@lionel- lionel- deleted the feature/breakpoints branch January 22, 2026 21:02
@github-actions github-actions bot locked and limited conversation to collaborators Jan 22, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants