HighlighterEngine::GetLine copied the span vector on every call (twice
on a miss) and did a dynamic_cast per call; it now returns the cached
entry (valid until the next call) and dispatches on Stateful().
PieceTable and HighlighterEngine took a mutex in their read paths while
their writes took none, and PieceTable.h claimed concurrent reads were
safe; they are not (reads update caches). Every Buffer is used from the
main thread only (the swap writer thread receives copies; the regex
large-stack thread runs while its caller waits), so the mutexes are
removed and the headers say so. ASan and the threaded tests under TSan
are clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
ImGui measured every span, highlight and the cursor from column 0 of the
line (and rescanned the line for each column conversion), which is
quadratic in line length on minified files. Columns now come from one
table per line and pixel offsets from the nearest position already
measured. Output is pixel-identical to before (compared under Xvfb, with
syntax colouring, tabs, UTF-8 and search highlights).
kte::kTabWidth replaces the literal 8s, and Qt's 4: Qt expanded tabs to
4 columns while the command layer scrolled by 8, misplacing the cursor
and horizontal scroll on lines with tabs. MeasureChar and
DisplayColumns (TermWidth.h) are the command layer's column model, now
shared, and Qt draws by it instead of one column per byte, which
shifted everything after a multi-byte character.
Qt also:
- drew operators in the dark-mode background colour, so they vanished;
- computed the editor's column count three ways (different glyph,
padding and status-row accounting) from painting, and positioned text
with a rounded cell width, so the cursor drifted off the glyphs on long
lines. One TextLayout now serves painting, resizing, font changes and
wheel scrolling, with a fractional cell width; runs are drawn only
where visible;
- drew each non-ASCII character at its cell, since a fallback-font glyph
wider than a cell pushed the rest of the run out of place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
SearchHighlight compiles the search pattern once per frame and yields each
line's match ranges; the terminal, ImGui and Qt renderers had three copies
of this, and Qt compiled a std::regex for every visible line on every
frame. Qt now uses kte::Regex like the others.
kte::Regex::RunGuarded replaces Command.cc's run_regex_work, which chose
the large stack by comparing engine names. ForEachRegexMatch had no
callers left and is removed.
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 session never unlinks a journal it does not own: Detach, ResetJournal
and NotifyFilenameChanged skip removal when this session was locked
out or another session holds the lock, and PruneSwapDir skips held
journals. A second session used to delete the first session's live
journal on save or close.
- The writer thread's periodic fsync now runs under mtx_. It fsynced fds
that the main thread could close concurrently (or that had been
reused); an in-flight fsync also kept a just-closed journal's flock
alive, so the owner's own cleanup saw its journal as held.
- Compaction locks the compacted file before renaming it into place and
keeps using it, so the journal is never unlocked in between; a failed
lock on the truncating reopen marks the session locked out.
- Unnamed-buffer journals include the pid, so sessions no longer share
(and delete) each other's unnamed-N.swp.
- RowsView keeps only the previous generation of cached rows: keeping
all of them made C-u 2000 M-d on a 1 MiB line use ~2 GB.
- Renderers run regex match highlighting through ForEachRegexMatch,
which uses the large-stack worker for lines over 512 bytes; some
patterns overflowed the main stack well under the previous 10,000
byte limit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
libstdc++'s std::regex matcher recurses about once per matched
character, so '.*', 'a+' or '(a|b)*' on a line of a few tens of
thousands of characters (minified JS/JSON) overflowed the 8 MiB stack
and crashed the editor from the regex search prompt or regex replace.
Regex search and regex replace now run their matching on a worker
thread with a 1 GiB reserved stack (RunWithLargeStack; the caller
waits, so nothing is shared concurrently). Renderers skip regex match
highlighting on lines over 10,000 bytes. Catastrophic backtracking
(e.g. (a*)*b) is still possible: std::regex has no time limit.
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
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>
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>
- 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>
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>
- 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 detailed journaling system (`SwapManager`) for crash recovery, including edit recording and replay.
- Integrated recovery prompts for handling swap files during file open flows.
- Implemented swap file cleanup, checkpointing, and compaction mechanisms.
- Added extensive unit tests for swap-related behaviors such as recovery prompts, file pruning, and corruption handling.
- Updated CMake to include new test files.
- Introduced SwapManager for sidecar journaling of buffer mutations, with a safe recovery mechanism.
- Added group undo/redo functionality, allowing atomic grouping of related edits.
- Implemented `SwapRecorder` and integrated it as a callback interface for mutations.
- Added unit tests for swap journaling (save/load/replay) and undo grouping.
- Refactored undo to support group tracking and ID management.
- Updated CMake to include the new tests and swap journaling logic.
- Introduced visual-line mode for multi-line selection and edits.
- Implemented commands, rendering, and keyboard shortcuts.
- Added tests for broadcast operations in visual-line mode.
- Introduced `CommandId::CenterOnCursor` to center viewport on the cursor line.
- Improved scrolling behavior in `ImGuiRenderer` to avoid aggressive centering and keep visible lines stable.
- Updated `make-app-release` to rename the output app to `kge-qt.app`.
- Adjusted padding in `ImGuiFrontend` to align with `ImGuiRenderer` settings for consistent scrolling.
- Bumped version to 1.4.1.
- Added `QtFrontend`, `QtRenderer`, and `QtInputHandler` for Qt-based UI rendering and input handling.
- Implemented support for theming, font customization, and palette overrides in GUITheme.
- Renamed and refactored ImGui-specific components (e.g., `GUIRenderer` -> `ImGuiRenderer`).
- Added cross-frontend integration for commands and visual font picker.