Skip to content

fix(files): handle stale treesitter nodes gracefully - #1076

Open
seflue wants to merge 2 commits into
nvim-orgmode:masterfrom
seflue:fix/treesitter-parsing-errors
Open

fix(files): handle stale treesitter nodes gracefully#1076
seflue wants to merge 2 commits into
nvim-orgmode:masterfrom
seflue:fix/treesitter-parsing-errors

Conversation

@seflue

@seflue seflue commented Dec 29, 2025

Copy link
Copy Markdown
Contributor

Summary

This PR fixes intermittent "Index out of bounds" errors when treesitter nodes become stale after buffer modifications.

The error occurs when:

  1. Buffer is parsed, nodes are cached
  2. Buffer content changes (user edits)
  3. Cached nodes are accessed via async callback (e.g., vim.schedule)
  4. Node positions now point to invalid ranges → crash

Example error:

Error executing vim.schedule lua callback: .../treesitter.lua:195: Index out of bounds
stack traceback:
        [C]: in function 'nvim_buf_get_text'
        .../treesitter.lua:195: in function 'get_node_text'
        .../orgmode/files/headline.lua:545: in function 'get_property'

Related Issues

Changes

  • Wrap get_node_text() in pcall to gracefully handle stale nodes
  • Return empty string on failure instead of crashing
  • Add _parse_tick field to track buffer state at parse time
  • Add OrgFile:is_tree_stale() for callers that need explicit staleness checks

Checklist

I confirm that I have:

  • Followed the Conventional Commits specification (e.g., feat: add new feature, fix: correct bug, docs: update documentation).
  • My PR title also follows the conventional commits specification.
  • Updated relevant documentation, if necessary.
  • Thoroughly tested my changes.
  • Added tests (if applicable) and verified existing tests pass with make test.
  • Checked for breaking changes and documented them, if any.

@kristijanhusak kristijanhusak left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the PR! It seems unfinished though.

Comment thread lua/orgmode/files/file.lua Outdated
Comment thread lua/orgmode/files/file.lua Outdated
@seflue
seflue force-pushed the fix/treesitter-parsing-errors branch from 8c61052 to 5a46807 Compare January 4, 2026 20:52
@seflue
seflue requested a review from kristijanhusak January 4, 2026 20:56
@seflue
seflue force-pushed the fix/treesitter-parsing-errors branch from 5a46807 to e7dd352 Compare January 25, 2026 13:00
Comment thread lua/orgmode/files/file.lua
@kristijanhusak

Copy link
Copy Markdown
Member

Do you know how can I reproduce this? Cache should revalidate when node changes, and I'm curious if we could handle it differently.

@seflue
seflue force-pushed the fix/treesitter-parsing-errors branch from e7dd352 to 7e9471b Compare February 9, 2026 01:17
Memoized results are keyed by the root node id, and the key is read
before the method runs. A buffer edited since the last parse still
carries the old root, so the cache hands back headlines whose nodes
point into text that no longer exists. Reading them threw
"Index out of bounds" for callers that walk files without a reload
first (telescope-orgmode building its refile picker, org-roam).

The memoize key getter now compares the buffer's changedtick with
the one recorded at the last parse and parses first when they
differ. The earlier approach of swallowing the error in
get_node_text is dropped: it hid the stale data instead of
refreshing it.

Refs: nvim-orgmode#1076
Walks a buffer-backed file the way telescope-orgmode builds its
refile picker: headlines, then a property, with no reload in
between. Fails with "Index out of bounds" without the reparse.

Refs: nvim-orgmode#1076
@seflue
seflue force-pushed the fix/treesitter-parsing-errors branch from 7e9471b to b4c8e67 Compare September 13, 2026 13:31

@kristijanhusak kristijanhusak left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the PR, and sorry for the delay.
I left a comment on the logic, we can simplify it a bit.

local memoize = Memoize:new(OrgFile, function(self)
local bufnr = self:bufnr()
if bufnr > -1 and self._parse_tick ~= vim.api.nvim_buf_get_changedtick(bufnr) then
self:parse()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No need to parse here, the method that is being called should do the parsing when it's really called. We just need to make sure that the cache is invalidated. We can append the changedtick to the id, and that does the trick. This works for me:

local memoize = Memoize:new(OrgFile, function(self)
  local tick = 0
  local bufnr = self:bufnr()
  if bufnr and bufnr > -1 then
    tick = vim.api.nvim_buf_get_changedtick(bufnr)
  end
  return {
    file = self,
    id = table.concat({ 'file', self.root and self.root:id() or '', tick }, '_'),
  }
end)

We also don't need to track the _parse_tick any more.

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