1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
|
# Split view — Ghidra-style synced listing ⇄ pseudocode
Goal: show the unified **listing** (left) and the **pseudocode** (right) side by
side, kept in **cursor sync**. Move the cursor in the C view and the covering
instructions light up (and scroll into view) on the asm side; move in the asm and
the owning C line lights up. Both panes track the same function and navigate
together.
## Why this is tractable (existing substrate)
- **Layout is free.** `ListingView`, `DecompView`, `HexView` are already siblings
in the one `#panes` `Horizontal`. Today `_show_active()` just flips `display` so
a single code view shows at a time. Split = show listing **and** decomp together
(each `width: 1fr`).
- **Per-line address already exists.** `DecompView._line_eas[line]` holds each C
line's `/*0xEA*/` marker (Hex-Rays' primary ea for that line). Crude sync needs
no new tooling.
- **Reverse jump already exists.** `_decomp_line_for(fn, ea)` maps an instruction
address → the best pseudocode line (largest marker ≤ ea).
- **Sync hook points exist.** `on_decomp_view_cursor_moved` /
`on_listing_view_cursor_moved` fire on every cursor move.
- **Highlight overlays are a known pattern.** `render_line` already overlays
styles (word-under-cursor, search matches); a "linked region" background is one
more overlay.
## The one hard part: line ↔ instruction mapping
Ghidra highlights **all** instructions a C line owns. We have one ea per line
(the marker), not the set. Getting the set is the only real work, and it's a
known technique:
ida-pro-mcp derives the per-line marker via
`cfunc.get_line_item(line, col=0, …).get_ea()`. To get the **full set**, sweep
every column of the line (`get_line_item(line, x, …).get_ea()` for `x` in
`0..len`) and collect distinct non-`BADADDR` EAs. Same proven API, swept across
the line. A custom `decomp_map(ea)` tool in `server/patch_server.py` returns
`[{line, primary_ea, eas:[…]}, …]`; invert for `ea → line`.
## State model
- `self._split: bool` — split mode on/off.
- While split: both panes displayed; `_active` names the **focused** pane
(`"listing"` or `"decomp"`); the other is the companion.
- `_show_active()` gains a split branch: display listing + decomp, hide hex.
- Entering split loads both panes for the current function; exiting collapses to
the focused pane.
## Sync model (Phase 2/3)
- **decomp cursor moves** → look up the C line's ea(s) → highlight those rows in
the listing + scroll the primary ea into view.
- **listing cursor moves** → `_decomp_line_for` (or the inverted rich map) → move
the decomp cursor to the owning line + highlight it.
- A guard flag (`_syncing`) prevents the two `cursor_moved` handlers from
ping-ponging each other.
- Highlight: a subtle background style (`_S_LINK`) overlaid on the linked rows;
the *focused* line keeps the normal block cursor.
## Rendering the highlight
`ListingView.render_line`: if a row's ea ∈ `self._link_eas` (a set the app sets on
sync), overlay `_S_LINK` on that row (below the block cursor / word overlays).
`DecompView`: mark `self._link_line` and tint that row.
## Phased roadmap
**Phase 1 — split layout, no sync.** `_split` mode; show both panes at `1fr` with
a divider; focus one; a toggle (key + palette command "Toggle split view").
Entering split loads listing + decomp for the current function; exiting returns to
the focused pane. `Tab` in split switches the focused pane (single-view `Tab`
still toggles disasm/pseudocode). No new tools. *← this doc's current target.*
**Phase 2 — crude sync (MVP). DONE.** Both `cursor_moved` handlers wired via
`_sync_split`: the focused pane drives; the companion gets a subtle `_S_LINK`
band + scroll-into-view (its cursor never moves, so there's no echo — no guard
needed). listing→decomp uses `DecompView.line_for_ea` (largest marker ≤ ea);
decomp→listing uses `ListingModel.ensure_ea`. Tab re-links from the new driver.
Still single-ea per line (one instruction highlighted); the region comes in
phase 3.
**Phase 3 — rich highlight. DONE.** `decomp_map` custom tool
(`server/patch_server.py`) sweeps `cfunc.get_line_item` across every column of
each pseudocode line and collects the EAs from each item's `dstr()` (`'EA: desc'`
— the same source as the `/*ea*/` marker, so it aligns). `Program.decomp_map(ea)`
returns the per-line ea lists (cached by name-gen); the app loads it async into
`_split_eamap` / `_split_ea2line` and `_sync_split` bands the **whole** instruction
region of a C line (and uses the exact ea→line inverse for the reverse). Falls
back to the single marker until the map lands. Verified on the pilot's real worker
(alignment + multi-instruction region band).
**Phase 4 — polish. DONE.**
- Split-aware status: `_split_status()` shows `name @ ea [split · pseudocode line
N ↔ K insn]` / `[split · listing]` instead of the single-view labels.
- Min-width gate: `action_toggle_split` refuses to enter split below
`_SPLIT_MIN_WIDTH` (100 cols) — two usable panes need the room.
- Navigation keeps both panes on the same function: verified — a goto/follow in
split routes through `_open_entry` (listing) → `_show_active` split branch
(decomp + `_load_split_map`), so both panes reload for the new function and
split stays on.
- Cross-function follow: the unified listing spans many functions, so when the
listing cursor leaves the decompiled function's ea span (`_split_range`),
`_sync_split` re-points the decomp pane to the function under the cursor
(`_resync_decomp` → `function_of` → re-decompile + reload the map). Over
data/undefined it just drops the band and keeps the last function.
- Non-issues after review: split is a persistent *mode* orthogonal to nav
entries, so back/forward simply navigate within split (no per-entry state
needed); search is already per-focused-pane.
## Open questions
- Entry-point key (proposed: `s` = split, plus the palette command). `Tab` in
split = switch focused pane.
- Width policy: 50/50 to start; maybe listing-biased later. Min terminal width to
allow split (à la the logo gate).
- Navigation-in-split: Phase 1 navigates the focused pane only; Phase 4 keeps both
on the same function.
|