diff options
| author | blasty <blasty@local> | 2026-07-23 21:18:55 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-23 21:18:55 +0200 |
| commit | 78c9e4280983464fd939e89b41af88f06d1c3844 (patch) | |
| tree | ecd402d54cad9dca05928214bf67047457cfd1f8 | |
| parent | listing: code labels on their own line; search loaded portion (no wedge) (diff) | |
| download | ida-tui-78c9e4280983464fd939e89b41af88f06d1c3844.tar.gz ida-tui-78c9e4280983464fd939e89b41af88f06d1c3844.tar.xz ida-tui-78c9e4280983464fd939e89b41af88f06d1c3844.zip | |
views: highlight all occurrences of the token under the cursor
Editor-style highlight-all: whenever the cursor lands on an identifier, every
other occurrence of that same whole-word token is highlighted across the visible
listing AND the decompiler view (IDA's identifier-highlight behaviour).
* new _word_occurrences() finds whole-word spans in a line (reuses the same
char==cell alignment search already relies on).
* ColumnCursor gains _hl_word + _refresh_hl(): after any cursor move (h/l, w/b,
0/eol, j/k, mouse click, search jump) it recomputes the token under the cursor
and repaints the viewport only when it changed. Tokens must be >=2 chars and
start with a letter/underscore.
* ListingView.render_line and DecompView.render_line overlay every occurrence
with the existing _S_WORD background; the block cursor still marks the current.
Verified: cursor on 'rbp' highlights 5 visible listing rows; cursor on 'v23'
highlights it across the decompiler; mouse/disasm_nav/decomp_nav/listing_view/
func_banners/follow_xrefs/continuous_view/decomp_follow_self 42/0.
| -rw-r--r-- | idatui/app.py | 52 |
1 files changed, 51 insertions, 1 deletions
diff --git a/idatui/app.py b/idatui/app.py index 396e466..7ca7da6 100644 --- a/idatui/app.py +++ b/idatui/app.py @@ -241,6 +241,27 @@ def _word_bounds(text: str, x: int) -> tuple[int, int]: return (s, e) +def _word_occurrences(text: str, word: str) -> list[tuple[int, int]]: + """[start, end) spans of every WHOLE-word occurrence of ``word`` in ``text`` + (for highlight-all-occurrences of the token under the cursor).""" + if not word or not text: + return [] + isw = lambda c: c.isalnum() or c == "_" # noqa: E731 + out: list[tuple[int, int]] = [] + n = len(word) + i = 0 + while True: + j = text.find(word, i) + if j < 0: + break + before_ok = j == 0 or not isw(text[j - 1]) + after_ok = j + n >= len(text) or not isw(text[j + n]) + if before_ok and after_ok: + out.append((j, j + n)) + i = j + n + return out + + def _overlay_over(strip: Strip, ranges: list[tuple[int, int]], style: Style) -> Strip: """Like _overlay_ranges but ``style`` OVERRIDES the existing cell styles (used for the cursor cell / word, which must win over the line background).""" @@ -290,9 +311,21 @@ class ColumnCursor: Binding("dollar_sign", "col_end", "eol", show=False), ] + _hl_word: str | None = None # token to highlight across all visible lines + def _line_plain(self, idx: int) -> str | None: raise NotImplementedError + def _refresh_hl(self) -> None: + """Recompute the highlight-all token from the word under the cursor; if it + changed, repaint the whole viewport (occurrences elsewhere changed).""" + w = self.word_under_cursor() + if not (w and len(w) >= 2 and (w[0].isalpha() or w[0] == "_")): + w = None + if w != self._hl_word: + self._hl_word = w + self.refresh() + def _hscroll(self) -> None: pass @@ -306,6 +339,7 @@ class ColumnCursor: self.cursor_x = max(0, min(max(len(plain) - 1, 0), self.cursor_x + dx)) self._hscroll() _refresh_lines(self, self.cursor) + self._refresh_hl() def action_col_left(self) -> None: self._move_x(-1) @@ -317,12 +351,14 @@ class ColumnCursor: self.cursor_x = 0 self._hscroll() _refresh_lines(self, self.cursor) + self._refresh_hl() def action_col_end(self) -> None: plain = self._line_plain(self.cursor) or "" self.cursor_x = max(len(plain) - 1, 0) self._hscroll() _refresh_lines(self, self.cursor) + self._refresh_hl() def action_col_word(self, direction: int) -> None: plain = self._line_plain(self.cursor) or "" @@ -346,6 +382,7 @@ class ColumnCursor: self.cursor_x = max(i, 0) self._hscroll() _refresh_lines(self, self.cursor) + self._refresh_hl() def word_under_cursor(self) -> str | None: plain = self._line_plain(self.cursor) @@ -373,6 +410,7 @@ class ColumnCursor: self._scroll_cursor_into_view() self._hscroll() self.refresh() + self._refresh_hl() self._after_cursor_move() def on_click(self, event) -> None: # type: ignore[no-untyped-def] @@ -584,6 +622,7 @@ class SearchMixin: self.scroll_to(y=max(self.cursor - self._visible_height() // 2, 0), animate=False) self._hscroll() # bring the match column into horizontal view self.refresh() + self._refresh_hl() def clear_search(self) -> None: self._term = "" @@ -861,6 +900,7 @@ class DisasmView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=True self.refresh() # scrolled: the whole viewport shifted else: _refresh_lines(self, old, self.cursor) # only the two changed rows + self._refresh_hl() self.post_message(DisasmView.CursorMoved(self.cursor, self._cursor_ea())) def _after_cursor_move(self) -> None: @@ -1143,10 +1183,15 @@ class ListingView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=Tru else: segs.append(Segment(h.text, _S_UNK)) strip = Strip(segs) + plain = self._line_plain(idx) if (self._hl_word or idx == self.cursor) else None if idx in self._ranges: strip = _overlay_ranges(strip, self._ranges[idx], self._match_style(idx)) + if self._hl_word and plain: + occ = _word_occurrences(plain, self._hl_word) + if occ: + strip = _overlay_ranges(strip, occ, _S_WORD) if idx == self.cursor: - strip = _cursor_decorate(strip, self._line_plain(idx) or "", self.cursor_x) + strip = _cursor_decorate(strip, plain or "", self.cursor_x) return strip.adjust_cell_length(width, _S_INSN) # -- navigation -------------------------------------------------------- # @@ -1401,6 +1446,10 @@ class DecompView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=True base = self._strips[idx] if idx in self._ranges: base = _overlay_ranges(base, self._ranges[idx], self._match_style(idx)) + if self._hl_word: + occ = _word_occurrences(self._texts[idx], self._hl_word) + if occ: + base = _overlay_ranges(base, occ, _S_WORD) if idx == self.cursor: base = _cursor_decorate(base, self._texts[idx], self.cursor_x) code_w = max(width - gw, 0) @@ -1435,6 +1484,7 @@ class DecompView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=True self.refresh() else: _refresh_lines(self, old, self.cursor) + self._refresh_hl() self._after_cursor_move() def action_cursor_down(self) -> None: |
