Files
nuwiki/lua/nuwiki/textobjects.lua
T
gffranco 63e5b6d514
CI / cargo fmt --check (push) Successful in 21s
CI / cargo clippy (push) Successful in 1m8s
CI / cargo test (push) Successful in 1m18s
phase 19: editor glue v2
Server-side:
- New `folding.rs` module with `folding_ranges(ast, total_lines)`.
  Emits one fold per top-level heading (start → line before the
  next same-or-higher-level heading, or EOF) plus one per top-level
  list block. Nested headings fold inside their parent thanks to the
  level-aware end-line computation; sublists fold implicitly via
  their parent item's source span. `FoldingRangeKind::Region` is set
  on every fold so collapsing UIs render them as section folds.
- `Backend::folding_range` handler wires the module into
  `textDocument/foldingRange`; `ServerCapabilities.folding_range_provider`
  advertises it.
- `folding::line_count` exposed for the handler and tests; treats a
  trailing newline as a virtual empty line (the line LSP positions
  use for EOF).

Editor glue (Neovim primary, Vim minimal):
- `lua/nuwiki/commands.lua` — async `workspace/executeCommand`
  wrappers for every server command shipped through Phase 18. The
  open-* family handles `{ uri }` responses by opening the file
  (with `tab` / `split` variants). `check_links` and `find_orphans`
  hand results to `setqflist` + `:copen` for quickfix-style review.
  §13.1-deferred commands (`list.changeSymbol`, table rewriters,
  `link.pasteWikilink/pasteUrl`, `colorize`) stub out with
  `vim.notify` so users get a clear "not yet implemented" signal
  instead of an LSP "unknown command" error.
- `lua/nuwiki/keymaps.lua` — buffer-local default mappings. Subgroups
  (`list_editing`, `header_nav`, `diary`, `html_export`,
  `text_objects`) flip independently via the new
  `mappings.<group>` config. Heading promote/demote uses `g=`/`g-`
  to avoid clobbering Vim's built-in `=`/`-` operators.
- `lua/nuwiki/textobjects.lua` — `ah`/`ih` (around/inside heading)
  using a buffer scan for the heading-block boundary. The four
  remaining text objects from SPEC §12.10 wait until §13.1 lands
  the table/list rewriters they share infrastructure with.
- `lua/nuwiki/folding.lua` — pure regex `foldexpr` + `foldtext`
  fallback for clients without `foldingRange`. Same heading-block
  model as the server.
- `lua/nuwiki/ftplugin.lua` — single per-buffer attach entry point.
  `folding = 'lsp'` (default) uses Neovim 0.11+'s `vim.lsp.foldexpr`,
  falling back to the regex on older versions; `'expr'` forces the
  fallback; `'off'` skips folding setup.
- `lua/nuwiki/config.lua` — extends defaults with `mappings = {...}`
  (P10 keymap layer) and `folding` (P14 resolved).
- `ftplugin/nuwiki.vim` — declares every `:Vimwiki*` / `:Nuwiki*`
  command from SPEC §12.10 by inlining `lua require(...).fn()`
  bodies (no script-local function indirection so commands stay
  callable after re-source). Plain-Vim users get only the buffer
  options; they're expected to drive the LSP via vim-lsp / coc's
  built-in commands.

Health check (§12.10 additions):
- `:checkhealth nuwiki` now reports the count of `executeCommand`
  entries the server advertises, whether `foldingRange` capability
  was negotiated, and whether the configured HTML output directory
  exists + is writable. Default keymap subgroup status is also
  surfaced.

Tests: 8 new in `phase19_folding.rs` covering empty docs, single
heading → EOF, sibling headings each getting their own fold,
nested heading bounded by parent, top-level list folds, single-line
heading non-fold, fold-kind classification, and `line_count`
semantics. Total 377 tests pass; fmt + clippy clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 21:57:05 +00:00

65 lines
1.9 KiB
Lua

-- lua/nuwiki/textobjects.lua — text objects (Phase 19).
--
-- Currently ships `ah` / `ih` (around / inside heading). The remaining
-- four objects from SPEC §12.10 (`aH`/`iH`, `a\`/`i\`, `ac`/`ic`,
-- `al`/`il`) need the table/list rewriters from §13.1 to be fully
-- useful — they ship together with those commands in a follow-up.
local M = {}
local function heading_level(line)
local lead = line and line:match('^%s*(=+)%s')
if not lead then return 0 end
local trail = line:match('%s(=+)%s*$')
if not trail or #trail ~= #lead then return 0 end
return #lead
end
--- Find the start and end line of the heading block containing `lnum`.
--- A heading block runs from the heading line to the line before the
--- next heading of same-or-higher level (or end-of-buffer).
local function heading_block(lnum)
local total = vim.api.nvim_buf_line_count(0)
-- Walk back to the nearest heading line.
local start
for i = lnum, 1, -1 do
local lvl = heading_level(vim.fn.getline(i))
if lvl > 0 then
start = i
break
end
end
if not start then return nil end
local level = heading_level(vim.fn.getline(start))
-- Walk forward for the next heading of same-or-higher level.
local stop = total
for i = start + 1, total do
local lvl = heading_level(vim.fn.getline(i))
if lvl > 0 and lvl <= level then
stop = i - 1
break
end
end
return start, stop
end
--- Operator-pending entry point. `inner` strips the heading line itself.
function M.select(inner)
local lnum = vim.fn.line('.')
local s, e = heading_block(lnum)
if not s then return end
if inner then
s = s + 1
end
if e < s then return end
vim.cmd(string.format('normal! %dG0V%dG$', s, e))
end
function M.attach(bufnr)
local opts = { buffer = bufnr or 0, silent = true }
vim.keymap.set({ 'o', 'x' }, 'ah', function() M.select(false) end, opts)
vim.keymap.set({ 'o', 'x' }, 'ih', function() M.select(true) end, opts)
end
return M