Skip to content

Commit 7e9471b

Browse files
committed
fix(files): handle stale treesitter nodes gracefully
When a buffer changes after parsing, node positions become invalid. Calling get_node_text on stale nodes throws "Index out of bounds" errors, crashing async callers like org-roam. Changes: - Wrap get_node_text in pcall, return '' on failure - Add is_tree_stale() for callers needing explicit detection - Track buffer changedtick at parse time for comparison
1 parent e448c72 commit 7e9471b

File tree

1 file changed

+25
-2
lines changed

1 file changed

+25
-2
lines changed

lua/orgmode/files/file.lua

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ local Buffers = require('orgmode.state.buffers')
3030
---@field metadata OrgFileMetadata
3131
---@field parser vim.treesitter.LanguageTree
3232
---@field root TSNode
33+
---@field _parse_tick? number Buffer changedtick at last parse (for staleness detection)
3334
local OrgFile = {}
3435

3536
local memoize = Memoize:new(OrgFile, function(self)
@@ -193,6 +194,13 @@ function OrgFile:parse(skip_if_not_modified)
193194
self.parser = self:_get_parser()
194195
local trees = self.parser:parse()
195196
self.root = trees[1]:root()
197+
198+
-- Track changedtick for staleness detection
199+
local bufnr = self:bufnr()
200+
if bufnr > -1 then
201+
self._parse_tick = vim.api.nvim_buf_get_changedtick(bufnr)
202+
end
203+
196204
return self.root
197205
end
198206

@@ -487,21 +495,36 @@ function OrgFile:get_node_at_cursor(cursor)
487495
return self.root:named_descendant_for_range(row, col, row, col)
488496
end
489497

498+
---Get text for a treesitter node
499+
---Uses pcall to gracefully handle stale nodes (when buffer changed after parse)
490500
---@param node? TSNode
491501
---@param range? number[]
492502
---@return string
493503
function OrgFile:get_node_text(node, range)
494504
if not node then
495505
return ''
496506
end
507+
508+
local source = self:get_source()
509+
local ok, result
510+
497511
if range then
498-
return ts.get_node_text(node, self:get_source(), {
512+
ok, result = pcall(ts.get_node_text, node, source, {
499513
metadata = {
500514
range = range,
501515
},
502516
})
517+
else
518+
ok, result = pcall(ts.get_node_text, node, source)
519+
end
520+
521+
if not ok then
522+
-- Node positions are stale (buffer changed since parse)
523+
-- Return empty string for graceful degradation
524+
return ''
503525
end
504-
return ts.get_node_text(node, self:get_source())
526+
527+
return result
505528
end
506529

507530
---@param node? TSNode

0 commit comments

Comments
 (0)