Skip to content

fix: Stop rebuilding parsers for files changed on disk - #1206

Merged
seflue merged 2 commits into
masterfrom
fix/notification-stale-reparse
Nov 9, 2026
Merged

seflue merged 2 commits into
masterfrom
fix/notification-stale-reparse

Conversation

@seflue

@seflue seflue commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

When an org file changes on disk while it is not open in a buffer, Neovim's memory keeps growing.

Such a file stays modified until it is reloaded, because only reload() updates the mtime that is_modified() compares against. Since #1204, a file whose buffer got unloaded ends up in the same state. _get_parser() takes "modified" to mean "the text changed", so every parse(true) on that file builds a new parser and tree from the unchanged self.content.

With notifications enabled, the check runs once a minute and never reloads, since files:load() returns early once the files are loaded. It reaches parse(true) through is_archived() and is_done(). Each new tree has a new root id, which clears the memoize cache, so the next headline parses again. So every check builds about two new trees per headline of such a file. The clock statusline does the same on every update while such a file is clocked in.

Two commits:

  • The notification check reloads a file that is not open in a buffer before parsing it. The reload reads the new content and stores the new mtime, so the file is no longer modified and later parses return the existing tree. A file that cannot be read again keeps its old content, and the other files are still checked. Files open in a buffer are left out, because they use the buffer's parser.
  • _get_parser() builds a new string parser only when the parser's source is not the current self.content. This covers every caller, not only notifications. A reload with new text gets a new parser, and so does a file whose buffer got unloaded, because that parser's source is a buffer number.

The second commit alone stops the growth, but only the first makes notifications use the new content from disk.

The reload still parses the changed file once, synchronously. Making that async would change every caller of reload(), so I left it out.

A file that changes on disk while it is not open in a buffer stays
modified forever, because only a reload updates the stored mtime.
Every skip-if-unmodified parse then builds a new tree from the old
content. The notification check runs this for each headline every
minute, so Neovim's memory keeps growing.

The notification check now reloads such files before parsing them.
This reads the new content and clears the modified state, so the
later parses return the existing tree. Files open in a buffer are
left out: they use the buffer's parser, so they do not rebuild trees.
A file that cannot be read again keeps its content, and the other
files are still checked.

The reload still parses the file once synchronously. Making it async
requires reload itself to stop parsing, which changes every caller.
Neovim's memory grows while a file that changed on disk is clocked in
or read repeatedly without a reload. Each read built a new tree from
the same text, and the new tree dropped the cached results, so the
next read parsed again. The clock statusline does this on every
update.

Such a file stays modified until it is reloaded, and a modified file
got a new parser on every parse. The parser is now reused as long as
its text is the current content of the file. A reload with new text,
or a buffer that got unloaded, still builds a new one.

This change only stops the rebuilding. The file keeps its old content
until something reloads it.
@seflue
seflue force-pushed the fix/notification-stale-reparse branch from 1158744 to be7adf1 Compare November 9, 2026 01:14
@seflue
seflue requested a review from kristijanhusak November 9, 2026 01:21
@seflue
seflue merged commit 08c3ea6 into master Nov 9, 2026
14 checks passed
@seflue
seflue deleted the fix/notification-stale-reparse branch November 9, 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