Skip to content

fix(diff): keep Neovim 0.8 and 0.9 alive when a handler raises during an openDiff setup - #574

Merged
ThomasK33 merged 11 commits into
mainfrom
fix/tool-coroutine-jit-guard
Nov 12, 2026
Merged

ThomasK33 merged 11 commits into
mainfrom
fix/tool-coroutine-jit-guard

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

Why

The goal of this series: land the behavior fixes and a code-shape refactor of this plugin on main in small PRs, fixes first. This PR is one fix.

On Neovim 0.8 and 0.9, a Lua autocmd handler of the user can make Neovim exit at once (exit 134 or 139) while the plugin opens or closes the windows of an openDiff diff. A user can lose the unsaved changes of open buffers when that happens. #538 fixed one case. Other cases still abort.

The cause, in three sentences. Neovim 0.8 and 0.9 bundle a LuaJIT older than its commit e86990f7f of 2023-09-21, which fixes LuaJIT issue 1066: after a C function runs Lua on another thread and then raises into a pcall, cur_L still names that thread, and the next exit from a compiled trace crashes (the issue shows a crash in lj_trace_exit). A Neovim API call in a coroutine that runs a Lua callback and then raises behaves like that C function, and Neovim aborts (measured, below). The plugin runs openDiff in a coroutine, and Neovim runs the handlers of the user while the plugin opens and closes its windows.

What a source shows: the issue text and the patch of e86990f7f, which sets cur_L again in err_unwind (src/lj_err.c, case FRAME_PCALL). We did not read the LuaJIT source for other code that uses cur_L. The proof below measures what happens instead.

What changed

  • utils.resume_tool_coroutine(co, ...) is new. On an affected LuaJIT it turns the JIT off and flushes the compiled traces before it resumes the coroutine, and it gives the JIT back as it was when the resume returns. Everywhere else it is a plain coroutine.resume. It returns exactly what coroutine.resume returns, nil values and their count included.
  • "Affected" means: the global jit exists and jit.version is not a rolling 2.1.<n> release with n of 1695261248 or more. LuaJIT 2.1.0-beta3 (Neovim v0.8.0) and LuaJIT 2.1.1692716794 (Neovim v0.9.5) are affected. A version that is no string, or that the check cannot read, counts as affected, which costs only a flush.
  • handle_invoke and the answer to a blocking diff (open_diff_blocking) resume the tool's coroutine through it. git grep -n 'coroutine\.resume' -- lua plugin shows one code line, inside the helper.
  • fix(diff): keep Neovim alive when a handler raises during a failed openDiff setup #538's call (nvim_get_current_tabpage() before the rethrow in _setup_blocking_diff) and its comment are gone. The cases of fix(diff): keep Neovim alive when a handler raises during a failed openDiff setup #538's spec stay, and their assertions are unchanged. Only the comment above the eager-JIT case changed.
  • .agents/skills/verify-diff-lifecycle/check.sh: the fact SetupAnswerReturned now needs the assignment line local ... = utils.resume_tool_coroutine(co, result). A comment on a line of its own cannot satisfy it. A comment at the end of a plain-resume line leaves the fact TRUE, and check.sh then exits 3 (stale). The job covered-lock runs every check.sh and fails on any non-zero exit, exit 3 included (.github/workflows/test.yml, the loop with status=1). The model is unchanged.
  • Docs: lua/claudecode/tools/README.md names the helper, the LuaJIT condition and the cost, and lua/claudecode/README.md lists the helper. The comment above the helper in utils.lua names the LuaJIT builds and the Neovim versions that the bug affects.

Why #538's call goes away

#538's call is on main in every run below, and Neovim still died. The probe runs the tool path (tools.handle_invoke) with 8 raising handlers, 2 passes, and an eager JIT. Two verifiers measured it on two main commits. Each cell counts the runs that killed Neovim, of 30. The counts depend on the host and its load: the same probe on main gave 13 of 30 in one run and 8 of 30 twice on v0.9.5.

Tree v0.8.0 v0.9.5
main 884f7e22 (round 1 verifier) 27 13
main 523ee200 (round 2 verifier) 24 8 and 8
this PR (both verifiers) 0 0

Source: the two verifier reports (verify-f55-r1-opus.md and verify-f55-r2-opus.md), kept outside the repository.

