ESC sets a flag that makes the next key a Meta command, but arrows,
mouse events, Enter, Tab and control chords returned before the flag
was checked and left it set. A later plain key then ran as a Meta
command: ESC, Right, d deleted a word instead of inserting 'd'. The
flag is now consumed up front, and the Meta lookup runs before the
control-chord and Enter handling so ESC ^H (meta-backspace) and ESC
Enter (SmartNewline) reach their bindings.
Verified by driving kte in a pty: ESC, Right, d on 'hello world' now
gives 'hdello world' (was 'hworld').
Audit finding B3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
- Regex replace-all, reflow and kill-region replaced or deleted whole
rows, which added or left a stray newline when the text ended on a
last line without one. They now edit the byte range in place.
- Regex replace-all no longer matches the empty position after a final
newline. Before, a zero-width pattern such as ^ matched it, added a
line, and matched the new empty last row again: the editor hung.
- Kill-line on the last row kills an unterminated line with the newline
before it, and does nothing on the empty row after a final newline
(it used to record an undo step that added a line).
- The visual-line newline records its splits as one undo step, and
places the cursor correctly for a repeat count.
- Buffer::delete_text computes its byte range directly instead of
materializing every line it spans.
Audit findings D3, D4, D5.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
- Replay recovers every complete record before a torn final record (the
normal result of a crash mid-append) and reports it through err,
instead of rejecting the whole journal. SwapReplay_TruncatedLog is
updated to the new behaviour.
- A record that cannot be written (open/write failure, breaker open)
marks the journal as having a gap. Later position-based records are
dropped until a checkpoint re-establishes the content; recorders
request one on the next edit (at most once per second).
- A failed append is truncated back off the file, and a failed header
write closes the file so the next attempt starts fresh.
- Inserts over the 24-bit record limit are split into consecutive
records; buffers too large to checkpoint are no longer snapshotted
and rejected on every checkpoint.
- Declining to delete an unreadable swap moves it to .corrupt so new
edits are not appended behind the bad data.
- NotifyFilenameChanged no longer writes a full checkpoint into the
journal it is about to delete.
Audit findings D7, P5.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
A secondary GUI window shared the primary's buffer vector but attached
buffers through its own SwapManager. Closing the window destroyed that
manager while buffers still held its recorders, so the next edit called
into freed memory; Rehome through the wrong manager also dropped
journaling for the primary's buffers.
Audit finding M3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
The worker read the Buffer (PieceTable pieces and add buffer) while the
main thread edited it with no shared lock, and kept a raw Buffer* that
dangled once the buffer vector reallocated or shifted. PrefetchViewport
now only highlights the visible rows synchronously.
Audit finding M2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
- SmartNewline recorded its indent insert after moving the cursor, so
undo deleted the wrong text; it is now recorded at the insertion
column and grouped with the newline. With a prompt open it now just
accepts the prompt (D1, B11).
- Scroll up/down, jump-to-mark and replace-all clamp the cursor to the
buffer, so edits are never made past end-of-line or end-of-buffer
(D2, M5).
- Replace-all is one undo step and no longer rebuilds Rows() per match
(D5, P4).
- Reflow on a blank line no longer merges adjacent paragraphs (D6).
- ensure_cursor_visible uses Nrows()/GetLineString instead of Rows() and
GetLineView, avoiding a whole-file rebuild per command (P1, P2).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
set_escdelay is an ncurses function, not a macro, so the #ifdef guard
compiled it out and ESC waited the 1 s default. KEY_RESIZE was mapped
to Refresh, the C-g cancel handler, which cleared the mark and
cancelled open prompts (including swap recovery) on every resize.
Audit findings B1, B2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
SetDirty(false) did not bump the version, so undo back to the saved
state (and reload, and replace_all_bytes) left the highlighter serving
spans for the old text. Reloading a file deleted on disk now also
clears undo history recorded against the old content.
Audit finding D9.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
Recursion depth equalled history length, so destroying or clearing a
buffer with a few hundred thousand undo steps overflowed the stack.
UndoSystem::clear() also freed only the first root branch, leaking the
root's siblings; it now frees the whole forest.
Audit finding M4.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
Copy/move assignment kept the destination's line index, so closing a
buffer (vector erase move-assigns later buffers down) left the shifted
buffer with its predecessor's line offsets and out-of-bounds line views.
Appends that merged into an existing piece also skipped invalidation.
Audit findings M1, B12.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB