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
+30 -9
View File
@@ -173,13 +173,15 @@ write_in_place(const std::string &path, const char *data, std::size_t len, std::
// Create a new file named from `tmpl` (a mkstemp template, which receives
// the chosen name) holding the content, fsynced.
static bool
write_new_file(std::string &tmpl, const char *data, std::size_t len, std::string &err)
write_new_file(std::string &tmpl, const char *data, std::size_t len, std::string &err, int &create_errno)
{
create_errno = 0;
std::vector<char> name(tmpl.begin(), tmpl.end());
name.push_back('\0');
const int fd = kte::syscall::Mkstemp(name.data());
if (fd < 0) {
err = "Failed to create " + tmpl + ": " + std::strerror(errno);
create_errno = errno;
err = "Failed to create " + tmpl + ": " + std::strerror(create_errno);
return false;
}
tmpl.assign(name.data());
@@ -196,6 +198,18 @@ write_new_file(std::string &tmpl, const char *data, std::size_t len, std::string
}
// Whether a failure to create a file beside the target means only that the
// directory will not take a new name (not writable, name too long), so the
// target itself may still be rewritten in place. Anything else (ENOSPC,
// EDQUOT, EIO, ...) would most likely hit the in-place write too, after it
// had already truncated the file.
static bool
may_write_in_place_after(int create_errno)
{
return create_errno == EACCES || create_errno == EPERM || create_errno == ENAMETOOLONG;
}
static bool
atomic_write_file(const std::string &path_in, const char *data, std::size_t len, std::string &err)
{
@@ -222,11 +236,17 @@ atomic_write_file(const std::string &path_in, const char *data, std::size_t len,
if (dst_exists && S_ISREG(dst_st.st_mode) && dst_st.st_nlink > 1) {
// Truncating in place risks the only copy if the write then fails
// (ENOSPC, EIO, crash), so first write and sync a copy beside it. If
// no copy can be made (directory not writable, name too long), write
// in place anyway rather than refuse to save.
// the directory will not take a new file (not writable, name too
// long), write in place anyway rather than refuse to save; any
// other failure (out of space, I/O error) refuses the save.
std::string copy = path + ".kte-save.XXXXXX";
std::string copy_err;
const bool have_copy = write_new_file(copy, data, len, copy_err);
int copy_errno = 0;
const bool have_copy = write_new_file(copy, data, len, copy_err, copy_errno);
if (!have_copy && !may_write_in_place_after(copy_errno)) {
err = "Save failed, file left unchanged: " + copy_err;
return false;
}
if (!write_in_place(path, data, len, err)) {
if (have_copy)
err += " (the new content is in " + copy + ")";
@@ -261,10 +281,11 @@ atomic_write_file(const std::string &path_in, const char *data, std::size_t len,
if (!kte::RetryOnTransientError(mkstemp_fn, kte::RetryPolicy::Aggressive(), err)) {
const int saved_errno = errno;
// No temp file in the directory (e.g. the file is writable but its
// directory is not): an existing regular file can still be written in
// place, as editors traditionally do.
if (fd < 0 && dst_exists && S_ISREG(dst_st.st_mode)) {
// No temp file in the directory because the directory is not
// writable (the file is): an existing regular file can still be
// written in place, as editors traditionally do. Not on ENOSPC and
// the like: the in-place write would truncate the file and then fail.
if (fd < 0 && dst_exists && S_ISREG(dst_st.st_mode) && may_write_in_place_after(saved_errno)) {
err.clear();
return write_in_place(path, data, len, err);
}