diff options
| author | blasty <blasty@local> | 2026-07-26 10:03:05 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-26 10:03:05 +0200 |
| commit | eea10d6230b1d6c460767b701fcd58d2f965c7b4 (patch) | |
| tree | 425e33b1cfe4f94dd72da47293cb1b53b2bd2435 /tests | |
| parent | listing: an edit must not move the view (diff) | |
| download | ida-tui-eea10d6230b1d6c460767b701fcd58d2f965c7b4.tar.gz ida-tui-eea10d6230b1d6c460767b701fcd58d2f965c7b4.tar.xz ida-tui-eea10d6230b1d6c460767b701fcd58d2f965c7b4.zip | |
app: one way to preserve view state across a model rebuild (ViewAnchor)
Two bugs in two days had the same shape — an edit rebuilds the model and
whatever ran last wins. The status message got clobbered by the reload's own
status write; the scroll position got recomputed from a row index that no longer
meant the same thing. Both were patched by hand. A third was coming.
ViewAnchor makes it one thing: where you are looking, in ADDRESSES, plus the
message the rebuild must not eat. _anchor() captures it on the UI thread before
the edit; _anchor_rows() resolves it against the rebuilt model; _edit_done()
settles the aftermath. The edit paths (_do_edit_item, _do_make_data) now hand one
object through instead of threading positions and messages separately.
Addresses, not indices, because an edit can change how many rows an item takes:
four undefined byte rows collapse into one instruction row, undefining does the
reverse. An index means a different place afterwards.
_reload_active_code deliberately does NOT use it. Renames and comments don't
change row structure, and the model that path rebuilds is constructed empty —
index_of_ea returns -1 until pages load, so an anchor would resolve to nothing
while costing an extra model build on the UI thread. Wrote that out and reverted
it rather than leave an abstraction applied where it does nothing.
Also fixed in passing: _do_make_data had the same latent bug (no scroll
preservation at all) and now goes through the same path.
The bigger find is in TODO. The scenario suite mutates targets/echo.i64 and
SAVES it, so a scenario that undefines an instruction breaks later runs
permanently — decomp_follow_self had been failing on a polluted database, not on
any code change. It also made an edit-position check look flaky one run in
three, which I nearly wrote up as a race. Coverage for this lives in
test_blob_ui.py instead, which builds a throwaway binary and can mutate freely.
tests: +1 blob UI (commenting leaves the view where it was), alongside the
carve checks. 26/0 blob, 202/0 scenarios (on a fresh .i64), 30/0 project UI.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_blob_ui.py | 29 |
1 files changed, 28 insertions, 1 deletions
diff --git a/tests/test_blob_ui.py b/tests/test_blob_ui.py index ca4dbb3..f930401 100644 --- a/tests/test_blob_ui.py +++ b/tests/test_blob_ui.py @@ -14,7 +14,7 @@ import tempfile sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from textual.widgets import Static # noqa: E402 +from textual.widgets import Input, Static # noqa: E402 from idatui.app import ConfirmScreen, IdaTui, ListingView # noqa: E402 @@ -163,6 +163,33 @@ async def run() -> int: m.get(m.index_of_ea(target - 1)).size == 1 and m.get(m.index_of_ea(target - 1)).ea == target - 1) + # -- an edit must not move the view -------------------------- # + # Every mutation rebuilds the model, and row indices don't survive + # that: carving collapses four byte rows into one instruction row. + # All the edit paths go through ViewAnchor, which remembers + # ADDRESSES. This runs in its own app, so unlike the shared scenario + # suite it can mutate the database freely. + lst.cursor = m.index_of_ea(0x4200) + lst._scroll_cursor_into_view() + await pilot.pause(0.4) + ctop = lst.model.get(round(lst.scroll_offset.y)).ea + ccur = lst._cursor_ea() + old = lst.model + await pilot.press("semicolon") + await wait(lambda: app.query_one("#comment", Input).display, pilot, 20) + for ch in "note": + await pilot.press(ch) + await pilot.press("enter") + await wait(lambda: lst.model is not old and lst.model is not None, + pilot, 30) + await pilot.pause(0.4) + check("commenting leaves the view where it was", + lst.model.get(round(lst.scroll_offset.y)).ea == ctop + and lst._cursor_ea() == ccur, + f"top {ctop:#x} -> " + f"{lst.model.get(round(lst.scroll_offset.y)).ea:#x}, " + f"cursor {ccur:#x} -> {lst._cursor_ea():#x}") + # -- carving must not move the view -------------------------- # # Defining code collapses rows (four byte rows become one # instruction), so anything that remembers a row INDEX puts you |
