Fix undo correctness, crash-recovery, and syntax highlighting bugs from full codebase review
Undo/redo: - cmd_newline recorded the undo node's position after the cursor moved past the split point, so undoing Enter joined the wrong pair of lines. - Backspace/Delete line-joins recorded UndoType::Newline, whose apply() semantics are inverted for a join (forward=split); add UndoType::JoinLines with correct forward/backward behavior. - Regex replace-all, indent/unindent region, kill-to-eol, kill-line, kill-region, and delete-word-prev/next mutated the buffer via the no-undo-recording raw APIs; they now record undo, grouped atomically via a shared UndoGroupGuard. - Plain-text replace-all with an empty replacement advanced the scan position one character too far, skipping adjacent/overlapping matches. Crash recovery / memory safety: - Buffer's move ctor/assignment never carried over swap_rec_, on_disk_identity_, or the visual-line-mode fields, so the crash-recovery journal silently stopped tracking a buffer whenever Editor's std::vector<Buffer> reallocated or shifted (e.g. opening/closing files). Added SwapManager::Rehome() plus Flush()-before-mutate in Editor::AddBuffer/CloseBuffer so the journal's Buffer* key and the background writer thread never reference a stale address. - UndoTree leaked its entire node graph (including all edit text) on every buffer close/reload; added ~UndoTree() to free it. - main.cc didn't restore the terminal if an exception escaped the run loop; added an RAII guard around Frontend::Shutdown(). Input handling: - Terminal frontend used getch() instead of get_wch(), silently dropping non-ASCII keyboard input despite linking wide-char ncurses. - KKeymap's C-k lookup switches on an already-lowercased key, making `case 'E'` unreachable dead code; C-k Shift-E silently aliased to C-k e. - ImGui file picker wasn't modal: keystrokes typed while it was open still reached the buffer underneath as edit commands, and Escape didn't close it. - Gated ImGuiInputHandler's unconditional fprintf/fflush diagnostics behind an IMGUI_IH_DEBUG macro, matching QtInputHandler's existing convention. Syntax highlighting: - Go/Rust/SQL highlighters weren't stateful, so multi-line /* */ comments mis-highlighted as code starting on the second line. - Python's triple-quoted-string handler didn't re-scan the remainder of a line after a string closed mid-line, missing a same-line reopen. - HighlighterEngine's state_last_contig_ field was declared but unused, forcing an O(n) scan of state_cache_ on every stateful lookup; wired it up as a real fast path and fixed InvalidateFrom() to keep it in sync. - kge's :syntax off was silently undone the next frame because apply_syntax_to_buffer() re-applied the config default unconditionally every frame; added a user-override flag mirroring the existing edit-mode-detection guard. Added regression tests for all of the above (test_undo.cc, test_kkeymap.cc, test_command_semantics.cc, test_search_replace_flow.cc, and a new test_syntax_highlighting.cc). Full suite (163 tests) passes, including under AddressSanitizer. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -108,3 +108,26 @@ TEST(CommandSemantics_CopyRegion_And_KillRegion)
|
||||
ASSERT_EQ(b.MarkSet(), false);
|
||||
ASSERT_EQ(h.Text(), std::string("hello "));
|
||||
}
|
||||
|
||||
|
||||
TEST(CommandSemantics_Syntax_OnOff_SetsUserOverride)
|
||||
{
|
||||
TestHarness h;
|
||||
Editor &ed = h.EditorRef();
|
||||
Buffer &b = h.Buf();
|
||||
|
||||
// Before any explicit :syntax command, nothing has overridden the
|
||||
// frontend's config-driven default.
|
||||
ASSERT_EQ(b.SyntaxUserOverride(), false);
|
||||
|
||||
ASSERT_TRUE(Execute(ed, CommandId::Syntax, "off"));
|
||||
ASSERT_EQ(b.SyntaxEnabled(), false);
|
||||
// This flag is what a frontend's per-frame "apply config default" pass
|
||||
// must check before re-enabling syntax, or a manual :syntax off gets
|
||||
// silently undone on the next frame.
|
||||
ASSERT_EQ(b.SyntaxUserOverride(), true);
|
||||
|
||||
ASSERT_TRUE(Execute(ed, CommandId::Syntax, "on"));
|
||||
ASSERT_EQ(b.SyntaxEnabled(), true);
|
||||
ASSERT_EQ(b.SyntaxUserOverride(), true);
|
||||
}
|
||||
|
||||
@@ -37,6 +37,21 @@ TEST(KKeymap_KPrefix_CanonicalChords)
|
||||
}
|
||||
|
||||
|
||||
TEST(KKeymap_KPrefix_UppercaseE_RejectedAndDistinctFromLowercaseE)
|
||||
{
|
||||
CommandId id{};
|
||||
|
||||
// 'e' is a real binding (OpenFileStart).
|
||||
ASSERT_TRUE(KLookupKCommand('e', false, id));
|
||||
ASSERT_EQ(id, CommandId::OpenFileStart);
|
||||
|
||||
// 'E' must be rejected, not silently aliased to 'e' via case-insensitive
|
||||
// lookup (the switch normalizes to lowercase, so this has to be special-
|
||||
// cased before it).
|
||||
ASSERT_EQ(KLookupKCommand('E', false, id), false);
|
||||
}
|
||||
|
||||
|
||||
TEST(KKeymap_CtrlChords_CanonicalChords)
|
||||
{
|
||||
CommandId id{};
|
||||
|
||||
@@ -414,17 +414,21 @@ TEST (Migration_EmptyBufferCheck_Pattern)
|
||||
|
||||
TEST (Migration_SyntaxHighlighter_Pattern)
|
||||
{
|
||||
// Test the pattern used in syntax highlighters
|
||||
// Test the pattern used in syntax highlighters: loop Nrows(), fetch each
|
||||
// line via GetLineString(row). Assert the pattern actually returns the
|
||||
// right content, not just that it doesn't crash - see syntax highlighter
|
||||
// correctness tests (test_syntax_highlighting.cc) for tokenization coverage.
|
||||
Buffer buf;
|
||||
buf.insert_text(0, 0, std::string("int main() {\n return 0;\n}"));
|
||||
|
||||
ASSERT_EQ(buf.Nrows(), static_cast<std::size_t>(3));
|
||||
const std::vector<std::string> expected = {"int main() {", " return 0;", "}"};
|
||||
for (std::size_t row = 0; row < buf.Nrows(); ++row) {
|
||||
// This is the pattern used in all migrated highlighters
|
||||
if (row >= buf.Nrows()) {
|
||||
break; // Should never happen
|
||||
}
|
||||
std::string line = buf.GetLineString(row);
|
||||
// Successfully accessed line - size() is always valid for std::string
|
||||
ASSERT_EQ(line, expected[row]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -101,6 +101,36 @@ TEST(SearchFlow_SearchReplace_EmptyFind_DoesNotMutateBuffer_And_ClearsState)
|
||||
}
|
||||
|
||||
|
||||
TEST(SearchFlow_SearchReplace_EmptyWith_ReplacesAdjacentOverlappingMatches)
|
||||
{
|
||||
TestHarness h;
|
||||
Editor &ed = h.EditorRef();
|
||||
Buffer &b = h.Buf();
|
||||
|
||||
// "aaaa" with "aa" -> "" must remove both non-overlapping occurrences, not
|
||||
// just the first: after deleting the match at column 0, the next "aa" now
|
||||
// sits at column 0 too (not column 1), so the scan must resume at the
|
||||
// deletion point rather than one character past it.
|
||||
b.insert_text(0, 0, "aaaa\n");
|
||||
b.SetCursor(0, 0);
|
||||
|
||||
ASSERT_TRUE(h.Exec(CommandId::SearchReplace));
|
||||
ASSERT_TRUE(ed.PromptActive());
|
||||
ASSERT_EQ(ed.CurrentPromptKind(), Editor::PromptKind::ReplaceFind);
|
||||
|
||||
ASSERT_TRUE(h.Exec(CommandId::InsertText, "aa"));
|
||||
ASSERT_TRUE(h.Exec(CommandId::Newline));
|
||||
ASSERT_TRUE(ed.PromptActive());
|
||||
ASSERT_EQ(ed.CurrentPromptKind(), Editor::PromptKind::ReplaceWith);
|
||||
|
||||
// Leave the replacement empty and accept.
|
||||
ASSERT_TRUE(h.Exec(CommandId::Newline));
|
||||
|
||||
ASSERT_TRUE(!ed.PromptActive());
|
||||
ASSERT_EQ(std::string(b.Rows()[0]), std::string(""));
|
||||
}
|
||||
|
||||
|
||||
TEST(SearchFlow_RegexFind_InvalidPattern_FailsSafely_And_ClearsStateOnEnter)
|
||||
{
|
||||
TestHarness h;
|
||||
|
||||
167
tests/test_syntax_highlighting.cc
Normal file
167
tests/test_syntax_highlighting.cc
Normal file
@@ -0,0 +1,167 @@
|
||||
// test_syntax_highlighting.cc - tokenization correctness for LanguageHighlighter
|
||||
// implementations, focused on multi-line state propagation (block comments,
|
||||
// triple-quoted strings) that stateless-looking per-line scans easily get wrong.
|
||||
#include "Test.h"
|
||||
#include "Buffer.h"
|
||||
#include "Highlight.h"
|
||||
#include "syntax/LanguageHighlighter.h"
|
||||
#include "syntax/GoHighlighter.h"
|
||||
#include "syntax/RustHighlighter.h"
|
||||
#include "syntax/SqlHighlighter.h"
|
||||
#include "syntax/PythonHighlighter.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using kte::HighlightSpan;
|
||||
using kte::StatefulHighlighter;
|
||||
using kte::TokenKind;
|
||||
|
||||
namespace {
|
||||
bool
|
||||
has_kind(const std::vector<HighlightSpan> &spans, TokenKind k)
|
||||
{
|
||||
for (const auto &sp: spans) {
|
||||
if (sp.kind == k)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
|
||||
TEST (Syntax_Go_MultiLineBlockComment_PropagatesAcrossLines)
|
||||
{
|
||||
Buffer b;
|
||||
b.replace_all_bytes("/* start\nmiddle line\nend */\nfunc foo() {}\n");
|
||||
|
||||
kte::GoHighlighter hl;
|
||||
StatefulHighlighter::LineState state;
|
||||
std::vector<HighlightSpan> spans;
|
||||
|
||||
// Line 0 opens an unclosed block comment; state must carry forward.
|
||||
state = hl.HighlightLineStateful(b, 0, state, spans);
|
||||
ASSERT_TRUE(state.in_block_comment);
|
||||
|
||||
// Line 1 is entirely inside the comment; the whole line must be Comment,
|
||||
// not re-tokenized as code (this is the bug: without state propagation
|
||||
// "middle" and "line" would come back as identifiers).
|
||||
spans.clear();
|
||||
state = hl.HighlightLineStateful(b, 1, state, spans);
|
||||
ASSERT_TRUE(state.in_block_comment);
|
||||
ASSERT_TRUE(!spans.empty());
|
||||
for (const auto &sp: spans) {
|
||||
ASSERT_TRUE(sp.kind == TokenKind::Comment);
|
||||
}
|
||||
|
||||
// Line 2 closes the comment.
|
||||
spans.clear();
|
||||
state = hl.HighlightLineStateful(b, 2, state, spans);
|
||||
ASSERT_TRUE(!state.in_block_comment);
|
||||
ASSERT_TRUE(has_kind(spans, TokenKind::Comment));
|
||||
|
||||
// Line 3 is ordinary code again: "func" must be a Keyword, not a Comment.
|
||||
spans.clear();
|
||||
state = hl.HighlightLineStateful(b, 3, state, spans);
|
||||
ASSERT_TRUE(!state.in_block_comment);
|
||||
ASSERT_TRUE(has_kind(spans, TokenKind::Keyword));
|
||||
ASSERT_TRUE(!has_kind(spans, TokenKind::Comment));
|
||||
}
|
||||
|
||||
|
||||
TEST (Syntax_Rust_MultiLineBlockComment_PropagatesAcrossLines)
|
||||
{
|
||||
Buffer b;
|
||||
b.replace_all_bytes("/* start\nmiddle line\nend */\nfn foo() {}\n");
|
||||
|
||||
kte::RustHighlighter hl;
|
||||
StatefulHighlighter::LineState state;
|
||||
std::vector<HighlightSpan> spans;
|
||||
|
||||
state = hl.HighlightLineStateful(b, 0, state, spans);
|
||||
ASSERT_TRUE(state.in_block_comment);
|
||||
|
||||
spans.clear();
|
||||
state = hl.HighlightLineStateful(b, 1, state, spans);
|
||||
ASSERT_TRUE(state.in_block_comment);
|
||||
for (const auto &sp: spans) {
|
||||
ASSERT_TRUE(sp.kind == TokenKind::Comment);
|
||||
}
|
||||
|
||||
spans.clear();
|
||||
state = hl.HighlightLineStateful(b, 2, state, spans);
|
||||
ASSERT_TRUE(!state.in_block_comment);
|
||||
|
||||
spans.clear();
|
||||
state = hl.HighlightLineStateful(b, 3, state, spans);
|
||||
ASSERT_TRUE(has_kind(spans, TokenKind::Keyword));
|
||||
ASSERT_TRUE(!has_kind(spans, TokenKind::Comment));
|
||||
}
|
||||
|
||||
|
||||
TEST (Syntax_Sql_MultiLineBlockComment_PropagatesAcrossLines)
|
||||
{
|
||||
Buffer b;
|
||||
b.replace_all_bytes("/* start\nmiddle line\nend */\nSELECT 1;\n");
|
||||
|
||||
kte::SqlHighlighter hl;
|
||||
StatefulHighlighter::LineState state;
|
||||
std::vector<HighlightSpan> spans;
|
||||
|
||||
state = hl.HighlightLineStateful(b, 0, state, spans);
|
||||
ASSERT_TRUE(state.in_block_comment);
|
||||
|
||||
spans.clear();
|
||||
state = hl.HighlightLineStateful(b, 1, state, spans);
|
||||
ASSERT_TRUE(state.in_block_comment);
|
||||
for (const auto &sp: spans) {
|
||||
ASSERT_TRUE(sp.kind == TokenKind::Comment);
|
||||
}
|
||||
|
||||
spans.clear();
|
||||
state = hl.HighlightLineStateful(b, 2, state, spans);
|
||||
ASSERT_TRUE(!state.in_block_comment);
|
||||
|
||||
spans.clear();
|
||||
state = hl.HighlightLineStateful(b, 3, state, spans);
|
||||
ASSERT_TRUE(has_kind(spans, TokenKind::Keyword));
|
||||
ASSERT_TRUE(!has_kind(spans, TokenKind::Comment));
|
||||
}
|
||||
|
||||
|
||||
TEST (Syntax_Python_TripleQuote_ClosesAndReopensOnSameLine)
|
||||
{
|
||||
Buffer b;
|
||||
// Line 0 opens a triple-quoted string that stays open.
|
||||
// Line 1 closes the first string ("end'''") and, on the SAME line, opens a
|
||||
// second triple-quoted string ("'''start of") that stays open into line 2.
|
||||
b.replace_all_bytes("x = '''abc\nend''' + '''start of\nnext string'''\n");
|
||||
|
||||
kte::PythonHighlighter hl;
|
||||
StatefulHighlighter::LineState state;
|
||||
std::vector<HighlightSpan> spans;
|
||||
|
||||
state = hl.HighlightLineStateful(b, 0, state, spans);
|
||||
ASSERT_TRUE(state.in_raw_string);
|
||||
ASSERT_EQ(state.raw_delim, std::string("'''"));
|
||||
|
||||
spans.clear();
|
||||
state = hl.HighlightLineStateful(b, 1, state, spans);
|
||||
// The bug: without re-scanning the remainder after the closing ''' the
|
||||
// second opening ''' on this line is never noticed, so state.in_raw_string
|
||||
// would incorrectly come back false here.
|
||||
ASSERT_TRUE(state.in_raw_string);
|
||||
ASSERT_EQ(state.raw_delim, std::string("'''"));
|
||||
|
||||
// Line 2 is inside the second string; it must be highlighted as String,
|
||||
// not as ordinary code.
|
||||
spans.clear();
|
||||
state = hl.HighlightLineStateful(b, 2, state, spans);
|
||||
ASSERT_TRUE(!spans.empty());
|
||||
bool all_string = true;
|
||||
for (const auto &sp: spans) {
|
||||
if (sp.kind != TokenKind::String)
|
||||
all_string = false;
|
||||
}
|
||||
ASSERT_TRUE(all_string);
|
||||
}
|
||||
@@ -1198,6 +1198,312 @@ TEST (Undo_Command_RedoCountSelectsBranch)
|
||||
}
|
||||
|
||||
|
||||
TEST (Undo_Command_Newline_UndoRejoinsCorrectLines)
|
||||
{
|
||||
InstallDefaultCommands();
|
||||
|
||||
Editor ed;
|
||||
ed.SetDimensions(24, 80);
|
||||
|
||||
Buffer b;
|
||||
ed.AddBuffer(std::move(b));
|
||||
Buffer *buf = ed.CurrentBuffer();
|
||||
ASSERT_TRUE(buf != nullptr);
|
||||
|
||||
buf->replace_all_bytes("abcdef\nghijkl");
|
||||
buf->SetCursor(3, 0);
|
||||
|
||||
ASSERT_TRUE(Execute(ed, CommandId::Newline));
|
||||
ASSERT_EQ(buf->Nrows(), static_cast<std::size_t>(3));
|
||||
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abc"));
|
||||
ASSERT_EQ(std::string(buf->Rows()[1]), std::string("def"));
|
||||
ASSERT_EQ(std::string(buf->Rows()[2]), std::string("ghijkl"));
|
||||
|
||||
ASSERT_TRUE(Execute(ed, CommandId::Undo));
|
||||
ASSERT_EQ(buf->Nrows(), static_cast<std::size_t>(2));
|
||||
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abcdef"));
|
||||
ASSERT_EQ(std::string(buf->Rows()[1]), std::string("ghijkl"));
|
||||
|
||||
validate_undo_tree(*buf->Undo());
|
||||
}
|
||||
|
||||
|
||||
TEST (Undo_Command_Backspace_JoinUndoRedo)
|
||||
{
|
||||
InstallDefaultCommands();
|
||||
|
||||
Editor ed;
|
||||
ed.SetDimensions(24, 80);
|
||||
|
||||
Buffer b;
|
||||
ed.AddBuffer(std::move(b));
|
||||
Buffer *buf = ed.CurrentBuffer();
|
||||
ASSERT_TRUE(buf != nullptr);
|
||||
|
||||
buf->replace_all_bytes("abc\ndef");
|
||||
buf->SetCursor(0, 1);
|
||||
|
||||
ASSERT_TRUE(Execute(ed, CommandId::Backspace));
|
||||
ASSERT_EQ(buf->Nrows(), static_cast<std::size_t>(1));
|
||||
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abcdef"));
|
||||
|
||||
ASSERT_TRUE(Execute(ed, CommandId::Undo));
|
||||
ASSERT_EQ(buf->Nrows(), static_cast<std::size_t>(2));
|
||||
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abc"));
|
||||
ASSERT_EQ(std::string(buf->Rows()[1]), std::string("def"));
|
||||
|
||||
ASSERT_TRUE(Execute(ed, CommandId::Redo));
|
||||
ASSERT_EQ(buf->Nrows(), static_cast<std::size_t>(1));
|
||||
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abcdef"));
|
||||
|
||||
validate_undo_tree(*buf->Undo());
|
||||
}
|
||||
|
||||
|
||||
TEST (Undo_Command_DeleteChar_JoinUndoRedo)
|
||||
{
|
||||
InstallDefaultCommands();
|
||||
|
||||
Editor ed;
|
||||
ed.SetDimensions(24, 80);
|
||||
|
||||
Buffer b;
|
||||
ed.AddBuffer(std::move(b));
|
||||
Buffer *buf = ed.CurrentBuffer();
|
||||
ASSERT_TRUE(buf != nullptr);
|
||||
|
||||
buf->replace_all_bytes("abc\ndef");
|
||||
buf->SetCursor(3, 0);
|
||||
|
||||
ASSERT_TRUE(Execute(ed, CommandId::DeleteChar));
|
||||
ASSERT_EQ(buf->Nrows(), static_cast<std::size_t>(1));
|
||||
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abcdef"));
|
||||
|
||||
ASSERT_TRUE(Execute(ed, CommandId::Undo));
|
||||
ASSERT_EQ(buf->Nrows(), static_cast<std::size_t>(2));
|
||||
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abc"));
|
||||
ASSERT_EQ(std::string(buf->Rows()[1]), std::string("def"));
|
||||
|
||||
ASSERT_TRUE(Execute(ed, CommandId::Redo));
|
||||
ASSERT_EQ(buf->Nrows(), static_cast<std::size_t>(1));
|
||||
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abcdef"));
|
||||
|
||||
validate_undo_tree(*buf->Undo());
|
||||
}
|
||||
|
||||
|
||||
TEST (Undo_Command_RegexReplaceAll_UndoRedo)
|
||||
{
|
||||
InstallDefaultCommands();
|
||||
|
||||
Editor ed;
|
||||
ed.SetDimensions(24, 80);
|
||||
|
||||
Buffer b;
|
||||
ed.AddBuffer(std::move(b));
|
||||
Buffer *buf = ed.CurrentBuffer();
|
||||
ASSERT_TRUE(buf != nullptr);
|
||||
|
||||
buf->replace_all_bytes("foo one\nfoo two\nbar three");
|
||||
buf->SetCursor(0, 0);
|
||||
|
||||
ASSERT_TRUE(Execute(ed, CommandId::RegexpReplace));
|
||||
ASSERT_TRUE(ed.PromptActive());
|
||||
ed.SetPromptText("foo");
|
||||
ASSERT_TRUE(Execute(ed, CommandId::Newline));
|
||||
ASSERT_TRUE(ed.PromptActive());
|
||||
ed.SetPromptText("baz");
|
||||
ASSERT_TRUE(Execute(ed, CommandId::Newline));
|
||||
|
||||
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("baz one"));
|
||||
ASSERT_EQ(std::string(buf->Rows()[1]), std::string("baz two"));
|
||||
ASSERT_EQ(std::string(buf->Rows()[2]), std::string("bar three"));
|
||||
|
||||
ASSERT_TRUE(Execute(ed, CommandId::Undo));
|
||||
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("foo one"));
|
||||
ASSERT_EQ(std::string(buf->Rows()[1]), std::string("foo two"));
|
||||
ASSERT_EQ(std::string(buf->Rows()[2]), std::string("bar three"));
|
||||
|
||||
ASSERT_TRUE(Execute(ed, CommandId::Redo));
|
||||
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("baz one"));
|
||||
ASSERT_EQ(std::string(buf->Rows()[1]), std::string("baz two"));
|
||||
ASSERT_EQ(std::string(buf->Rows()[2]), std::string("bar three"));
|
||||
|
||||
validate_undo_tree(*buf->Undo());
|
||||
}
|
||||
|
||||
|
||||
TEST (Undo_Command_IndentUnindentRegion_UndoRedo)
|
||||
{
|
||||
InstallDefaultCommands();
|
||||
|
||||
Editor ed;
|
||||
ed.SetDimensions(24, 80);
|
||||
|
||||
Buffer b;
|
||||
ed.AddBuffer(std::move(b));
|
||||
Buffer *buf = ed.CurrentBuffer();
|
||||
ASSERT_TRUE(buf != nullptr);
|
||||
|
||||
buf->replace_all_bytes("one\ntwo\nthree");
|
||||
buf->SetMark(0, 0);
|
||||
buf->SetCursor(0, 2);
|
||||
|
||||
ASSERT_TRUE(Execute(ed, CommandId::IndentRegion));
|
||||
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("\tone"));
|
||||
ASSERT_EQ(std::string(buf->Rows()[1]), std::string("\ttwo"));
|
||||
ASSERT_EQ(std::string(buf->Rows()[2]), std::string("\tthree"));
|
||||
|
||||
ASSERT_TRUE(Execute(ed, CommandId::Undo));
|
||||
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("one"));
|
||||
ASSERT_EQ(std::string(buf->Rows()[1]), std::string("two"));
|
||||
ASSERT_EQ(std::string(buf->Rows()[2]), std::string("three"));
|
||||
|
||||
ASSERT_TRUE(Execute(ed, CommandId::Redo));
|
||||
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("\tone"));
|
||||
ASSERT_EQ(std::string(buf->Rows()[1]), std::string("\ttwo"));
|
||||
ASSERT_EQ(std::string(buf->Rows()[2]), std::string("\tthree"));
|
||||
|
||||
buf->SetMark(0, 0);
|
||||
buf->SetCursor(0, 2);
|
||||
ASSERT_TRUE(Execute(ed, CommandId::UnindentRegion));
|
||||
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("one"));
|
||||
ASSERT_EQ(std::string(buf->Rows()[1]), std::string("two"));
|
||||
ASSERT_EQ(std::string(buf->Rows()[2]), std::string("three"));
|
||||
|
||||
ASSERT_TRUE(Execute(ed, CommandId::Undo));
|
||||
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("\tone"));
|
||||
ASSERT_EQ(std::string(buf->Rows()[1]), std::string("\ttwo"));
|
||||
ASSERT_EQ(std::string(buf->Rows()[2]), std::string("\tthree"));
|
||||
|
||||
validate_undo_tree(*buf->Undo());
|
||||
}
|
||||
|
||||
|
||||
TEST (Undo_Command_KillToEol_UndoRedo)
|
||||
{
|
||||
InstallDefaultCommands();
|
||||
|
||||
Editor ed;
|
||||
ed.SetDimensions(24, 80);
|
||||
|
||||
Buffer b;
|
||||
ed.AddBuffer(std::move(b));
|
||||
Buffer *buf = ed.CurrentBuffer();
|
||||
ASSERT_TRUE(buf != nullptr);
|
||||
|
||||
buf->replace_all_bytes("abcdef\nghijkl");
|
||||
buf->SetCursor(3, 0);
|
||||
|
||||
ASSERT_TRUE(Execute(ed, CommandId::KillToEOL));
|
||||
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abc"));
|
||||
|
||||
ASSERT_TRUE(Execute(ed, CommandId::Undo));
|
||||
ASSERT_EQ(buf->Nrows(), static_cast<std::size_t>(2));
|
||||
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abcdef"));
|
||||
ASSERT_EQ(std::string(buf->Rows()[1]), std::string("ghijkl"));
|
||||
|
||||
ASSERT_TRUE(Execute(ed, CommandId::Redo));
|
||||
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abc"));
|
||||
|
||||
validate_undo_tree(*buf->Undo());
|
||||
}
|
||||
|
||||
|
||||
TEST (Undo_Command_KillLine_UndoRedo)
|
||||
{
|
||||
InstallDefaultCommands();
|
||||
|
||||
Editor ed;
|
||||
ed.SetDimensions(24, 80);
|
||||
|
||||
Buffer b;
|
||||
ed.AddBuffer(std::move(b));
|
||||
Buffer *buf = ed.CurrentBuffer();
|
||||
ASSERT_TRUE(buf != nullptr);
|
||||
|
||||
buf->replace_all_bytes("abc\ndef\nghi");
|
||||
buf->SetCursor(0, 1);
|
||||
|
||||
ASSERT_TRUE(Execute(ed, CommandId::KillLine));
|
||||
ASSERT_EQ(buf->Nrows(), static_cast<std::size_t>(2));
|
||||
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abc"));
|
||||
ASSERT_EQ(std::string(buf->Rows()[1]), std::string("ghi"));
|
||||
|
||||
ASSERT_TRUE(Execute(ed, CommandId::Undo));
|
||||
ASSERT_EQ(buf->Nrows(), static_cast<std::size_t>(3));
|
||||
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abc"));
|
||||
ASSERT_EQ(std::string(buf->Rows()[1]), std::string("def"));
|
||||
ASSERT_EQ(std::string(buf->Rows()[2]), std::string("ghi"));
|
||||
|
||||
ASSERT_TRUE(Execute(ed, CommandId::Redo));
|
||||
ASSERT_EQ(buf->Nrows(), static_cast<std::size_t>(2));
|
||||
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abc"));
|
||||
ASSERT_EQ(std::string(buf->Rows()[1]), std::string("ghi"));
|
||||
|
||||
validate_undo_tree(*buf->Undo());
|
||||
}
|
||||
|
||||
|
||||
TEST (Undo_Command_KillRegion_UndoRedo)
|
||||
{
|
||||
InstallDefaultCommands();
|
||||
|
||||
Editor ed;
|
||||
ed.SetDimensions(24, 80);
|
||||
|
||||
Buffer b;
|
||||
ed.AddBuffer(std::move(b));
|
||||
Buffer *buf = ed.CurrentBuffer();
|
||||
ASSERT_TRUE(buf != nullptr);
|
||||
|
||||
buf->replace_all_bytes("abc def ghi");
|
||||
buf->SetMark(4, 0);
|
||||
buf->SetCursor(8, 0);
|
||||
|
||||
ASSERT_TRUE(Execute(ed, CommandId::KillRegion));
|
||||
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abc ghi"));
|
||||
|
||||
ASSERT_TRUE(Execute(ed, CommandId::Undo));
|
||||
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abc def ghi"));
|
||||
|
||||
ASSERT_TRUE(Execute(ed, CommandId::Redo));
|
||||
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abc ghi"));
|
||||
|
||||
validate_undo_tree(*buf->Undo());
|
||||
}
|
||||
|
||||
|
||||
TEST (Undo_Command_DeleteWordPrevNext_UndoRedo)
|
||||
{
|
||||
InstallDefaultCommands();
|
||||
|
||||
Editor ed;
|
||||
ed.SetDimensions(24, 80);
|
||||
|
||||
Buffer b;
|
||||
ed.AddBuffer(std::move(b));
|
||||
Buffer *buf = ed.CurrentBuffer();
|
||||
ASSERT_TRUE(buf != nullptr);
|
||||
|
||||
buf->replace_all_bytes("abc def ghi");
|
||||
buf->SetCursor(8, 0);
|
||||
|
||||
ASSERT_TRUE(Execute(ed, CommandId::DeleteWordPrev));
|
||||
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abc ghi"));
|
||||
ASSERT_TRUE(Execute(ed, CommandId::Undo));
|
||||
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abc def ghi"));
|
||||
|
||||
buf->SetCursor(4, 0);
|
||||
ASSERT_TRUE(Execute(ed, CommandId::DeleteWordNext));
|
||||
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abc ghi"));
|
||||
ASSERT_TRUE(Execute(ed, CommandId::Undo));
|
||||
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abc def ghi"));
|
||||
|
||||
validate_undo_tree(*buf->Undo());
|
||||
}
|
||||
|
||||
|
||||
TEST (Undo_InsertRow_UndoDeletesRow)
|
||||
{
|
||||
Buffer b;
|
||||
|
||||
Reference in New Issue
Block a user