Skip to content

Debugger better UI#411

Merged
KristofferC merged 18 commits into
masterfrom
Debugger_better_UI
Jul 20, 2026
Merged

Debugger better UI#411
KristofferC merged 18 commits into
masterfrom
Debugger_better_UI

Conversation

@KristofferC

Copy link
Copy Markdown
Member
  • Redesign the status display and variable printing
  • Add interactive menus for breakpoints, frames and the watch list
  • Add a top-level debug> REPL mode entered with )
  • Run the debugger full screen (sticky mode) by default
  • Cache the tree-sitter parser and highlight query
  • Preview the upcoming call when paused on an argument global
  • Scrub debugger internals from evaluation-mode backtraces
  • Precompile a scripted debugger session
  • Regenerate the UI fixtures and update the README
  • Add a changelog for the UI overhaul
  • Allow adding a breakpoint from the bp menu
  • Don't print the list again after closing a menu
  • Syntax highlight menu rows
  • Don't specialize repr_limited on the value type
  • Adopt the REPL stdlib's LimitIO implementation
  • Don't let one long variable name inflate the alignment column

KristofferC and others added 18 commits July 19, 2026 20:04
- Header shows stack position and location: `[level/depth] f(x, y) at file.jl:12`
  with the signature highlighted and the path dimmed
- The current source line is printed in bold; line-number gutter dimmed
- Local variables are shown as part of the status: aligned columns, arguments
  first in signature order with dimmed (arg)/(param)/(captured) tags, values
  truncated to the terminal width, capped (configurable) with a '… more' tail