In the round 1 verifier's runs on main 884f7e22, a handler on WinEnter makes every setup fail and killed Neovim in 17 of 20 runs on v0.8.0 and in 17 of 20 on v0.9.5. Handlers on WinClosed, BufWinLeave, BufHidden, BufUnload, BufWipeout and a mix of them, which the plugin's own pcall swallows while the setup goes on, killed Neovim in 0 of 20 runs on both versions. The new spec case for a setup that succeeds therefore guards that the answers of openDiff stay as they are. It does not show the bug.

Proof

Evidence folder: local files, not in the repository (evidence/round1/). File names below are relative to it. The facts below are the ones that a verifier reproduced, or that I measured on the head named in the file.

  1. The eager case shows the bug and the guard removes it. With the helper forced to a plain coroutine.resume, the eager case of diff_failed_setup_guards_spec.lua alone ended without a plenary summary in 6 of 10 runs on v0.8.0 and 8 of 10 on v0.9.5 (round 2 verifier, all with exit 134 bad stack slot), and in 9 of 10 and 6 of 10 (round 1 verifier). The three other cases of that file kept their summary in 10 of 10 runs on each version (round 2 verifier). With the guard, the head had 0 of 10 without a summary in round 1.

  2. Each part of the fix has a test that turns red without it (a scratch mutant per row, never committed). The two verifiers ran the mutants again. "Unit" is utils_resume_tool_coroutine_spec.lua and tool_coroutine_resume_spec.lua. "Spec" is tests/integration/diff_tool_coroutine_jit_spec.lua on v0.8.0 and v0.9.5. The unmutated tree is green everywhere. Source: R1_mutants_matrix.txt and R1_mutants_failed_tests.txt.

    Removed part Red in the unit suite Red in the spec
    no jit.off() "turns the JIT off, flushes its traces and turns it on again" and others "runs the coroutine of a tool with the JIT off and no compiled trace, and turns the JIT on again"
    no jit.flush() the same cases the same case, and "keeps a JIT that was off off"
    no restore (jit.on()) the same cases the first case
    restore turns the JIT on although it was off "flushes the traces but keeps the JIT off when the JIT was off before" "keeps a JIT that was off off"
    first resume not on the helper "goes through the helper when handle_invoke runs the tool" both cases
    second resume not on the helper "goes through the helper when a blocking diff is answered" the first case (it records the JIT at the answer too)
    version check reversed "leaves the JIT alone on ..." and others both cases
    tostring removed from the version check "treats no version / a number / a table as the version as affected" none: real Neovim always sets a string

    The verifiers also showed that with a plain coroutine.resume(co, result) at the diff answer, SetupAnswerReturned goes FALSE and check.sh exits 1.

  3. Nothing else changes for a user. One openDiff accept (:w) and one reject (:q) through the WebSocket, with .agents/skills/verify-claudecode-nvim, on main and on this PR, on v0.8.0 and 0.12.3: the answers (FILE_SAVED plus the proposal text, DIFF_REJECTED plus the tab name, TAB_CLOSED) and the window and buffer states (names, diff mode, text) are byte-equal in 10 of 10 files on each version. The round 2 verifier reproduced this. Source: R1_vcn_accept_reject_main_vs_head.txt, vcn/.

  4. Stability under load. Whole-file runs of diff_tool_coroutine_jit_spec.lua and diff_failed_setup_guards_spec.lua, one Neovim per run, pinned to one core with one busy loop on the same core. Runs that failed or ended without a summary: 0 of 100 on v0.8.0, 0 of 100 on 0.12.3 (both verifiers) and 0 of 30 on v0.9.5 (round 1 verifier). My own runs gave the same (R1_load_counts.txt). No run used a retry.

  5. Gates. On this head (579df505d800f11fa019607b99b219dbe3a52d6e, which merges main c43b6a308ea97f9dcfb0844ce6770e7ce7699ec0): covered.lock was the only conflict of that merge. I took main's file and ran check.sh --record, and the merge commit holds the recorded file. bash .agents/skills/verify-diff-lifecycle/check.sh ends == RESULT: PASS. mise run format-check: 438 files, 0 changed. mise run check: exit 0, and luacheck reports 0 warnings and 0 errors in 196 files. mise run test: 1858 successes, 0 failures, 0 errors, 34 pending. Four integration specs, once on 0.12.3 and once on v0.8.0, with 0 failures and 0 errors each time: diff_tool_coroutine_jit_spec.lua 3 successes, diff_failed_setup_guards_spec.lua 4, and the two specs that fix(diff): remove the legacy diff's temp file and its buffer when a step fails #516 added or changed, diff_legacy_cleanup_spec.lua 18 and diff_legacy_swap_spec.lua 1. The whole integration runner did not run on this head (CI runs it). My change is the same as at 230be2d2 (the head that the round 2 verifier passed): the added and removed lines outside covered.lock are byte-equal, 469 lines in 10 files. On an earlier head (661e4e52, main 48ca93be merged), the integration runner gave 0.12.3 Total Success: 603, Failed: 0, Errors: 0, and v0.8.0 Total Success: 585, Failed: 0, Errors: 0. A first 0.12.3 run under a host load above 100 lost the summary of three files to the runner's 40 s limit (diff_close_save_spec.lua, diff_exit_reject_spec.lua, diff_left_window_spec.lua). Each passed alone (77, 28 and 33 successes), and the whole runner passed on the second run. The two new specs ran 20 times per version on v0.8.0, v0.9.5 and 0.12.3 on 661e4e52 with 0 failed runs and 0 runs without a summary. The round 2 verifier ran the gates on 230be2d2: mise run test 1758 successes, 0 failures, 0 errors, 29 pending, the integration runner 530/0/0 on 0.12.3 and 393/0/0 on v0.8.0, and check.sh PASS. Its counts are lower than mine because main had fewer tests then. Sources: ../round4/ for this head, ../round3/ for the head before it, and ../round2/R2_*.txt and ../round2/m2_int_*.txt for the older heads.

  6. Where the Neovim binaries come from (R1_binary_provenance.txt, reproduced by the round 2 verifier).

    • v0.8.0: the asset nvim-linux64.tar.gz of the official GitHub release v0.8.0 (published 2022-09-30), fetched with gh release download v0.8.0 -R neovim/neovim -p 'nvim-linux64.tar.gz*'. Its sha256 matches the .sha256sum file and the binary that was run. --version prints NVIM v0.8.0-1210-gd367ed9b2.
    • v0.9.5: the asset nvim-linux64.tar.gz of the official GitHub release v0.9.5 (published 2023-12-30), fetched the same way, with the same sha256 checks.
    • 0.12.3 is the mise install of this repository.

