* 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. * feat(buffer): add configurable `eol_format` for EOL virtual text Problem: EOL virtual text order (category → recurrence → due) and the double-space separator are hardcoded in `apply_extmarks()`. Users cannot reorder, omit, or restyle metadata fields. Solution: Add `eol_format` config field (default `'%c %r %d'`) with `%c`, `%r`, `%d` specifiers. `parse_eol_format()` tokenizes the format string; `build_eol_virt()` resolves specifiers against `LineMeta` and collapses literals around absent fields. * ci: format
144 lines
3.4 KiB
Lua
144 lines
3.4 KiB
Lua
---@class pending.FoldingConfig
|
|
---@field foldtext? string|false
|
|
|
|
---@class pending.ResolvedFolding
|
|
---@field enabled boolean
|
|
---@field foldtext string|false
|
|
|
|
---@class pending.Icons
|
|
---@field pending string
|
|
---@field done string
|
|
---@field priority string
|
|
---@field due string
|
|
---@field recur string
|
|
---@field category string
|
|
|
|
---@class pending.GcalConfig
|
|
---@field remote_delete? boolean
|
|
---@field credentials_path? string
|
|
---@field client_id? string
|
|
---@field client_secret? string
|
|
|
|
---@class pending.GtasksConfig
|
|
---@field remote_delete? boolean
|
|
---@field credentials_path? string
|
|
---@field client_id? string
|
|
---@field client_secret? string
|
|
|
|
---@class pending.SyncConfig
|
|
---@field remote_delete? boolean
|
|
---@field gcal? pending.GcalConfig
|
|
---@field gtasks? pending.GtasksConfig
|
|
|
|
---@class pending.Keymaps
|
|
---@field close? string|false
|
|
---@field toggle? string|false
|
|
---@field view? string|false
|
|
---@field priority? string|false
|
|
---@field date? string|false
|
|
---@field undo? string|false
|
|
---@field filter? string|false
|
|
---@field open_line? string|false
|
|
---@field open_line_above? string|false
|
|
---@field a_task? string|false
|
|
---@field i_task? string|false
|
|
---@field a_category? string|false
|
|
---@field i_category? string|false
|
|
---@field next_header? string|false
|
|
---@field prev_header? string|false
|
|
---@field next_task? string|false
|
|
---@field prev_task? string|false
|
|
|
|
---@class pending.Config
|
|
---@field data_path string
|
|
---@field default_view 'category'|'priority'
|
|
---@field default_category string
|
|
---@field date_format string
|
|
---@field date_syntax string
|
|
---@field recur_syntax string
|
|
---@field someday_date string
|
|
---@field input_date_formats? string[]
|
|
---@field category_order? string[]
|
|
---@field drawer_height? integer
|
|
---@field debug? boolean
|
|
---@field keymaps pending.Keymaps
|
|
---@field folding? boolean|pending.FoldingConfig
|
|
---@field sync? pending.SyncConfig
|
|
---@field eol_format? string
|
|
---@field icons pending.Icons
|
|
|
|
---@class pending.config
|
|
local M = {}
|
|
|
|
---@type pending.Config
|
|
local defaults = {
|
|
data_path = vim.fn.stdpath('data') .. '/pending/tasks.json',
|
|
default_view = 'category',
|
|
default_category = 'Todo',
|
|
date_format = '%b %d',
|
|
date_syntax = 'due',
|
|
recur_syntax = 'rec',
|
|
someday_date = '9999-12-30',
|
|
eol_format = '%c %r %d',
|
|
folding = true,
|
|
category_order = {},
|
|
keymaps = {
|
|
close = 'q',
|
|
toggle = '<CR>',
|
|
view = '<Tab>',
|
|
priority = '!',
|
|
date = 'D',
|
|
undo = 'U',
|
|
filter = 'F',
|
|
open_line = 'o',
|
|
open_line_above = 'O',
|
|
a_task = 'at',
|
|
i_task = 'it',
|
|
a_category = 'aC',
|
|
i_category = 'iC',
|
|
next_header = ']]',
|
|
prev_header = '[[',
|
|
next_task = ']t',
|
|
prev_task = '[t',
|
|
},
|
|
sync = {},
|
|
icons = {
|
|
pending = ' ',
|
|
done = 'x',
|
|
priority = '!',
|
|
due = '.',
|
|
recur = '~',
|
|
category = '#',
|
|
},
|
|
}
|
|
|
|
---@type pending.Config?
|
|
local _resolved = nil
|
|
|
|
---@return pending.Config
|
|
function M.get()
|
|
if _resolved then
|
|
return _resolved
|
|
end
|
|
local user = vim.g.pending or {}
|
|
_resolved = vim.tbl_deep_extend('force', defaults, user)
|
|
return _resolved
|
|
end
|
|
|
|
---@return nil
|
|
function M.reset()
|
|
_resolved = nil
|
|
end
|
|
|
|
---@return pending.ResolvedFolding
|
|
function M.resolve_folding()
|
|
local raw = M.get().folding
|
|
if raw == false then
|
|
return { enabled = false, foldtext = false }
|
|
elseif raw == true or raw == nil then
|
|
return { enabled = true, foldtext = '%c (%n tasks)' }
|
|
end
|
|
return { enabled = true, foldtext = raw.foldtext or false }
|
|
end
|
|
|
|
return M
|