- Variable types display compactly by default, eliding long type parameters
  to `Dict{…}`; the `T` key cycles compact/none/types-only/full (#396)
- Watch expressions are evaluated and rendered in the status on every step;
  watch errors render via `showerror` instead of dumping the exception struct
- The next expression is marked with a dimmed `→` instead of 'About to run:'
- `bt` is a compact one-line-per-frame backtrace with a marker on the active
  frame; `bt v` gives the old verbose listing including all variables
- `p x` shows the full REPL-style text/plain value instead of a byte-truncated one
- Keyword-body methods display as `f(x, y; z)` instead of `#f#5(z, , x, y)`
  (detects the nameless boundary slot, covering Core.kwcall lowering on 1.12)
- New central `Debugger.config(...)` for theme, highlighting, context lines,
  variable types, status variable cap and unicode/ascii charset

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUQRBLr8aoN2p1Q8BrLdzE
Bare `bp`, `f` and `w` now open REPL.TerminalMenus-based menus instead of
requiring index-addressed subcommands:

- `bp`: toggle breakpoints with space/enter, delete with `d`, open the
  breakpoint location in an editor with `o`; break-on-error and
  break-on-throw are toggleable rows in the same menu
- `f`: pick the active frame from the call stack
- `w`: delete watch expressions with `d`; the list is evaluated once per
  command since watch expressions may have side effects

All menus are driven by a generic `ActionMenu` (rows plus writerow/onkey/
onpick callbacks, cursor shared with `request` through a `Ref`). They are
only used on a real TTY (`Debugger.config(menus=false)` disables them);
everything else, including all existing subcommands, keeps the plain text
interface. The text breakpoint listing gains the same status characters as
the source-line gutter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUQRBLr8aoN2p1Q8BrLdzE
Loading Debugger in an interactive session installs a `debug>` REPL mode
(via atreplinit when loaded from startup.jl), entered by pressing `)` at
the beginning of an empty `julia>` prompt and left with backspace:

- any expression entered is debugged as if run through `@enter` (evaluated
  on the REPL backend; multiple statements are wrapped in a block since a
  `:toplevel` expression cannot be spliced into the thunk `@enter` builds)
- `bp` commands, including the interactive menu, work without an active
  debug session, so breakpoints can be managed up front; names resolve in
  the module the REPL is currently active in (`REPL.activate`)
- julia completions and a shared history (`:debug` mode tag)

`Debugger.install_repl_mode(repl; key)` installs it manually.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUQRBLr8aoN2p1Q8BrLdzE
The session runs on the terminal's alternate screen (like `less` or `vim`),
so entering the debugger does not pollute the scrollback and quitting
restores the terminal exactly as it was. The status is redrawn in place:
cursor home plus erase-per-line plus erase-below, never clear-screen-first,
which would render an empty frame in between and blink.

- `S` toggles it live (switching back to the plain scrolling transcript);
  `Debugger.config(sticky=false)` turns it off permanently
- only active on a real TTY: tests, dumb terminals and redirected output
  keep the scrolling behavior
- errors that abort the session are deferred and re-printed on the main
  screen, so they do not vanish with the alternate screen
- nested sessions do not take over (or prematurely close) the outer
  session's alternate screen

Also adds the headless test suite for the new UI: menus driven through a
TTYTerminal over scripted keystrokes, REPL-mode installation, debug-mode
parsing, config round-trips and the alternate-screen lifecycle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUQRBLr8aoN2p1Q8BrLdzE
`Highlights.highlight` creates a tree-sitter parser and, dominating
everything else, re-compiles the highlights query on every call (~29 ms;
the tokenization itself is ~0.01 ms). With the status display highlighting
every variable line, a single step rendered ~10 snippets and took ~230 ms —
very noticeable lag after each stepping command.

Cache the parser, compiled query and theme for the session; a full status
render is now ~1 ms with byte-identical output. The fast path uses
Highlights internals, so it falls back to the public API if they change.

Upstream issue: JuliaDocs/Highlights.jl#90

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUQRBLr8aoN2p1Q8BrLdzE
Julia 1.12 can pause on a global load immediately before a call. The
next-expression preview only recognized the case where that global was the
upcoming call's callee; when it was an argument — e.g. pausing on the `*`
load in `map(*, x, y)` — the status showed a bare `Base.Math.:*` with no
arguments. Match the paused global anywhere in the upcoming call, so the
preview renders `→ map(*, [1, 2], [3, 4])`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUQRBLr8aoN2p1Q8BrLdzE
An error thrown by code evaluated at the `|julia>` prompt printed twelve
frames of machinery (eval_code, LineEdit, run_interface, RunDebugger, ...)
below the actual error. `_eval_code` returned `Base.catch_stack()`, a
`Vector{Any}` that `Base.scrub_repl_backtrace` deliberately ignores — and
the REPL's own scrubbing only truncates at its `__repl_entry*` backend
frames, which this code path never passes through.

Return a proper `ExceptionStack` and truncate each backtrace at the
debugger's own boundary: everything from JuliaInterpreter's `eval_code`
frame (and the `Core.eval` frame it calls into) downward is removed. A
typo now prints like a plain REPL error:

    ERROR: UndefVarError: `absx` not defined in `Base.Math`
    Stacktrace:
     [1] top-level scope
       @ none:1

Frames above the boundary (including nested `eval` in the evaluated code)
are preserved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUQRBLr8aoN2p1Q8BrLdzE
First use of the debugger paid ~5 s of runtime compilation (RunDebugger
inference and its LineEdit closures, stepping commands, status printing,
tree-sitter parser/query/theme setup). Run a small scripted session during
package precompilation — a minimal UnixTerminal with a no-op raw! feeding
commands through RunDebugger — plus install_repl_mode/debug_mode_parse and
an explicit directive for the one-argument RunDebugger method that @Enter
calls.

Measured with --trace-compile-timing on a scripted session:
4976 ms in 176 statements -> 249 ms in 42 statements.

The highlight cache is reset at the end of the workload: it holds
tree-sitter pointers that must not be serialized into the package image.
The remaining ~250 ms at load time are invalidation-driven recompiles
(JuliaInterpreter.__init__, Base._atexit) that predate this change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUQRBLr8aoN2p1Q8BrLdzE
The fixtures reflect the redesigned status output. Each terminal test now
starts with an empty watch list: the watch list is a global that persists
between sessions, and with watches rendered in the status the tests were
leaking watch expressions (and their errors) into each other's snapshots.

The README documents the debug> REPL mode, the interactive menus, the new
keys and Debugger.config, and the examples use the new output format.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUQRBLr8aoN2p1Q8BrLdzE
Documents the new keys, the interactive menus, the debug> REPL mode, the
full-screen default, display and command changes, configuration, and the
performance fixes, so the release notes are in one place.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUQRBLr8aoN2p1Q8BrLdzE
Adding was the one breakpoint operation the menu could not do. Since a
breakpoint location is free-form text while the menu reads single keys,
`a` closes the menu, prompts `bp add>` for one line (parsed exactly like
the `bp add` command), and reopens the menu with the cursor on the new
breakpoint. An empty line cancels. This also works in the sessionless
`debug>` mode, where the menu is the main way to set up breakpoints.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUQRBLr8aoN2p1Q8BrLdzE
TerminalMenus leaves the menu's final frame on screen when it closes, so
re-printing the watch list / breakpoint listing after quitting the `w` or
`bp` menu showed everything twice. The menu itself is the record; the text
listing is only printed when the menus are unavailable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUQRBLr8aoN2p1Q8BrLdzE
The menus displayed plain text — a leftover from when they were a transient
overlay over the highlighted text listing. Now that the menu's final frame
is the record, highlight it the same way: watch expressions and results,
breakpoint descriptions, and frame signatures (with the location dimmed,
matching `bt` and the status header). Highlighting is applied after
truncation, so the zero-width escape codes do not affect the row width
accounting.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUQRBLr8aoN2p1Q8BrLdzE
repr_limited takes arbitrary user values and only forwards them to show,
but without @nospecialize it recompiled for every new value type appearing
in the variable display — dragging inference through Base's show machinery
specialized on the LimitIO wrapper each time (e.g. 119 ms for the first
Type-valued variable). With @nospecialize the single Any specialization is
compiled by the precompile workload and never recompiles at runtime.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUQRBLr8aoN2p1Q8BrLdzE
Improvements over the previous minimal version:

- `unsafe_write` override: bulk writes (long strings, arrays) are limited
  in one call instead of falling back to Base's byte-by-byte write loop,
  and content up to the limit is preserved before throwing — so the
  `<partial...>` display keeps maximal content
- `write` returns the number of bytes written instead of the running total
- `LimitIOException` carries the limit and has a showerror method
- `displaysize` forwards to the wrapped io
- `io` and `maxbytes` are const fields; only the byte count mutates

Truncation now cuts exactly at maxbytes (the per-byte path effectively
allowed one extra byte), hence the one-byte fixture change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUQRBLr8aoN2p1Q8BrLdzE
The locals display aligned the `=` column to the widest name::type entry,
so a single long variable name (or long type) padded every other variable
with whitespace. Align to the widest entry within reach of the median
width instead; wider outliers print unaligned:

  x::Int64     = 1  (arg)
  a_very_long_variable_name_here::Float64 = 2.5  (arg)
  short::Int64 = 1

When all entries are long (e.g. vartypes = :full), the median is large
too, so they still align with each other.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUQRBLr8aoN2p1Q8BrLdzE
test/menus.jl uses Logging (NullLogger) to silence the expected raw-mode
warnings when driving TerminalMenus headlessly; it only worked undeclared
because the test sandbox keeps @stdlib on the load path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUQRBLr8aoN2p1Q8BrLdzE
The alternate screen has no scrollback, so output like the `?` help or a
deep `bt` scrolled past the top with no way to read it. Route command
output (help, bt, fr, p, the text watch/breakpoint listings) through
`print_or_page`: when in full-screen mode and the output is taller than
the screen, it opens in a scrollable `TerminalMenus.Pager` (arrow keys,
PgUp/PgDn, `q` to close) instead of being printed. Everything still
prints normally in the scrolling transcript mode.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUQRBLr8aoN2p1Q8BrLdzE
@KristofferC
KristofferC merged commit 3399455 into master Jul 20, 2026
12 checks passed
@KristofferC
KristofferC deleted the Debugger_better_UI branch July 20, 2026 06:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant