aboutsummaryrefslogtreecommitdiffstats
path: root/tests (follow)
Commit message (Collapse)AuthorAgeFilesLines
* listing: F5/Tab decompiles the function under the cursor (IDA-style) — step 2blasty2026-07-231-0/+18
| | | | | | | | | | | In the continuous listing, F5 (or Tab) decompiles the defined function the cursor is inside; F5/Tab again returns to the linear listing exactly where you left. Not-in-a-function -> a message; decompile failure falls back to the listing (not the bounded disasm). Listing position is snapshotted so the round-trip is lossless. Verified: continuous_view F5 round-trip + view_toggle/disasm_nav/follow_xrefs/ region_define/listing_view 45/0.
* listing: render opcode bytes (shared engine with disasm) — M4/UX step 1blasty2026-07-231-1/+11
| | | | | | | | | | | | | | | The flat listing was a subset of the disasm view (no opcodes). Now the listing carries an opcode-bytes column with the same format + o toggle: Head gains a raw field, ListingModel fills it for code heads via one bulk read per page (bounded, variable-length safe) + tracks the widest opcode; ListingView renders/pads the column, settling width as pages stream in. render_line/_line_plain share the addr+opcodes+label+mnem/text layout with DisasmView. Test harness all_funcs() reloads the index if a prior bump_items() cleared it (was crashing biggest() with max([])). Verified: code heads carry raw (max_raw_len 11); listing suite + continuous_view opcode-parity 26/0.
* app: 'L' opens the continuous segment listing (functions+data interleaved)blasty2026-07-231-0/+27
| | | | | | | | | | | | Function views are bounded to one function; 'L' from any code view opens the whole-segment ListingView at the cursor instead -- one long flat listing where functions/data/undefined interleave and you scroll straight from one function into the next. Reuses the region listing path (_goto_continuous). New continuous_view scenario: open a function, press L, assert continuous listing opens, cursor at the origin function, listing spans past the function bounds, code interleaves with data. 22/0 with disasm_nav/view_toggle/ region_define (L is additive).
* listing: expand struct-typed globals into member rows (M4 item 3)blasty2026-07-231-0/+48
| | | | | | | | | | | A struct-typed data item rendered as one dense 'MyS <...>' line. Now the heads walker emits indented member rows after the summary (from get_udt_details): +off name type, each a head of kind member at ea+offset. ListingView renders them indented/dimmed; _line_plain matches for cursor/search. Verified: a struct global expands to +0 a int / +4 b char[4] / +8 c __int16, member rows carry field addresses. New listing_struct_expand scenario; listing suite 22/0 (in-process).
* app: 'a' — make string literal in the listing (M4 richness)blasty2026-07-231-0/+49
| | | | | | | | | | | | IDA's 'A' key. New make_string server tool (ida_bytes.create_strlit, auto-length to the terminator, undefines items in the way first, returns size + decoded contents). Program.make_string wraps it; 'a' on a listing head routes through the existing edit plumbing (EditItemRequested kind=string -> _do_edit_item -> make_string -> bump_items -> reopen). Verified: make_string at a .rodata addr creates the literal and IDA auto-names it (aUsage for 'usage'). New listing_make_string scenario drives it through the UI; listing suite 15/0. Needs a supervisor restart.
* app: listing 'n' names the address (can name a bare/undefined byte)blasty2026-07-231-0/+63
| | | | | | | | | | | | | | | | Repro: shrink a u16 data field to u8 in the listing; the freed byte at addr+1 becomes undefined, but you couldn't name it. 'n' posted RenameRequested with word_under_cursor(), and _do_rename treats that as an existing symbol to rename -- a 'db ?' line has no symbol, so it failed. In the ListingView, 'n' now names the ADDRESS under the cursor: opens the name prompt prefilled with the head's current name (empty for an unnamed byte) and applies it via rename {data:{addr,new}} (the server's address form of the data rename, which set_name's the ea even when nothing is defined there), then bump_items + reopen so the label shows. Code-view symbol rename is unchanged. Server already supported it; new listing_name_addr scenario reproduces the full workflow. Full suite 123 pass / 1 pre-existing flaky (filter).
* app: on startup, land on main() (else pop the symbol picker)blasty2026-07-231-3/+36
| | | | | | | | | | | | | | | Instead of staring at an empty pane after the function list loads, jump somewhere useful: once the index is complete and nothing's open yet, open main() (tries main/_main/wmain/WinMain/wWinMain in order); if there's no entry function, pop the fuzzy symbol picker so you can pick one. Fires exactly once (guarded by _cur + _did_auto_land) and never steals focus from a user who already navigated during load. Tests: new auto_land scenario covers both branches (jump-to-main when present, picker fallback otherwise) + the idempotency guard. Boot/startup now key "load complete" off _func_index.complete instead of the status string (auto-land legitimately overwrites the "N functions" status with the landed fn). Full suite 120 pass / 1 pre-existing flaky (filter).
* app: 'd' — define typed data in the listing (make_data, M3)blasty2026-07-221-0/+41
| | | | | | | | | | | | | | Press 'd' on a listing head to define typed data at that address via a prompt (the ".data type definitions" backlog item). Mirrors the retype prompt flow: MakeDataRequested -> #makedata Input (prefilled with a size-appropriate default type: unsigned __int8/16/32/64 or char[N]) -> _do_make_data worker -> Program.make_data -> bump_items -> reopen the listing in place. Accepts any C type IDA's SetType understands: int, char[16], my_struct, T *arr[4], ... Pilot: listing_view gains a deterministic sub-test — undefine a data head to synthesize an unknown run, assert it renders as `unknown`, then 'd' char[4] over it and assert it becomes a `data` head. 118 pass / 1 pre-existing flaky (filter); rpc_smoke 29/0.
* app: ListingView — virtualized flat code+data listing for regions (M2)blasty2026-07-221-15/+81
| | | | | | | | | | | | | | | | | | | | | | | | | | | The real disassembly-listing view. ListingView is a line-virtualized ScrollView (reusing ColumnCursor/SearchMixin/NavMixin) backed by ListingModel, rendering code, data (db/dw/dd, strings, jump tables) and undefined bytes interleaved with per-kind styling. Non-function regions now open in it instead of the M0 DisasmModel stopgap. Integration (IdaTui): a new `listing` active mode. _open_entry routes region entries to ListingView; _show_active/compose add it; follow/xrefs/comment and the c/p/u edit verbs handle it (define_func upgrades a region straight to the function view); backslash reaches hex and returns via _code_mode (so leaving hex from a region lands back on the listing, not a func view); Tab explains there's no pseudocode for a region. rpc._active_widget learns `listing`. Server fix (the important one): the `heads` walker now steps by get_item_end instead of next_head. next_head SKIPS undefined bytes, so after undefining a function its start address wasn't even a listing line and the cursor snapped to the next defined item; stepping by item-end renders undefined bytes as `db ?` lines (IDA-accurate) and makes navigation land exactly on any address — essential for "go to unmarked bytes and hit c". Needs a supervisor restart. Tests: region_define rewritten to assert the ListingView path + segment-wide listing + p-upgrade; new listing_view scenario (data-segment listing renders data heads, cursor reports head ea, backslash->hex->back). Pilot 115 pass / 1 pre-existing flaky (filter); rpc_smoke 29/0; test_domain 27/0.
* server+domain: heads walker + ListingModel — flat code/data listing (M1)blasty2026-07-221-0/+36
| | | | | | | | | | | | | | | | | | | | | | | | | | | The keystone for a real disassembly-listing view (unlike DisasmModel, which is one function, code-only). Two pieces: server/patch_server.py: inject a `heads` tool. It walks item heads over a segment with next_head/prev_head and renders each via generate_disasm_line, so it returns a flat listing where code, data (db/dw/dd, strings, jump tables) and undefined bytes all appear as typed rows {ea,kind,size,text,name}. Unlike `disasm` (code-only, bails at the first data byte) it shows the whole segment. Address-paged: chain forward via cursor.next, page up with back=true (returns the N heads ending before addr, in forward order, + cursor.prev). domain.py: ListingModel — a lazily-grown, segment-scoped head index (FunctionIndex-style forward paging via cursor.next; line index == position in the walked list). ensure_ea() gives random access to an address (resolving a mid-item byte to its containing head). New Head dataclass, Program.listing() (cached per segment) + segment_bounds(); section_of() now derives from it; bump_items() clears the listing cache too. Verified live: heads renders strings/jump-tables/unknown correctly, forward chaining + back-paging work; ListingModel walks echo .text (5036 heads, ~276ms) with cached windows and mid-item address resolution. tests/test_domain gains a [listing] section (27 passed). Pilot hex/region_define/disasm_nav green. Note: adding a server tool needs a supervisor restart (workers respawn).
* app: non-function regions — open a flat listing + c/p/u edit verbs (M0)blasty2026-07-221-0/+50
| | | | | | | | | | | | | | | | | | | | | | | | | Navigating to an address not inside a function no longer refuses ("no function contains X"): _do_navigate now opens a region view (flat listing anchored at the EA, forced to disasm since there's no pseudocode). The server's no-function disasm already walks heads to the segment end, so DisasmModel is reused as-is; NavEntry gains is_region. Adds the IDA c/p/u structure-edit verbs on the disasm view: c = define_code (undefine-first, since create_insn won't carve a live item) p = define_func u = undefine wired via EditItemRequested -> _do_edit_item worker -> Program.define_code/ define_func/undefine. define_func upgrades the region to a real function view in place. New Program.bump_items() invalidates the item/function structure caches (disasm blocks, decomp, function indices + name gen) — broader than bump_names(), which only covers renames. Pilot: new `region_define` scenario undefines a small function, navigates to the bare region via the 'g' prompt (asserts it opens, not refused), then 'p' recreates the function and upgrades the view; restores the IDB in a finally. 107 passed / 1 pre-existing flaky (filter, fails on baseline).
* app: multi-line comments via literal \\n (long notes were clipping)blasty2026-07-101-4/+12
| | | | | | | | | The comment prompt is single-line, so a long note rendered as one giant '//' line that ran off the right edge and got clipped. Interpret a literal '\\n' (backslash-n) in the comment text as a real newline in _do_comment (the single choke point for the ';' prompt and the RPC 'comment' verb) — Hex-Rays renders each as its own '//' line. Verified live and locked in the comment_func scenario (multi-line render); suite 104 green.
* drive: ergonomic RE helper over the RPC socket (terse text, auto socket)blasty2026-07-101-0/+10
| | | | | | | | | | | | | | | | Driving via rpcclient meant re-exporting the socket every fresh shell, long invocations, and piping ~every call through python -c to pull one field or grep pseudocode. idatui.drive fixes that: it auto-resolves the socket (the single live pane from the registry), prints compact text instead of JSON, and bundles the common gestures: where | go | pc <fn> [substr] | dis <fn> [n] | callees/callers <fn> | names <substr> | rename <old> <new> | mv old=new... | note <fn> <text> | screen | raw <method> k=v So 'goto+rename+parse' becomes 'drive rename old new', and 'pseudocode | python -c grep' becomes 'drive pc fn needle'. rpcclient stays the raw transport. rpc_smoke drives it end-to-end (29 green).
* app: comment on a no-address line -> function comment (was refused)blasty2026-07-101-0/+32
| | | | | | | | | | Pressing ';' on the decompiler's signature or local-declaration lines did nothing ('no address on this line to comment') because those lines carry no /*0xEA*/ marker. Fall back to the current function's entry ea so commenting the header annotates the function itself; the prompt says 'function comment @ ...'. Body-line comments are unchanged. New scenario comment_func locks it (suite 104 green).
* app: drop the Header (keep the Footer); rpc: function-scope xrefs_fromblasty2026-07-101-0/+14
| | | | | | | | | | | | | | The clock/title Header added noise (and made screen() non-deterministic); remove it from the app itself — the Footer stays. Layout shifts up a row; scenario suite still 101 green. xrefs_from on a function was near-useless: the server's xrefs_from is address-scoped, so passing the entry ea only returned the fall-through from the first instruction. For a function target it now returns whole-body references from the decompiler (callees + string/data refs: {to,name,string,is_func,type}); an explicit 0xADDR stays address-scoped. Verified live: xrefs_from main now lists setlocale/getopt_long/sub_* etc. rpc_smoke: 25 green.
* pane: auto-start the ida-pro-mcp supervisor if it's downblasty2026-07-101-0/+87
| | | | | | | | | | | | Before creating the TUI pane, spawn probes 127.0.0.1:8745 and, if nothing is listening, launches ./spawn.sh in its own detached tmux pane and waits for the port (server_started/server_pane are reported in the JSON). Only for a local server, never a duplicate (spawn.sh binds the port), and it's not killed on 'stop' (shared across TUIs). --no-ensure-server opts out; IDATUI_SERVER_CMD overrides the launch command (used by the test). tests/pane_smoke.py exercises the machinery with a dummy port-binder in a pane (start / detect / idempotent / remote-guard), 5 green.
* rpc: cursor_on + word= edits, single-driver gate, colored screenblasty2026-07-101-5/+26
| | | | | | | | | | | | | | cursor_on {word,line?,occurrence?} places the cursor on a token, verified with the app's own tokenizer (so 'main' won't match inside 'domain'); rename/retype/follow gain an optional word= that runs it first — the ergonomic way to edit a named symbol without a manual view+cursor dance. Single-driver gate: the socket now serves one client at a time; a second concurrent connection is refused with 'busy' (no multi-driver support yet, by request). Sequential connections are unaffected. screen gains format=text|html|svg (html/svg are colored — for an out-of-band web viewer). rpc_smoke: 24 green.
* rpc: readiness, quit, methods discovery, not-ready guardblasty2026-07-101-2/+23
| | | | | | | | | | | Spawn-and-drive needs to know when the freshly-launched TUI is usable: ping/state now carry {ready,functions,complete} (cheap; no health call) and ping adds the module name. Add a graceful 'quit' verb (acks, then exits on a short delay so the reply still flushes) and a self-documenting 'methods' table. Program-dependent verbs are refused with a clear 'not ready' instead of exploding in a worker while still loading. rpc_smoke covers readiness, methods and quit (21 green).
* rpc: more verbs — structured reads, modal select, in-view search, saveblasty2026-07-101-0/+56
| | | | | | | | | | | | Introspection (offloaded to a thread so a big fetch can't freeze the render): pseudocode (full body), disassembly (bounded), xrefs_to/from (structured), resolve (name->ea). These let an agent reason about a function and the call graph without scraping screen(). Semantic: select (choose the highlighted/nth item in the open xrefs or symbol-palette list and activate it — the natural follow-up to xrefs/ symbols), search (/ or ? incremental in the active code view), save (Ctrl+S). rpc_smoke exercises all of them end-to-end (18 green).
* rpc: semantic verbs (typed-with-delay ops + fast movement)blasty2026-07-101-2/+43
| | | | | | | | | | | | | | Layer high-level verbs over the raw injection: goto/open, rename, comment, retype, follow, back, toggle_view, hex, xrefs, symbols, structs, close, plus fast move/cursor. Editing/goto verbs type through the real prompts with a per-char delay (default 35ms) so viewers see it typed; movement uses bare keypresses with a pump-only settle to stay snappy. Each verb settles on an op-specific predicate where one exists (goto lands on the target, follow grows the nav stack, toggle flips _active, modals open), so the returned state is never stale. rpc_smoke now drives the semantic path end-to-end (rename round-trip via the prompt-fill seam), 11 green.
* rpc: unix-socket puppeteering server (raw keys + introspection + screen)blasty2026-07-101-0/+121
| | | | | | | | | | | | | | | Run the TUI with --rpc <sock> and it renders normally while an asyncio listener on the same loop lets another process drive it. Handlers touch the UI directly (same loop, no thread hop), so every injected key has the identical on-screen effect a keyboard would — the point for livestreaming. Newline-delimited JSON. v1 methods: keys/text (raw injection, via the same _press_keys the pilot uses; text can interleave wait:<ms> for a typed-out look), state/view/screen/functions (introspection; screen() is a full plain-text render of exactly what the viewer sees). No auth by design — the socket is 0600, local only. tests/rpc_smoke.py drives it end-to-end over a real socket (7 green).
* sync: extract shared settle/wait helpers (idatui/_sync.py)blasty2026-07-101-7/+2
| | | | | | | | | | | Factor the pilot suite's ad-hoc 'poll until the UI reacted' loop into one reusable module so the upcoming RPC driver and the tests share a single source of truth for quiescence. wait_for() takes the yield strategy (pilot.pause vs asyncio.sleep); drain()/workers_idle()/settle() give the live driver a 'block until quiescent, then until pred holds' primitive built on Textual's own _wait_for_screen + the worker manager. Ctx.wait now delegates to wait_for; suite unchanged (101 green).
* tests: retire the monolithic test_tui.py (superseded by test_scenarios)blasty2026-07-101-1236/+0
| | | | | | The scenario suite now covers every behavior the monolith did (all check names accounted for, modulo renames), so drop the old single-session runner.
* tests: isolated scenario suite (parity with the monolith, fast --only)blasty2026-07-101-0/+1272
| | | | | | | | | | | | | | | | | | | | Replace the 1200-line monolithic run() — one giant pilot session whose 100 checks shared mutable app state, so a single break cascaded and every edit meant re-running the whole ~49s suite — with independent @scenario functions over one shared boot. Each scenario re-establishes its own function context and is wrapped so a crash (or a check failure) fails that scenario alone instead of aborting the rest. A reset() baselines between scenarios: dismiss modals, hide the names pane and input widgets, clear the filter, default _pref=decomp. A Ctx helper wraps app+pilot (check/wait/press/open/goto_ui, biggest(), pick_decomp_ref(), ...). CLI: --only <substr,...> runs a subset in a few seconds (structs in ~1.5s vs 49s), --stop-after, --list. Per-scenario timing is printed, which is what surfaced a decomp-default view-flip that had been passing a jump+back check trivially. Full run: 101 checks green in ~41s.
* hex view: raw image bytes (VA-addressed), synced to the code cursorblasty2026-07-101-2/+28
| | | | | | | | | | | | | | | | New HexView: a line-virtualized 16-bytes/row dump of the whole loaded image, addressed by virtual address. Format-agnostic — the range/segments come from IDA (image_range over sections()), not any file header; gaps read back as zeros. Backslash toggles it, synced to the address under the disasm/pseudocode cursor (see the naked bytes of some code/data). In hex: hjkl/paging navigate a byte cursor (status shows the section), Enter jumps the code view to the byte, Tab/Esc/backslash return to the preferred code view. A third _active state; block -cached + prefetched like the disasm model. domain: HexModel + Program.hex_model/read_bytes/image_range (get_bytes regions). Fixed a self-deadlock (hex_model held _lock while sections() re-locked). Pilot: sync, actual bytes, byte-step, return. full suite 98/98.
* retype: set variable/function types with 'y' (IDA-style), via structured toolsblasty2026-07-101-3/+37
| | | | | | | | | | | | | | | | Instead of parsing pseudocode text, add structured server tools (server/ patch_server.py, alongside del_type): - func_types(addr): prototype + local variables (name/type/is_arg) - set_lvar_type(addr,var,type): retype a decompiler local, working on auto/ register vars too (stock set_type only updates already-user-modified lvars) 'y' in a code view retypes what's under the cursor: a local variable (prompt prefilled with its current type) or a function (prompt prefilled with its full prototype). Applies via set_lvar_type / set_type, then recompiles + refreshes. Copy-line moved off 'y' to Ctrl+Y. domain: Program.func_types / set_function_type / set_lvar_type (LVar/FuncTypes). Pilot: function-prototype retype (prefill + apply). full suite 94/94.
* struct editor: auto-format the definition on saveblasty2026-07-101-0/+5
| | | | | | | | After a successful declare, re-fetch the canonical struct_source and put it back in the editor, so the normalized layout/types show immediately instead of only after re-selecting the struct from the list (e.g. a pasted one-liner becomes multi-line, 'unsigned long' -> 'unsigned __int64'). _loaded_src is updated to the formatted text so it isn't considered dirty. Pilot check. full suite 92/92.
* struct editor: loud save errors + guard unsaved editsblasty2026-07-101-0/+22
| | | | | | | | | | | | | | | A struct with a field IDA's C parser rejects (notably '__unused', a predefined macro) failed to re-declare, but the failure was silent: the edit looked applied, then reloading showed the old definition (lost work). Now: - a failed declare shows a loud, styled 'save failed' status and, when it can, names the offending reserved field ('__unused') instead of IDA's empty 'Failed to parse'; the type is left unchanged and the editor keeps your text. - switching structs / starting new / closing with unsaved edits prompts to discard first (ConfirmScreen), so edits aren't silently reloaded away. Pilot: rejected save is loud/named/non-destructive; switching structs guards unsaved edits. full suite 91/91.
* clipboard: keyboard copy (OSC 52 + tmux) since mouse-select is capturedblasty2026-07-101-0/+17
| | | | | | | | | | | | | The app captures the mouse (SGR tracking), so terminal/tmux drag-select can't grab text. Add explicit copy actions: - code views: 'y' copies the current line - struct editor: Ctrl+Y copies the selection, else the whole C definition App._copy() emits OSC 52 (Textual copy_to_clipboard) AND, inside tmux, pipes through 'tmux load-buffer -w -' — the combination is what actually reaches the system clipboard over tmux/ssh. Status shows the char count. full suite 87/87.
* struct editor: discoverable, confirmed delete (d/Del + ConfirmScreen)blasty2026-07-101-5/+13
| | | | | | | | | | Delete was wired to the Del key but undiscoverable (modal has no footer) and unguarded. Add a 'd' alias (Del still works), and route delete through a small ConfirmScreen (Enter/y confirm, Esc/n cancel) since it's destructive. 'd' in the editor pane types normally; only the list triggers delete. Hint updated to 'd/Del delete'. Pilot: 'd' opens the confirm, Esc cancels (kept), Enter deletes. full suite 85/85.
* struct editor: C-style CRUD for local types (Ctrl+T)blasty2026-07-101-2/+61
| | | | | | | | | | | | | | | | | New StructEditor overlay: left is the list of local structs/unions, right is an editable C definition. Enter loads a struct (reconstructed as C from its member layout via type_inspect), Ctrl+S declares it (create or update via declare_type), Ctrl+N starts a new one, Del deletes, Esc returns to the list then closes. Domain: Struct model + Program.list_structs / struct_source / declare_type / delete_type (anonymous $-types filtered). Delete needs a 'del_type' tool the ida-pro-mcp server doesn't currently expose; delete_type detects its absence and reports a clear message instead of failing. Create/read/update work fully. Pilot checks: open, list, view C, create, update-in-place, delete(-or-report), close. full suite 84/84.
* decompiler: fall back to disassembly when decompilation failsblasty2026-07-091-4/+31
| | | | | | | | | | | Opening a function that Hex-Rays can't decompile (import/PLT stubs, mis-analyzed monsters) now shows the disassembly instead of a '/* decompilation failed */' panel. A separate _pref (the preferred view, set by Tab) is kept, so only the failing function falls back — the next decompilable function still opens as pseudocode. _apply_decomp ignores a stale result if you've navigated away. Pilot checks: failing func -> disasm (pref kept), next func -> pseudocode. full suite 77/77.
* default to the pseudocode view instead of disassemblyblasty2026-07-091-8/+26
| | | | | | | | | | Opening a function now shows the decompiler by default (Tab still toggles to disassembly; the choice persists across opens). Startup focuses the decompiler view, and Ctrl+B / back focus the active view rather than hard-coding disasm. Tests: assert pseudocode-by-default on the first open, then normalize per-section (the disasm-scrolling/search/follow blocks switch to disassembly explicitly). full suite 75/75.
* names: command-palette fuzzy finder (Ctrl+N), overlay-firstblasty2026-07-091-1/+40
| | | | | | | | | | | | | | Replace the docked names pane as the primary way to jump to symbols with a command-palette overlay: Ctrl+N opens SymbolPalette, type to fuzzy-find (scored subsequence match with matched-char highlighting), up/down to select, Enter to open, Esc to close. Mouse-clickable too. The docked FunctionsPanel now starts hidden and is still reachable/toggleable with Ctrl+B (classic table view, sort, filter, goto all unchanged); a code view takes focus on startup. Status hints 'Ctrl+N: find symbol'. Pilot checks for open/fuzzy/subsequence/select/close. Tests reveal the docked pane (Ctrl+B) for the existing table-driven checks. full suite 74/74.
* xrefs: pre-select the site the dialog was invoked fromblasty2026-07-091-0/+45
| | | | | | | | | | | | | | Opening the xref dialog with 'x' now highlights the entry for the current site (the instruction/pseudocode line under the cursor), so a long xref list can be stepped through systematically without losing your place. Capture the cursor's ea span when 'x' is pressed (disasm: the instruction .. the next; decomp: the line's /*0xEA*/ anchor .. the next), and pre-select the xref whose frm matches exactly, else the one within that span. Threaded through _xrefs_{disasm,decomp} -> _xrefs_present -> XrefsScreen(preselect), which sets OptionList.highlighted (also scrolls it into view). Pilot check for the decomp path. full suite 68/68.
* xrefs: informative labels — function+offset, and section instead of '?'blasty2026-07-091-0/+43
| | | | | | | | | | | | | | | The xref list showed just the containing function name, so multiple sites in the same function were indistinguishable ('sub_2300' repeated), and references not in any function collapsed to a bare '?'. - In-function sites now show fn_name+offset (e.g. main+0xb08), so each site is distinct and you can see where in the function it is. - Non-function sites (GOT/reloc data slots, loose thunks) show the section they live in (.got, .data.rel.ro, .text, LOAD, ...) instead of '?'. Add Program.sections()/section_of() over survey_binary's segment map (fetched once, cached lazily). Pilot checks for both. full suite 67/67.
* decompiler: xref jump lands the cursor on the reference token's columnblasty2026-07-091-0/+13
| | | | | | | | | | | | | | | Previously an xref jump only positioned the pseudocode line; the cursor sat at column 0. Thread the referenced symbol's name (the token xrefs was invoked on, or the callee's name) from the xref dialog through _on_xref_chosen -> _do_navigate, and place the column at that token's whole-word start on the target line (_decomp_col_for, computed on the marker-stripped line so it matches the display). Works for both cross-function (via _apply_decomp cursor_x) and same-function (via DecompView.goto) jumps. Add a column assertion to the xref pilot test (asserts token-start when the token is on the line, line-start otherwise -- robust to a stale server Hex-Rays cache). full suite 65/65.
* decompiler: xref jump within the current function moves the cursorblasty2026-07-091-0/+19
| | | | | | | | | | | | | | | The earlier xref->pseudocode-line fix only worked when the jump target was in a DIFFERENT function (which reloads the pane). When the target was inside the function already displayed, _show_active skipped the reload -> and never applied the new dec_cursor, so the cursor stayed put (the reported 'jumping to xref in decompile view doesn't work'; most xrefs land in the shown function). _open_entry now detects the already-loaded case and repositions the decompiler via a new DecompView.goto (moves cursor/scroll on the loaded text, no re-highlight). Toggle (Tab) still goes through _show_active untouched. Add a same-function xref-jump pilot check. full suite 64/64.
* decompiler: hide the per-line /*0xEA*/ address markers (clutter)blasty2026-07-091-4/+7
| | | | | | | | | | | | | | | Hex-Rays appends a /*0xEA*/ VMA marker to every pseudocode line (we fetch with include_addresses). It's the only per-line address anchor the app has, so we can't turn it off server-side without breaking follow/comments/xref-jumps. Instead, strip it at display time: DecompView.show pulls each line's marker into a parallel _line_eas list and highlights the cleaned text. _line_ea now reads that list; _follow_decomp takes the line ea explicitly (was re-parsing the marker). Stripping only edits within lines, so indices still align with the domain's raw code (used by _decomp_line_for). Var-location comments (// [rsp+..]) are Hex-Rays output, not VMAs -> kept. full suite 63/63.
* comments: ';' sets a comment on the current line (disasm + pseudocode)blasty2026-07-091-0/+29
| | | | | | | | | | | | | Bind ';' (NavMixin, so both code views) to a comment prompt for the line under the cursor. Resolve the line's address (_cursor_ea in disasm, the /*0xEA*/ marker in pseudocode), prefill any existing comment (parsed from the rendered '//' text), and apply via the set_comments tool; an empty value clears it. After setting, reuse the name-generation invalidation (bump_names) so Hex-Rays is force_recompiled lazily and the pseudocode/disasm refresh in place at the saved position -- the new '// comment' shows on the line. Marks the IDB dirty (Ctrl+S). Pilot checks for the pseudocode round-trip. full suite 63/63.
* rename: refuse pseudocode goto-labels with a clear messageblasty2026-07-091-0/+37
| | | | | | | | | | | | | | Pressing 'n' on a Hex-Rays goto label (LABEL_n, or any 'goto TARGET') in the decompiler routed to a 'local' rename, which the API rejects with a misleading "Local variable 'LABEL_114' not found" — the user just saw a cryptic failure. The rename tool has no label category (only func/global/local/stack) and idalib exposes no other way to rename pseudocode labels. Detect a label token under the cursor (_is_pseudocode_label) and report the limitation up front instead of opening a prompt that's doomed to fail. Add a pilot check (uses main's labels when the current function has none). full suite 61/61.
* test: --stop-after for fast single-feature iteration; skip needless decompileblasty2026-07-091-1/+22
| | | | | | | | | | | - test_tui.py: add --stop-after <substr>, ending the run once a matching check has been evaluated so you can iterate on one feature without paying for the expensive tail (e.g. 'search finds' -> 5.6s vs 34.6s full). Default unchanged. - app: skip _decomp_line_for on a plain function-entry jump (line 0 in both views), avoiding a needless decompile on every by-name navigation in the decompiler pane. full suite 60/60.
* decompiler: xref-select jumps to the reference line in pseudocodeblasty2026-07-091-64/+95
| | | | | | | | | | | | | | Selecting an xref (or any address navigation) only positioned the disasm cursor (NavEntry.cursor via index_of_ea); the pseudocode pane's dec_cursor stayed 0, so in the decompiler view the jump landed at the top of the target function instead of on the referencing line. When the decompiler pane is active, resolve the target ea to its pseudocode line via the per-line /*0xEA*/ markers (_decomp_line_for) and seed NavEntry.dec_cursor so _apply_decomp lands + scrolls there. Add a pilot check for it. Also halve the literal pilot.pause() settle delays in the suite (wait_until poll step untouched): 40s -> 34.6s wall, stable across 3 runs.
* test: fix stale row/function reuse across filter + xref-select blocksblasty2026-07-091-0/+13
| | | | | | | | | | | | | | Two latent ordering bugs, exposed once the suite actually opened the big function: - the 'sub_1*' filter round-trip left the list filtered, so reusing biggest_i (an unfiltered row index) clamped move_cursor to a tiny stub function (4-line decompile, no calls), cascading into ~7 skipped/failed checks. Clear the filter before reusing biggest_i. - the xref-select sub-test navigates 'dis' to another function; the following column-cursor block assumed the original function was still loaded at call_idx, so h/l ran on an uncached (empty) line and clamped x to 0. Reload the original function and materialize the call line first. pilot suite 59/59.
* fix: xref-select navigated to the wrong place (ordinary-flow contamination)blasty2026-07-091-0/+35
| | | | | | | | | | | xrefs data doesn't distinguish a call/jump from ordinary flow (both type=code), so _xrefs_disasm's 'first code from-xref' subject picked the fall-through to the NEXT instruction on a plain line. Its only xref is the flow edge back, so selecting it jumped to the current function at line 0 instead of a real caller. Drop the from-xref subject entirely: use the symbol under the cursor if any, else the current address (xrefs-to a function entry == its callers). Now selecting an xref lands on the exact referencing function AND line. pilot suite 59/59.
* fix: decomp follow fails on a just-renamed symbol (stale name)blasty2026-07-091-0/+19
| | | | | | | | | | | | Right after a rename there's a window where the pseudocode cursor word is still the OLD name while the symbol table already has the new one, so name-based decomp follow (refs / resolve / _ref_on_line) all miss -> 'nothing to follow'. Disasm never hit this because it falls back to an address xref. Add the same address-based fallback to _follow_decomp: parse the line's /*0xEA*/ marker and follow a code xref from that statement — immune to a stale name. Verified: with the old token 'sub_53D20' (renamed away) it still follows to the right address. pilot suite 58/58.
* cap the function pane width so it doesn't dominate wide terminalsblasty2026-07-091-0/+3
| | | | | | | Was 42% (≈100 cols on a 240-wide term). Now 30% clamped to min 42 / max 44, and columns tightened to 10/21/7 -> a stable ~41-43 cols that fits the content: 46% on a 90-col term but only 18% at 240 (code area 194 vs ~140 before). Ctrl+B still hides it. pilot suite 57/57.
* decompiler: line-number gutterblasty2026-07-091-0/+7
| | | | | | | Fixed left gutter with right-aligned line numbers (dim; brighter on the cursor line); the code scrolls horizontally past the gutter while the numbers stay put. Column cursor, search-column, and mouse mapping account for the gutter offset (_col_offset); virtual width includes it. pilot suite 56/56.
* decompiler: grayed 'decompiling…' overlay during (re)decompileblasty2026-07-091-0/+19
| | | | | | | | | | | | When a decompile runs (tab to pseudocode, or an implicit refresh after rename), DecompView.loading covers the pane with a centered, grayed '― decompiling… ―' (custom get_loading_widget + .decomp-loading CSS), cleared when the body arrives. The loading cover blurs the widget, which made Tab fall back to focus-nav instead of toggling; made the app Tab/Shift+Tab binding priority so it toggles regardless of focus, and restore focus to the pane when the decompile finishes. pilot suite 55/55 (adds overlay-shown / overlay-cleared).
* fix stale top frame on jump-back; test the actual repaintblasty2026-07-091-0/+13
| | | | | | | | | | | | | The pane could keep showing the top (scroll 0) after jumping back even though scroll_offset was correct: the deferred call_after_refresh set the scroll AFTER the paint and didn't force a repaint. Now the deferred callback re-applies the scroll AND refreshes, so the pane is repainted at the restored offset. Crucially, the prior test only checked scroll_offset.y (which was correct even when the bug was visible). Added a render-level assertion: trace render_line and assert the last repaint happened at the restored scroll. (Also: my earlier scratch repros used a non-existent function name so 'back' was a no-op -- the real suite test uses a valid second function.) pilot suite 53/53.