Regex: optional PCRE2 engine behind kte::Regex

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
This commit is contained in:
Claude
2026-09-24 09:16:04 -07:00
committed by Kyle Isom
parent a33c1759e7
commit e782d24e32
14 changed files with 670 additions and 107 deletions
+18 -4
View File
@@ -1246,12 +1246,12 @@ SwapManager::maybe_request_checkpoint(Buffer &buf, const std::size_t approx_edit
if (ctx.last_chkpt_ns == 0)
ctx.last_chkpt_ns = now;
// A checkpoint writes the whole buffer. For a large one, checkpoint
// after edits amounting to half its size (and on the timer only
// once edits reach a sixteenth), so the bytes written stay in
// proportion to the bytes edited; retry a gap less often too.
// after edits amounting to its size (and on the timer only once
// edits reach a sixteenth), so the bytes written stay in proportion
// to the bytes edited; retry a gap less often too.
const std::size_t size = buf.ContentBytes();
const bool large = size > kLargeCheckpointBytes;
const std::size_t bytes_limit = std::max<std::size_t>(cfg.checkpoint_bytes, size / 2);
const std::size_t bytes_limit = std::max<std::size_t>(cfg.checkpoint_bytes, large ? size : 0);
const bool bytes_hit = (cfg.checkpoint_bytes > 0) && ctx.edit_bytes_since_chkpt >= bytes_limit;
const bool time_hit = (cfg.checkpoint_interval_ms > 0) &&
(((now - ctx.last_chkpt_ns) / 1000000ULL) >= cfg.checkpoint_interval_ms) &&
@@ -1324,6 +1324,13 @@ SwapManager::RecordCheckpoint(Buffer &buf, const bool urgent_flush)
auto it = journals_.find(&buf);
if (it == journals_.end() || it->second.suspended)
return;
// One queued checkpoint at a time: records queued after it apply on
// top of it, so a second one adds nothing but another full copy of
// the buffer in memory (several whole-file edits in a row on a large
// buffer queued one each).
if (it->second.chkpt_queued)
return;
it->second.chkpt_queued = true;
}
// Any size: the writer frames large checkpoints as several records.
@@ -1416,6 +1423,13 @@ SwapManager::process_one(const Pending &p)
{
if (!p.buf)
return;
if (p.type == SwapRecType::CHKPT) {
// Being handled now: a later request queues a new checkpoint.
std::lock_guard<std::mutex> lg(mtx_);
auto it = journals_.find(p.buf);
if (it != journals_.end())
it->second.chkpt_queued = false;
}
// Any record that is not written leaves a gap in the journal.
auto mark_gap = [&]() {