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