Cost on Neovim 0.8 and 0.9

On Neovim 0.8 and 0.9, each tool call that runs in a coroutine (openDiff) discards the compiled traces of the whole editor, and LuaJIT compiles them again when the code gets hot. The round 1 verifier measured it with about 740 compiled traces: a guarded resume took 18 to 41 µs (a plain resume took 1.1 µs or less), and the first run of a hot loop of 300 small functions after the flush took 3.6 to 10.3 ms against 0.3 to 0.6 ms when hot. Nobody measured a whole editing session. A LuaJIT that is not affected gets a plain coroutine.resume.

What this PR does not cover

  • A coroutine of another plugin or of a test harness. The helper covers the coroutines of the plugin's tools only. Plenary runs each spec file in its own coroutine, so a spec that calls _setup_blocking_diff directly is not covered. Such a spec must go through tools.handle_invoke.
  • scripts/manual_test_helper.lua resumes its own coroutine. It is a manual script and not part of the plugin.

Generated with xum • Model: anthropic:claude-sonnet-5-5 • Thinking: xhigh

… an openDiff setup

A LuaJIT older than its commit e86990f7f (Neovim 0.8 and 0.9) keeps
pointing at the main Lua thread when a Neovim API call runs a Lua callback
and then raises into a pcall inside a coroutine. The next exit from a
compiled trace corrupts that thread's stack and Neovim aborts.

