From 7e9471bb491ff5efccee8498538065c8e19c7c81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Fl=C3=BCgge?= Date: Mon, 29 Dec 2025 08:26:18 +0100 Subject: [PATCH] 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 --- lua/orgmode/files/file.lua | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/lua/orgmode/files/file.lua b/lua/orgmode/files/file.lua index 3829ce21b..1e32d7173 100644 --- a/lua/orgmode/files/file.lua +++ b/lua/orgmode/files/file.lua @@ -30,6 +30,7 @@ local Buffers = require('orgmode.state.buffers') ---@field metadata OrgFileMetadata ---@field parser vim.treesitter.LanguageTree ---@field root TSNode +---@field _parse_tick? number Buffer changedtick at last parse (for staleness detection) local OrgFile = {} local memoize = Memoize:new(OrgFile, function(self) @@ -193,6 +194,13 @@ function OrgFile:parse(skip_if_not_modified) self.parser = self:_get_parser() local trees = self.parser:parse() self.root = trees[1]:root() + + -- Track changedtick for staleness detection + local bufnr = self:bufnr() + if bufnr > -1 then + self._parse_tick = vim.api.nvim_buf_get_changedtick(bufnr) + end + return self.root end @@ -487,6 +495,8 @@ function OrgFile:get_node_at_cursor(cursor) return self.root:named_descendant_for_range(row, col, row, col) end +---Get text for a treesitter node +---Uses pcall to gracefully handle stale nodes (when buffer changed after parse) ---@param node? TSNode ---@param range? number[] ---@return string @@ -494,14 +504,27 @@ function OrgFile:get_node_text(node, range) if not node then return '' end + + local source = self:get_source() + local ok, result + if range then - return ts.get_node_text(node, self:get_source(), { + ok, result = pcall(ts.get_node_text, node, source, { metadata = { range = range, }, }) + else + ok, result = pcall(ts.get_node_text, node, source) + end + + if not ok then + -- Node positions are stale (buffer changed since parse) + -- Return empty string for graceful degradation + return '' end - return ts.get_node_text(node, self:get_source()) + + return result end ---@param node? TSNode