refactor: redesign cursor API and update all doc annotations - #2
Conversation
- Replace __call metamethod with explicit exec(bufnr) method on cursor
- Simplify exec result from {node, range}[] to plain TSNode[] since TSNode already has :range()
- Convert all doc example blocks from --- Example: --->lua format to @Usage [[ ]] annotation format
- Fix all filetype test files to follow actual/expected naming convention
- Rename doc/bufsitter.txt to doc/bufsitter.nvim.txt
- Remove redundant config.lua module
There was a problem hiding this comment.
Pull request overview
This PR refactors the bufsitter cursor execution API (explicit :exec(bufnr) and TSNode[] returns), updates IO/ref/scratch modules and tests accordingly, and migrates documentation to lemmy-help–generated annotations with CI checks.
Changes:
- Redesign cursor evaluation to use explicit
:exec(bufnr)and returnTSNode[](range vianode:range()). - Update IO/scratch/ref implementations + expand test suite with filetype fixtures (markdown/go/typst).
- Regenerate/help-tag documentation and add CI tooling (docs check, commitlint, Nix shell).
Reviewed changes
Copilot reviewed 34 out of 35 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/minimal_init.lua | Minimal Neovim init for running Plenary tests. |
| tests/helpers.lua | Shared buffer helpers for tests (create buffers from files, cleanup, TS root). |
| tests/filetypes/typst/typ_spec.lua | Typst traversal/IO integration coverage using new cursor API. |
| tests/filetypes/typst/sample.typ.tree | Typst fixture parse tree snapshot. |
| tests/filetypes/typst/sample.typ | Typst fixture document. |
| tests/filetypes/markdown/sample.md.tree | Markdown fixture parse tree snapshot. |
| tests/filetypes/markdown/sample.md | Markdown fixture document. |
| tests/filetypes/markdown/markdown_spec.lua | Markdown traversal + IO integration tests. |
| tests/filetypes/go/sample.go.tree | Go fixture parse tree snapshot. |
| tests/filetypes/go/sample.go | Go fixture source used by traversal + IO tests. |
| tests/filetypes/go/go_spec.lua | Go traversal + IO integration tests. |
| tests/bufsitter/scratch_spec.lua | Scratch buffer lifecycle tests. |
| tests/bufsitter/ref_spec.lua | Ref string generation tests (buffer + visual selection). |
| tests/bufsitter/io_spec.lua | IO select/insert/delete/replace/clear + error handling tests. |
| tests/bufsitter/cursor_spec.lua | Cursor chaining semantics + integration tests. |
| tests/bufsitter/config_spec.lua | Config defaults and deep-merge behavior tests. |
| shell.nix | Developer environment (Lua, stylua, lemmy-help, commitlint). |
| lua/bufsitter/scratch.lua | Scratch buffer/window implementation + lemmy-help annotations. |
| lua/bufsitter/ref.lua | Buffer/selection reference string generation + docs. |
| lua/bufsitter/io.lua | Cursor-driven buffer IO implementation + docs. |
| lua/bufsitter/init.lua | Plugin config defaults + setup + docs entrypoint. |
| lua/bufsitter/cursor.lua | New cursor implementation with lazy traversal + :exec(bufnr). |
| doc/tags | Updated help tags for renamed help file. |
| doc/bufsitter.nvim.txt | Generated help documentation (lemmy-help output). |
| Makefile | Add dependency setup, parser install, test/docs/check-docs targets. |
| .stylua.toml | StyLua formatting configuration. |
| .gitignore | Ignore local deps/cache/dev artifacts. |
| .github/workflows/ci.yml | CI: commitlint, tests, docs diff check. |
| .envrc | direnv integration for Nix shell. |
| .commitlintrc.js | Commitlint conventional commits configuration. |
| .claude/settings.json | Local automation hooks for tests/docs/commitlint. |
| .claude/rules/testing.md | Testing guidelines for contributors/automation. |
| .claude/rules/security.md | Security guidelines (avoid hardcoded absolute paths). |
| .claude/rules/formatting.md | Formatting guidelines (StyLua). |
| .claude/rules/documentation.md | Lua annotation + doc example conventions. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| test: _install-parsers | ||
| nvim \ | ||
| --headless \ | ||
| -u tests/minimal_init.lua \ | ||
| -c "PlenaryBustedDirectory tests/ { minimal_init = 'tests/minimal_init.lua' }" |
There was a problem hiding this comment.
In the test recipe, the continuation lines (--headless, -u ..., -c ...) appear to be indented with spaces rather than a leading tab. In Makefiles every recipe line must begin with a tab, otherwise make test can fail with a “missing separator”/parse error. Ensure each of these lines is part of the recipe (tab-indented), or rewrite this as a single tab-indented shell line.
| local wopts = vim.tbl_deep_extend("force", self._win_opts, win_opts or {}) | ||
|
|
||
| if self:is_visible() then | ||
| vim.api.nvim_win_set_buf(self._winid, self._bufnr) |
There was a problem hiding this comment.
Scratch:show() merges win_opts into wopts, but when the window is already visible it only reattaches the buffer and never applies the new window config. This makes the documented usage s:show({ width = 100, height = 30 }) misleading because it won’t actually resize/reposition an existing window. Consider calling nvim_win_set_config(self._winid, wopts) (or update the docs to state that win_opts are only used on first open).
| vim.api.nvim_win_set_buf(self._winid, self._bufnr) | |
| vim.api.nvim_win_set_buf(self._winid, self._bufnr) | |
| vim.api.nvim_win_set_config(self._winid, wopts) |
| end | ||
| end | ||
| return result | ||
| end) |
There was a problem hiding this comment.
Base:children() creates a new cursor step but does not pass prev into new_multi(...), so _prev is nil for the resulting cursor. That effectively disables :or_else() for chains where children() is the step that might be empty, even though or_else is documented as a general “fallback to previous step”. Pass prev as the second argument to new_multi here to preserve the fallback chain.
| end) | |
| end, prev) |
| end | ||
| end | ||
| return result | ||
| end) |
There was a problem hiding this comment.
Multi:parents() returns new_multi(...) without setting _prev, so :or_else() cannot fall back from a parents() step to the prior cursor step. If or_else is meant to work consistently across traversal steps, this should pass prev into new_multi(..., prev) like filter() does.
| end) | |
| end, prev) |
| ]; | ||
| shellHook = # sh | ||
| '' | ||
| export name="nix:promdown.nvim" |
There was a problem hiding this comment.
The shellHook exports name="nix:promdown.nvim", which doesn’t match this plugin/repo name (bufsitter.nvim). If this variable is used by tooling (prompts, logging, etc.), it can cause confusion or mislabeling. Consider updating it to nix:bufsitter.nvim (or removing it if unused).
| export name="nix:promdown.nvim" | |
| export name="nix:bufsitter.nvim" |
| Username *string `json:"username"` | ||
| Email string `json:"email"` | ||
|
|
||
| // 3. Collections: Slices and Maps | ||
| Roles []string `json:"roles"` | ||
| Settings map[string]string `json:"settings"` | ||
|
|
||
| // 4. Nested Anonymous Struct | ||
| Address struct { | ||
| City string `json:"city"` | ||
| ZipCode int `json:"zip_code"` | ||
| } `json:"address"` | ||
|
|
||
| // 5. Interface member (for polymorphism tests) | ||
| Permissions any `json:"permissions"` | ||
|
|
||
| // 6. Channel for concurrency tests |
There was a problem hiding this comment.
This sample file contains whitespace-only lines (e.g. the blank lines at 21 and 34 include spaces). Trailing whitespace makes diffs noisy and can cause formatting/lint issues; consider converting these to truly empty lines and updating the corresponding expected test fixtures if needed.
| local idx = n > 0 and n or (#nodes + n + 1) | ||
| local node = nodes[idx] | ||
| return node and { node } or {} | ||
| end) |
There was a problem hiding this comment.
Multi:nth() returns a SingleCursor via new_single(...) but doesn’t set _prev (it doesn’t pass prev). This means cursor.root():children():filter(fn):first():or_else() (shown in the Single:or_else() docs) won’t actually fall back, because first()/nth() breaks the _prev chain. Pass prev into new_single(..., prev) so or_else works after nth/first/last as documented.
| end) | |
| end, prev) |
| contents | ||
| ) | ||
| else | ||
| vim.api.nvim_buf_set_text(bufnr, er, ec, er, ec, contents) |
There was a problem hiding this comment.
In cursor-based insert with inline=true + prepend=false, the code inserts at (er, ec) directly. For Treesitter ranges, ec == 0 commonly means the end is exclusive at the start of row er, so this will insert at the beginning of the next line rather than at the end of the node’s last line. Use the existing clamp_end(bufnr, er, ec) (or equivalent) before calling nvim_buf_set_text in this branch.
| vim.api.nvim_buf_set_text(bufnr, er, ec, er, ec, contents) | |
| local end_row, end_col = clamp_end(bufnr, er, ec) | |
| vim.api.nvim_buf_set_text( | |
| bufnr, | |
| end_row, | |
| end_col, | |
| end_row, | |
| end_col, | |
| contents | |
| ) |
| ---@brief [[ | ||
| --- Treesitter-powered buffer manipulation for Neovim. | ||
| --- | ||
| --- Quick start: | ||
| --->lua | ||
| --- require("bufsitter").setup() | ||
| ---< |
There was a problem hiding this comment.
The PR description says doc example blocks were converted to @usage [[ ... ]] for lemmy-help compatibility, but the module ---@brief here still uses the old --->lua ... ---< example block. Update this block to the new @usage style (or adjust the PR description if brief-block examples are intentionally left as-is).
- cursor: pass prev to new_multi/new_single in children(), parents(), nth() so or_else() works correctly after those steps - io: apply clamp_end before inline append insert to handle ec=0 correctly - scratch: apply win_opts to existing window via nvim_win_set_config in show() - shell.nix: fix plugin name nix:promdown.nvim -> nix:bufsitter.nvim - sample.go: remove trailing whitespace on blank lines
- resolve width/height as ratio of editor size when value is 0–1 - compute centered row/col at show() time when not explicitly set - remove fixed row/col from default config - default width/height changed to 0.6/0.4 - remove unused VimResized autocmd code
…d io comments - scratch: add min_width/min_height support (absolute or 0-1 ratio) to win.opts - scratch: expose row/col as explicit nil defaults; add bufsitter.config.scratch.win.opts type - io: fix cursor-based operations inserting after trailing blank lines by adding resolve_end() with trim_end option (default true) across select/insert/delete/replace - io: annotate all internal logic with inline comments
Summary
__callmetamethod with explicit:exec(bufnr)method on cursor — makes the lazy query pattern explicit and self-documenting:exec()return type from{ node, range }[]to plainTSNode[], sinceTSNodealready exposes:range()--- Example:\n--->luaformat to@usage [[ ]]annotation format (lemmy-help compatible)actual/expectednaming conventiondoc/bufsitter.txt→doc/bufsitter.nvim.txtconfig.luamoduleTest plan
make test)stylua . --check)