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:
@@ -9,6 +9,7 @@
|
||||
#include "ErrorRecovery.h"
|
||||
#include "PieceTable.h"
|
||||
#include "RegexGuard.h"
|
||||
#include "RegexEngine.h"
|
||||
#include "Swap.h"
|
||||
#include "UndoTree.h"
|
||||
#include "syntax/HighlighterEngine.h"
|
||||
@@ -1285,14 +1286,16 @@ TEST(Audit_RegexSearch_LongLineFoundByNext)
|
||||
{
|
||||
TestHarness h;
|
||||
Editor &ed = h.EditorRef();
|
||||
h.Buf().insert_text(0, 0, "short\n" + std::string(25000, 'a') + "NEEDLE\nend\n");
|
||||
// Longer than search-as-you-type scans (depends on the regex engine).
|
||||
const std::size_t long_len = kte::Regex::IncrementalLineLimit() + 1000;
|
||||
h.Buf().insert_text(0, 0, "short\n" + std::string(long_len, 'a') + "NEEDLE\nend\n");
|
||||
h.Buf().SetCursor(0, 0);
|
||||
ASSERT_TRUE(h.Exec(CommandId::RegexFindStart));
|
||||
ASSERT_TRUE(h.Exec(CommandId::InsertText, "NEE+DLE"));
|
||||
ASSERT_TRUE(ed.Status().find("long line") != std::string::npos);
|
||||
ASSERT_TRUE(h.Exec(CommandId::MoveRight));
|
||||
ASSERT_EQ(h.Buf().Cury(), (std::size_t) 1);
|
||||
ASSERT_EQ(h.Buf().Curx(), (std::size_t) 25000);
|
||||
ASSERT_EQ(h.Buf().Curx(), long_len);
|
||||
ASSERT_TRUE(h.Exec(CommandId::Refresh));
|
||||
ASSERT_TRUE(!ed.PromptActive());
|
||||
// The regex replace prompt steps through matches the same way.
|
||||
@@ -1302,7 +1305,7 @@ TEST(Audit_RegexSearch_LongLineFoundByNext)
|
||||
ASSERT_TRUE(ed.Status().find("Left/Right search all") != std::string::npos);
|
||||
ASSERT_TRUE(h.Exec(CommandId::MoveRight));
|
||||
ASSERT_EQ(h.Buf().Cury(), (std::size_t) 1);
|
||||
ASSERT_EQ(h.Buf().Curx(), (std::size_t) 25000);
|
||||
ASSERT_EQ(h.Buf().Curx(), long_len);
|
||||
}
|
||||
|
||||
|
||||
@@ -2179,3 +2182,49 @@ TEST(Swap_DeferredBaseCrc)
|
||||
b.SetSwapRecorder(nullptr);
|
||||
sm.Detach(&b, true);
|
||||
}
|
||||
|
||||
|
||||
// kte::Regex replacements follow std::regex_replace (ECMAScript), whichever
|
||||
// engine is built in. (References to groups the pattern does not have, like
|
||||
// $3 with two groups, are implementation-defined and left out.)
|
||||
TEST(RegexEngine_ReplaceMatchesStdRegex)
|
||||
{
|
||||
const char *subjects[] = {"", "baaac", "one two three", "a-b-c", "xyz", "aaa", " lead", "tail "};
|
||||
const std::pair<const char *, const char *> cases[] = {
|
||||
{"a*", "X"}, {"a", "[$&]"}, {"(\\w+)", "<$1>"}, {"(o)(n)?", "$2$1"}, {"-", "$`|$'"},
|
||||
{"\\s+", "$$"}, {"^", ">"}, {"$", "<"}, {"b|", "_"}, {"\\b", "|"},
|
||||
{"([a-z])([a-z])", "$2$1"}, {"(a)(x)?", "[$2]"}
|
||||
};
|
||||
for (const char *subj: subjects) {
|
||||
for (const auto &[pat, fmt]: cases) {
|
||||
kte::Regex rx;
|
||||
std::string err;
|
||||
ASSERT_TRUE(rx.Compile(pat, err));
|
||||
const std::string got = rx.ReplaceAll(subj, fmt);
|
||||
const std::string want = std::regex_replace(std::string(subj), std::regex(pat), fmt);
|
||||
if (got != want)
|
||||
fprintf(stderr, "subj=[%s] pat=[%s] fmt=[%s] got=[%s] want=[%s]\n", subj, pat, fmt,
|
||||
got.c_str(), want.c_str());
|
||||
ASSERT_EQ(got, want);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// With PCRE2, catastrophic backtracking stops at the match limit and says
|
||||
// so, instead of hanging the editor.
|
||||
TEST(RegexEngine_CatastrophicPatternStops)
|
||||
{
|
||||
if (std::string(kte::Regex::EngineName()) != "PCRE2")
|
||||
return; // std::regex has no limit (a known limitation)
|
||||
TestHarness h;
|
||||
h.Buf().insert_text(0, 0, std::string(40, 'a') + "!\n");
|
||||
h.Buf().SetCursor(0, 0);
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
ASSERT_TRUE(h.Exec(CommandId::RegexFindStart));
|
||||
ASSERT_TRUE(h.Exec(CommandId::InsertText, "^(a+)+$"));
|
||||
const double secs = std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count();
|
||||
ASSERT_TRUE(secs < 10.0);
|
||||
ASSERT_TRUE(h.EditorRef().Status().find("too complex") != std::string::npos);
|
||||
ASSERT_TRUE(h.Exec(CommandId::Refresh));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user