Fix round-four regressions: save fallback, replace memory, regex skip

- 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
This commit is contained in:
Claude
2026-09-24 09:16:04 -07:00
committed by Kyle Isom
parent ca3a6e675b
commit b1b0b3de57
5 changed files with 230 additions and 58 deletions
+98
View File
@@ -1239,3 +1239,101 @@ TEST(Audit_BufferId_StableAcrossMovesUniqueForCopies)
f = e;
ASSERT_TRUE(f.Id() != id);
}
// Replace-all records only the changed span in the undo tree (it recorded
// the whole buffer twice per run), and undo/redo still restore the text,
// including when the change sits next to multibyte characters.
TEST(Audit_ReplaceAll_RecordsOnlyChangedSpan)
{
TestHarness h;
const std::string big(100000, 'x');
const std::string before = big + "\nh\xC3\xA9llo w\xC3\xB8rld\n" + big + "\n";
h.Buf().insert_text(0, 0, before);
h.Buf().SetCursor(0, 0);
regex_replace_all(h, "\xC3\xB8", "\xC3\xA5");
const std::string after = big + "\nh\xC3\xA9llo w\xC3\xA5rld\n" + big + "\n";
ASSERT_EQ(h.Text(), after);
const UndoNode *cur = h.Buf().Undo()->TreeForTests().current;
ASSERT_TRUE(cur != nullptr);
ASSERT_TRUE(cur->text.size() < 16);
ASSERT_TRUE(cur->parent != nullptr && cur->parent->text.size() < 16);
ASSERT_TRUE(h.Undo());
ASSERT_EQ(h.Text(), before);
ASSERT_TRUE(h.Redo());
ASSERT_EQ(h.Text(), after);
// Growing, and splitting lines.
regex_replace_all(h, "rld", "rld!\n");
regex_replace_all(h, "o w", "o\nw");
const std::string after2 = big + "\nh\xC3\xA9llo\nw\xC3\xA5rld!\n\n" + big + "\n";
ASSERT_EQ(h.Text(), after2);
ASSERT_TRUE(h.Undo());
ASSERT_TRUE(h.Undo());
ASSERT_EQ(h.Text(), after);
ASSERT_TRUE(h.Redo());
ASSERT_TRUE(h.Redo());
ASSERT_EQ(h.Text(), after2);
}
// Search-as-you-type skips very long lines (std::regex can be quadratic in
// line length) but says so, and moving to the next match searches them.
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");
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);
}
// Replace-all against a per-line std::regex_replace model, with overlapping
// common prefixes and suffixes; undo restores the original each time.
TEST(Audit_ReplaceAll_TrimmedEditMatchesModel)
{
const char *texts[] = {"aaa\nbab\n", "abc", "\xC3\xA9\xC3\xA9x\xC3\xA9\n\n", "aXa\naa\n", "\n\n"};
const std::pair<const char *, const char *> reps[] = {
{"a", ""}, {"a", "aa"}, {"b", "a"}, {"^", "a"}, {"X", "aXa"}, {"a$", ""}, {"x", "\xC3\xA9"},
{"\xC3\xA9", "e"}, {"aa", "a"}, {"^$", "a"}
};
for (const char *t: texts) {
for (const auto &[find, with]: reps) {
TestHarness h;
h.Buf().insert_text(0, 0, t);
h.Buf().SetCursor(0, 0);
std::string expect;
const std::regex rx(find);
std::string src(t);
std::size_t start = 0;
while (true) {
const std::size_t nl = src.find('\n', start);
if (start == src.size() && start > 0)
break; // no line after a trailing newline
const std::string line = src.substr(start, nl == std::string::npos ? std::string::npos : nl - start);
expect += std::regex_replace(line, rx, with);
if (nl == std::string::npos)
break;
expect += '\n';
start = nl + 1;
}
regex_replace_all(h, find, with);
if (h.Text() != expect)
fprintf(stderr, "text=[%s] find=[%s] with=[%s] got=[%s] want=[%s]\n", t, find, with,
h.Text().c_str(), expect.c_str());
ASSERT_EQ(h.Text(), expect);
if (expect != t) {
ASSERT_TRUE(h.Undo());
ASSERT_EQ(h.Text(), std::string(t));
ASSERT_TRUE(h.Redo());
ASSERT_EQ(h.Text(), expect);
}
}
}
}