Commands: keep line structure intact at end of buffer; undo visual newline

- Regex replace-all, reflow and kill-region replaced or deleted whole
  rows, which added or left a stray newline when the text ended on a
  last line without one. They now edit the byte range in place.
- Regex replace-all no longer matches the empty position after a final
  newline. Before, a zero-width pattern such as ^ matched it, added a
  line, and matched the new empty last row again: the editor hung.
- Kill-line on the last row kills an unterminated line with the newline
  before it, and does nothing on the empty row after a final newline
  (it used to record an undo step that added a line).
- The visual-line newline records its splits as one undo step, and
  places the cursor correctly for a repeat count.
- Buffer::delete_text computes its byte range directly instead of
  materializing every line it spans.

Audit findings D3, D4, D5.

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 aa53d75f96
commit a69664037d
3 changed files with 255 additions and 151 deletions
+10 -43
View File
@@ -699,51 +699,18 @@ Buffer::delete_text(int row, int col, std::size_t len)
if (col < 0)
col = 0;
// `len` counts each newline as one character, so the deletion is simply
// the next `len` bytes from the (clamped) start, capped at end of buffer.
const std::size_t start = content_.LineColToByteOffset(static_cast<std::size_t>(row),
static_cast<std::size_t>(col));
std::size_t r = static_cast<std::size_t>(row);
std::size_t c = static_cast<std::size_t>(col);
std::size_t remaining = len;
const std::size_t lc = content_.LineCount();
while (remaining > 0 && r < lc) {
const std::string line = content_.GetLine(r); // logical line (without trailing '\n')
const std::size_t L = line.size();
if (c < L) {
const std::size_t take = std::min(remaining, L - c);
c += take;
remaining -= take;
}
if (remaining == 0)
break;
// Consume newline between lines as one char, if there is a next line
if (r + 1 < lc) {
remaining -= 1; // the newline
r += 1;
c = 0;
} else {
// At last line and still remaining: delete to EOF
const std::size_t total = content_.Size();
const std::size_t actual = (total > start) ? (total - start) : 0;
if (actual == 0)
return;
content_.Delete(start, actual);
rows_cache_dirty_ = true;
if (swap_rec_)
swap_rec_->OnDelete(row, col, actual);
return;
}
}
// Compute end offset at (r,c)
std::size_t end = content_.LineColToByteOffset(r, c);
if (end > start) {
const std::size_t actual = end - start;
content_.Delete(start, actual);
rows_cache_dirty_ = true;
if (swap_rec_)
swap_rec_->OnDelete(row, col, actual);
}
const std::size_t total = content_.Size();
const std::size_t actual = (total > start) ? std::min(len, total - start) : 0;
if (actual == 0)
return;
content_.Delete(start, actual);
rows_cache_dirty_ = true;
if (swap_rec_)
swap_rec_->OnDelete(row, col, actual);
}