From b750000d065ff34e46ea093aa56d3a7d0a7e4fe4 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Thu, 23 Jul 2026 21:45:33 -0400 Subject: [PATCH] fix(fold): key the managed Lua widening on the EFFECTIVE edit site MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #149 review round 5, finding 1 — correct, and both round-4 bugs did survive through the intercept path. Round 4 moved the widening off the point and onto `edit_start_of(&op)`, but ran it in `run_buffer_edit` BEFORE `run_managed_edit`. A managed buffer intercept may legally rewrite `pos` / `start` / `end` (`LuaInterceptView::intercept_edit`), so the requested op is not where the edit lands: - requested outside -> intercept relocates inside: the edit stayed hidden; - requested inside -> intercept relocates outside: an unrelated fold opened. The seam still covers BOTH paths — hooking only `run_managed_edit` would let an interactive `bypass_intercept` edit escape, which is why the framing put it on the common entry — but each path now keys on its own effective site: - `run_bypass_edit` applies its op verbatim, so `run_buffer_edit` hooks it there; - `run_managed_edit` hooks after the intercept chain settles and before the apply, on the op the chain returned. A chain that raises applies nothing, so it unfolds nothing. The registry borrow is released at that point, and the helper reads only `SharedCore` + the fold registry, so no borrow conflicts with phase 3. Three new tests, all through real `M-x` with a real `pmacs.buffer.add_intercept`: relocate-into-a-fold must unfold, relocate-out-of-a-fold must not, and a rejected chain must not. Each asserts the buffer text first, so the test fails loudly if the intercept stops relocating rather than silently passing. Suite 45 -> 48. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV --- src/lua_bindings/mod.rs | 33 +++++++-- tests/folding_stage2_acceptance.rs | 112 +++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 5 deletions(-) diff --git a/src/lua_bindings/mod.rs b/src/lua_bindings/mod.rs index 290df7a..7cf0bc4 100644 --- a/src/lua_bindings/mod.rs +++ b/src/lua_bindings/mod.rs @@ -1309,12 +1309,16 @@ fn run_buffer_edit( bypass_intercept: bool, ) -> mlua::Result { // Arc 6 Stage 2 (Q#FD19): the interactive-Lua-command unfold seam. - // Hooked HERE — above both `run_managed_edit` and `run_bypass_edit` — - // so an interactive command that passes `bypass_intercept` does not - // escape the widening. Keyed on where the edit LANDS, read before - // `op` is consumed below. - unfold_before_interactive_lua_edit(lua, id, edit_start_of(&op)); + // BOTH paths are covered — hooking only `run_managed_edit` would let + // an interactive `bypass_intercept` edit escape — but each keys on + // *its own* effective edit site (round-5 F1): + // + // - bypass applies `op` verbatim, so the site is known right here; + // - managed runs the intercept chain first, and an intercept may + // legally relocate `pos` / `start` / `end`, so only the op the + // chain settles on names where the edit will land. if bypass_intercept { + unfold_before_interactive_lua_edit(lua, id, edit_start_of(&op)); run_bypass_edit(lua, id, op) } else { run_managed_edit(lua, id, op) @@ -1359,6 +1363,12 @@ fn edit_start_of(op: &EditOp<'_>) -> u64 { /// [`apply_active_edit`](crate::editor_core::EditorCore::apply_active_edit)'s /// funnel keys on the point; only this Lua path can diverge. /// +/// "Edit site" means the **effective** one (round-5 F1). On the managed +/// path a buffer intercept may legally rewrite `pos` / `start` / `end` +/// before the op is applied, so [`run_managed_edit`] calls this *after* +/// the intercept chain settles; only [`run_bypass_edit`], which applies +/// its op verbatim, can name the site up front. +/// /// Matches Stage 1's data-API exemption: `pmacs.buffer.insert` called /// from a plugin, a hook, or a bare Lua chunk unfolds nothing. fn unfold_before_interactive_lua_edit(lua: &Lua, id: BufferId, edit_start: u64) { @@ -1435,6 +1445,19 @@ fn run_managed_edit(lua: &Lua, id: BufferId, op: EditOp<'_>) -> mlua::Result