Running and debugging

.graphoos/run.toml in the workspace describes what to run. Run and Debug from the palette (palette.run_and_debug) lists its entries; pick one to run it in the terminal panel, or to debug it when it names a debugger.

[[configs]]
name = "check"
# The debugger type: leave it out to only run the program.
type = "lldb"
program = "${workspace}/target/debug/check"
args = ["--verbose"]
cwd = "${workspace}"

[configs.env]
RUST_LOG = "debug"

# Runs before every launch.
[configs.prelaunch]
program = "cargo"
args = ["build"]

Opening run.toml from Run and Debug creates it with a commented template when the workspace has none. ${workspace} expands to the workspace root.

Breakpoints are put on and taken off with F9 on the cursor's line (or Toggle Breakpoint in the palette, since macOS keeps F9 for itself unless the function keys are set to behave as such), and by clicking the margin left of the line numbers.

Conditional breakpoints and logpoints

A breakpoint set in the gutter can be given more than a line, from the palette, acting on the one on the cursor's line:

  • Debug: Set the Breakpoint's Condition — an expression the debugger evaluates; it only stops when it is true (x > 3).
  • Debug: Set the Breakpoint's Hit Count — 5, >= 3, % 2: stop only on those hits.
  • Debug: Turn the Breakpoint into a Logpoint — a message printed instead of stopping, with {expression} interpolated by the adapter.

Each opens with what the breakpoint has now, so it can be edited, and an empty answer clears it (a logpoint becomes a plain breakpoint again). The change reaches a running session at once. What the debugger does with each one is the adapter's business: codelldb, debugpy and Node support all three.

The debug console

The Debug Console section of the Debug panel shows what the program writes when its adapter sends it there (Python's "console": "internalConsole", an attach), its errors in red, and what the adapter itself says, dimmed; each session starts with its name. It keeps the last 5,000 lines and follows the newest.

It is also the REPL: Debug: Evaluate an Expression in the palette — or the last row of the console, › Evaluate an expression… — sends what you type to the adapter in the frame the session is stopped in; the expression and its answer, with its type (42 (int)), stay in the console one under the other. With nothing stopped it says so instead of failing quietly.

Values where the program stopped

At a stop, the values of the frame's variables are shown at the end of the lines they appear on, from a little above the stop down to it (total = 6, n = 3). They go away when the program continues, and as soon as the file is edited.

Exceptions

The adapter says which exceptions it can stop on — debugpy: Raised, Uncaught, User Uncaught; js-debug: Caught and Uncaught — and they are listed at the top of Breakpoints with a checkbox each, set as the adapter likes them by default. A change applies to the running session at once and is kept for the next session of the same debugger type. A configuration can choose them itself:

exception-filters = ["raised", "uncaught"]

Attaching to a running program

request = "attach" joins a program that is already running instead of starting one. What the adapter needs to find it goes in [configs.adapter], as the adapter names it:

[[configs]]
name = "attach to the server"
type = "debugpy"
request = "attach"

[configs.adapter]
connect = { host = "127.0.0.1", port = 5678 }

[configs.adapter] works for a launch as well: its keys go to the adapter as they are, over what GraphoOS fills in (justMyCode, stopOnEntry, console…). A session with no terminal of its own — an attach, or output sent to the debug console — is listed in Processes and driven like the others.

launch.json

A project's .vscode/launch.json is read as it is: its configurations appear in Run and Debug next to those of run.toml, and a compound starts the configurations it names together. The VS Code types become the debugger GraphoOS runs (python → debugpy, node → pwa-node, cppdbg/codelldb → lldb), ${workspaceFolder} is ${workspace}, and every key GraphoOS has no field for goes to the adapter.

Watching expressions

Debug: Watch an Expression adds it to the Watch section of the Debug panel, where it is evaluated again at every stop, with its type; when it cannot be evaluated in that frame the reason is shown in red instead of a value. The × on a row stops watching it, and Debug: Stop Watching Everything clears the list. With nothing stopped the values stay as they were, saying so, rather than going blank.

Changing a variable

Double-clicking a variable in the Variables section asks for its new value, with the current one ready to be typed over, and sends it to the adapter (setVariable). The row then shows the value as the debugger has it, which is not always the text that was typed, and the watched expressions are evaluated again in case they depended on it. An adapter that refuses the change says why.

Build tasks and problems

Run and Debug in the palette lists, after the configurations of .graphoos/run.toml, the tasks the workspace's files suggest: cargo build / test / run / check / clippy from a Cargo.toml, every script of a package.json (npm run …, or pnpm / yarn when their lock file is there), the targets of a Makefile, gradle build / test / assemble (./gradlew when present), go build / test / vet and pytest. Each runs in a terminal like any run configuration.

Run Build Task (⌘⇧B / Ctrl+Shift+B) and Run Test Task (in the palette) run the first task whose name says build or test — a run.toml configuration named that way wins over the detected ones.

When a run task ends, its output goes through the problem matchers (rustc, tsc, gcc/clang, go, javac, Python tracebacks, eslint) and every error and warning appears in the Problems panel under its file, with the line to click; a message says how many there were. The next run replaces them; the language servers' own diagnostics are left alone.

Debugging

GraphoOS speaks the Debug Adapter Protocol. The Debug panel (left bar) shows the call stack, variables and the process; the toolbar continues, steps over, into and out of, restarts and stops; breakpoints are set from the gutter (click), and the run configuration's type chooses the adapter.

The adapter for a type is registered by a plugin (register_debugger_type in the plugin API): the plugin names the program that implements it. The Rust and C/C++ plugins register lldb: they look for codelldb, then lldb-dap (LLVM's adapter — brew install llvm, Xcode's command line tools, or the lldb package of your distribution), or the program named in their debuggerPath setting. The Python plugin registers debugpy, run as python -m debugpy.adapter by the interpreter of its pythonPath setting or the python3 on the PATH, and only when that interpreter really has debugpy (pip install debugpy). The plugin's log line says when none was found.

[[configs]]
name = "python: the tests"
type = "debugpy"
program = "${workspace}/run_tests.py"
cwd = "${workspace}"

On macOS the first debug session asks for Developer Tools Access (a password or Touch ID prompt from the system): it lets the adapter take control of the process, and is remembered afterwards.

Debugging on a remote host works the same: the adapter runs next to the proxy.

Source: docs/debugging.md