Skip to content

refactor: migrate to built-in treesitter API and extract surround module#13

Closed
fecet wants to merge 7 commits intomasterfrom
main
Closed

refactor: migrate to built-in treesitter API and extract surround module#13
fecet wants to merge 7 commits intomasterfrom
main

Conversation

@fecet
Copy link
Copy Markdown
Member

@fecet fecet commented Mar 24, 2026

Summary

  • Remove nvim-treesitter dependency, migrate to built-in vim.treesitter API
  • Extract surround detection into surround.lua using TS node structure (anonymous children as delimiters) instead of text-based regex matching
  • Fix selection expansion across injected language trees (e.g. JS inside HTML)
  • Add bounds checking and coordinate conversion fixes

State Protocol: Pluggable Checkpoint

Current selection state is managed by two module-level tables:

selections[buf]  -- stack of selections (TSNode | {srow,scol,erow,ecol})
nodes_all[buf]   -- stack of TSNode references

This works but couples the state representation tightly to the expansion algorithm. With neovim/neovim@72d3a57 landing built-in vim.treesitter._select (providing an/in/]n/[n default mappings), Neovim will ship its own incremental selection with its own history/checkpoint mechanism.

To prepare for this, the next step is to define a checkpoint protocol — an interface that decouples "what to remember" from "how to remember it":

---@class wildfire.Checkpoint
---@field save fun(self, buf: integer, node: TSNode, range: table)
---@field restore fun(self, buf: integer): TSNode|table|nil
---@field reset fun(self, buf: integer)
---@field current fun(self, buf: integer): TSNode|table|nil

This allows swapping the backing implementation:

  • Built-in backend — delegate to vim.treesitter._select history when available (Neovim nightly+)
  • Custom backend — current stack-based approach as fallback for stable Neovim

With this separation, wildfire's unique value (surround-aware inner selection) becomes a thin layer on top of whichever checkpoint backend is active, rather than reimplementing the full node traversal + injection handling that Neovim now ships.

Test plan

  • <CR> on cursor starts selection at TS node under cursor
  • Repeated <CR> expands through parent nodes, selecting inner content first for surround nodes ((), {}, [], <>)
  • <BS> shrinks back to previous selection
  • Count prefix works (3<CR> jumps 3 levels)
  • Selection crosses injected language boundaries (e.g. <script> in HTML)
  • Empty surrounds ((), {}) are handled without error
  • No nvim-treesitter required at runtime

Starslayerx and others added 7 commits October 10, 2025 16:22
- Remove dependency on nvim-treesitter.ts_utils module
- Replace ts_utils.get_node_at_cursor() with vim.treesitter.get_node()
- Replace ts_utils.get_vim_range() with vim.treesitter.get_node_range()
- Replace ts_utils.get_node_text() with vim.treesitter.get_node_text()
- Add proper coordinate conversion from 0-based to 1-based indexing
- Ensure compatibility with modern nvim-treesitter versions
The previous migration incorrectly added +1 to end_col when converting
from treesitter's 0-based exclusive indexing to vim's 1-based inclusive
indexing. This caused selections to include an extra character on the
right side.

The correct conversion is:
- start_row, start_col, end_row: 0-based -> 1-based (add +1)
- end_col: 0-based exclusive == 1-based inclusive (no change needed)

This fixes the bug where selecting text inside brackets/quotes would
incorrectly include the closing bracket/quote character.
  - Replace deprecated parsers.get_parser() with vim.treesitter.get_parser(buf)
  - Add buffer/column bounds validation in unsurround_coordinates()
  - Add cursor position clamping in update_selection()
  - Use pcall for safe API calls and graceful error handling
  - Fix E5108 crashes on blank lines and unsupported filetypes
Use utils.get_range() instead of duplicating node-range conversions and simplify selection_mode normalization.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a60bd4e9e2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +101 to +102
if start_row < 1 or end_row < 1 or start_row > line_count or end_row > line_count then
-- Invalid row range, cannot update selection
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Allow selecting nodes that end at EOF

This guard rejects valid Tree-sitter ranges at file end because ranges are end-exclusive before conversion. For nodes that end at EOF (common when the buffer has a trailing newline), end_row becomes line_count + 1 after +1, so update_selection returns early and incremental expansion silently stops before the outer node can be selected; meanwhile callers still push that node into history, which can desynchronize selection state from what is visible.

Useful? React with 👍 / 👎.

local last = node:child(child_count - 1)

-- Anonymous nodes are delimiters
return first and last and not first:named() and not last:named()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Limit surround heuristic to bracket delimiters

This heuristic classifies any node with anonymous first/last children as a surround, which incorrectly matches many non-delimiter constructs (for example statement nodes like return x; in JS grammars). In those cases unsurround_coordinates treats keywords/punctuation as wrappers and can introduce unintended intermediate selections, changing expansion behavior beyond bracket-like surrounds that the plugin previously targeted.

Useful? React with 👍 / 👎.

@fecet fecet closed this Mar 24, 2026
@fecet fecet deleted the main branch March 24, 2026 09:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants