Sitelet https://github.com/jrop/morph.nvim/tags
Skip to content

Tags: jrop/morph.nvim

Tags

v0.2.1

Toggle v0.2.1's commit message
fix(guard): derive the dirty band from a content diff, not event frames

<C-w> deleting the last word of the filter at its end was judged a
violation: the guard flashed the locked chrome, reverted the deletion,
and restored a pre-edit cursor that no longer existed in the emptied
line -- landing the cursor to the right of the closing bracket. The root
cause sits in how the guard knew where the edit happened: it derived the
change region from raw on_bytes geometry, and a chained edit emits one
event per deleted character in per-event frames that go stale across the
batch, so the derived window could miss the edit and the hole's
ownership could not be established.

The sensor is rebuilt around content truth. The watcher is now a pure
change detector: it counts non-render on_bytes events into
`pending_edits` (coordinates are never read) and reports the closed
burst to the guard, which owns the geometry. The drain reads the live
lines and diffs them against the render's line snapshot --
`Morph.diff_to_band` -- settled states in final coordinates, judged
once per burst through the usual collect/decide/accept/dispatch phases.
The diff base commits on accept and stands on revert: a revert restores
the snapshot's content, so the next drain's diff comes back empty, and a
net-zero burst (typed and deleted within one window) polices nothing.

v0.2.0

Toggle v0.2.0's commit message
fix(mount): bound the not-ready wait instead of retrying forever

`Morph:mount` and `Morph:render` answered an unready buffer by warning and
calling `vim.schedule(function() ... end)`, with no bound and no failure path.
For a permanently-unready buffer that chain re-schedules itself forever: a
named-but-unloaded buffer stays `bufloaded == 0`, so nothing ever changes and
the retry cannot converge. The observed symptom was 1,123,454 identical
warnings (129 MB) from a single startup mount before the process was killed.

The two reasons `is_buffer_api_ready` returns false need different handling,
because only one of them self-corrects. Before VimEnter the buffer is
mid-setup and becomes ready the moment VimEnter fires, so that wait rides the
event rather than polling -- a poll would spin for the whole startup window.
Past startup the blocker is a named-but-unloaded buffer, whose `bufloaded`
never flips on its own; loading it once is what mounting into it means anyway.
If the predicate is still false after both, the call errors loudly rather than
looping. Both call sites go through one `when_buffer_ready` helper, so mount
and render cannot drift apart on this policy.

Re-entering mount from the helper now forwards `opts`; the old retry called
`self:mount(tree)`, silently dropping `debounce_ms` on every deferral.

The regression test drives the retry chain by hand (intercepting
`vim.schedule`) and fails unless mount terminates on its own; a second test
pins the chosen outcome -- load the buffer and render -- rather than merely
erroring, so a future change cannot satisfy the first by giving up.

v0.1.28

Toggle v0.1.28's commit message
fix(FloatingWindow): defer on_closed out of the ModeChanged autocmd

Closing a float that was in insert mode restores the prior mode via
`restore_mode_and_wait`, which ran the `on_closed` callback synchronously
inside the `i:n` ModeChanged autocmd. When `on_closed` opened a *second*
float, that float's `on_win_create` `startinsert` executed nested in the
autocmd and did not stick, leaving the second float in normal mode and
uneditable (reproduced via tuis.nvim's vim.ui.select picker).

Defer the callback onto a clean event-loop tick (`vim.schedule`) in both
the ModeChanged path and the 500ms fallback-timer path. This decouples
callback execution from the autocmd stack while preserving the autocmd's
"wait until the mode change takes effect" timing semantics.

Regression test absorbed into `spec/floating_window_spec.lua` (child-nvim
harness): float A opens in insert, A's `on_closed` opens B whose
`on_win_create` calls `startinsert`, then real input into B must land in
B's buffer (`{'w'}` with the fix, `{''}` without). This directly observes
the user-facing symptom; no interactive TTY run is needed.

Also correct the bug report's "headless cannot confirm the fix" caveat
(the child `--embed`ded nvim has a real event loop and is not subject to
the host busted runner's insert-pending-state leak) and document the
host-vs-child distinction across AGENTS.md, the floating_window_spec
after_each comment, the morph._test.nvim docstring, and the
on_change_spec header — so the host/child boundary stops being conflated
with "headless".

v0.1.27

Toggle v0.1.27's commit message
feat(undo): add causation-based `undo = 'merge'` render merging

v0.1.26

Toggle v0.1.26's commit message
feat: add Portal and FloatingWindow components with full unmount life…

…cycle

- Add Morph.Portal: renders children into a separate buffer, managing its
  own inner Morph document with mount/update/unmount + remount support
- Add Morph.FloatingWindow: prop-driven open/close with focus, cursor, and
  mode capture/restore, on_win_create/on_buf_create/on_closed callbacks,
  and VimResized re-render
- Introduce Morph:unmount(): fires all unmount phases, restores pre-morph
  buffer keymaps, clears morph extmarks, drains do_after_render callbacks
  (including those registered during unmount), and resets instance state
  so the same buffer/instance can be re-mounted
- Fix on_change edge cases: atomic multi-line insert at extmark end,
  content inserted before/after an extmark range, and same-length edits
- Add child-nvim test harness (morph._test.nvim) and migrate on_change /
  undo-redo specs to a real event loop + real keystrokes, since TextChanged
  autocmds never fire in headless mode
- Add unified diff helper (morph._test.assert.diff) for readable failures
- Add Portal screenshot and FloatingWindow integration specs