utils.resume_tool_coroutine resumes the coroutine of a tool with the JIT
off and its traces flushed on such a LuaJIT, and gives the JIT back
afterwards. handle_invoke and the answer to a blocking diff both use it.
The API call before the rethrow in _setup_blocking_diff (#538) is gone: it
fixed one case and left the others.

---
_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-sonnet-5-5` • Thinking: `xhigh`_
A jit table with no version string made claudecode.utils fail to load.
tostring turns a missing or non-string version into a string that the
check cannot read, and such a version counts as affected like the others.

The comment above the check now cites LuaJIT issue 1066 and its fix
commit, and says that we did not read the LuaJIT source for other code
that uses cur_L.

---
_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-sonnet-5-5` • Thinking: `xhigh`_
<!-- xum-attribution: model=anthropic:claude-sonnet-5-5 thinking=xhigh -->
The fact matched the resume line anywhere in the block, comment lines
included. So a plain coroutine.resume plus a comment that names
utils.resume_tool_coroutine kept the fact TRUE and check.sh passed. The
pattern now needs the assignment line of the code.

---
_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-sonnet-5-5` • Thinking: `xhigh`_
<!-- xum-attribution: model=anthropic:claude-sonnet-5-5 thinking=xhigh -->
The README named Neovim versions, but the code decides by the LuaJIT
version string. It now names the condition (a LuaJIT build older than
2023-09-21, or an unreadable version) and says what the plugin does:
it turns the JIT off and flushes the traces before it resumes a tool's
coroutine, and gives the JIT back as it was.

The spec comments cite LuaJIT issue 1066 for the crash and drop the
claims about the cause that no source shows.

---
_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-sonnet-5-5` • Thinking: `xhigh`_
<!-- xum-attribution: model=anthropic:claude-sonnet-5-5 thinking=xhigh -->
…t-guard

covered.lock: took main's version. The next commit records it again.
… merging main

---
_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-sonnet-5-5` • Thinking: `xhigh`_
<!-- xum-attribution: model=anthropic:claude-sonnet-5-5 thinking=xhigh -->
The comment said that the three earlier cases hit the bug in a few of
100 runs without the guard. Without the guard, they ended with a plenary
summary in 100 of 100 runs each on v0.8.0 and on v0.9.5. Only the eager
case shows the bug, and the comment now says so.

---
_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-sonnet-5-5` • Thinking: `xhigh`_
<!-- xum-attribution: model=anthropic:claude-sonnet-5-5 thinking=xhigh -->
…t-guard

covered.lock: took main's version. The next commit records it again.
… merging main

---
_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-sonnet-5-5` • Thinking: `xhigh`_
<!-- xum-attribution: model=anthropic:claude-sonnet-5-5 thinking=xhigh -->
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-12T19:17:08.949536Z 579df50 Manual request
🔒 Security Review ✅ Completed 2026-11-12T19:17:01.749354Z 579df50 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

Reviewed commit: 67b1ec79a6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 67b1ec79a6

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

covered.lock: took main's version. check.sh --record wrote it again for the merged diff.lua.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 579df505d8

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

Reviewed commit: 579df505d8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ThomasK33
ThomasK33 added this pull request to the merge queue Nov 12, 2026
Merged via the queue into main with commit 7dbad04 Nov 12, 2026
7 checks passed
@ThomasK33
ThomasK33 deleted the fix/tool-coroutine-jit-guard branch November 12, 2026 19:45
hareki pushed a commit to hareki/claudecode.nvim that referenced this pull request Nov 13, 2026
…oder#577)

## Why

In the `vertical` and `horizontal` layouts, a proposal whose line breaks
are all CRLF was loaded into the proposal buffer with `'fileformat'`
`unix` and a CR at the end of each line. The save joined the lines with
LF. So a line that the user typed, or the join of two lines, came back
with LF, or with a CR in the middle of a line, where Neovim's `:w`
writes CRLF. A CRLF proposal that equals a CRLF file also showed every
line as changed, because the file's own buffer is `dos` and holds its
lines without CRs. I found it while verifying coder#551.

## What changed

1. `_setup_blocking_diff` (`lua/claudecode/diff.lua`) loads a proposal
as Neovim loads a file. When `'fileformats'` includes `dos`, and the
proposal has a line break and only CRLF line breaks, the buffer gets
`'fileformat'` `dos` and lines without their CRs. Any other proposal
gets `'fileformat'` `unix` and is split at LF, as before. The diff sets
`'fileformat'` in both cases, because a new buffer takes the first entry
of `'fileformats'`, and that entry can be `dos`.
2. `_resolve_diff_as_saved` joins the lines with the line break of the
buffer's `'fileformat'` (LF, CRLF for `dos`, CR for `mac`), and adds the
final one when `'eol'` is on. Before, it always used LF. So `:setlocal
fileformat=dos` on an LF proposal now answers CRLF, as `:w` writes it.
3. The `unified` layout does not change. Its buffer is read-only, and
its answer is the proposal.
4. `lua/claudecode/README.md` states both rules, and that one empty line
is saved as one line break of the buffer's `'fileformat'`.
5. `tests/integration/diff_accept_crlf_spec.lua` is new (159 tests).
`tests/mocks/vim.lua` gives the mock `vim.o` the default `fileformats =
"unix,dos"`, because the unit specs of the diff now read it.
6. `covered.lock` of `verify-diff-lifecycle` records the new hashes of
`diff.lua` (the file, `resolve_saved` and `setup_blocking`).

## What a user sees

1. A line that the user types into an accepted CRLF proposal, or two
lines that the user joins, comes back with CRLF, as `:w` writes it. For
the proposal `"a\r\nb\r\n"` and a typed line after the last line, main
answers `"a\r\nb\r\nx\n"` and this PR answers `"a\r\nb\r\nx\r\n"`.
2. A CRLF proposal that equals the CRLF file shows no difference. Main
marked every line.
3. An unedited proposal still comes back as exactly the proposal, in
every layout, under every `'fileformats'` value that I ran.
4. Nothing else changes for the default `'fileformats'` (`unix,dos`):
the controls of the spec (an LF proposal, a proposal with CRLF and LF
line breaks, a lone CR, no line break, unedited, emptied) give the same
bytes on main and here.
5. `'fileformats'` `unix` (no `dos`) keeps a CRLF proposal as `unix`
with its CRs, as the file's own buffer does. The equal-text view shows
no difference there, and a typed line gets LF, as `:w` writes it.

## Verification

The spec compares the answer with the bytes that Neovim's own `:w`
writes for the same text after the same commands (an oracle that reads
the text as a file under the same `'fileformats'`), or with an exact
expected text.

1. Red, then green, run on Neovim 0.12.3, 0.11.5, 0.10.4, 0.9.5 and
0.8.0 (all five give the same counts): 159 tests pass with the fix. With
`origin/main`'s `diff.lua`, 105 pass and 54 fail. The unified cases run
on 0.8.0 too (coder#564).
2. Fourteen mutants of the changed lines are each red on Neovim 0.12.3
(the last seven also on 0.8.0, with the same counts). Failed tests per
mutant:
- Split and join: the CRs stay in the lines 70, the join always uses LF
66, the final break is always LF 56, `mac` is ignored 2, dos for any
text with a CRLF 12, dos for a text with no line break 2, dos even with
a lone LF 32.
- `'fileformats'` check: no check (always dos) 8, an exact match of
`unix,dos` or `dos,unix` 4, dos for any value except `unix` 4, dos only
when it is the first entry 42, `'fileformat'` always `unix` 64, always
`dos` 48, set only for `dos` 12.
3. A probe of 1860 cases per run on Neovim 0.12.3 and 0.8.0 (three
layouts, four old files, 15 proposals, 15 edits) compares each answer
with the bytes of a fresh `nvim --clean` that reads the proposal as a
file under the same `'fileformats'`, runs the same commands and writes
with `:w`. The columns give mismatches. The two Neovim versions give the
same numbers.

| `'fileformats'` | main | this PR | unedited answers that differ from
the proposal (main / this PR) |
   |---|---|---|---|
   | `unix,dos` | 748 | 144 | 0 / 0 |
   | `dos,unix` | 788 | 184 | 0 / 0 |
   | `unix` | 304 | 192 | 0 / 0 |
   | `dos` | 1104 | 516 | 0 / 0 |
   | `unix,mac` | 608 | 544 | 0 / 0 |

Every mismatch that remains is one of the items under "Not fixed here",
or a fold (see below). Cleanup leaves no state, buffer, window or tab
behind in any case. Under `unix,dos`, `dos,unix` and `unix`, the
equal-text view marks 0 lines in every case.
4. The only cases where this PR has a mismatch that main does not have
are `dup_line` and `dd_first` (`yyp` and `dd` on line one) against an
old file that equals the proposal, for CRLF proposals: 28 cases for each
of `unix,dos`, `dos,unix` and `dos`. Diff mode folds every line of an
equal text, so those commands act on the whole fold. Main does the same
for an equal LF text (`same|lf|dup_line`), and showed every line of an
equal CRLF text as changed, so it did not fold. This follows from item 2
of "What a user sees".
5. An independent verifier ran its own probe (2604 cases per run) under
`unix,dos`, `dos,unix`, `unix` and `dos` on all five Neovim versions,
and under `unix,dos,mac`, `unix,mac` and `dos,mac` on 0.12.3. In every
run, 0 unedited answers differ from the proposal.

## Not fixed here

These items are tracked in coder#581. All of them exist on `origin/main`
already. An independent probe that compares each answer with `:w` of the
same buffer found this PR never worse than main.

1. A CR inside a line, then `:setlocal ff=mac`. `:w` writes that CR as
NL, and the save keeps the CR. The proposal `"a\rb\n"` gives `"a\rb\r"`,
and `:w` gives `"a\nb\r"`. Main joined with LF, which was also wrong.
2. With `'fileformats'` `unix,mac`, Neovim reads a text such as
`"a\r\nb\r\r\n"` as `mac`. The diff never detects `mac` (main neither).
In 24 cases (an `ff=mac` edit only), main's answer equals the file-read
oracle and this PR's answer does not, because main ignores
`'fileformat'` when it joins. Against `:w` of the same buffer, both are
wrong in these 24 cases.
3. An empty proposal, then typed lines. `'eol'` stays off (coder#542), so
`Gox` gives `"\nx"`, where `:w` of an empty file gives `"\nx\n"`.
4. `'fileformats'` that starts with `dos`, and a proposal with no LF
(`""`, `"a"`, `"a\r"`). Neovim reads it as `dos`. The diff keeps `unix`,
so a typed line gets LF where `:w` writes CRLF.
5. `'fileformats'` with the single entry `dos`. Neovim then reads every
file as `dos`, an LF file too. An LF or mixed proposal stays `unix`, so
a typed line gets LF where `:w` of the file writes CRLF. Neovim reads
the equal file as `dos`, so the equal-text view still marks lines for a
text that is not all CRLF: this PR marks 2 of the 15 probe texts (a
mixed text, and one with a leading lone LF), and main marks 11 of 15.

## Gates

The head is `1c7b5adcfc165963ffc250ab9ebeedad00ddf954`. It merges
`origin/main` at `7dbad04b`. Main moved three times while this PR was in
progress, and each time only `covered.lock` conflicted. The gates below
ran in full at `4525dad1` (the merge with `origin/main` at `c43b6a30`).
The last merge brought coder#574. At the head I ran the checks of item 6, and
CI runs the rest on this head.

1. `mise run all` (`BUSTED_SEED=97531`) exits 0: 1837 successes, 0
failures, 0 errors, 34 pending. luacheck: 0 warnings, 0 errors in 194
files.
2. `mise run format-check` exits 0.
3. `scripts/run_integration_tests_individually.sh` exits 0 on Neovim
0.12.3 (Total Success 828, Failed 0, Errors 0) and on Neovim 0.8.0
(Total Success 810, Failed 0, Errors 0). The new spec alone gives 159
passes on Neovim 0.12.3, 0.11.5, 0.10.4, 0.9.5 and 0.8.0.
4. `check.sh --hash-only` of the four `verify-*` skills exits 0. The
full `check.sh` of `verify-diff-lifecycle` exits 0 (`main` 93477
distinct states, `live-DisconnectedCallsAnswered` 5557). The merge with
main conflicted in `covered.lock` only. I took my side, ran the full
`check.sh` on the merged tree (all 23 facts TRUE, the model passes), and
then ran `--record`. The last merge moved only `file` hashes
(`diff.lua`, `init.lua`, `selection.lua`) and no block. Against
`origin/main`, the lock moves 3 lines: the file hash of `diff.lua`, and
the blocks `resolve_saved` and `setup_blocking`. No guard moved. The
change decides which bytes the accepted text holds. It does not change a
diff's status, who gets its answer, or when.

5. At the head, after the last merge: the full `check.sh` of
`verify-diff-lifecycle` (all 23 facts TRUE, `main` 93477,
`live-DisconnectedCallsAnswered` 5557) and then `--record`. coder#574 changed
the blocks `open_blocking`, `handle_invoke` and `setup_blocking` on
main, and the lock of this PR moves only the 3 lines of this PR against
`origin/main` (the file hash of `diff.lua`, `resolve_saved`,
`setup_blocking`).
6. At the head: `format-check` exits 0, `check.sh --hash-only` of the
four skills exits 0, the new spec gives 159 passes on Neovim 0.12.3 and
0.8.0, and the 44 unit specs of the diff, tools and server modules give
752 successes, 0 failures.

---
_Generated with [`xum`](https://github.com/coder/xum) • Model:
`anthropic:claude-sonnet-5-5` • Thinking: `xhigh`_
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.

1 participant