Debugger better UI#411
Merged
Merged
Conversation
- 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
)