Consolidation rewrites pieces without changing content or offsets but
invalidated the line index, so once a buffer passed the piece limit
every edit paid a full rescan again. A moved-from table kept the index
of the text it gave away, and Insert now updates that index in place,
producing ranges past the end of its buffer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
Every edit bumped the buffer version (through SetDirty) and called
InvalidateFrom(0), so all cached spans and line states were discarded
and stateful highlighters re-lexed from row 0 to the viewport on each
keystroke.
Buffer mutators now report the row they changed (edited_at_), which
bumps the version and tells the engine to drop only rows from there
down; a row's highlighting depends only on rows above it. A version
change that arrives without that notice still clears everything, so a
missed path cannot show stale spans. The engine keeps states as a
contiguous vector and caches spans for every row it lexes. SetDirty no
longer touches the version: the edits themselves do, including undo
and redo back to the saved state (D9 still holds; its test now checks
the highlighting itself).
Typing near the end of a 100k-line C++ file: 46.3 ms -> 0.14 ms of
highlighting work per keystroke.
Audit finding P3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
- PieceTable::Insert/Delete adjust a valid line index directly (shift
later line starts, add or drop starts for inserted or deleted
newlines) instead of invalidating it, which forced a byte-by-byte
rescan of the whole buffer on the next line query. On a 38 MB,
1M-line buffer, insert + line query drops from 14.7 ms to 0.28 ms.
- The terminal and ImGui renderers fetch only the rows they draw
(Nrows + GetLineString) instead of Buffer::Rows(), which rebuilt a
string for every line of the file on the first frame after each edit.
Qt's line-count uses switch to Nrows().
ImGuiRenderer.cc was syntax-checked with g++ and clang++ against the
vendored ImGui headers; the GUI targets could not be built here (no
SDL2 or Qt6).
Audit findings P1, P2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
- Backspace and delete-char remove a whole UTF-8 character (they
removed one byte, leaving an invalid lead byte in the file), and
record the full character for undo.
- Left/right step over whole characters; up/down snap the column to a
character boundary and clamp the row, which also removes an unsigned
underflow in move-down when the cursor was past the last row.
- Horizontal scroll offsets are computed in display cells, the way the
terminal renderer draws (tabs, wide characters), instead of bytes, so
long non-ASCII lines no longer scroll to blank with the cursor off
screen. The mouse column mapping uses the same measure.
Audit findings B5, B6.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
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
Two latent defects broke the kte target on macOS once TerminalInputHandler
switched from getch() to get_wch().
include_directories() referenced CURSES_INCLUDE_DIR, a name FindCurses never
sets (it exports CURSES_INCLUDE_DIRS). The variable expanded to nothing, so
no curses include path was added and the compiler silently fell back to the
macOS SDK's narrow-char header on the default include path.
Both the SDK and Homebrew headers declare get_wch and friends only when
NCURSES_WIDECHAR is set, defaulting it to 0 absent a feature-test macro, so
the build failed with "use of undeclared identifier 'get_wch'". We always
request and link the wide library, so define NCURSES_WIDECHAR=1; redefining
it identically is benign on platforms whose headers already set it.
kte and kge now compile and link against libncursesw, and the app bundle
vendors it. Full test suite passes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CMake 4.x FindCurses with CURSES_NEED_WIDE finds the macOS SDK's
libcurses.tbd but no matching wide-char header, so configure failed with
"Could NOT find Curses (missing: CURSES_INCLUDE_PATH)". Homebrew's
ncurses is keg-only, so it is not on the default search path either.
Existing build directories only worked by accident, via a hand-passed
-DCURSES_INCLUDE_PATH left in their CMakeCache; a clean configure failed,
which broke make-app-release.
Query `brew --prefix ncurses` on APPLE and prepend it to CMAKE_PREFIX_PATH
when present. A clean configure now succeeds with no manual flags and
links the real libncursesw.dylib instead of the SDK's narrow libcurses.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Releases the undo/crash-recovery/syntax-highlighting correctness work and
the GUI CRLF paste fix accumulated since v1.11.3.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The ImGui Cmd+V/Ctrl+V paste handler split clipboard text only on '\n',
leaving '\r' inside each segment. Pasting text with CRLF (Windows) or bare
CR (classic-Mac / some macOS apps) line endings enqueued an InsertText arg
containing '\r', which the InsertText command rejects with "InsertText arg
must not contain newlines".
Extract the paste-to-commands logic into a pure, SDL-free helper
(PasteSplit.h) that treats '\n', '\r\n', and bare '\r' each as one line
break, and add unit tests covering all line-ending forms plus blank-line
and trailing-newline preservation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Undo/redo:
- cmd_newline recorded the undo node's position after the cursor moved past
the split point, so undoing Enter joined the wrong pair of lines.
- Backspace/Delete line-joins recorded UndoType::Newline, whose apply()
semantics are inverted for a join (forward=split); add UndoType::JoinLines
with correct forward/backward behavior.
- Regex replace-all, indent/unindent region, kill-to-eol, kill-line,
kill-region, and delete-word-prev/next mutated the buffer via the
no-undo-recording raw APIs; they now record undo, grouped atomically via a
shared UndoGroupGuard.
- Plain-text replace-all with an empty replacement advanced the scan
position one character too far, skipping adjacent/overlapping matches.
Crash recovery / memory safety:
- Buffer's move ctor/assignment never carried over swap_rec_,
on_disk_identity_, or the visual-line-mode fields, so the crash-recovery
journal silently stopped tracking a buffer whenever Editor's
std::vector<Buffer> reallocated or shifted (e.g. opening/closing files).
Added SwapManager::Rehome() plus Flush()-before-mutate in
Editor::AddBuffer/CloseBuffer so the journal's Buffer* key and the
background writer thread never reference a stale address.
- UndoTree leaked its entire node graph (including all edit text) on every
buffer close/reload; added ~UndoTree() to free it.
- main.cc didn't restore the terminal if an exception escaped the run loop;
added an RAII guard around Frontend::Shutdown().
Input handling:
- Terminal frontend used getch() instead of get_wch(), silently dropping
non-ASCII keyboard input despite linking wide-char ncurses.
- KKeymap's C-k lookup switches on an already-lowercased key, making
`case 'E'` unreachable dead code; C-k Shift-E silently aliased to C-k e.
- ImGui file picker wasn't modal: keystrokes typed while it was open still
reached the buffer underneath as edit commands, and Escape didn't close it.
- Gated ImGuiInputHandler's unconditional fprintf/fflush diagnostics behind
an IMGUI_IH_DEBUG macro, matching QtInputHandler's existing convention.
Syntax highlighting:
- Go/Rust/SQL highlighters weren't stateful, so multi-line /* */ comments
mis-highlighted as code starting on the second line.
- Python's triple-quoted-string handler didn't re-scan the remainder of a
line after a string closed mid-line, missing a same-line reopen.
- HighlighterEngine's state_last_contig_ field was declared but unused,
forcing an O(n) scan of state_cache_ on every stateful lookup; wired it up
as a real fast path and fixed InvalidateFrom() to keep it in sync.
- kge's :syntax off was silently undone the next frame because
apply_syntax_to_buffer() re-applied the config default unconditionally
every frame; added a user-override flag mirroring the existing
edit-mode-detection guard.
Added regression tests for all of the above (test_undo.cc, test_kkeymap.cc,
test_command_semantics.cc, test_search_replace_flow.cc, and a new
test_syntax_highlighting.cc). Full suite (163 tests) passes, including under
AddressSanitizer.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Large files made idle CPU spike because every frame re-measured every
line, recompiled the search regex per line, and ran full per-line
rendering work even for off-screen rows.
- Render only visible rows (with a small margin for smooth scrolling)
and advance the layout cursor to preserve total content height.
- Hoist the search std::regex compilation out of the per-line loop.
- Cache the max line width on the renderer; reset only when the buffer
or font changes, not on every edit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
On macOS GUI builds, chdir(HOME) runs before deferred file opens are
processed, breaking relative paths passed on the command line. Resolve
each argv path to absolute immediately during argument parsing.
Bump version to 1.11.2.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
apply_syntax_to_buffer() was called every frame and unconditionally reset
edit mode from the file extension, making it impossible to toggle out of
writing mode for .txt/.md files. Add edit_mode_detected_ flag to Buffer so
auto-detection runs once per buffer. Writing mode now also disables syntax
highlighting as intended. Propagate edit_mode_ through Buffer copy/move ops.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The GUI renderer had two competing horizontal scroll systems: a
character-based one (coloffs × space_w) and a pixel-based one. For
proportional fonts the character-based system used "M" width to
calculate viewport columns, triggering premature scrolling at ~50%
of the actual display width.
Switch the GUI renderer to purely pixel-based horizontal scrolling:
- Remove coloffs↔ImGui scroll_x bidirectional sync
- Measure rx_to_px from column 0 (absolute) instead of from coloffs
- Draw full expanded lines; let ImGui clip via its scroll viewport
- Report content width via SetCursorPosX+Dummy for the scrollbar
- Use average character width for cols estimate (not "M" width)
The terminal renderer continues using coloffs correctly—no changes
needed there.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
KTE_STATIC_LINK defaulted to ON, which fails on systems where ncurses
is only available as a shared library. The Nix build already passed
-DKTE_STATIC_LINK=OFF explicitly; this makes the default match.
Also add add_link_options("-stdlib=libc++") for Clang builds — without
it, compilation uses libc++ but the linker defaults to libstdc++,
causing undefined symbol errors.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Defer edit-mode font switch to next frame via RequestLoadFont() to
avoid modifying the locked ImFontAtlas between NewFrame() and Render().
Rework Nix packaging: split nativeBuildInputs/buildInputs correctly,
add devShells for nix develop, desktop file for kge, per-variant pname
and meta.mainProgram, and an overlay for NixOS configs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The edit-mode font switcher called LoadFont() directly between
NewFrame() and Render(), invalidating the font atlas ImGui was
actively using. Use RequestLoadFont() to defer the change to
the safe inter-frame point, matching the existing zoom pattern.
Also default code_font/writing_font to the main font when not
explicitly configured, preventing a mismatch that triggered the
switch on every first frame.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add three proportional serif fonts: Crimson Pro, ET Book, Spectral
- Fix text rendering for variable-width fonts: selection, cursor,
mouse click mapping, search highlights, and syntax-colored text
now use pixel-accurate measurement via ImGui::CalcTextSize()
- Add per-buffer edit mode (code/writing) with auto-detection from
file extension (.txt, .md, .rst, .org, .tex default to writing)
- Add C-k m keybinding and :mode command to toggle edit modes
- Switch config format from INI to TOML (kge.toml), with legacy
INI fallback; vendor toml++ v3.4.0
- New config keys: font.code and font.writing for per-mode defaults
- Add font tab completion for ImGui builds
- Add tab completion for :mode command
- Update help text, themes.md, and add CONFIG.md
- Bump version to 1.10.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Themes:
- Add leuchtturm theme (fountain pen ink on cream paper, brass/leather dark)
- Add per-theme syntax palettes for leuchtturm, tufte, and everforest
- Fix static inline globals giving each TU its own copy of gCurrentTheme
and gBackgroundMode (changed to inline for proper C++17 linkage)
- :background with no args now shows current mode
Font zoom:
- CMD-=/CMD--/CMD-0 to increase/decrease/reset font size
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Multi-window:
- Per-window ImGui contexts (fixes input, scroll, and rendering isolation)
- Per-instance scroll and mouse state in ImGuiRenderer (no more statics)
- Proper GL context activation during window destruction
- ValidateBufferIndex guards against stale curbuf_ across shared buffers
- Editor methods (CurrentBuffer, SwitchTo, CloseBuffer, etc.) use Buffers()
accessor to respect shared buffer lists
- New windows open with an untitled buffer
- Scratch buffer reuse works in secondary windows
- CMD-w on macOS closes only the focused window
- Deferred new-window creation to avoid mid-frame ImGui context corruption
Swap file cleanup:
- SaveAs prompt handler now calls ResetJournal
- cmd_save_and_quit now calls ResetJournal
- Editor::Reset detaches all buffers before clearing
- Tests for save-and-quit and editor-reset swap cleanup
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Warm cream paper, near-black ink, zero rounding, minimal chrome,
restrained dark red and navy accents following Tufte's design principles.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Static linking - Added KTE_STATIC_LINK CMake option and
disabled it in default.nix to avoid the "attempted
static link of dynamic object" error
2. Missing include - Added <cstring> to
test_swap_edge_cases.cc for std::memset/std::memcpy (GCC
14 is stricter about transitive includes)
- Introduced support for multiple windows, sharing the primary editor's buffer list.
- Added `GUIFrontend::OpenNewWindow_` for creating secondary windows with independent dimensions and input handlers.
- Redesigned `WindowState` to encapsulate per-window attributes (dimensions, renderer, input, etc.).
- Updated input processing and command execution to route events based on active window, preserving window-level states.
- Enhanced SDL2 and ImGui integration for proper context management across multiple windows.
- Increased robustness by handling window closing, resizing, and cleanup of secondary windows without affecting the primary editor.
- Updated documentation and key bindings for multi-window operations (e.g., Cmd+N / Ctrl+Shift+N).
- Version updated to 1.8.0 to reflect the major GUI enhancement.
- Added early initialization of `ErrorHandler` in `main.cc` for robust error handling.
- Modified `ErrorHandler` to create the log file immediately, ensuring its presence in the state directory.
- Simplified conditional checks for log file operations and updated timestamp handling to use `system_clock`.
- Added `test_reflow_undo.cc` to validate undo/redo workflows for reflow operations.
- Introduced `UndoType::InsertRow` in `UndoSystem` for tracking row insertion changes in undo history.
- Updated `UndoNode.h` and `UndoSystem.cc` to support row insertion as a standalone undo step.
- Enhanced reflow paragraph functionality to properly record undo/redo actions for both row deletion and insertion.
- Enabled legacy/extended undo tests in `test_undo.cc` for comprehensive validation.
- Updated `CMakeLists.txt` to include new test file in the build target.
- Introduced `CommandId::SmartNewline` for auto-indented newlines, enhancing text editing workflows.
- Added `cmd_smart_newline` to implement indentation-aware newline logic.
- Integrated SmartNewline with keymaps, mouse/keyboard input handlers, and terminal/editor commands.
- Wrote comprehensive tests in `test_smart_newline.cc` to validate behavior for spaces, tabs, and no-indentation cases.
- Updated `Command.h` and `CMakeLists.txt` to register and build the new command.
- Added `ErrorRecovery.cc` and `ErrorRecovery.h` for retry and circuit breaker implementations.
- Enhanced swap file handling with transient error retries and exponential backoff (e.g., ENOSPC, EDQUOT).
- Integrated circuit breaker into SwapManager to gracefully handle repeated failures, prevent system overload, and enable automatic recovery.
- Updated `DEVELOPER_GUIDE.md` with comprehensive documentation on error recovery patterns and graceful degradation strategies.
- Refined fsync, temp file creation, and swap file logic with retry-on-failure mechanisms for improved resilience.
- 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.
- Introduced `test_swap_edge_cases.cc` with extensive tests for minimum payload sizes, truncated payloads, data overflows, unsupported encoding versions, CRC mismatches, and mixed valid/invalid records to ensure reliability under complex scenarios.
- Enhanced `main.cc` with a top-level exception handler to prevent data loss and ensure cleanup during unexpected failures.