From ae3399da08a54e3ecdc8b4c3e622efbf54b55dae Mon Sep 17 00:00:00 2001 From: blasty Date: Fri, 10 Jul 2026 13:02:05 +0200 Subject: struct editor: auto-format the definition on save After a successful declare, re-fetch the canonical struct_source and put it back in the editor, so the normalized layout/types show immediately instead of only after re-selecting the struct from the list (e.g. a pasted one-liner becomes multi-line, 'unsigned long' -> 'unsigned __int64'). _loaded_src is updated to the formatted text so it isn't considered dirty. Pilot check. full suite 92/92. --- idatui/app.py | 21 ++++++++++++++++++--- tests/test_tui.py | 5 +++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/idatui/app.py b/idatui/app.py index 271d8f7..32a0698 100644 --- a/idatui/app.py +++ b/idatui/app.py @@ -1334,9 +1334,18 @@ class StructEditor(ModalScreen): return m = re.search(r"\b(?:struct|union)\s+([A-Za-z_]\w*)", text) name = m.group(1) if m else None - self.app.call_from_thread(self._after_save, name, err, text) + # On success, pull the canonical source back so the editor shows the + # normalized layout/types (auto-format on save). + formatted = None + if not err and name: + try: + formatted = self._program.struct_source(name) + except Exception: # noqa: BLE001 + formatted = None + self.app.call_from_thread(self._after_save, name, err, text, formatted) - def _after_save(self, name: str | None, err: str | None, text: str) -> None: + def _after_save(self, name: str | None, err: str | None, text: str, + formatted: str | None) -> None: if err: # IDA's parse error is usually empty/cryptic; name the likely cause. msg = err.strip() @@ -1348,7 +1357,13 @@ class StructEditor(ModalScreen): self._set_status(f"save failed — {msg}", error=True) return self._loaded = name - self._loaded_src = text.strip() # editor now matches the saved type + if formatted: + # Reformat the editor to IDA's canonical layout (keeps cursor at top). + ta = self.query_one("#se-edit", TextArea) + ta.text = formatted + self._loaded_src = formatted + else: + self._loaded_src = text.strip() # editor already matches the saved type if getattr(self.app, "_dirty", None) is not None: self.app._dirty = True # unsaved-to-disk until Ctrl+S in the app self._refresh(select=name) diff --git a/tests/test_tui.py b/tests/test_tui.py index e990bef..6a8f898 100644 --- a/tests/test_tui.py +++ b/tests/test_tui.py @@ -180,6 +180,11 @@ async def run(db): await wait_until(pilot, lambda: any(s.name == sname for s in se._structs), 15) check("Ctrl+S declares a new struct", any(s.name == sname for s in se._structs), "not created") + # save auto-formats the (one-line) definition to canonical layout + await wait_until(pilot, lambda: "\n" in ta.text, 10) + check("save auto-formats the definition in the editor", + ta.text.count("\n") >= 3 and f"struct {sname}" in ta.text + and not se._is_dirty(), f"text={ta.text[:50]!r}") # update it (add a field -> member count grows) idx = next(i for i, s in enumerate(se._structs) if s.name == sname) se.on_option_list_option_selected(type("E", (), {"option_index": idx})()) -- cgit v1.3.1-sl0p