SDL3 bundling, the setuid fix, the libc++ std::regex test issues and
the C-k modifier fix; the std::regex fallback's libc++ bugs listed as a
known limitation; B10 notes the Qt removal.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- PCRE2: compile with the ANYCRLF newline convention and DOLLAR_ENDONLY,
as ECMAScript reads '.' and '$'. With PCRE2's default (LF only) '.'
matched the CR of a CRLF line, so regex replace-all deleted CRs.
- Replacement $0 is the whole match, two digits are read greedily and a
group the pattern lacks expands to nothing, as std::regex_replace
(libstdc++) does; these cases are now compared in the tests.
- Plain search visits overlapping matches going forward too, and counts
them, as the backward search found them ('aa' in 'aaa' read 2/1).
- The recovery prompt's replayed content is used on 'y' only if the
journal is unchanged since; otherwise it is replayed again.
- A version 1 journal continued after recovery is rewritten as a
compacted version 2 journal when it needs a chunked checkpoint,
instead of receiving records an older kte would skip. (The version is
read through a separate descriptor: the journal fd is write-only.)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
RegexEngine.h/.cc wrap the regex engine for search, regex replace-all and
the terminal and ImGui renderers' match highlighting:
- With KTE_USE_PCRE2 (default ON; used when pkg-config finds libpcre2-8,
linked as -lpcre2-8) patterns are compiled by PCRE2 with its JIT and
matched under a match limit: catastrophic backtracking stops with a
status note instead of hanging the editor, and matching does not
recurse on the C stack (no large-stack thread; the per-keystroke and
per-frame line limit is 1 MiB instead of 2,000 bytes). Otherwise
std::regex is used as before.
- Replacement strings follow std::regex_replace (ECMAScript as libstdc++
reads it) with either engine: the PCRE2 path expands $&, $n, $`, $',
$$ itself and steps over empty matches the same way.
- Regex search walks lines over the materialized text instead of calling
GetLineView per row. The terminal renderer compiles the pattern once
per frame (it compiled it for every row).
- Only one checkpoint per journal is queued at a time: whole-file edits
in a row on a large buffer each queued a full copy of the buffer.
- default.nix and the Dockerfile add pcre2 and pkg-config.
On 26 MB: regex keystroke 1.6 -> 0.14 ms, next 62 -> 6 ms, replace-all
929 -> 438 ms; a rarely matching regex over 105 MB: 1.55 s -> 0.10 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
- A checkpoint larger than one record (16 MiB) is written as CHKPT_BEGIN
[size u64, CRC-32], CHKPT_DATA chunks and CHKPT_END. Replay assembles it
and applies it only when complete with the size and CRC matching; an
unfinished one is a torn tail (valid_bytes stops before it). Buffers
over 16 MiB could not be checkpointed before: a lost record left their
journal behind for good, and they were never compacted. Records are
written as segments straight from the snapshot. Header version 2;
version 1 journals are still read.
- Checkpoint thresholds scale with the buffer size (edits of half its
size; the timer only after a sixteenth; compaction at twice its size;
gap retries spaced by size), and a checkpoint that would compact is
written straight into the compacted journal instead of being appended
and then copied.
- The recovery prompt keeps the content it replayed for its preview and
installs it on 'y', instead of replaying the journal a second time.
- A base file over 64 MiB gets its CRC from the writer thread before it
creates the journal (it had none, so touching such a file made its
journal unusable for recovery).
- kte_tests takes optional name substrings to run a subset of tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
run_search() replaces collecting every match in the buffer (and building
the full list again for every next/previous):
- Plain search finds forward from the search origin (then the current
match) in the materialized text, or backwards through 1 MiB blocks
searched forward (string_view::rfind scans a byte at a time: 511 ms
per previous-match on 105 MB, now 6 ms). Regex search walks lines
from the position with early exit.
- The i/N count stops at 1,000 matches, and for regex after 4 MiB; the
status says 'N+' when capped.
- The first match is the first at or after the cursor (it was the first
in the buffer). Extending the query keeps the current match while it
still matches. Left/Right also step through matches in the regex
replace prompt; Up/Down in a regex prompt use the regex.
On a 26 MB file: plain keystroke 30 ms -> 0.1 ms, next 24 -> 3 ms, regex
keystroke 434 -> 1.6 ms, regex next 420 -> 66 ms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
- Undo texts of 4 KiB or more (Insert/Paste from the buffer, Delete from
the spans the piece table remembers for its last large deletion) are
held as TextSpan references into the piece table's append-only
storage, after checking they hold exactly the recorded bytes. Undo and
redo insert them with Buffer::insert_spans, which adds pieces over the
stored bytes instead of copying them into storage again (the journal
still gets the bytes, as one record). A replace-all session on a 26 MB
file peaks at 181 MB instead of 332 MB; undo/redo cycles of a 6.6 MB
kill no longer grow memory by 6.6 MB each.
- UndoSystem keeps a byte budget (default max(64 MiB, twice the buffer
size); SetByteBudget). Over it, history is pruned to 75% of the budget:
branches off the current path, then the oldest edits (an undo group
goes whole, and never the current state's group), then the redo line.
When the saved state is dropped the buffer stays modified until saved.
- replace_all_bytes clears undo history, which was recorded against (and
may reference the storage of) the previous content.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
- HighlighterEngine caches spans only for rows near the requested row
and trims the cache around it past 16k entries; per-row end states are
still kept to resume lexing. Jumping to the end of a 78 MB C++ file
cached every row's spans (854 MB RSS, now 197 MB), and the first edit
near the top spent 270 ms freeing them.
- Plain and regex search read lines as views into the materialized
buffer instead of Buffer::Rows(), which built (and kept) a string per
line, O(lines x pieces) after edits. Plain search scans the whole text
once and tracks rows by newline.
- transform_rows() applies a per-row rewrite of a row range as one edit
(replace_rows_text). Indent/unindent region and visual-line insert,
backspace, delete, newline and yank used one piece-table edit per row,
each linear in the buffer: 1.1 s per keystroke with 5,000 rows selected
in a 26 MB file (now 4 ms), and minutes to indent a large region.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
- Indent-region skips empty lines. With the whole buffer selected, the
empty row after the final newline became a tab-only last line.
- Reflow no longer wraps before a line's first word: a first word longer
than the width (a URL) got a prefix-only line before it, a blank line
or a bullet marker split from its text, and another on every reflow.
- A whitespace-only line (spaces, tabs, a CR) separates paragraphs for
reflow; such paragraphs were merged.
- Reflow of CRLF lines strips the CR before wrapping and restores it on
every output line; the CRs ended up in the middle of joined lines.
- A counted backspace or delete-char is one undo step even when it joins
lines.
- The C++ highlighter stops looking for a raw-string '(' after 16
delimiter characters; scanning the whole line at every R" was
quadratic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
- The shortened fallback swap name was capped at exactly NAME_MAX, so
compaction's '<journal>.tmp' failed with ENAMETOOLONG on every
checkpoint and the journal grew by a full copy of the buffer each
time. Cap it at NAME_MAX - 4.
- A lone UTF-8 lead byte (8-bit Meta terminals) waited a second for
continuation bytes on every key; wait 250 ms. An invalid byte now ends
a pending ESC/C-k prefix instead of leaving it for the next key.
- Journal notices are shown after a deferred open's own status (which
used to overwrite them), all pending ones at once, and the lock-out
notice is not repeated after every save while the lock is still held.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
- SwapManager queues user notices when a journal is held by another kte
(this session's edits are then not journaled) and, once per gap, when
journal writes fail. Editor shows them in the status line from its
per-frame hook. Both went only to the error log, so crash recovery was
silently off or incomplete.
- The fallback swap name '<basename>.<hash>.swp' exceeded NAME_MAX for
basenames over ~233 bytes, so such files were never journaled; the
basename is shortened (on a UTF-8 boundary) only in that case.
- Audit doc: sixth-round findings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
- Buffer::insert_row recorded its text and newline as two swap records
after inserting both. A checkpoint triggered by the first (1 MiB of
edits, or the checkpoint timer) snapshotted the newline already, and
the second added it again on replay: recovery after undoing C-k C-k
silently gained a blank line. It is now one record.
- Visual-line yank placed the cursor relative to its row assuming that
row was inside the selection; after C-k a or Enter it was not, and the
cursor ended past the end of a line or the buffer, where typed text was
recorded for undo at a position it was not inserted at and could not
be undone. The formula uses the nearest selected row, and run_handler
now clamps the cursor to the buffer before and after every command.
- ENABLE_ASAN added -fsanitize=address to CMAKE_C_FLAGS only; the
project is C++, so nothing was instrumented. Add it to
CMAKE_CXX_FLAGS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
After a lost record the journal drops position-based records until a
full checkpoint. That checkpoint was only requested from later edits,
rate-limited to once a second; when the writer handled the failing
record before its edit asked for a checkpoint, the checkpoint failed as
well and consumed the slot, so edits made within the next second were
not recovered and, if the user stopped typing, the journal stayed behind
indefinitely. SwapManager::RetryGapCheckpoints(), called once per frame
from Editor::ProcessPendingOpens, requests it once the interval has
passed. This also made SwapReplay_LostRecord_ResyncsWithCheckpoint flaky
under load; the test now exercises the retry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
- Save's in-place fallbacks (no temp file; no hard-link copy) now apply
only when the directory refuses a new name (EACCES, EPERM,
ENAMETOOLONG). On ENOSPC, EDQUOT or EIO the in-place write truncated
the file and then failed, leaving it corrupt; now the save fails and
the file is untouched (checked on a full tmpfs).
- replace_rows_text/replace_buffer_text delete and insert only the span
between the old and new text's common prefix and suffix (kept on
UTF-8 boundaries). Replace-all recorded the whole buffer twice in the
undo tree per run.
- Regex search skips long lines only while typing (limit lowered to
2,000 bytes, as for renderer highlighting; '.*q' on a 20 KB line took
12 s), reports how many were skipped, and next/previous match search
every line. Previously matches on long lines could not be found.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
- Added a comprehensive error propagation standardization report detailing dominant patterns, inconsistencies, and recommended remediations (`docs/audits/error-propagation-standardization.md`).
- Integrated `ErrorHandler` into key components, including `main.cc` for robust exception reporting, and added centralized logging to a user state path.
- Introduced EINTR-safe syscall wrappers (`SyscallWrappers.h`, `.cc`) to improve resilience of file and metadata operations.
- Enhanced `DEVELOPER_GUIDE.md` with an error handling conventions section, covering pattern guidelines and best practices.
- Identified gaps in `PieceTable` and internal helpers; deferred fixes with detailed recommendations for improved memory allocation error reporting.