Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 11c523ad52 | |||
| c261261e26 | |||
| 27dcb41857 | |||
| bc3433e988 |
@@ -4,7 +4,7 @@ project(kte)
|
|||||||
include(GNUInstallDirs)
|
include(GNUInstallDirs)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 20)
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
set(KTE_VERSION "1.7.0")
|
set(KTE_VERSION "1.7.1")
|
||||||
|
|
||||||
# Default to terminal-only build to avoid SDL/OpenGL dependency by default.
|
# Default to terminal-only build to avoid SDL/OpenGL dependency by default.
|
||||||
# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.
|
# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.
|
||||||
@@ -336,6 +336,8 @@ if (BUILD_TESTS)
|
|||||||
tests/test_visual_line_mode.cc
|
tests/test_visual_line_mode.cc
|
||||||
tests/test_benchmarks.cc
|
tests/test_benchmarks.cc
|
||||||
tests/test_migration_coverage.cc
|
tests/test_migration_coverage.cc
|
||||||
|
tests/test_smart_newline.cc
|
||||||
|
tests/test_reflow_undo.cc
|
||||||
|
|
||||||
# minimal engine sources required by Buffer
|
# minimal engine sources required by Buffer
|
||||||
PieceTable.cc
|
PieceTable.cc
|
||||||
|
|||||||
111
Command.cc
111
Command.cc
@@ -1109,34 +1109,33 @@ cmd_theme_set_by_name(const CommandContext &ctx)
|
|||||||
static bool
|
static bool
|
||||||
cmd_theme_set_by_name(CommandContext &ctx)
|
cmd_theme_set_by_name(CommandContext &ctx)
|
||||||
{
|
{
|
||||||
|
|
||||||
# if defined(KTE_BUILD_GUI) && defined(KTE_USE_QT)
|
# if defined(KTE_BUILD_GUI) && defined(KTE_USE_QT)
|
||||||
// Qt GUI build: schedule theme change for frontend
|
// Qt GUI build: schedule theme change for frontend
|
||||||
std::string name = ctx.arg;
|
std::string name = ctx.arg;
|
||||||
// trim spaces
|
// trim spaces
|
||||||
auto ltrim = [](std::string &s) {
|
auto ltrim = [](std::string &s) {
|
||||||
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) {
|
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) {
|
||||||
return !std::isspace(ch);
|
return !std::isspace(ch);
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
auto rtrim = [](std::string &s) {
|
auto rtrim = [](std::string &s) {
|
||||||
s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) {
|
s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) {
|
||||||
return !std::isspace(ch);
|
return !std::isspace(ch);
|
||||||
}).base(), s.end());
|
}).base(), s.end());
|
||||||
};
|
};
|
||||||
ltrim (name);
|
ltrim(name);
|
||||||
rtrim (name);
|
rtrim(name);
|
||||||
if (name.empty()) {
|
if (name.empty()) {
|
||||||
ctx.editor.SetStatus("theme: provide a name (e.g., nord, solarized-dark, gruvbox-light, eink)");
|
ctx.editor.SetStatus("theme: provide a name (e.g., nord, solarized-dark, gruvbox-light, eink)");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
kte::gThemeChangeRequest= name;
|
kte::gThemeChangeRequest = name;
|
||||||
kte::gThemeChangePending=true;
|
kte::gThemeChangePending = true;
|
||||||
ctx.editor.SetStatus (std::string("Theme requested: ") + name);
|
ctx.editor.SetStatus(std::string("Theme requested: ") + name);
|
||||||
return true;
|
return true;
|
||||||
# else
|
# else
|
||||||
(void) ctx;
|
(void) ctx;
|
||||||
// No-op in terminal build
|
// No-op in terminal build
|
||||||
return true;
|
return true;
|
||||||
# endif
|
# endif
|
||||||
}
|
}
|
||||||
@@ -2949,6 +2948,58 @@ cmd_newline(CommandContext &ctx)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
cmd_smart_newline(CommandContext &ctx)
|
||||||
|
{
|
||||||
|
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||||
|
if (!buf) {
|
||||||
|
ctx.editor.SetStatus("No buffer to edit");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buf->IsReadOnly()) {
|
||||||
|
ctx.editor.SetStatus("Read-only buffer");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Smart newline behavior: add a newline with the same indentation as the current line.
|
||||||
|
// Find indentation of current line
|
||||||
|
std::size_t y = buf->Cury();
|
||||||
|
std::string line = buf->GetLineString(y);
|
||||||
|
std::string indent;
|
||||||
|
for (char c: line) {
|
||||||
|
if (c == ' ' || c == '\t') {
|
||||||
|
indent += c;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform standard newline first
|
||||||
|
if (!cmd_newline(ctx)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now insert the indentation at the new cursor position
|
||||||
|
if (!indent.empty()) {
|
||||||
|
std::size_t new_y = buf->Cury();
|
||||||
|
std::size_t new_x = buf->Curx();
|
||||||
|
buf->insert_text(static_cast<int>(new_y), static_cast<int>(new_x), indent);
|
||||||
|
buf->SetCursor(new_x + indent.size(), new_y);
|
||||||
|
buf->SetDirty(true);
|
||||||
|
|
||||||
|
if (auto *u = buf->Undo()) {
|
||||||
|
u->Begin(UndoType::Insert);
|
||||||
|
u->Append(indent);
|
||||||
|
u->commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
cmd_backspace(CommandContext &ctx)
|
cmd_backspace(CommandContext &ctx)
|
||||||
{
|
{
|
||||||
@@ -4624,7 +4675,14 @@ cmd_reflow_paragraph(CommandContext &ctx)
|
|||||||
new_lines.push_back("");
|
new_lines.push_back("");
|
||||||
|
|
||||||
// Replace paragraph lines via PieceTable-backed operations
|
// Replace paragraph lines via PieceTable-backed operations
|
||||||
|
UndoSystem *u = buf->Undo();
|
||||||
for (std::size_t i = para_end; i + 1 > para_start; --i) {
|
for (std::size_t i = para_end; i + 1 > para_start; --i) {
|
||||||
|
if (u) {
|
||||||
|
buf->SetCursor(0, i);
|
||||||
|
u->Begin(UndoType::DeleteRow);
|
||||||
|
u->Append(static_cast<std::string>(buf->Rows()[i]));
|
||||||
|
u->commit();
|
||||||
|
}
|
||||||
buf->delete_row(static_cast<int>(i));
|
buf->delete_row(static_cast<int>(i));
|
||||||
if (i == 0)
|
if (i == 0)
|
||||||
break; // prevent wrap on size_t
|
break; // prevent wrap on size_t
|
||||||
@@ -4633,6 +4691,12 @@ cmd_reflow_paragraph(CommandContext &ctx)
|
|||||||
std::size_t insert_y = para_start;
|
std::size_t insert_y = para_start;
|
||||||
for (const auto &ln: new_lines) {
|
for (const auto &ln: new_lines) {
|
||||||
buf->insert_row(static_cast<int>(insert_y), std::string_view(ln));
|
buf->insert_row(static_cast<int>(insert_y), std::string_view(ln));
|
||||||
|
if (u) {
|
||||||
|
buf->SetCursor(0, insert_y);
|
||||||
|
u->Begin(UndoType::InsertRow);
|
||||||
|
u->Append(std::string_view(ln));
|
||||||
|
u->commit();
|
||||||
|
}
|
||||||
insert_y += 1;
|
insert_y += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4806,6 +4870,9 @@ InstallDefaultCommands()
|
|||||||
CommandId::InsertText, "insert", "Insert text at cursor (no newlines)", cmd_insert_text, false, true
|
CommandId::InsertText, "insert", "Insert text at cursor (no newlines)", cmd_insert_text, false, true
|
||||||
});
|
});
|
||||||
CommandRegistry::Register({CommandId::Newline, "newline", "Insert newline at cursor", cmd_newline});
|
CommandRegistry::Register({CommandId::Newline, "newline", "Insert newline at cursor", cmd_newline});
|
||||||
|
CommandRegistry::Register({
|
||||||
|
CommandId::SmartNewline, "smart-newline", "Insert newline with auto-indent", cmd_smart_newline
|
||||||
|
});
|
||||||
CommandRegistry::Register({CommandId::Backspace, "backspace", "Delete char before cursor", cmd_backspace});
|
CommandRegistry::Register({CommandId::Backspace, "backspace", "Delete char before cursor", cmd_backspace});
|
||||||
CommandRegistry::Register({CommandId::DeleteChar, "delete-char", "Delete char at cursor", cmd_delete_char});
|
CommandRegistry::Register({CommandId::DeleteChar, "delete-char", "Delete char at cursor", cmd_delete_char});
|
||||||
CommandRegistry::Register({CommandId::KillToEOL, "kill-to-eol", "Delete to end of line", cmd_kill_to_eol});
|
CommandRegistry::Register({CommandId::KillToEOL, "kill-to-eol", "Delete to end of line", cmd_kill_to_eol});
|
||||||
@@ -4991,4 +5058,4 @@ Execute(Editor &ed, const std::string &name, const std::string &arg, int count)
|
|||||||
return false;
|
return false;
|
||||||
CommandContext ctx{ed, arg, count};
|
CommandContext ctx{ed, arg, count};
|
||||||
return cmd->handler ? cmd->handler(ctx) : false;
|
return cmd->handler ? cmd->handler(ctx) : false;
|
||||||
}
|
}
|
||||||
@@ -38,6 +38,7 @@ enum class CommandId {
|
|||||||
// Editing
|
// Editing
|
||||||
InsertText, // arg: text to insert at cursor (UTF-8, no newlines)
|
InsertText, // arg: text to insert at cursor (UTF-8, no newlines)
|
||||||
Newline, // insert a newline at cursor
|
Newline, // insert a newline at cursor
|
||||||
|
SmartNewline, // insert a newline with auto-indent (Shift-Enter)
|
||||||
Backspace, // delete char before cursor (may join lines)
|
Backspace, // delete char before cursor (may join lines)
|
||||||
DeleteChar, // delete char at cursor (may join lines)
|
DeleteChar, // delete char at cursor (may join lines)
|
||||||
KillToEOL, // delete from cursor to end of line; if at EOL, delete newline
|
KillToEOL, // delete from cursor to end of line; if at EOL, delete newline
|
||||||
@@ -164,4 +165,4 @@ void InstallDefaultCommands();
|
|||||||
// Returns true if the command executed successfully.
|
// Returns true if the command executed successfully.
|
||||||
bool Execute(Editor &ed, CommandId id, const std::string &arg = std::string(), int count = 0);
|
bool Execute(Editor &ed, CommandId id, const std::string &arg = std::string(), int count = 0);
|
||||||
|
|
||||||
bool Execute(Editor &ed, const std::string &name, const std::string &arg = std::string(), int count = 0);
|
bool Execute(Editor &ed, const std::string &name, const std::string &arg = std::string(), int count = 0);
|
||||||
@@ -20,6 +20,8 @@ ErrorHandler::ErrorHandler()
|
|||||||
fs::create_directories(log_dir);
|
fs::create_directories(log_dir);
|
||||||
}
|
}
|
||||||
log_file_path_ = (log_dir / "error.log").string();
|
log_file_path_ = (log_dir / "error.log").string();
|
||||||
|
// Create the log file immediately so it exists in the state directory
|
||||||
|
ensure_log_file();
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
// If we can't create the directory, disable file logging
|
// If we can't create the directory, disable file logging
|
||||||
file_logging_enabled_ = false;
|
file_logging_enabled_ = false;
|
||||||
@@ -34,11 +36,7 @@ ErrorHandler::ErrorHandler()
|
|||||||
ErrorHandler::~ErrorHandler()
|
ErrorHandler::~ErrorHandler()
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lg(mtx_);
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
if (log_file_ &&log_file_
|
if (log_file_ && log_file_->is_open()) {
|
||||||
->
|
|
||||||
is_open()
|
|
||||||
)
|
|
||||||
{
|
|
||||||
log_file_->flush();
|
log_file_->flush();
|
||||||
log_file_->close();
|
log_file_->close();
|
||||||
}
|
}
|
||||||
@@ -249,11 +247,8 @@ void
|
|||||||
ErrorHandler::ensure_log_file()
|
ErrorHandler::ensure_log_file()
|
||||||
{
|
{
|
||||||
// Must be called with mtx_ held
|
// Must be called with mtx_ held
|
||||||
if (log_file_ &&log_file_
|
if (log_file_ && log_file_->is_open())
|
||||||
->
|
return;
|
||||||
is_open()
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (log_file_path_.empty())
|
if (log_file_path_.empty())
|
||||||
return;
|
return;
|
||||||
@@ -313,6 +308,6 @@ std::uint64_t
|
|||||||
ErrorHandler::now_ns()
|
ErrorHandler::now_ns()
|
||||||
{
|
{
|
||||||
using namespace std::chrono;
|
using namespace std::chrono;
|
||||||
return duration_cast<nanoseconds>(steady_clock::now().time_since_epoch()).count();
|
return duration_cast<nanoseconds>(system_clock::now().time_since_epoch()).count();
|
||||||
}
|
}
|
||||||
} // namespace kte
|
} // namespace kte
|
||||||
|
|||||||
@@ -125,7 +125,11 @@ map_key(const SDL_Keycode key,
|
|||||||
case SDLK_KP_ENTER:
|
case SDLK_KP_ENTER:
|
||||||
k_prefix = false;
|
k_prefix = false;
|
||||||
k_ctrl_pending = false;
|
k_ctrl_pending = false;
|
||||||
out = {true, CommandId::Newline, "", 0};
|
if (mod & KMOD_SHIFT) {
|
||||||
|
out = {true, CommandId::SmartNewline, "", 0};
|
||||||
|
} else {
|
||||||
|
out = {true, CommandId::Newline, "", 0};
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
case SDLK_ESCAPE:
|
case SDLK_ESCAPE:
|
||||||
k_prefix = false;
|
k_prefix = false;
|
||||||
@@ -439,12 +443,14 @@ ImGuiInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If editor universal argument is active, consume digit TEXTINPUT
|
// If editor universal argument is active, consume digit TEXTINPUT
|
||||||
if (ed_ && ed_
|
if (ed_ &&ed_
|
||||||
|
|
||||||
|
|
||||||
->
|
|
||||||
UArg() != 0
|
->
|
||||||
) {
|
UArg() != 0
|
||||||
|
)
|
||||||
|
{
|
||||||
const char *txt = e.text.text;
|
const char *txt = e.text.text;
|
||||||
if (txt && *txt) {
|
if (txt && *txt) {
|
||||||
unsigned char c0 = static_cast<unsigned char>(txt[0]);
|
unsigned char c0 = static_cast<unsigned char>(txt[0]);
|
||||||
@@ -606,4 +612,4 @@ ImGuiInputHandler::Poll(MappedInput &out)
|
|||||||
out = q_.front();
|
out = q_.front();
|
||||||
q_.pop();
|
q_.pop();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -209,19 +209,35 @@ ImGuiRenderer::Draw(Editor &ed)
|
|||||||
return {by, best_col};
|
return {by, best_col};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mouse-driven selection: set mark on press, update cursor on drag
|
// Mouse-driven selection: set mark on double-click or drag, update cursor on any press/drag
|
||||||
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
||||||
mouse_selecting = true;
|
mouse_selecting = true;
|
||||||
auto [by, bx] = mouse_pos_to_buf();
|
auto [by, bx] = mouse_pos_to_buf();
|
||||||
char tmp[64];
|
char tmp[64];
|
||||||
std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, bx);
|
std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, bx);
|
||||||
Execute(ed, CommandId::MoveCursorTo, std::string(tmp));
|
Execute(ed, CommandId::MoveCursorTo, std::string(tmp));
|
||||||
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
|
|
||||||
mbuf->SetMark(bx, by);
|
// Only set mark on double click.
|
||||||
|
// Dragging will also set the mark if not already set (handled below).
|
||||||
|
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
|
||||||
|
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
|
||||||
|
mbuf->SetMark(bx, by);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (mouse_selecting && ImGui::IsWindowHovered() && ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
|
if (mouse_selecting && ImGui::IsWindowHovered() && ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
|
||||||
auto [by, bx] = mouse_pos_to_buf();
|
auto [by, bx] = mouse_pos_to_buf();
|
||||||
|
// If we are dragging (mouse moved while down), ensure mark is set to start selection
|
||||||
|
if (ImGui::IsMouseDragging(ImGuiMouseButton_Left, 1.0f)) {
|
||||||
|
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
|
||||||
|
if (!mbuf->MarkSet()) {
|
||||||
|
// We'd need to convert click_pos to buf coords, but it's complex here.
|
||||||
|
// Setting it to where the cursor was *before* we started moving it
|
||||||
|
// in this frame is a good approximation, or just using current.
|
||||||
|
mbuf->SetMark(mbuf->Curx(), mbuf->Cury());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
char tmp[64];
|
char tmp[64];
|
||||||
std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, bx);
|
std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, bx);
|
||||||
Execute(ed, CommandId::MoveCursorTo, std::string(tmp));
|
Execute(ed, CommandId::MoveCursorTo, std::string(tmp));
|
||||||
@@ -927,4 +943,4 @@ ImGuiRenderer::Draw(Editor &ed)
|
|||||||
ed.SetFilePickerVisible(false);
|
ed.SetFilePickerVisible(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,8 +226,12 @@ KLookupEscCommand(const int ascii_key, CommandId &out) -> bool
|
|||||||
case 'q':
|
case 'q':
|
||||||
out = CommandId::ReflowParagraph; // Esc q (reflow paragraph)
|
out = CommandId::ReflowParagraph; // Esc q (reflow paragraph)
|
||||||
return true;
|
return true;
|
||||||
|
case '\n':
|
||||||
|
case '\r':
|
||||||
|
out = CommandId::SmartNewline; // Shift+Enter (some terminals send this as Alt+Enter sequences)
|
||||||
|
return true;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -67,13 +67,20 @@ map_key_to_command(const int ch,
|
|||||||
if (pressed) {
|
if (pressed) {
|
||||||
mouse_selecting = true;
|
mouse_selecting = true;
|
||||||
Execute(*ed, CommandId::MoveCursorTo, std::string(buf));
|
Execute(*ed, CommandId::MoveCursorTo, std::string(buf));
|
||||||
if (Buffer *b = ed->CurrentBuffer()) {
|
// We don't set the mark on simple click anymore in ncurses either,
|
||||||
b->SetMark(b->Curx(), b->Cury());
|
// to be consistent. ncurses doesn't easily support double-click
|
||||||
}
|
// or drag-threshold in a platform-independent way here,
|
||||||
|
// but we can at least only set mark on MOVED.
|
||||||
out.hasCommand = false;
|
out.hasCommand = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (mouse_selecting && moved) {
|
if (mouse_selecting && moved) {
|
||||||
|
if (Buffer *b = ed->CurrentBuffer()) {
|
||||||
|
if (!b->MarkSet()) {
|
||||||
|
// Set mark at CURRENT cursor position (which is where we were before this move)
|
||||||
|
b->SetMark(b->Curx(), b->Cury());
|
||||||
|
}
|
||||||
|
}
|
||||||
Execute(*ed, CommandId::MoveCursorTo, std::string(buf));
|
Execute(*ed, CommandId::MoveCursorTo, std::string(buf));
|
||||||
out.hasCommand = false;
|
out.hasCommand = false;
|
||||||
return true;
|
return true;
|
||||||
@@ -329,4 +336,4 @@ TerminalInputHandler::Poll(MappedInput &out)
|
|||||||
{
|
{
|
||||||
out = {};
|
out = {};
|
||||||
return decode_(out) && out.hasCommand;
|
return decode_(out) && out.hasCommand;
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,7 @@ enum class UndoType : std::uint8_t {
|
|||||||
Paste,
|
Paste,
|
||||||
Newline,
|
Newline,
|
||||||
DeleteRow,
|
DeleteRow,
|
||||||
|
InsertRow,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct UndoNode {
|
struct UndoNode {
|
||||||
@@ -20,4 +21,4 @@ struct UndoNode {
|
|||||||
UndoNode *parent = nullptr; // previous state; null means pre-first-edit
|
UndoNode *parent = nullptr; // previous state; null means pre-first-edit
|
||||||
UndoNode *child = nullptr; // next in current timeline
|
UndoNode *child = nullptr; // next in current timeline
|
||||||
UndoNode *next = nullptr; // redo branch
|
UndoNode *next = nullptr; // redo branch
|
||||||
};
|
};
|
||||||
@@ -36,7 +36,8 @@ UndoSystem::Begin(UndoType type)
|
|||||||
const int col = static_cast<int>(buf_->Curx());
|
const int col = static_cast<int>(buf_->Curx());
|
||||||
|
|
||||||
// Some operations should always be standalone undo steps.
|
// Some operations should always be standalone undo steps.
|
||||||
const bool always_standalone = (type == UndoType::Newline || type == UndoType::DeleteRow);
|
const bool always_standalone = (type == UndoType::Newline || type == UndoType::DeleteRow || type ==
|
||||||
|
UndoType::InsertRow);
|
||||||
if (always_standalone) {
|
if (always_standalone) {
|
||||||
commit();
|
commit();
|
||||||
}
|
}
|
||||||
@@ -75,6 +76,7 @@ UndoSystem::Begin(UndoType type)
|
|||||||
}
|
}
|
||||||
case UndoType::Newline:
|
case UndoType::Newline:
|
||||||
case UndoType::DeleteRow:
|
case UndoType::DeleteRow:
|
||||||
|
case UndoType::InsertRow:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -314,6 +316,15 @@ UndoSystem::apply(const UndoNode *node, int direction)
|
|||||||
buf_->SetCursor(0, static_cast<std::size_t>(node->row));
|
buf_->SetCursor(0, static_cast<std::size_t>(node->row));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case UndoType::InsertRow:
|
||||||
|
if (direction > 0) {
|
||||||
|
buf_->insert_row(node->row, node->text);
|
||||||
|
buf_->SetCursor(0, static_cast<std::size_t>(node->row));
|
||||||
|
} else {
|
||||||
|
buf_->delete_row(node->row);
|
||||||
|
buf_->SetCursor(0, static_cast<std::size_t>(node->row));
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,6 +422,8 @@ UndoSystem::type_str(UndoType t)
|
|||||||
return "Newline";
|
return "Newline";
|
||||||
case UndoType::DeleteRow:
|
case UndoType::DeleteRow:
|
||||||
return "DeleteRow";
|
return "DeleteRow";
|
||||||
|
case UndoType::InsertRow:
|
||||||
|
return "InsertRow";
|
||||||
}
|
}
|
||||||
return "?";
|
return "?";
|
||||||
}
|
}
|
||||||
@@ -452,4 +465,4 @@ UndoSystem::debug_log(const char *op) const
|
|||||||
#else
|
#else
|
||||||
(void) op;
|
(void) op;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
3
main.cc
3
main.cc
@@ -117,6 +117,9 @@ main(int argc, char *argv[])
|
|||||||
{
|
{
|
||||||
std::setlocale(LC_ALL, "");
|
std::setlocale(LC_ALL, "");
|
||||||
|
|
||||||
|
// Ensure the error handler (and its log file) is initialised early.
|
||||||
|
kte::ErrorHandler::Instance();
|
||||||
|
|
||||||
Editor editor;
|
Editor editor;
|
||||||
|
|
||||||
// CLI parsing using getopt_long
|
// CLI parsing using getopt_long
|
||||||
|
|||||||
69
tests/test_reflow_undo.cc
Normal file
69
tests/test_reflow_undo.cc
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
#include "Test.h"
|
||||||
|
|
||||||
|
#include "Buffer.h"
|
||||||
|
#include "Command.h"
|
||||||
|
#include "Editor.h"
|
||||||
|
#include "UndoSystem.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
|
||||||
|
static std::string
|
||||||
|
to_string_rows(const Buffer &buf)
|
||||||
|
{
|
||||||
|
std::string out;
|
||||||
|
for (const auto &r: buf.Rows()) {
|
||||||
|
out += static_cast<std::string>(r);
|
||||||
|
out.push_back('\n');
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (ReflowUndo)
|
||||||
|
{
|
||||||
|
InstallDefaultCommands();
|
||||||
|
|
||||||
|
Editor ed;
|
||||||
|
ed.SetDimensions(24, 80);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
const std::string initial =
|
||||||
|
"This is a very long line that should be reflowed into multiple lines to see if undo works correctly.\n";
|
||||||
|
b.insert_text(0, 0, initial);
|
||||||
|
b.SetCursor(0, 0);
|
||||||
|
|
||||||
|
// Commit initial insertion so it's its own undo step
|
||||||
|
if (auto *u = b.Undo())
|
||||||
|
u->commit();
|
||||||
|
|
||||||
|
ed.AddBuffer(std::move(b));
|
||||||
|
|
||||||
|
Buffer *buf = ed.CurrentBuffer();
|
||||||
|
ASSERT_TRUE(buf != nullptr);
|
||||||
|
|
||||||
|
const std::string original_dump = to_string_rows(*buf);
|
||||||
|
|
||||||
|
// Reflow with small width
|
||||||
|
const int width = 20;
|
||||||
|
ASSERT_TRUE(Execute(ed, "reflow-paragraph", "", width));
|
||||||
|
|
||||||
|
const std::string reflowed_dump = to_string_rows(*buf);
|
||||||
|
ASSERT_TRUE(reflowed_dump != original_dump);
|
||||||
|
ASSERT_TRUE(buf->Rows().size() > 1);
|
||||||
|
|
||||||
|
// Undo reflow
|
||||||
|
ASSERT_TRUE(Execute(ed, "undo", "", 1));
|
||||||
|
const std::string after_undo_dump = to_string_rows(*buf);
|
||||||
|
|
||||||
|
if (after_undo_dump != original_dump) {
|
||||||
|
fprintf(stderr, "Undo failed.\nExpected:\n%s\nGot:\n%s\n", original_dump.c_str(),
|
||||||
|
after_undo_dump.c_str());
|
||||||
|
}
|
||||||
|
EXPECT_TRUE(after_undo_dump == original_dump);
|
||||||
|
|
||||||
|
// Redo reflow
|
||||||
|
ASSERT_TRUE(Execute(ed, "redo", "", 1));
|
||||||
|
const std::string after_redo_dump = to_string_rows(*buf);
|
||||||
|
EXPECT_TRUE(after_redo_dump == reflowed_dump);
|
||||||
|
}
|
||||||
79
tests/test_smart_newline.cc
Normal file
79
tests/test_smart_newline.cc
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
#include "Test.h"
|
||||||
|
#include "Buffer.h"
|
||||||
|
#include "Editor.h"
|
||||||
|
#include "Command.h"
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
|
||||||
|
TEST (SmartNewline_AutoIndent)
|
||||||
|
{
|
||||||
|
Editor editor;
|
||||||
|
InstallDefaultCommands();
|
||||||
|
Buffer &buf = editor.Buffers().emplace_back();
|
||||||
|
|
||||||
|
// Set up initial state: " line1"
|
||||||
|
buf.insert_text(0, 0, " line1");
|
||||||
|
buf.SetCursor(7, 0); // At end of line
|
||||||
|
|
||||||
|
// Execute SmartNewline
|
||||||
|
bool ok = Execute(editor, CommandId::SmartNewline);
|
||||||
|
ASSERT_TRUE(ok);
|
||||||
|
|
||||||
|
// Should have two lines now
|
||||||
|
ASSERT_EQ(buf.Nrows(), 2);
|
||||||
|
// Line 0 remains " line1"
|
||||||
|
ASSERT_EQ(buf.GetLineString(0), " line1");
|
||||||
|
// Line 1 should have " " (two spaces)
|
||||||
|
ASSERT_EQ(buf.GetLineString(1), " ");
|
||||||
|
// Cursor should be at (2, 1)
|
||||||
|
ASSERT_EQ(buf.Curx(), 2);
|
||||||
|
ASSERT_EQ(buf.Cury(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (SmartNewline_TabIndent)
|
||||||
|
{
|
||||||
|
Editor editor;
|
||||||
|
InstallDefaultCommands();
|
||||||
|
Buffer &buf = editor.Buffers().emplace_back();
|
||||||
|
|
||||||
|
// Set up initial state: "\tline1"
|
||||||
|
buf.insert_text(0, 0, "\tline1");
|
||||||
|
buf.SetCursor(6, 0); // At end of line
|
||||||
|
|
||||||
|
// Execute SmartNewline
|
||||||
|
bool ok = Execute(editor, CommandId::SmartNewline);
|
||||||
|
ASSERT_TRUE(ok);
|
||||||
|
|
||||||
|
// Should have two lines now
|
||||||
|
ASSERT_EQ(buf.Nrows(), 2);
|
||||||
|
// Line 1 should have "\t"
|
||||||
|
ASSERT_EQ(buf.GetLineString(1), "\t");
|
||||||
|
// Cursor should be at (1, 1)
|
||||||
|
ASSERT_EQ(buf.Curx(), 1);
|
||||||
|
ASSERT_EQ(buf.Cury(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (SmartNewline_NoIndent)
|
||||||
|
{
|
||||||
|
Editor editor;
|
||||||
|
InstallDefaultCommands();
|
||||||
|
Buffer &buf = editor.Buffers().emplace_back();
|
||||||
|
|
||||||
|
// Set up initial state: "line1"
|
||||||
|
buf.insert_text(0, 0, "line1");
|
||||||
|
buf.SetCursor(5, 0); // At end of line
|
||||||
|
|
||||||
|
// Execute SmartNewline
|
||||||
|
bool ok = Execute(editor, CommandId::SmartNewline);
|
||||||
|
ASSERT_TRUE(ok);
|
||||||
|
|
||||||
|
// Should have two lines now
|
||||||
|
ASSERT_EQ(buf.Nrows(), 2);
|
||||||
|
// Line 1 should be empty
|
||||||
|
ASSERT_EQ(buf.GetLineString(1), "");
|
||||||
|
// Cursor should be at (0, 1)
|
||||||
|
ASSERT_EQ(buf.Curx(), 0);
|
||||||
|
ASSERT_EQ(buf.Cury(), 1);
|
||||||
|
}
|
||||||
@@ -57,7 +57,7 @@ validate_undo_tree(const UndoSystem &u)
|
|||||||
// The undo suite aims to cover invariants with a small, adversarial test matrix.
|
// The undo suite aims to cover invariants with a small, adversarial test matrix.
|
||||||
|
|
||||||
|
|
||||||
TEST(Undo_InsertRun_Coalesces_OneStep)
|
TEST (Undo_InsertRun_Coalesces_OneStep)
|
||||||
{
|
{
|
||||||
Buffer b;
|
Buffer b;
|
||||||
UndoSystem *u = b.Undo();
|
UndoSystem *u = b.Undo();
|
||||||
@@ -81,7 +81,7 @@ TEST(Undo_InsertRun_Coalesces_OneStep)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
TEST(Undo_InsertRun_BreaksOnNonAdjacentCursor)
|
TEST (Undo_InsertRun_BreaksOnNonAdjacentCursor)
|
||||||
{
|
{
|
||||||
Buffer b;
|
Buffer b;
|
||||||
UndoSystem *u = b.Undo();
|
UndoSystem *u = b.Undo();
|
||||||
@@ -109,7 +109,7 @@ TEST(Undo_InsertRun_BreaksOnNonAdjacentCursor)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
TEST(Undo_BackspaceRun_Coalesces_OneStep)
|
TEST (Undo_BackspaceRun_Coalesces_OneStep)
|
||||||
{
|
{
|
||||||
Buffer b;
|
Buffer b;
|
||||||
UndoSystem *u = b.Undo();
|
UndoSystem *u = b.Undo();
|
||||||
@@ -143,7 +143,7 @@ TEST(Undo_BackspaceRun_Coalesces_OneStep)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
TEST(Undo_DeleteKeyRun_Coalesces_OneStep)
|
TEST (Undo_DeleteKeyRun_Coalesces_OneStep)
|
||||||
{
|
{
|
||||||
Buffer b;
|
Buffer b;
|
||||||
UndoSystem *u = b.Undo();
|
UndoSystem *u = b.Undo();
|
||||||
@@ -176,7 +176,7 @@ TEST(Undo_DeleteKeyRun_Coalesces_OneStep)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
TEST(Undo_Newline_IsStandalone)
|
TEST (Undo_Newline_IsStandalone)
|
||||||
{
|
{
|
||||||
Buffer b;
|
Buffer b;
|
||||||
UndoSystem *u = b.Undo();
|
UndoSystem *u = b.Undo();
|
||||||
@@ -211,7 +211,7 @@ TEST(Undo_Newline_IsStandalone)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
TEST(Undo_ExplicitGroup_UndoesAsUnit)
|
TEST (Undo_ExplicitGroup_UndoesAsUnit)
|
||||||
{
|
{
|
||||||
Buffer b;
|
Buffer b;
|
||||||
UndoSystem *u = b.Undo();
|
UndoSystem *u = b.Undo();
|
||||||
@@ -239,7 +239,7 @@ TEST(Undo_ExplicitGroup_UndoesAsUnit)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
TEST(Undo_Branching_RedoBranchSelectionDeterministic)
|
TEST (Undo_Branching_RedoBranchSelectionDeterministic)
|
||||||
{
|
{
|
||||||
Buffer b;
|
Buffer b;
|
||||||
UndoSystem *u = b.Undo();
|
UndoSystem *u = b.Undo();
|
||||||
@@ -283,7 +283,7 @@ TEST(Undo_Branching_RedoBranchSelectionDeterministic)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
TEST(Undo_DirtyFlag_CrossesMarkSaved)
|
TEST (Undo_DirtyFlag_CrossesMarkSaved)
|
||||||
{
|
{
|
||||||
Buffer b;
|
Buffer b;
|
||||||
UndoSystem *u = b.Undo();
|
UndoSystem *u = b.Undo();
|
||||||
@@ -312,7 +312,7 @@ TEST(Undo_DirtyFlag_CrossesMarkSaved)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
TEST(Undo_RoundTrip_Lossless_RandomEdits)
|
TEST (Undo_RoundTrip_Lossless_RandomEdits)
|
||||||
{
|
{
|
||||||
Buffer b;
|
Buffer b;
|
||||||
UndoSystem *u = b.Undo();
|
UndoSystem *u = b.Undo();
|
||||||
@@ -368,7 +368,7 @@ TEST(Undo_RoundTrip_Lossless_RandomEdits)
|
|||||||
|
|
||||||
// Legacy/extended undo tests follow. Keep them available for debugging,
|
// Legacy/extended undo tests follow. Keep them available for debugging,
|
||||||
// but disable them by default to keep the suite focused (~10 tests).
|
// but disable them by default to keep the suite focused (~10 tests).
|
||||||
#if 0
|
#if 1
|
||||||
|
|
||||||
|
|
||||||
TEST (Undo_Branching_RedoPreservedAfterNewEdit)
|
TEST (Undo_Branching_RedoPreservedAfterNewEdit)
|
||||||
@@ -713,6 +713,7 @@ TEST (Undo_StructuralInvariants_BranchingAndRoots)
|
|||||||
validate_undo_tree(*u);
|
validate_undo_tree(*u);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
TEST (Undo_BranchSelection_ThreeSiblingsAndHeadPersists)
|
TEST (Undo_BranchSelection_ThreeSiblingsAndHeadPersists)
|
||||||
{
|
{
|
||||||
Buffer b;
|
Buffer b;
|
||||||
@@ -796,7 +797,7 @@ TEST (Undo_BranchSelection_ThreeSiblingsAndHeadPersists)
|
|||||||
|
|
||||||
|
|
||||||
// Additional legacy tests below are useful, but kept disabled by default.
|
// Additional legacy tests below are useful, but kept disabled by default.
|
||||||
#if 0
|
#if 1
|
||||||
|
|
||||||
TEST (Undo_Branching_SwitchBetweenTwoRedoBranches_TextAndCursor)
|
TEST (Undo_Branching_SwitchBetweenTwoRedoBranches_TextAndCursor)
|
||||||
{
|
{
|
||||||
@@ -1196,4 +1197,167 @@ TEST (Undo_Command_RedoCountSelectsBranch)
|
|||||||
validate_undo_tree(*u);
|
validate_undo_tree(*u);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif // legacy tests
|
|
||||||
|
TEST (Undo_InsertRow_UndoDeletesRow)
|
||||||
|
{
|
||||||
|
Buffer b;
|
||||||
|
UndoSystem *u = b.Undo();
|
||||||
|
ASSERT_TRUE(u != nullptr);
|
||||||
|
|
||||||
|
// Seed two lines so insert_row has proper newline context.
|
||||||
|
b.insert_text(0, 0, std::string_view("first\nlast"));
|
||||||
|
ASSERT_EQ(b.Rows().size(), (std::size_t) 2);
|
||||||
|
|
||||||
|
// Insert a row at position 1 (between first and last), then record it.
|
||||||
|
b.insert_row(1, std::string_view("second"));
|
||||||
|
ASSERT_EQ(b.Rows().size(), (std::size_t) 3);
|
||||||
|
ASSERT_EQ(std::string(b.Rows()[1]), std::string("second"));
|
||||||
|
|
||||||
|
b.SetCursor(0, 1);
|
||||||
|
u->Begin(UndoType::InsertRow);
|
||||||
|
u->Append(std::string_view("second"));
|
||||||
|
u->commit();
|
||||||
|
|
||||||
|
// Undo should remove the inserted row.
|
||||||
|
u->undo();
|
||||||
|
ASSERT_EQ(b.Rows().size(), (std::size_t) 2);
|
||||||
|
ASSERT_EQ(std::string(b.Rows()[0]), std::string("first"));
|
||||||
|
ASSERT_EQ(std::string(b.Rows()[1]), std::string("last"));
|
||||||
|
|
||||||
|
// Redo should re-insert it.
|
||||||
|
u->redo();
|
||||||
|
ASSERT_EQ(b.Rows().size(), (std::size_t) 3);
|
||||||
|
ASSERT_EQ(std::string(b.Rows()[1]), std::string("second"));
|
||||||
|
|
||||||
|
validate_undo_tree(*u);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (Undo_DeleteRow_UndoRestoresRow)
|
||||||
|
{
|
||||||
|
Buffer b;
|
||||||
|
UndoSystem *u = b.Undo();
|
||||||
|
ASSERT_TRUE(u != nullptr);
|
||||||
|
|
||||||
|
b.insert_text(0, 0, std::string_view("alpha\nbeta\ngamma"));
|
||||||
|
ASSERT_EQ(b.Rows().size(), (std::size_t) 3);
|
||||||
|
|
||||||
|
// Record a DeleteRow for row 1 ("beta").
|
||||||
|
b.SetCursor(0, 1);
|
||||||
|
u->Begin(UndoType::DeleteRow);
|
||||||
|
u->Append(static_cast<std::string>(b.Rows()[1]));
|
||||||
|
u->commit();
|
||||||
|
b.delete_row(1);
|
||||||
|
|
||||||
|
ASSERT_EQ(b.Rows().size(), (std::size_t) 2);
|
||||||
|
ASSERT_EQ(std::string(b.Rows()[0]), std::string("alpha"));
|
||||||
|
ASSERT_EQ(std::string(b.Rows()[1]), std::string("gamma"));
|
||||||
|
|
||||||
|
// Undo should restore "beta" at row 1.
|
||||||
|
u->undo();
|
||||||
|
ASSERT_EQ(b.Rows().size(), (std::size_t) 3);
|
||||||
|
ASSERT_EQ(std::string(b.Rows()[1]), std::string("beta"));
|
||||||
|
|
||||||
|
// Redo should delete it again.
|
||||||
|
u->redo();
|
||||||
|
ASSERT_EQ(b.Rows().size(), (std::size_t) 2);
|
||||||
|
ASSERT_EQ(std::string(b.Rows()[1]), std::string("gamma"));
|
||||||
|
|
||||||
|
validate_undo_tree(*u);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (Undo_InsertRow_IsStandalone)
|
||||||
|
{
|
||||||
|
Buffer b;
|
||||||
|
UndoSystem *u = b.Undo();
|
||||||
|
ASSERT_TRUE(u != nullptr);
|
||||||
|
|
||||||
|
// Seed with two lines so InsertRow has proper newline context.
|
||||||
|
b.insert_text(0, 0, std::string_view("x\nend"));
|
||||||
|
ASSERT_EQ(b.Rows().size(), (std::size_t) 2);
|
||||||
|
|
||||||
|
// Start a pending insert on row 0.
|
||||||
|
b.SetCursor(1, 0);
|
||||||
|
u->Begin(UndoType::Insert);
|
||||||
|
b.insert_text(0, 1, std::string_view("y"));
|
||||||
|
u->Append('y');
|
||||||
|
b.SetCursor(2, 0);
|
||||||
|
|
||||||
|
// InsertRow should seal the pending "y" and become its own step.
|
||||||
|
b.insert_row(1, std::string_view("row2"));
|
||||||
|
b.SetCursor(0, 1);
|
||||||
|
u->Begin(UndoType::InsertRow);
|
||||||
|
u->Append(std::string_view("row2"));
|
||||||
|
u->commit();
|
||||||
|
|
||||||
|
ASSERT_EQ(std::string(b.Rows()[0]), std::string("xy"));
|
||||||
|
ASSERT_EQ(std::string(b.Rows()[1]), std::string("row2"));
|
||||||
|
ASSERT_EQ(b.Rows().size(), (std::size_t) 3);
|
||||||
|
|
||||||
|
// Undo InsertRow only.
|
||||||
|
u->undo();
|
||||||
|
ASSERT_EQ(b.Rows().size(), (std::size_t) 2);
|
||||||
|
ASSERT_EQ(std::string(b.Rows()[0]), std::string("xy"));
|
||||||
|
|
||||||
|
// Undo the insert "y".
|
||||||
|
u->undo();
|
||||||
|
ASSERT_EQ(std::string(b.Rows()[0]), std::string("x"));
|
||||||
|
|
||||||
|
validate_undo_tree(*u);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (Undo_GroupedDeleteAndInsertRows_UndoesAsUnit)
|
||||||
|
{
|
||||||
|
Buffer b;
|
||||||
|
UndoSystem *u = b.Undo();
|
||||||
|
ASSERT_TRUE(u != nullptr);
|
||||||
|
|
||||||
|
// Seed three lines (with trailing newline so delete_row/insert_row work cleanly).
|
||||||
|
b.insert_text(0, 0, std::string_view("aaa\nbbb\nccc\n"));
|
||||||
|
ASSERT_EQ(b.Rows().size(), (std::size_t) 4); // 3 content + 1 empty trailing
|
||||||
|
const std::string original = b.AsString();
|
||||||
|
|
||||||
|
// Group: delete content rows then insert replacements (simulates reflow).
|
||||||
|
(void) u->BeginGroup();
|
||||||
|
|
||||||
|
// Delete rows 2,1,0 in reverse order (like reflow does).
|
||||||
|
for (int i = 2; i >= 0; --i) {
|
||||||
|
b.SetCursor(0, static_cast<std::size_t>(i));
|
||||||
|
u->Begin(UndoType::DeleteRow);
|
||||||
|
u->Append(static_cast<std::string>(b.Rows()[static_cast<std::size_t>(i)]));
|
||||||
|
u->commit();
|
||||||
|
b.delete_row(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert replacement rows.
|
||||||
|
b.insert_row(0, std::string_view("aaa bbb"));
|
||||||
|
b.SetCursor(0, 0);
|
||||||
|
u->Begin(UndoType::InsertRow);
|
||||||
|
u->Append(std::string_view("aaa bbb"));
|
||||||
|
u->commit();
|
||||||
|
|
||||||
|
b.insert_row(1, std::string_view("ccc"));
|
||||||
|
b.SetCursor(0, 1);
|
||||||
|
u->Begin(UndoType::InsertRow);
|
||||||
|
u->Append(std::string_view("ccc"));
|
||||||
|
u->commit();
|
||||||
|
|
||||||
|
u->EndGroup();
|
||||||
|
|
||||||
|
const std::string reflowed = b.AsString();
|
||||||
|
|
||||||
|
// Single undo should restore original content.
|
||||||
|
u->undo();
|
||||||
|
ASSERT_EQ(b.AsString(), original);
|
||||||
|
|
||||||
|
// Redo should restore the reflowed state.
|
||||||
|
u->redo();
|
||||||
|
ASSERT_EQ(b.AsString(), reflowed);
|
||||||
|
|
||||||
|
validate_undo_tree(*u);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#endif // legacy tests
|
||||||
Reference in New Issue
Block a user