summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--idatui/app.py95
1 files changed, 79 insertions, 16 deletions
diff --git a/idatui/app.py b/idatui/app.py
index c37846f..4a58b16 100644
--- a/idatui/app.py
+++ b/idatui/app.py
@@ -616,6 +616,73 @@ class ColumnCursor:
self.call_after_refresh(_fix)
+class _MatchRanges:
+ """Which lines matched, plus where in each line — the *where* computed lazily.
+
+ Searching a big segment for one character matches most of it: `c` over bash
+ hits 177 000 lines at 310 000 places. Building a range list for every one of
+ them costs more than finding them did, and all but the forty on screen are
+ thrown away unread.
+
+ So the line set is eager (search needs it to count and to jump) and the
+ offsets within a line are worked out when that line is painted, or when the
+ cursor lands on it, and cached from then on.
+
+ Quacks like the ``{line: [(start, end), ...]}`` dict it replaced: ``in``,
+ ``get``, ``[]``, ``items``, ``len``. Assigning a plain ``{}`` to reset stays
+ valid, because every reader only uses that same subset.
+ """
+
+ __slots__ = ("_lines", "_needle", "_n", "_ci", "_text", "_cache")
+
+ def __init__(self, lines, needle: str, n: int, ci: bool, text) -> None:
+ self._lines = lines # set[int]
+ self._needle = needle # already case-folded when ci
+ self._n = n # len(term); the needle may be folded
+ self._ci = ci
+ self._text = text # callable: line index -> str | None
+ self._cache: dict[int, list[tuple[int, int]]] = {}
+
+ def _find(self, i: int) -> list[tuple[int, int]]:
+ s = self._text(i)
+ if not s:
+ return []
+ hay = s.lower() if self._ci else s
+ needle, n = self._needle, self._n
+ out = []
+ j = hay.find(needle)
+ while j >= 0:
+ out.append((j, j + n))
+ j = hay.find(needle, j + n)
+ return out
+
+ def __contains__(self, i) -> bool:
+ return i in self._lines
+
+ def __len__(self) -> int:
+ return len(self._lines)
+
+ def __iter__(self):
+ return iter(self._lines)
+
+ def get(self, i, default=None):
+ if i not in self._lines:
+ return default
+ got = self._cache.get(i)
+ if got is None:
+ got = self._cache[i] = self._find(i)
+ return got
+
+ def __getitem__(self, i):
+ got = self.get(i)
+ if got is None:
+ raise KeyError(i)
+ return got
+
+ def items(self):
+ return ((i, self.get(i)) for i in sorted(self._lines))
+
+
class SearchMixin:
"""Vim-style in-view search shared by the disasm and pseudocode views.
@@ -822,26 +889,27 @@ class SearchMixin:
# looked at, and narrowing would silently never find them.
n = len(term)
matches: list[int] = []
- ranges: dict[int, list[tuple[int, int]]] = {}
+ ranges: object = {}
hay = self._search_haystack(count, src)
if hay is not None:
starts, blob, folded = hay
body = folded if ci else blob
nlines = len(starts)
+ blen = len(body)
line = 0
j = body.find(needle)
while j >= 0:
# find() walks forward, so the line only ever advances.
while line + 1 < nlines and starts[line + 1] <= j:
line += 1
- off = j - starts[line]
- rs = ranges.get(line)
- if rs is None:
- ranges[line] = [(off, off + n)]
- matches.append(line)
- else:
- rs.append((off, off + n))
- j = body.find(needle, j + n)
+ matches.append(line)
+ # Only the LINE matters here; the offsets inside it are worked
+ # out on demand. So skip the rest of this line rather than
+ # finding every further occurrence in it.
+ nxt = starts[line + 1] if line + 1 < nlines else blen
+ j = body.find(needle, nxt)
+ ranges = _MatchRanges(set(matches), needle, n, ci,
+ self._search_line_text)
else:
# Typing forward can only ever REMOVE lines: a line holding "mov"
# holds "mo". So when the term just grew (and nothing else moved --
@@ -862,15 +930,10 @@ class SearchMixin:
if not s:
continue
h = s.lower() if ci else s
- j = h.find(needle)
- if j < 0:
+ if h.find(needle) < 0:
continue
- rs = []
- while j >= 0:
- rs.append((j, j + n))
- j = h.find(needle, j + n)
matches.append(i)
- ranges[i] = rs
+ ranges = _MatchRanges(set(matches), needle, n, ci, text_of)
self._matches = matches
self._ranges = ranges
self._matched_key = (term, ci, count, src)