From 5205c245a2db283c2ff50cfeb8a670e62b09340d Mon Sep 17 00:00:00 2001 From: blasty Date: Sat, 25 Jul 2026 14:33:05 +0200 Subject: projects: project-wide symbol search over a SQLite FTS5 index (phase 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit idatui/index.py — one on-disk index (/idx/project.db) over every binary in a project, so search works for binaries whose worker isn't running. Indexing choice, measured rather than guessed: * SQLite FTS5 with the TRIGRAM tokenizer — stdlib, no dependency (nothing else was installed and nothing is needed), and unlike a prefix index it matches arbitrary substrings, which is what symbol names and string bodies need. * 300k-entry corpus: 1.9 ms per query vs 11.8 ms for a Python scan and 28.9 ms for plain LIKE; 0.2 ms per incremental insert. * Size was the stated worry and turned out not to bite: bash contributes 5.9k entries / 0.15MB of text, libcrypto.so.3 30.7k / 0.52MB. At ~5.7x the text a 20-binary project is ~12-23MB — against .i64 files already in the sidecar (libcrypto's alone is 72MB), roughly 1% of what the project already costs. The reason to be on disk is residency, not size. * Trigram can't answer queries under 3 chars and returns nothing rather than erroring, so search() falls back to LIKE — otherwise incremental typing would look broken until the third keystroke. Wiring: after a binary's functions load, its symbols + strings are folded into the index (skipped when the source's size/mtime is unchanged). Ctrl+N gains a scope toggle on F2 — not ctrl+a, which the focused Input binds to "home" so it never reaches the palette. Project scope narrows via the index then ranks with the existing _fuzzy, keeping the same feel; hits are prefixed with their binary, and choosing one elsewhere switches binary and jumps to it. Also fixes another instance of the Textual-markup trap: the palette titles ate "[project]" as a style tag (same class of bug as the status bar), so the pal titles are markup=False now. tests/test_index.py: 24 stdlib checks — substring/case-insensitive matching, kind filter, the <3 char fallback, multi-binary search, per-binary incremental reindex, staleness, forget, persistence. Suite 191/0. Strings (") still needs the same scope toggle; the index already carries them. --- tests/test_scenarios.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'tests/test_scenarios.py') diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index c1d315f..7c41f1f 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -300,18 +300,18 @@ async def s_palette(c: Ctx): pal = app.screen pinp = pal.query_one(Input) pinp.value = "main" - await c.wait(lambda: pal._results and pal._results[0].name == "main", 10) + await c.wait(lambda: pal._results and pal._results[0][2] == "main", 10) c.check("palette fuzzy-finds (top result matches the query)", - bool(pal._results) and pal._results[0].name == "main", - f"top={pal._results[0].name if pal._results else None}") + bool(pal._results) and pal._results[0][2] == "main", + f"top={pal._results[0][2] if pal._results else None}") pinp.value = "eror" # scattered subsequence of 'error' - await c.wait(lambda: any(f.name == "error" for f in pal._results), 10) + await c.wait(lambda: any(n == "error" for _, _, n in pal._results), 10) c.check("palette matches a fuzzy subsequence", - any(f.name == "error" for f in pal._results), - f"results={[f.name for f in pal._results[:4]]}") + any(n == "error" for _, _, n in pal._results), + f"results={[n for _, _, n in pal._results[:4]]}") pinp.value = "main" - await c.wait(lambda: pal._results and pal._results[0].name == "main", 10) - want = pal._results[0].addr + await c.wait(lambda: pal._results and pal._results[0][2] == "main", 10) + want = pal._results[0][1] await c.press("enter") await c.wait(lambda: not isinstance(app.screen, SymbolPalette), 10) await c.wait(lambda: app._cur and app._cur.ea == want, 20) -- cgit v1.3.1-sl0p