feat(buffer): persist extmarks during editing (#96)

* refactor(buffer): split extmark namespace into `ns_eol` and `ns_inline`

Problem: all extmarks shared a single `pending` namespace, making it
impossible to selectively clear position-sensitive extmarks (overlays,
highlights) while preserving stable EOL virtual text (due dates,
recurrence).

Solution: introduce `ns_eol` for end-of-line virtual text and
`ns_inline` for overlays and highlights. `clear_marks()` and
`apply_extmarks()` operate on both namespaces independently.

* feat(buffer): track line changes via `on_bytes` to keep `_meta` aligned

Problem: `_meta` is a positional array keyed by line number. Line
insertions and deletions during editing desync it from actual buffer
content, breaking `get_fold()`, cursor-based task lookups, and extmark
re-application.

Solution: attach an `on_bytes` callback that adjusts `_meta` on line
insertions/deletions and tracks dirty rows. Remove the manual
`_meta` insert from `open_line()` since `on_bytes` now handles it.
Reset dirty rows on each full render.

* feat(buffer): clear only inline extmarks on dirty rows during edits

Problem: `TextChanged` cleared all extmarks (both namespaces) on every
edit, causing EOL virtual text (due dates, recurrence) to vanish while
the user types.

Solution: replace blanket `clear_marks()` with per-row
`clear_inline_row()` that only removes `ns_inline` extmarks on rows
flagged dirty by `on_bytes`. EOL virtual text is preserved untouched.

* feat(buffer): re-apply inline extmarks after edits

Problem: inline extmarks (checkbox overlays, strikethrough, header
highlights) were cleared during edits and only restored on `:w`,
leaving the buffer visually bare while editing.

Solution: extract `apply_inline_row()` from `apply_extmarks()` and
call it via `reapply_dirty_inline()` on `InsertLeave` and normal-mode
`TextChanged`. Insert-mode `TextChangedI` still only clears inline
marks on dirty rows to avoid overlay flicker while typing.

* fix(buffer): suppress `on_bytes` during render and fix definition order

Problem: `on_bytes` fired during `render()`'s `nvim_buf_set_lines`,
corrupting `_meta` with duplicate entries and causing out-of-range
extmark errors. Also, `apply_inline_row` was defined after its first
caller `reapply_dirty_inline`.

Solution: add `_rendering` guard flag around `nvim_buf_set_lines` in
`render()` so `on_bytes` is a no-op during authoritative renders.
Move `apply_inline_row` above `reapply_dirty_inline` to satisfy Lua
local scoping rules.
This commit is contained in:
Barrett Ruth 2026-03-08 14:19:47 -04:00
parent f56de46b4d
commit 5161ef00a0
2 changed files with 162 additions and 48 deletions

View file

@ -251,12 +251,34 @@ function M._setup_autocmds(bufnr)
end
end,
})
vim.api.nvim_create_autocmd({ 'TextChanged', 'TextChangedI' }, {
vim.api.nvim_create_autocmd('TextChangedI', {
group = group,
buffer = bufnr,
callback = function()
if not vim.bo[bufnr].modified then
return
end
for row in pairs(buffer.dirty_rows()) do
buffer.clear_inline_row(bufnr, row)
end
end,
})
vim.api.nvim_create_autocmd('TextChanged', {
group = group,
buffer = bufnr,
callback = function()
if not vim.bo[bufnr].modified then
return
end
buffer.reapply_dirty_inline(bufnr)
end,
})
vim.api.nvim_create_autocmd('InsertLeave', {
group = group,
buffer = bufnr,
callback = function()
if vim.bo[bufnr].modified then
buffer.clear_marks(bufnr)
buffer.reapply_dirty_inline(bufnr)
end
end,
})