diff options
| author | blasty <blasty@local> | 2026-07-23 22:36:12 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-23 22:36:12 +0200 |
| commit | c24afb90c575bd2950a5fbfbc4b86c6253db2566 (patch) | |
| tree | 037292fc745544abbbbfeca2c75e7b4ba6f8ed6b | |
| parent | listing: on-screen jumps just move the cursor, don't re-scroll (diff) | |
| download | ida-tui-c24afb90c575bd2950a5fbfbc4b86c6253db2566.tar.gz ida-tui-c24afb90c575bd2950a5fbfbc4b86c6253db2566.tar.xz ida-tui-c24afb90c575bd2950a5fbfbc4b86c6253db2566.zip | |
nav: jumping from the decompiler stays in the decompiler
Following a reference or picking an xref target from the pseudocode dropped you
back into the linear listing. Now such jumps stay in the decompiler when the
target is a decompilable function, landing on the pseudocode line that matches
the target address; non-decompilable targets still fall back to the listing.
* NavEntry gains a `view` field ("listing"/"decomp"); _open_entry restores an
entry in whichever view it was seen in (so back/forward also keep pseudocode).
* _do_navigate/_goto_ea take prefer_decomp; when set and the target function
decompiles, _open_decomp_entry opens it in the decompiler as a real nav-history
push (snapshotting the source pseudocode position so 'back' returns to it).
* _follow_decomp and _on_xref_chosen pass prefer_decomp when the source view is
the decompiler.
Verified: follow a ref from decomp -> lands in the target's pseudocode; 'back'
returns to the source pseudocode; listing follow still lands in the listing.
decomp_nav/decomp_follow_self/follow_xrefs/xref_labels/view_toggle/disasm_nav/
search/mouse/listing_view/func_banners/continuous_view 62/0.
| -rw-r--r-- | idatui/app.py | 69 |
1 files changed, 61 insertions, 8 deletions
diff --git a/idatui/app.py b/idatui/app.py index 90f1a83..6c1d679 100644 --- a/idatui/app.py +++ b/idatui/app.py @@ -97,6 +97,7 @@ class NavEntry: dec_scroll_y: int = -1 # pseudocode viewport top (-1 = derive) dec_scroll_x: int = 0 # pseudocode horizontal scroll is_region: bool = False # not inside a function (flat listing view) + view: str = "listing" # which code view to restore this entry in class SearchRequested(Message): @@ -2811,7 +2812,9 @@ class IdaTui(App): if addr is None: self.app.call_from_thread(self._status, "nothing to follow on this line") return - self._do_navigate(addr, push=True) + # Following FROM pseudocode: keep the reader in the decompiler when the + # target is decompilable, instead of dropping to the linear listing. + self._do_navigate(addr, push=True, prefer_decomp=True) def _ref_on_line(self, line: str) -> int | None: """Address of the first decompiler ref whose name appears on ``line``.""" @@ -2916,7 +2919,10 @@ class IdaTui(App): def _on_xref_chosen(self, addr: int | None) -> None: if addr is not None: - self._goto_ea(addr, push=True, focus_name=self._xref_focus_name) + # If xrefs was invoked from the decompiler, land the jump back in the + # decompiler (when the target is decompilable) rather than the listing. + self._goto_ea(addr, push=True, focus_name=self._xref_focus_name, + prefer_decomp=(self._active == "decomp")) # -- rename ------------------------------------------------------------ # @staticmethod @@ -3396,22 +3402,57 @@ class IdaTui(App): # -- navigation to an arbitrary address ------------------------------- # @work(thread=True, group="nav") def _goto_ea(self, ea: int, push: bool = True, - focus_name: str | None = None) -> None: - self._do_navigate(ea, push, focus_name) + focus_name: str | None = None, prefer_decomp: bool = False) -> None: + self._do_navigate(ea, push, focus_name, prefer_decomp) - def _do_navigate(self, ea: int, push: bool, - focus_name: str | None = None) -> None: # worker context + def _do_navigate(self, ea: int, push: bool, focus_name: str | None = None, + prefer_decomp: bool = False) -> None: # worker context assert self.program is not None - # Unified: everything opens the one continuous listing at ``ea``. A + fn = self.program.function_of(ea) + # Jumping FROM the decompiler: stay in pseudocode when the target is a + # decompilable function, landing on the line that matches ``ea``. + if prefer_decomp and fn is not None: + dec = self.program.decompile(fn.addr) + if not dec.failed and dec.code: + dec_idx = self._decomp_line_for(fn.addr, ea) + self.app.call_from_thread( + self._open_decomp_entry, fn.addr, fn.name, max(dec_idx, 0), push) + return + # Otherwise: everything opens the one continuous listing at ``ea``. A # function name is used for the status label; a region gets a segment # label. F5/Tab decompiles the function under the cursor from here. - fn = self.program.function_of(ea) lm = self.program.listing(ea) idx = max(lm.ensure_ea(ea), 0) if lm is not None else 0 name = fn.name if fn is not None else self.program.region_label(ea) self.app.call_from_thread( self._open_at, ea, name, idx, push, -1, 0, fn is None) + def _open_decomp_entry(self, fn_addr: int, fn_name: str, dec_idx: int, + push: bool) -> None: + """Open ``fn_addr`` in the decompiler as a real navigation (nav history + aware), landing on pseudocode line ``dec_idx``.""" + if push: + # Snapshot where we jumped from so 'back' returns there. If that was + # the pseudocode (F5 makes a transient _cur not yet on the stack), + # record its position and push it as a decomp entry. + if self._active == "decomp" and self._cur is not None: + dv = self.query_one(DecompView) + src = self._cur + src.view = "decomp" + src.dec_cursor = dv.cursor + src.dec_cursor_x = dv.cursor_x + src.dec_scroll_y = round(dv.scroll_offset.y) + if not self._nav or self._nav[-1] is not src: + self._nav.append(src) + else: + self._save_current_pos() + self._decomp_return = None # a real navigation abandons the F5 return + entry = NavEntry(ea=fn_addr, name=fn_name, view="decomp", + dec_cursor=dec_idx) + if push: + self._nav.append(entry) + self._open_entry(entry, push=False) + 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. @@ -3680,6 +3721,18 @@ class IdaTui(App): if self.program is None: return self._cur = entry + if entry.view == "decomp": + # This entry was viewed in the decompiler (a jump from pseudocode, or + # a back/forward to one) — restore it there instead of the listing. + self._active = "decomp" + dec = self.query_one(DecompView) + if dec.loaded_ea == entry.ea: + # already decompiled: reposition without a recompile + dec.goto(entry.dec_cursor, entry.dec_cursor_x, + entry.dec_scroll_y if entry.dec_scroll_y >= 0 else -1, + entry.dec_scroll_x) + self._show_active() # loads the pseudocode if loaded_ea != entry.ea + return # Unified model: the code view is always the continuous listing, # positioned at this entry. The decompiler is a per-function toggle # (F5/Tab -> _decomp_from_listing), never opened implicitly. |
