From f5aaf50476e5bf72eaec0c09ddd984b28960ca8d Mon Sep 17 00:00:00 2001 From: blasty Date: Thu, 23 Jul 2026 20:35:13 +0200 Subject: listing: code labels on their own line; search loaded portion (no wedge) Code labels (loc_XXX/jump targets) get their OWN line at depth 0, like IDA, instead of inline with the instruction: the heads walker (annotate) emits a kind=label row ('loc_XXX:') before a named non-function-start code head and strips the name from the instruction. ListingModel doesn't index label rows (goto/xref/follow still land on the code); ListingView renders them at depth 0. Also fix a search regression the extra rows exposed: search no longer forces a blocking full-segment load_all (which stalled/thrashed the worker and wedged the session on the larger annotated listing). It searches the loaded portion; the background grower streams the rest and unloaded lines are skipped until they arrive. Verified: labels on their own line; search 2.9s (was 51.9s+wedge); search/mouse/ rename_history/continuous_view/func_banners/follow_xrefs 26/0. --- idatui/app.py | 28 ++++++++++++---------------- idatui/domain.py | 7 ++++--- server/patch_server.py | 8 ++++++++ 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/idatui/app.py b/idatui/app.py index 59f0af3..396e466 100644 --- a/idatui/app.py +++ b/idatui/app.py @@ -975,9 +975,9 @@ class ListingView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=Tru h = self._head(idx) if h is None: return None - # Function-name headers sit at depth 0 (with the address); everything - # else is indented one level, code/data opcode+text included. - if h.kind == "funchdr": + # Function headers and code labels sit at depth 0 (with the address); + # everything else is indented one level (opcode+text included). + if h.kind in ("funchdr", "label"): return f"{h.ea:08X} {h.text}" base = f"{h.ea:08X} " + _LST_INDENT if h.kind == "sep": @@ -1090,19 +1090,11 @@ class ListingView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=Tru return self._line_plain(i) def _search_ensure(self, done) -> None: - # Search needs the whole segment; finish loading it first if streaming. - if self.model is not None and not self.model.complete: - self._search_load_all(done) - else: - done() - - @work(thread=True, exclusive=True, group="listing-search-load") - def _search_load_all(self, done) -> None: # type: ignore[no-untyped-def] - model = self.model - if model is not None: - model.load_all() - self.app.call_from_thread(self._grew, len(model)) - self.app.call_from_thread(done) + # Search the currently-loaded portion; the background grower streams the + # rest of the segment in on its own (a blocking full load_all here would + # stall/thrash on a big segment). Unloaded lines read back as None from + # _search_line_text and are skipped until they arrive. + done() # -- rendering --------------------------------------------------------- # def render_line(self, y: int) -> Strip: @@ -1124,6 +1116,10 @@ class ListingView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=Tru # depth-0: address + 'name proc'/'endp' (no indent) strip = Strip([Segment(f"{h.ea:08X} ", _S_ADDR), Segment(h.text, _S_FUNCHDR)]) + elif h.kind == "label": + # depth-0: address + 'loc_XXX:' on its own line + strip = Strip([Segment(f"{h.ea:08X} ", _S_ADDR), + Segment(h.text, _S_LABEL)]) else: # depth-1: address, one indent, then opcode+text segs: list[Segment] = [Segment(f"{h.ea:08X} ", _S_ADDR), diff --git a/idatui/domain.py b/idatui/domain.py index 7452ffd..e8e2996 100644 --- a/idatui/domain.py +++ b/idatui/domain.py @@ -609,9 +609,10 @@ class ListingModel: with self._lock: base = len(self._heads) for i, h in enumerate(page): - # Banner rows (function headers/separators) are display-only; - # don't index them so navigation lands on real code/data. - if h.kind not in ("sep", "funchdr"): + # Banner/label rows (function headers, separators, code labels) + # are display-only; don't index them so navigation lands on the + # real code/data head at that address. + if h.kind not in ("sep", "funchdr", "label"): self._by_ea.setdefault(h.ea, base + i) self._heads.append(h) nxt = cur.get("next") diff --git a/server/patch_server.py b/server/patch_server.py index 73144fe..7ee490a 100644 --- a/server/patch_server.py +++ b/server/patch_server.py @@ -451,6 +451,14 @@ def heads( if at_start: row = dict(row) row["name"] = None # the name is shown on the proc header line + elif annotate and row.get("kind") == "code" and row.get("name"): + # A code label (loc_XXX/jump target) gets its OWN line at depth 0, + # like IDA; strip it from the instruction row below. + nm = row["name"] + out.append({"ea": hex(e), "kind": "label", "size": 0, + "text": nm + ":", "name": nm}) + row = dict(row) + row["name"] = None out.append(row) if row.get("kind") == "data": out.extend(_idatui_struct_member_rows(e)) # expand struct fields -- cgit v1.3.1-sl0p