- Introduced SwapManager for sidecar journaling of buffer mutations, with a safe recovery mechanism. - Added group undo/redo functionality, allowing atomic grouping of related edits. - Implemented `SwapRecorder` and integrated it as a callback interface for mutations. - Added unit tests for swap journaling (save/load/replay) and undo grouping. - Refactored undo to support group tracking and ID management. - Updated CMake to include the new tests and swap journaling logic.
142 lines
3.8 KiB
C++
142 lines
3.8 KiB
C++
#include "Test.h"
|
|
#include "Buffer.h"
|
|
#include <algorithm>
|
|
#include <limits>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
|
|
static std::vector<std::string>
|
|
split_lines_preserve_trailing_empty(const std::string &s)
|
|
{
|
|
std::vector<std::string> out;
|
|
std::size_t start = 0;
|
|
for (std::size_t i = 0; i <= s.size(); i++) {
|
|
if (i == s.size() || s[i] == '\n') {
|
|
out.push_back(s.substr(start, i - start));
|
|
start = i + 1;
|
|
}
|
|
}
|
|
if (out.empty())
|
|
out.push_back(std::string());
|
|
return out;
|
|
}
|
|
|
|
|
|
static std::vector<std::size_t>
|
|
line_starts_for(const std::string &s)
|
|
{
|
|
std::vector<std::size_t> starts;
|
|
starts.push_back(0);
|
|
for (std::size_t i = 0; i < s.size(); i++) {
|
|
if (s[i] == '\n')
|
|
starts.push_back(i + 1);
|
|
}
|
|
return starts;
|
|
}
|
|
|
|
|
|
static std::size_t
|
|
ref_linecol_to_offset(const std::string &s, std::size_t row, std::size_t col)
|
|
{
|
|
auto starts = line_starts_for(s);
|
|
if (starts.empty())
|
|
return 0;
|
|
if (row >= starts.size())
|
|
return s.size();
|
|
std::size_t start = starts[row];
|
|
std::size_t end = (row + 1 < starts.size()) ? starts[row + 1] : s.size();
|
|
if (end > start && s[end - 1] == '\n')
|
|
end -= 1; // clamp before trailing newline
|
|
return start + std::min(col, end - start);
|
|
}
|
|
|
|
|
|
static void
|
|
check_buffer_matches_model(const Buffer &b, const std::string &model)
|
|
{
|
|
auto expected_lines = split_lines_preserve_trailing_empty(model);
|
|
const auto &rows = b.Rows();
|
|
ASSERT_EQ(rows.size(), expected_lines.size());
|
|
ASSERT_EQ(b.Nrows(), rows.size());
|
|
|
|
auto starts = line_starts_for(model);
|
|
ASSERT_EQ(starts.size(), expected_lines.size());
|
|
|
|
std::string via_views;
|
|
for (std::size_t i = 0; i < rows.size(); i++) {
|
|
ASSERT_EQ(std::string(rows[i]), expected_lines[i]);
|
|
ASSERT_EQ(b.GetLineString(i), expected_lines[i]);
|
|
|
|
std::size_t exp_start = starts[i];
|
|
std::size_t exp_end = (i + 1 < starts.size()) ? starts[i + 1] : model.size();
|
|
auto r = b.GetLineRange(i);
|
|
ASSERT_EQ(r.first, exp_start);
|
|
ASSERT_EQ(r.second, exp_end);
|
|
|
|
auto v = b.GetLineView(i);
|
|
ASSERT_EQ(std::string(v), model.substr(exp_start, exp_end - exp_start));
|
|
via_views.append(v.data(), v.size());
|
|
}
|
|
ASSERT_EQ(via_views, model);
|
|
}
|
|
|
|
|
|
TEST (Buffer_RowsCache_MultiLineEdits_StayConsistent)
|
|
{
|
|
Buffer b;
|
|
std::string model;
|
|
|
|
check_buffer_matches_model(b, model);
|
|
|
|
// Insert text and newlines in a few different ways.
|
|
b.insert_text(0, 0, std::string("abc"));
|
|
model.insert(0, "abc");
|
|
check_buffer_matches_model(b, model);
|
|
|
|
b.split_line(0, 1); // a\nbc
|
|
model.insert(ref_linecol_to_offset(model, 0, 1), "\n");
|
|
check_buffer_matches_model(b, model);
|
|
|
|
b.insert_text(1, 2, std::string("X")); // a\nbcX
|
|
model.insert(ref_linecol_to_offset(model, 1, 2), "X");
|
|
check_buffer_matches_model(b, model);
|
|
|
|
b.join_lines(0); // abcX
|
|
{
|
|
std::size_t off = ref_linecol_to_offset(model, 0, std::numeric_limits<std::size_t>::max());
|
|
if (off < model.size() && model[off] == '\n')
|
|
model.erase(off, 1);
|
|
}
|
|
check_buffer_matches_model(b, model);
|
|
|
|
// Insert a multi-line segment in one shot.
|
|
b.insert_text(0, 2, std::string("\n123\nxyz"));
|
|
model.insert(ref_linecol_to_offset(model, 0, 2), "\n123\nxyz");
|
|
check_buffer_matches_model(b, model);
|
|
|
|
// Delete spanning across a newline.
|
|
b.delete_text(0, 1, 5);
|
|
{
|
|
std::size_t start = ref_linecol_to_offset(model, 0, 1);
|
|
std::size_t actual = std::min<std::size_t>(5, model.size() - start);
|
|
model.erase(start, actual);
|
|
}
|
|
check_buffer_matches_model(b, model);
|
|
|
|
// Insert/delete whole rows.
|
|
b.insert_row(1, std::string_view("ROW"));
|
|
model.insert(ref_linecol_to_offset(model, 1, 0), "ROW\n");
|
|
check_buffer_matches_model(b, model);
|
|
|
|
b.delete_row(1);
|
|
{
|
|
auto starts = line_starts_for(model);
|
|
if (1 < (int) starts.size()) {
|
|
std::size_t start = starts[1];
|
|
std::size_t end = (2 < starts.size()) ? starts[2] : model.size();
|
|
model.erase(start, end - start);
|
|
}
|
|
}
|
|
check_buffer_matches_model(b, model);
|
|
} |