From a865213d04f621f3f87ad3d1873faea47709ed79 Mon Sep 17 00:00:00 2001 From: blasty Date: Thu, 23 Jul 2026 21:39:08 +0200 Subject: listing: restore full-segment search load (find every match) Revert the "search only the loaded portion" shortcut from f5aaf50 and go back to a single, guarded background load_all before matching, so a search finds every occurrence in the segment (not just whatever streamed in so far). The guard (_search_loading + queued callbacks) shares ONE load across per-keystroke updates instead of spawning/cancelling a worker each keypress. The wedge that shortcut was meant to avoid turned out to be unrelated to the search load: it is a pre-existing navigation bug (opening a function right after scrolling to the bottom of another leaves the listing cursor out of range and '/' fails to focus the search input, so typed chars leak into the view as listing verbs, one of which is 'u'=undefine). Reproduces at 0c097f5, before any of this work; tracked separately. Verified in isolation: search 3.1s, matches found; search/mouse/func_banners/ listing_view/continuous_view/decomp_nav/follow_xrefs 45/0. --- idatui/app.py | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/idatui/app.py b/idatui/app.py index 7ca7da6..3fa6515 100644 --- a/idatui/app.py +++ b/idatui/app.py @@ -987,6 +987,8 @@ class ListingView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=Tru self._ranges: dict[int, list[tuple[int, int]]] = {} self._op_mode = 1 # opcode column: 0=off, 1=limited, 2=full ('o' cycles) self._op_w = 0 # char width of the hex-bytes field (excl. gap) + self._search_loading = False + self._search_pending: list = [] # done-callbacks awaiting the load # -- text helpers ------------------------------------------------------ # def _head(self, idx: int) -> Head | None: @@ -1130,11 +1132,33 @@ class ListingView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=Tru return self._line_plain(i) def _search_ensure(self, done) -> None: - # 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() + # Search needs the whole segment loaded to find every match. Kick off a + # SINGLE background load (guarded so per-keystroke updates don't spawn + # one worker each — an exclusive worker would cancel+restart on every + # keypress and never finish) and queue the callbacks until it lands. + model = self.model + if model is None or model.complete: + done() + return + self._search_pending.append(done) + if not self._search_loading: + self._search_loading = True + self._search_load_all() + + @work(thread=True, group="listing-search-load") + def _search_load_all(self) -> None: + model = self.model + if model is not None: + model.load_all() + self.app.call_from_thread(self._search_loaded) + + def _search_loaded(self) -> None: + self._search_loading = False + if self.model is not None: + self._grew(len(self.model)) + pending, self._search_pending = self._search_pending, [] + for cb in pending: + cb() # -- rendering --------------------------------------------------------- # def render_line(self, y: int) -> Strip: -- cgit v1.3.1-sl0p