Repository navigation
fix(api): Keep handles valid and unsaved edits out of files - #1204
Merged
Merged
Conversation
seflue
force-pushed
the
fix/api-handle-bugs
branch
from
October 31, 2026 20:36
f7eaae5 to
85a0391
Compare
seflue
force-pushed
the
fix/api-handle-bugs
branch
from
October 31, 2026 22:31
85a0391 to
7d687b4
Compare
seflue
force-pushed
the
fix/api-handle-bugs
branch
from
October 31, 2026 22:48
7d687b4 to
4e65a5a
Compare
seflue
force-pushed
the
fix/api-handle-bugs
branch
from
October 31, 2026 23:15
4e65a5a to
2062f42
Compare
Loading files by a list of names always returned an empty list, because each file name was compared with the whole list. Orgmode itself only loads files by a single name, so only callers outside orgmode were affected. Match file names against the list entries, after resolving each entry to an absolute path the way a single file name is resolved.
Creating an ID through a headline handle, for a file that is not the current buffer, returned the ID before the property was written. A link made from that ID right away pointed to a headline that did not have the ID yet. Orgmode itself creates IDs on its internal headlines, so only callers outside orgmode were affected. Wait for the property to be written before returning the ID.
seflue
force-pushed
the
fix/api-handle-bugs
branch
from
November 2, 2026 08:04
2062f42 to
1895019
Compare
seflue
force-pushed
the
fix/api-handle-bugs
branch
from
November 2, 2026 20:12
1895019 to
20c0663
Compare
seflue
force-pushed
the
fix/api-handle-bugs
branch
from
November 3, 2026 07:05
20c0663 to
cc5c466
Compare
seflue
force-pushed
the
fix/api-handle-bugs
branch
2 times, most recently
from
November 3, 2026 07:21
5333ccc to
5c528d1
Compare
seflue
force-pushed
the
fix/api-handle-bugs
branch
from
November 8, 2026 14:12
ade1401 to
3918011
Compare
seflue
force-pushed
the
fix/api-handle-bugs
branch
from
November 8, 2026 18:26
3918011 to
cb0e54f
Compare
seflue
force-pushed
the
fix/api-handle-bugs
branch
from
November 8, 2026 19:08
cb0e54f to
4eb7693
Compare
A headline handle found its headline again by index or line number. After lines were added above it, reload returned another headline and mutators changed another headline. A handle with an ID now finds its headline only by that ID. Without an ID, it uses: - the line number while the file is unchanged since the handle was built, - otherwise the outline path and headline text, leaving out statistics cookies, which change when child headlines change. When the headline cannot be found unambiguously, the call raises an error instead of acting on a guess. Refile, links and ID creation use the same lookup. Each mutator refreshes its handle, so it can be used for several calls. If a write hook changes the text during the save, a handle without an ID follows an extmark set on its headline line.
A file open in a buffer was read from disk when the file on disk changed, although edits act on the buffer text. After the buffer was unloaded, the file kept the text of the discarded buffer. While a buffer is loaded, its text is the file content, also after the buffer was replaced by a new one. A buffer without unsaved edits is first read again when the file on disk changed, without asking. Once the buffer is gone, the file is read from disk again. Loading all files through the API reloads them first, so their handles are built from the current text, and keeps the loaded content of a file that cannot be read again.
Changing a file from the API, the agenda or the clock saved the whole buffer. If the buffer already had unsaved edits, they were written to disk with the change. Closing the temporary window unloaded a hidden buffer and discarded the change and the user's edits. Save only buffers that had no unsaved edits before the change. Close the temporary window without unloading the buffer. A refile within the same file changes the headline level in the same step, so the level change is saved with the move. A change that throws or rejects is never saved. It is undone, unless the buffer was edited while the change was pending or the undo history no longer reaches back before it. Then it stays unsaved in the buffer, where the user can see and undo it.
seflue
force-pushed
the
fix/api-handle-bugs
branch
from
November 8, 2026 19:41
4eb7693 to
4fbc097
Compare
kristijanhusak
approved these changes
Nov 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes five bugs in the Lua API and in how files are read and saved. Each commit adds the specs that fail without it. None of these cases had a test before, because the API specs build every handle on an unchanged file, call
id_get_or_create()only on the current buffer, and never pass a list toload().load()with a list of file namesCalling
OrgApi.load()with a list of file names always returned an empty list, because each file name was compared with the whole table. It now checks whether the list contains the name, after resolving each entry to an absolute path the way a single name is resolved. Orgmode itself only loads files by a single name, so only callers outside orgmode were affected.id_get_or_create()in another fileCalling
id_get_or_create()on a headline in a file other than the current buffer returned the ID before the property was written. A link built from that ID right away pointed to a headline that did not have the ID yet. It now waits forset_property()before returning. Orgmode itself creates IDs on its internal headlines, so this also only affected callers outside orgmode.Handles act on another headline after edits
A headline handle stops matching its headline once lines are inserted above it.
reload()returned the headline at the old index, mutators put the cursor on the old start line, andrefile()andget_link()used the internal headline of the old parse. So a plugin calling the API could change, move or link another headline than the one it meant, without any error.Now a handle acts on its own headline or raises an error. It finds the headline like this:
OrgFilegets aversionthat changes only when its content changes.Project A/** Notesis different fromProject B/** Notes:[1/2]are left out of the match, because changing the state of a child or sibling rewrites them.reload()still returns nil when the headline is gone.After a mutator:
get_link()creates a missing ID through the handle, so the handle knows the ID afterwards. Like the other mutators, this saves a buffer that had no unsaved edits.id_get_or_create()reuses an ID the headline got after the handle was built.Limitation: a handle without an ID fails when its own headline line or a parent's title is changed outside the handle between the moment the handle is built and its next use, for example when the user changes the TODO keyword in the buffer in between. That call raises an error, where master still acted on the right headline as long as no lines moved above it. This only affects a plugin that keeps a handle across user edits instead of loading it again before it acts, so I expect it to be rare. Headlines with an ID are not affected. The
OrgApiHeadlinedocs now describe this error and how to recover: load again, find the headline and repeat the call. Extmarks would follow such edits, but every load would have to place them. This could be worth a follow-up.Files read from disk while open in a buffer
OrgFile:reload()read a file from disk when the file on disk changed, even while the file was open in a buffer, although edits act on the buffer text. Handles, and the edits made through them, could then come from text that was not in the buffer. After the buffer was unloaded, the file kept the text of the discarded buffer, so orgmode kept working with edits the user had thrown away.Now, for every caller of
reload(), includingOrgFiles:load_file()for a file that is already loaded:noautoread, so the next save does not overwrite the change on disk. A buffer with unsaved edits is never reloaded, so no edit is lost. The user only misses the prompt. The reload runs the normal read autocmds, as:editdoes.OrgApi.load()reloads the files before it builds handles, so the handles come from the current text. A file that cannot be read again keeps the content it was loaded with.Saves write unsaved edits to disk
A change made through the API, the agenda or the clock saved the whole buffer, so edits the user had not saved yet went to disk with it. For a file open in a hidden buffer, the temporary window was closed with
:q!, which unloaded the buffer and threw away both the change and the user's edits.Now a change never writes or discards the user's unsaved edits:
OrgFile:update()saves only a buffer that had no unsaved edits before the change. Edits that autocmds make while the change opens the buffer are saved with it. Otherwise the change stays in the buffer unsaved, and the user saves it together with their own edits.