diff options
| author | blasty <blasty@local> | 2026-07-09 21:58:51 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-09 21:58:51 +0200 |
| commit | d37f4a693f64bebcc5614e085ab80be5eeede2a5 (patch) | |
| tree | b2931286d0a72629825be6bd0ce17a430dfa6f04 | |
| parent | decompiler: fix stale scroll frame on same-function jump (diff) | |
| download | ida-tui-d37f4a693f64bebcc5614e085ab80be5eeede2a5.tar.gz ida-tui-d37f4a693f64bebcc5614e085ab80be5eeede2a5.tar.xz ida-tui-d37f4a693f64bebcc5614e085ab80be5eeede2a5.zip | |
decompiler: xref jump lands the cursor on the reference token's column
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.
| -rw-r--r-- | idatui/app.py | 56 | ||||
| -rw-r--r-- | tests/test_tui.py | 13 |
2 files changed, 57 insertions, 12 deletions
diff --git a/idatui/app.py b/idatui/app.py index 6b76e63..cc4cca7 100644 --- a/idatui/app.py +++ b/idatui/app.py @@ -1108,6 +1108,7 @@ class IdaTui(App): self._search_ctx: tuple[object | None, int] = (None, 1) self._rename_ctx: tuple[object | None, str] = (None, "") self._comment_ctx: tuple[object | None, int, str] = (None, 0, "") + self._xref_focus_name: str | None = None self._dirty = False # -- layout ------------------------------------------------------------ # @@ -1467,7 +1468,7 @@ class IdaTui(App): # fall-through to the next instruction, which would point xrefs at # the wrong place. At a function entry, xrefs-to == the callers.) subj = ea - self._xrefs_present(subj) + self._xrefs_present(subj, word) @work(thread=True, group="xrefs") def _xrefs_decomp(self, line: str, word: str | None) -> None: @@ -1483,9 +1484,9 @@ class IdaTui(App): if subj is None: subj = self._ref_on_line(line) or (self._cur.ea if self._cur else None) if subj is not None: - self._xrefs_present(subj) + self._xrefs_present(subj, word) - def _xrefs_present(self, subj: int) -> None: # worker context + def _xrefs_present(self, subj: int, subj_name: str | None = None) -> None: # worker assert self.program is not None xr = self.program.xrefs_to(subj) fn = self.program.function_of(subj) @@ -1495,19 +1496,26 @@ class IdaTui(App): label += f"+{subj - fn.addr:#x}" else: label = f"xrefs to {subj:#x}" + # Token to land the cursor on at each site: the symbol xrefs was invoked + # on, else the referenced function's name. + focus = subj_name if self._looks_like_symbol(subj_name) else None + if focus is None and fn is not None and fn.addr == subj: + focus = fn.name items = [(x.frm, f"{x.frm:08X} {x.fn_name or '?':<24} [{x.type}]") for x in xr] - self.app.call_from_thread(self._present_xrefs, label, items) + self.app.call_from_thread(self._present_xrefs, label, items, focus) - def _present_xrefs(self, label: str, items: list[tuple[int, str]]) -> None: + def _present_xrefs(self, label: str, items: list[tuple[int, str]], + focus_name: str | None = None) -> None: if not items: self._status(f"{label}: none") return + self._xref_focus_name = focus_name self._status(f"{label}: {len(items)}") self.push_screen(XrefsScreen(label, items), self._on_xref_chosen) def _on_xref_chosen(self, addr: int | None) -> None: if addr is not None: - self._goto_ea(addr, push=True) + self._goto_ea(addr, push=True, focus_name=self._xref_focus_name) # -- rename ------------------------------------------------------------ # @staticmethod @@ -1719,10 +1727,12 @@ class IdaTui(App): # -- navigation to an arbitrary address ------------------------------- # @work(thread=True, group="nav") - def _goto_ea(self, ea: int, push: bool = True) -> None: - self._do_navigate(ea, push) + def _goto_ea(self, ea: int, push: bool = True, + focus_name: str | None = None) -> None: + self._do_navigate(ea, push, focus_name) - def _do_navigate(self, ea: int, push: bool) -> None: # worker context + def _do_navigate(self, ea: int, push: bool, + focus_name: str | None = None) -> None: # worker context assert self.program is not None fn = self.program.function_of(ea) if fn is None: @@ -1735,10 +1745,31 @@ class IdaTui(App): # its pseudocode line (via the per-line /*0xEA*/ markers) so the jump # lands on the reference there too (e.g. selecting an xref). A plain # function-entry jump is line 0 in both views -> skip the decompile. - dec_idx = -1 + # With a focus_name (the symbol the xref was on), also land the column + # on that token where it appears in the line. + dec_idx, dec_col = -1, 0 if self._active == "decomp" and ea != fn.addr: dec_idx = self._decomp_line_for(fn.addr, ea) - self.app.call_from_thread(self._open_at, fn.addr, fn.name, idx, push, dec_idx) + if dec_idx >= 0 and focus_name: + dec_col = self._decomp_col_for(fn.addr, dec_idx, focus_name) + self.app.call_from_thread( + self._open_at, fn.addr, fn.name, idx, push, dec_idx, dec_col) + + def _decomp_col_for(self, fn_addr: int, line_idx: int, name: str) -> int: + """Column of ``name`` (whole-word) on pseudocode line ``line_idx``, so an + xref jump lands on the referenced token, not the line start. 0 if absent. + Computed on the marker-stripped line so it matches the displayed text.""" + assert self.program is not None + try: + dec = self.program.decompile(fn_addr) + except Exception: # noqa: BLE001 + return 0 + lines = (dec.code or "").splitlines() + if not (0 <= line_idx < len(lines)): + return 0 + clean = _ADDR_MARK_STRIP_RE.sub("", lines[line_idx]) + m = re.search(rf"\b{re.escape(name)}\b", clean) + return m.start() if m else 0 def _decomp_line_for(self, fn_addr: int, ea: int) -> int: """Pseudocode line index best matching address ``ea``: the line whose @@ -1760,12 +1791,13 @@ class IdaTui(App): return best_idx def _open_at(self, ea: int, name: str, cursor: int, push: bool, - dec_cursor: int = -1) -> None: + dec_cursor: int = -1, dec_cursor_x: int = 0) -> None: if push: self._save_current_pos() entry = NavEntry(ea=ea, name=name, cursor=cursor) if dec_cursor >= 0: entry.dec_cursor = dec_cursor + entry.dec_cursor_x = dec_cursor_x if push: self._nav.append(entry) self._open_entry(entry, push=False) diff --git a/tests/test_tui.py b/tests/test_tui.py index 1641368..60ba2c5 100644 --- a/tests/test_tui.py +++ b/tests/test_tui.py @@ -378,11 +378,24 @@ async def run(db): app._active = "decomp" app._show_active() await wait_until(pilot, lambda: dec.loaded_ea == xfn.addr, 25) + # xfn is the referenced function; its name is the token that + # appears at the call site the xref points to. + app._xref_focus_name = xfn.name app._on_xref_chosen(xref.frm) # pick the caller from the dialog await wait_until(pilot, lambda: dec.loaded_ea == xref.fn_addr, 25) await pilot.pause(0.1) check("xref-select lands on the reference LINE in pseudocode", dec.cursor == dexp, f"dec.cursor={dec.cursor} want={dexp}") + # Cursor lands on the reference token's column (or the line + # start if that token isn't literally on the line, e.g. a + # stale Hex-Rays cache renders a different name). + landed = dec._texts[dec.cursor] if dec.cursor < len(dec._texts) else "" + tokm = re.search(rf"\b{re.escape(xfn.name)}\b", landed) + want_col = tokm.start() if tokm else 0 + check("xref-select lands the column on the reference token", + dec.cursor_x == want_col, + f"cursor_x={dec.cursor_x} want={want_col} " + f"token={xfn.name!r} line={landed.strip()!r}") # And a jump whose target is INSIDE the already-displayed # function must still move the cursor (the decompiler pane |
