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:
62
Buffer.cc
62
Buffer.cc
@@ -235,7 +235,8 @@ Buffer::Buffer(const Buffer &other)
|
|||||||
edit_mode_ = other.edit_mode_;
|
edit_mode_ = other.edit_mode_;
|
||||||
edit_mode_detected_ = other.edit_mode_detected_;
|
edit_mode_detected_ = other.edit_mode_detected_;
|
||||||
version_ = other.version_;
|
version_ = other.version_;
|
||||||
syntax_enabled_ = other.syntax_enabled_;
|
syntax_enabled_ = other.syntax_enabled_;
|
||||||
|
syntax_user_override_ = other.syntax_user_override_;
|
||||||
filetype_ = other.filetype_;
|
filetype_ = other.filetype_;
|
||||||
// Fresh undo system for the copy
|
// Fresh undo system for the copy
|
||||||
undo_tree_ = std::make_unique<UndoTree>();
|
undo_tree_ = std::make_unique<UndoTree>();
|
||||||
@@ -286,7 +287,8 @@ Buffer::operator=(const Buffer &other)
|
|||||||
edit_mode_ = other.edit_mode_;
|
edit_mode_ = other.edit_mode_;
|
||||||
edit_mode_detected_ = other.edit_mode_detected_;
|
edit_mode_detected_ = other.edit_mode_detected_;
|
||||||
version_ = other.version_;
|
version_ = other.version_;
|
||||||
syntax_enabled_ = other.syntax_enabled_;
|
syntax_enabled_ = other.syntax_enabled_;
|
||||||
|
syntax_user_override_ = other.syntax_user_override_;
|
||||||
filetype_ = other.filetype_;
|
filetype_ = other.filetype_;
|
||||||
// Recreate undo system for this instance
|
// Recreate undo system for this instance
|
||||||
undo_tree_ = std::make_unique<UndoTree>();
|
undo_tree_ = std::make_unique<UndoTree>();
|
||||||
@@ -327,6 +329,9 @@ Buffer::Buffer(Buffer &&other) noexcept
|
|||||||
mark_set_(other.mark_set_),
|
mark_set_(other.mark_set_),
|
||||||
mark_curx_(other.mark_curx_),
|
mark_curx_(other.mark_curx_),
|
||||||
mark_cury_(other.mark_cury_),
|
mark_cury_(other.mark_cury_),
|
||||||
|
visual_line_active_(other.visual_line_active_),
|
||||||
|
visual_line_anchor_y_(other.visual_line_anchor_y_),
|
||||||
|
visual_line_active_y_(other.visual_line_active_y_),
|
||||||
undo_tree_(std::move(other.undo_tree_)),
|
undo_tree_(std::move(other.undo_tree_)),
|
||||||
undo_sys_(std::move(other.undo_sys_))
|
undo_sys_(std::move(other.undo_sys_))
|
||||||
{
|
{
|
||||||
@@ -334,11 +339,18 @@ Buffer::Buffer(Buffer &&other) noexcept
|
|||||||
edit_mode_ = other.edit_mode_;
|
edit_mode_ = other.edit_mode_;
|
||||||
edit_mode_detected_ = other.edit_mode_detected_;
|
edit_mode_detected_ = other.edit_mode_detected_;
|
||||||
version_ = other.version_;
|
version_ = other.version_;
|
||||||
syntax_enabled_ = other.syntax_enabled_;
|
syntax_enabled_ = other.syntax_enabled_;
|
||||||
|
syntax_user_override_ = other.syntax_user_override_;
|
||||||
filetype_ = std::move(other.filetype_);
|
filetype_ = std::move(other.filetype_);
|
||||||
highlighter_ = std::move(other.highlighter_);
|
highlighter_ = std::move(other.highlighter_);
|
||||||
content_ = std::move(other.content_);
|
content_ = std::move(other.content_);
|
||||||
rows_cache_dirty_ = other.rows_cache_dirty_;
|
rows_cache_dirty_ = other.rows_cache_dirty_;
|
||||||
|
on_disk_identity_ = other.on_disk_identity_;
|
||||||
|
// Non-owning: the recorder object itself is owned by SwapManager and outlives
|
||||||
|
// this move. The caller (Editor) is responsible for calling SwapManager::Rehome()
|
||||||
|
// so the journal's Buffer* key follows this object to its new address.
|
||||||
|
swap_rec_ = other.swap_rec_;
|
||||||
|
other.swap_rec_ = nullptr;
|
||||||
// Update UndoSystem's buffer reference to point to this object
|
// Update UndoSystem's buffer reference to point to this object
|
||||||
if (undo_sys_) {
|
if (undo_sys_) {
|
||||||
undo_sys_->UpdateBufferReference(*this);
|
undo_sys_->UpdateBufferReference(*this);
|
||||||
@@ -353,32 +365,42 @@ Buffer::operator=(Buffer &&other) noexcept
|
|||||||
if (this == &other)
|
if (this == &other)
|
||||||
return *this;
|
return *this;
|
||||||
|
|
||||||
curx_ = other.curx_;
|
curx_ = other.curx_;
|
||||||
cury_ = other.cury_;
|
cury_ = other.cury_;
|
||||||
rx_ = other.rx_;
|
rx_ = other.rx_;
|
||||||
nrows_ = other.nrows_;
|
nrows_ = other.nrows_;
|
||||||
rowoffs_ = other.rowoffs_;
|
rowoffs_ = other.rowoffs_;
|
||||||
coloffs_ = other.coloffs_;
|
coloffs_ = other.coloffs_;
|
||||||
rows_ = std::move(other.rows_);
|
rows_ = std::move(other.rows_);
|
||||||
filename_ = std::move(other.filename_);
|
filename_ = std::move(other.filename_);
|
||||||
is_file_backed_ = other.is_file_backed_;
|
is_file_backed_ = other.is_file_backed_;
|
||||||
dirty_ = other.dirty_;
|
dirty_ = other.dirty_;
|
||||||
read_only_ = other.read_only_;
|
read_only_ = other.read_only_;
|
||||||
mark_set_ = other.mark_set_;
|
mark_set_ = other.mark_set_;
|
||||||
mark_curx_ = other.mark_curx_;
|
mark_curx_ = other.mark_curx_;
|
||||||
mark_cury_ = other.mark_cury_;
|
mark_cury_ = other.mark_cury_;
|
||||||
undo_tree_ = std::move(other.undo_tree_);
|
visual_line_active_ = other.visual_line_active_;
|
||||||
undo_sys_ = std::move(other.undo_sys_);
|
visual_line_anchor_y_ = other.visual_line_anchor_y_;
|
||||||
|
visual_line_active_y_ = other.visual_line_active_y_;
|
||||||
|
undo_tree_ = std::move(other.undo_tree_);
|
||||||
|
undo_sys_ = std::move(other.undo_sys_);
|
||||||
|
|
||||||
// Move edit mode + syntax/highlighting state
|
// Move edit mode + syntax/highlighting state
|
||||||
edit_mode_ = other.edit_mode_;
|
edit_mode_ = other.edit_mode_;
|
||||||
edit_mode_detected_ = other.edit_mode_detected_;
|
edit_mode_detected_ = other.edit_mode_detected_;
|
||||||
version_ = other.version_;
|
version_ = other.version_;
|
||||||
syntax_enabled_ = other.syntax_enabled_;
|
syntax_enabled_ = other.syntax_enabled_;
|
||||||
|
syntax_user_override_ = other.syntax_user_override_;
|
||||||
filetype_ = std::move(other.filetype_);
|
filetype_ = std::move(other.filetype_);
|
||||||
highlighter_ = std::move(other.highlighter_);
|
highlighter_ = std::move(other.highlighter_);
|
||||||
content_ = std::move(other.content_);
|
content_ = std::move(other.content_);
|
||||||
rows_cache_dirty_ = other.rows_cache_dirty_;
|
rows_cache_dirty_ = other.rows_cache_dirty_;
|
||||||
|
on_disk_identity_ = other.on_disk_identity_;
|
||||||
|
// Non-owning: the recorder object itself is owned by SwapManager and outlives
|
||||||
|
// this move. The caller (Editor) is responsible for calling SwapManager::Rehome()
|
||||||
|
// so the journal's Buffer* key follows this object to its new address.
|
||||||
|
swap_rec_ = other.swap_rec_;
|
||||||
|
other.swap_rec_ = nullptr;
|
||||||
// Update UndoSystem's buffer reference to point to this object
|
// Update UndoSystem's buffer reference to point to this object
|
||||||
if (undo_sys_) {
|
if (undo_sys_) {
|
||||||
undo_sys_->UpdateBufferReference(*this);
|
undo_sys_->UpdateBufferReference(*this);
|
||||||
|
|||||||
20
Buffer.h
20
Buffer.h
@@ -548,6 +548,25 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Marks that the user explicitly set syntax state via a command (:syntax
|
||||||
|
// on/off, :set filetype=...), as opposed to it being auto-applied from
|
||||||
|
// GUIConfig. Frontends that re-apply the config-driven default every
|
||||||
|
// frame (e.g. ImGuiFrontend::apply_syntax_to_buffer) must check this
|
||||||
|
// first, mirroring EditModeDetected()'s "don't stomp a manual toggle"
|
||||||
|
// pattern - otherwise a manual :syntax off is silently undone on the very
|
||||||
|
// next frame.
|
||||||
|
void SetSyntaxUserOverride(bool on)
|
||||||
|
{
|
||||||
|
syntax_user_override_ = on;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[[nodiscard]] bool SyntaxUserOverride() const
|
||||||
|
{
|
||||||
|
return syntax_user_override_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void SetFiletype(const std::string &ft)
|
void SetFiletype(const std::string &ft)
|
||||||
{
|
{
|
||||||
filetype_ = ft;
|
filetype_ = ft;
|
||||||
@@ -673,6 +692,7 @@ private:
|
|||||||
// Syntax/highlighting state
|
// Syntax/highlighting state
|
||||||
std::uint64_t version_ = 0; // increment on edits
|
std::uint64_t version_ = 0; // increment on edits
|
||||||
bool syntax_enabled_ = true;
|
bool syntax_enabled_ = true;
|
||||||
|
bool syntax_user_override_ = false; // true once user explicitly set syntax state via a command
|
||||||
std::string filetype_;
|
std::string filetype_;
|
||||||
std::unique_ptr<kte::HighlighterEngine> highlighter_;
|
std::unique_ptr<kte::HighlighterEngine> highlighter_;
|
||||||
// Non-owning pointer to swap recorder managed by Editor/SwapManager
|
// Non-owning pointer to swap recorder managed by Editor/SwapManager
|
||||||
|
|||||||
@@ -344,6 +344,7 @@ if (BUILD_TESTS)
|
|||||||
tests/test_migration_coverage.cc
|
tests/test_migration_coverage.cc
|
||||||
tests/test_smart_newline.cc
|
tests/test_smart_newline.cc
|
||||||
tests/test_reflow_undo.cc
|
tests/test_reflow_undo.cc
|
||||||
|
tests/test_syntax_highlighting.cc
|
||||||
|
|
||||||
# minimal engine sources required by Buffer
|
# minimal engine sources required by Buffer
|
||||||
PieceTable.cc
|
PieceTable.cc
|
||||||
|
|||||||
204
Command.cc
204
Command.cc
@@ -159,6 +159,27 @@ ensure_at_least_one_line(Buffer &buf)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// RAII helper: brackets a multi-edit command so its individual undo nodes
|
||||||
|
// undo/redo as a single atomic step, per the project's undo-grouping convention.
|
||||||
|
struct UndoGroupGuard {
|
||||||
|
UndoSystem *u;
|
||||||
|
|
||||||
|
|
||||||
|
explicit UndoGroupGuard(UndoSystem *u_) : u(u_)
|
||||||
|
{
|
||||||
|
if (u)
|
||||||
|
u->BeginGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
~UndoGroupGuard()
|
||||||
|
{
|
||||||
|
if (u)
|
||||||
|
u->EndGroup();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// Determine if a command mutates the buffer contents (text edits)
|
// Determine if a command mutates the buffer contents (text edits)
|
||||||
static bool
|
static bool
|
||||||
is_mutating_command(CommandId id)
|
is_mutating_command(CommandId id)
|
||||||
@@ -264,8 +285,12 @@ extract_region_text(const Buffer &buf, std::size_t sx, std::size_t sy, std::size
|
|||||||
|
|
||||||
|
|
||||||
// Helper: delete region and leave cursor at start (sx,sy). Adjust lines appropriately.
|
// Helper: delete region and leave cursor at start (sx,sy). Adjust lines appropriately.
|
||||||
|
// If `u` is non-null, each underlying mutation is recorded as its own undo node;
|
||||||
|
// callers that want the whole region-delete to undo/redo as one step should
|
||||||
|
// wrap the call in an UndoGroupGuard.
|
||||||
static void
|
static void
|
||||||
delete_region(Buffer &buf, std::size_t sx, std::size_t sy, std::size_t ex, std::size_t ey)
|
delete_region(Buffer &buf, std::size_t sx, std::size_t sy, std::size_t ex, std::size_t ey,
|
||||||
|
UndoSystem *u = nullptr)
|
||||||
{
|
{
|
||||||
std::size_t nrows = buf.Nrows();
|
std::size_t nrows = buf.Nrows();
|
||||||
if (nrows == 0)
|
if (nrows == 0)
|
||||||
@@ -282,7 +307,14 @@ delete_region(Buffer &buf, std::size_t sx, std::size_t sy, std::size_t ex, std::
|
|||||||
std::size_t xe = std::min(ex, line.size());
|
std::size_t xe = std::min(ex, line.size());
|
||||||
if (xe < xs)
|
if (xe < xs)
|
||||||
std::swap(xs, xe);
|
std::swap(xs, xe);
|
||||||
|
std::string deleted = std::string(line.substr(xs, xe - xs));
|
||||||
buf.delete_text(static_cast<int>(sy), static_cast<int>(xs), xe - xs);
|
buf.delete_text(static_cast<int>(sy), static_cast<int>(xs), xe - xs);
|
||||||
|
if (u && !deleted.empty()) {
|
||||||
|
buf.SetCursor(xs, sy);
|
||||||
|
u->Begin(UndoType::Delete);
|
||||||
|
u->Append(std::string_view(deleted));
|
||||||
|
u->commit();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Multi-line: delete from (sx,sy) to (ex,ey)
|
// Multi-line: delete from (sx,sy) to (ex,ey)
|
||||||
// Strategy:
|
// Strategy:
|
||||||
@@ -303,11 +335,25 @@ delete_region(Buffer &buf, std::size_t sx, std::size_t sy, std::size_t ex, std::
|
|||||||
|
|
||||||
// Delete tail of first line (from xs to end)
|
// Delete tail of first line (from xs to end)
|
||||||
if (xs < first_line_len) {
|
if (xs < first_line_len) {
|
||||||
|
std::string tail = std::string(rows[sy].substr(xs));
|
||||||
buf.delete_text(static_cast<int>(sy), static_cast<int>(xs), first_line_len - xs);
|
buf.delete_text(static_cast<int>(sy), static_cast<int>(xs), first_line_len - xs);
|
||||||
|
if (u && !tail.empty()) {
|
||||||
|
buf.SetCursor(xs, sy);
|
||||||
|
u->Begin(UndoType::Delete);
|
||||||
|
u->Append(std::string_view(tail));
|
||||||
|
u->commit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete lines from ey down to sy+1 (reverse order to preserve indices)
|
// Delete lines from ey down to sy+1 (reverse order to preserve indices)
|
||||||
for (std::size_t i = ey; i > sy; --i) {
|
for (std::size_t i = ey; i > sy; --i) {
|
||||||
|
if (u) {
|
||||||
|
std::string row_text = static_cast<std::string>(buf.Rows()[i]);
|
||||||
|
buf.SetCursor(0, i);
|
||||||
|
u->Begin(UndoType::DeleteRow);
|
||||||
|
u->Append(std::string_view(row_text));
|
||||||
|
u->commit();
|
||||||
|
}
|
||||||
buf.delete_row(static_cast<int>(i));
|
buf.delete_row(static_cast<int>(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,6 +363,12 @@ delete_region(Buffer &buf, std::size_t sx, std::size_t sy, std::size_t ex, std::
|
|||||||
const auto &rows_after = buf.Rows();
|
const auto &rows_after = buf.Rows();
|
||||||
std::size_t line_len = rows_after[sy].size();
|
std::size_t line_len = rows_after[sy].size();
|
||||||
buf.insert_text(static_cast<int>(sy), static_cast<int>(line_len), suffix);
|
buf.insert_text(static_cast<int>(sy), static_cast<int>(line_len), suffix);
|
||||||
|
if (u) {
|
||||||
|
buf.SetCursor(line_len, sy);
|
||||||
|
u->Begin(UndoType::Insert);
|
||||||
|
u->Append(std::string_view(suffix));
|
||||||
|
u->commit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
buf.SetCursor(sx, sy);
|
buf.SetCursor(sx, sy);
|
||||||
@@ -908,6 +960,10 @@ cmd_unknown_esc_command(CommandContext &ctx)
|
|||||||
static void
|
static void
|
||||||
apply_filetype(Buffer &buf, const std::string &ft)
|
apply_filetype(Buffer &buf, const std::string &ft)
|
||||||
{
|
{
|
||||||
|
// Only reachable from explicit user commands (:syntax on, :set
|
||||||
|
// filetype=...) - mark so frontends that re-apply config-driven syntax
|
||||||
|
// defaults every frame (e.g. ImGuiFrontend) don't stomp this choice.
|
||||||
|
buf.SetSyntaxUserOverride(true);
|
||||||
buf.EnsureHighlighter();
|
buf.EnsureHighlighter();
|
||||||
auto *eng = buf.Highlighter();
|
auto *eng = buf.Highlighter();
|
||||||
if (!eng)
|
if (!eng)
|
||||||
@@ -977,6 +1033,7 @@ cmd_syntax(CommandContext &ctx)
|
|||||||
};
|
};
|
||||||
trim(arg);
|
trim(arg);
|
||||||
if (arg == "on") {
|
if (arg == "on") {
|
||||||
|
b->SetSyntaxUserOverride(true);
|
||||||
b->SetSyntaxEnabled(true);
|
b->SetSyntaxEnabled(true);
|
||||||
// If no highlighter but filetype is cpp by extension, set it
|
// If no highlighter but filetype is cpp by extension, set it
|
||||||
if (!b->Highlighter() || !b->Highlighter()->HasHighlighter()) {
|
if (!b->Highlighter() || !b->Highlighter()->HasHighlighter()) {
|
||||||
@@ -984,6 +1041,7 @@ cmd_syntax(CommandContext &ctx)
|
|||||||
}
|
}
|
||||||
ctx.editor.SetStatus("syntax: on");
|
ctx.editor.SetStatus("syntax: on");
|
||||||
} else if (arg == "off") {
|
} else if (arg == "off") {
|
||||||
|
b->SetSyntaxUserOverride(true);
|
||||||
b->SetSyntaxEnabled(false);
|
b->SetSyntaxEnabled(false);
|
||||||
ctx.editor.SetStatus("syntax: off");
|
ctx.editor.SetStatus("syntax: off");
|
||||||
} else if (arg == "reload") {
|
} else if (arg == "reload") {
|
||||||
@@ -2509,12 +2567,10 @@ cmd_newline(CommandContext &ctx)
|
|||||||
}
|
}
|
||||||
pos = p + with.size();
|
pos = p + with.size();
|
||||||
} else {
|
} else {
|
||||||
// When replacing with empty, continue after the deletion point to avoid re-matching
|
// Replacing with empty leaves nothing inserted at the deletion
|
||||||
|
// point, so resume scanning from `p` itself (not p+1) to catch
|
||||||
|
// adjacent/overlapping matches, e.g. "aaaa" -> "" replacing "aa".
|
||||||
pos = p;
|
pos = p;
|
||||||
if (pos < static_cast<std::size_t>(buf->Rows()[y].size()))
|
|
||||||
++pos;
|
|
||||||
else
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
++total;
|
++total;
|
||||||
}
|
}
|
||||||
@@ -2923,14 +2979,28 @@ cmd_newline(CommandContext &ctx)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
std::size_t changed = 0;
|
std::size_t changed = 0;
|
||||||
|
UndoSystem *ru = buf->Undo();
|
||||||
|
UndoGroupGuard rguard(ru);
|
||||||
// Iterate by index to allow modifications via PieceTable helpers
|
// Iterate by index to allow modifications via PieceTable helpers
|
||||||
for (std::size_t y = 0; y < buf->Rows().size(); ++y) {
|
for (std::size_t y = 0; y < buf->Rows().size(); ++y) {
|
||||||
std::string before = static_cast<std::string>(buf->Rows()[y]);
|
std::string before = static_cast<std::string>(buf->Rows()[y]);
|
||||||
std::string after = std::regex_replace(before, rx, repl);
|
std::string after = std::regex_replace(before, rx, repl);
|
||||||
if (after != before) {
|
if (after != before) {
|
||||||
// Replace entire line y with 'after' using PieceTable ops
|
// Replace entire line y with 'after' using PieceTable ops
|
||||||
|
if (ru) {
|
||||||
|
buf->SetCursor(0, y);
|
||||||
|
ru->Begin(UndoType::DeleteRow);
|
||||||
|
ru->Append(std::string_view(before));
|
||||||
|
ru->commit();
|
||||||
|
}
|
||||||
buf->delete_row(static_cast<int>(y));
|
buf->delete_row(static_cast<int>(y));
|
||||||
buf->insert_row(static_cast<int>(y), std::string_view(after));
|
buf->insert_row(static_cast<int>(y), std::string_view(after));
|
||||||
|
if (ru) {
|
||||||
|
buf->SetCursor(0, y);
|
||||||
|
ru->Begin(UndoType::InsertRow);
|
||||||
|
ru->Append(std::string_view(after));
|
||||||
|
ru->commit();
|
||||||
|
}
|
||||||
++changed;
|
++changed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3003,18 +3073,28 @@ cmd_newline(CommandContext &ctx)
|
|||||||
ensure_cursor_visible(ctx.editor, *buf);
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
UndoSystem *u = buf->Undo();
|
||||||
|
if (u && repeat > 1)
|
||||||
|
u->BeginGroup();
|
||||||
for (int i = 0; i < repeat; ++i) {
|
for (int i = 0; i < repeat; ++i) {
|
||||||
|
// Sync the buffer's cursor to the split point before Begin(), which
|
||||||
|
// records its node's row/col from the buffer's *current* cursor. Undo
|
||||||
|
// must reverse this exact split, not wherever the cursor ends up after
|
||||||
|
// the whole loop finishes.
|
||||||
|
buf->SetCursor(x, y);
|
||||||
|
if (u) {
|
||||||
|
u->Begin(UndoType::Newline);
|
||||||
|
u->commit();
|
||||||
|
}
|
||||||
buf->split_line(static_cast<int>(y), static_cast<int>(x));
|
buf->split_line(static_cast<int>(y), static_cast<int>(x));
|
||||||
// Move to start of next line
|
// Move to start of next line
|
||||||
y += 1;
|
y += 1;
|
||||||
x = 0;
|
x = 0;
|
||||||
}
|
}
|
||||||
|
if (u && repeat > 1)
|
||||||
|
u->EndGroup();
|
||||||
buf->SetCursor(x, y);
|
buf->SetCursor(x, y);
|
||||||
buf->SetDirty(true);
|
buf->SetDirty(true);
|
||||||
if (auto *u = buf->Undo()) {
|
|
||||||
u->Begin(UndoType::Newline);
|
|
||||||
u->commit();
|
|
||||||
}
|
|
||||||
ensure_cursor_visible(ctx.editor, *buf);
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -3198,7 +3278,9 @@ cmd_backspace(CommandContext &ctx)
|
|||||||
x = prev_len;
|
x = prev_len;
|
||||||
buf->SetCursor(x, y);
|
buf->SetCursor(x, y);
|
||||||
if (u) {
|
if (u) {
|
||||||
u->Begin(UndoType::Newline);
|
// Forward action here is a join, not a split: JoinLines has the
|
||||||
|
// correct (inverted) apply() semantics, unlike Newline.
|
||||||
|
u->Begin(UndoType::JoinLines);
|
||||||
u->commit();
|
u->commit();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -3277,7 +3359,9 @@ cmd_delete_char(CommandContext &ctx)
|
|||||||
} else if (y + 1 < rows_view.size()) {
|
} else if (y + 1 < rows_view.size()) {
|
||||||
buf->join_lines(static_cast<int>(y));
|
buf->join_lines(static_cast<int>(y));
|
||||||
if (u) {
|
if (u) {
|
||||||
u->Begin(UndoType::Newline);
|
// Forward action here is a join, not a split: JoinLines has the
|
||||||
|
// correct (inverted) apply() semantics, unlike Newline.
|
||||||
|
u->Begin(UndoType::JoinLines);
|
||||||
u->commit();
|
u->commit();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -3350,18 +3434,32 @@ cmd_kill_to_eol(CommandContext &ctx)
|
|||||||
std::size_t x = buf->Curx();
|
std::size_t x = buf->Curx();
|
||||||
int repeat = ctx.count > 0 ? ctx.count : 1;
|
int repeat = ctx.count > 0 ? ctx.count : 1;
|
||||||
std::string killed_total;
|
std::string killed_total;
|
||||||
|
UndoSystem *u = buf->Undo();
|
||||||
|
UndoGroupGuard guard(u);
|
||||||
for (int i = 0; i < repeat; ++i) {
|
for (int i = 0; i < repeat; ++i) {
|
||||||
const auto &rows_view = buf->Rows();
|
const auto &rows_view = buf->Rows();
|
||||||
if (y >= rows_view.size())
|
if (y >= rows_view.size())
|
||||||
break;
|
break;
|
||||||
if (x < rows_view[y].size()) {
|
if (x < rows_view[y].size()) {
|
||||||
// delete from cursor to end of line
|
// delete from cursor to end of line
|
||||||
killed_total += rows_view[y].substr(x);
|
std::string seg = static_cast<std::string>(rows_view[y].substr(x));
|
||||||
|
killed_total += seg;
|
||||||
std::size_t len = rows_view[y].size() - x;
|
std::size_t len = rows_view[y].size() - x;
|
||||||
buf->delete_text(static_cast<int>(y), static_cast<int>(x), len);
|
buf->delete_text(static_cast<int>(y), static_cast<int>(x), len);
|
||||||
|
if (u) {
|
||||||
|
buf->SetCursor(x, y);
|
||||||
|
u->Begin(UndoType::Delete);
|
||||||
|
u->Append(std::string_view(seg));
|
||||||
|
u->commit();
|
||||||
|
}
|
||||||
} else if (y + 1 < rows_view.size()) {
|
} else if (y + 1 < rows_view.size()) {
|
||||||
// at EOL: delete the newline (join with next line)
|
// at EOL: delete the newline (join with next line)
|
||||||
killed_total += "\n";
|
killed_total += "\n";
|
||||||
|
if (u) {
|
||||||
|
buf->SetCursor(x, y);
|
||||||
|
u->Begin(UndoType::JoinLines);
|
||||||
|
u->commit();
|
||||||
|
}
|
||||||
buf->join_lines(static_cast<int>(y));
|
buf->join_lines(static_cast<int>(y));
|
||||||
} else {
|
} else {
|
||||||
// nothing to delete
|
// nothing to delete
|
||||||
@@ -3395,20 +3493,37 @@ cmd_kill_line(CommandContext &ctx)
|
|||||||
(void) x; // cursor x will be reset to 0
|
(void) x; // cursor x will be reset to 0
|
||||||
int repeat = ctx.count > 0 ? ctx.count : 1;
|
int repeat = ctx.count > 0 ? ctx.count : 1;
|
||||||
std::string killed_total;
|
std::string killed_total;
|
||||||
|
UndoSystem *u = buf->Undo();
|
||||||
|
UndoGroupGuard guard(u);
|
||||||
for (int i = 0; i < repeat; ++i) {
|
for (int i = 0; i < repeat; ++i) {
|
||||||
const auto &rows_view = buf->Rows();
|
const auto &rows_view = buf->Rows();
|
||||||
if (rows_view.empty())
|
if (rows_view.empty())
|
||||||
break;
|
break;
|
||||||
if (rows_view.size() == 1) {
|
if (rows_view.size() == 1) {
|
||||||
// last remaining line: clear its contents
|
// last remaining line: clear its contents
|
||||||
killed_total += static_cast<std::string>(rows_view[0]);
|
std::string content = static_cast<std::string>(rows_view[0]);
|
||||||
if (!rows_view[0].empty())
|
killed_total += content;
|
||||||
buf->delete_text(0, 0, rows_view[0].size());
|
if (!content.empty()) {
|
||||||
|
buf->delete_text(0, 0, content.size());
|
||||||
|
if (u) {
|
||||||
|
buf->SetCursor(0, 0);
|
||||||
|
u->Begin(UndoType::Delete);
|
||||||
|
u->Append(std::string_view(content));
|
||||||
|
u->commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
y = 0;
|
y = 0;
|
||||||
} else if (y < rows_view.size()) {
|
} else if (y < rows_view.size()) {
|
||||||
// erase current line; keep y pointing at the next line
|
// erase current line; keep y pointing at the next line
|
||||||
killed_total += static_cast<std::string>(rows_view[y]);
|
std::string content = static_cast<std::string>(rows_view[y]);
|
||||||
|
killed_total += content;
|
||||||
killed_total += "\n";
|
killed_total += "\n";
|
||||||
|
if (u) {
|
||||||
|
buf->SetCursor(0, y);
|
||||||
|
u->Begin(UndoType::DeleteRow);
|
||||||
|
u->Append(std::string_view(content));
|
||||||
|
u->commit();
|
||||||
|
}
|
||||||
buf->delete_row(static_cast<int>(y));
|
buf->delete_row(static_cast<int>(y));
|
||||||
const auto &rows_after = buf->Rows();
|
const auto &rows_after = buf->Rows();
|
||||||
if (y >= rows_after.size()) {
|
if (y >= rows_after.size()) {
|
||||||
@@ -3622,7 +3737,10 @@ cmd_kill_region(CommandContext &ctx)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
std::string text = extract_region_text(*buf, sx, sy, ex, ey);
|
std::string text = extract_region_text(*buf, sx, sy, ex, ey);
|
||||||
delete_region(*buf, sx, sy, ex, ey);
|
{
|
||||||
|
UndoGroupGuard guard(buf->Undo());
|
||||||
|
delete_region(*buf, sx, sy, ex, ey, buf->Undo());
|
||||||
|
}
|
||||||
ensure_cursor_visible(ctx.editor, *buf);
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
if (!text.empty()) {
|
if (!text.empty()) {
|
||||||
if (ctx.editor.KillChain())
|
if (ctx.editor.KillChain())
|
||||||
@@ -4234,6 +4352,7 @@ cmd_delete_word_prev(CommandContext &ctx)
|
|||||||
std::size_t x = buf->Curx();
|
std::size_t x = buf->Curx();
|
||||||
int repeat = ctx.count > 0 ? ctx.count : 1;
|
int repeat = ctx.count > 0 ? ctx.count : 1;
|
||||||
std::string killed_total;
|
std::string killed_total;
|
||||||
|
UndoGroupGuard guard(buf->Undo());
|
||||||
for (int i = 0; i < repeat; ++i) {
|
for (int i = 0; i < repeat; ++i) {
|
||||||
if (y >= rows.size()) {
|
if (y >= rows.size()) {
|
||||||
y = rows.empty() ? 0 : rows.size() - 1;
|
y = rows.empty() ? 0 : rows.size() - 1;
|
||||||
@@ -4275,7 +4394,7 @@ cmd_delete_word_prev(CommandContext &ctx)
|
|||||||
}
|
}
|
||||||
// Now delete from (x, y) to (start_x, start_y) using PieceTable
|
// Now delete from (x, y) to (start_x, start_y) using PieceTable
|
||||||
std::string deleted = extract_region_text(*buf, x, y, start_x, start_y);
|
std::string deleted = extract_region_text(*buf, x, y, start_x, start_y);
|
||||||
delete_region(*buf, x, y, start_x, start_y);
|
delete_region(*buf, x, y, start_x, start_y, buf->Undo());
|
||||||
// Prepend to killed_total (since we're deleting backwards)
|
// Prepend to killed_total (since we're deleting backwards)
|
||||||
killed_total = deleted + killed_total;
|
killed_total = deleted + killed_total;
|
||||||
}
|
}
|
||||||
@@ -4307,6 +4426,7 @@ cmd_delete_word_next(CommandContext &ctx)
|
|||||||
std::size_t x = buf->Curx();
|
std::size_t x = buf->Curx();
|
||||||
int repeat = ctx.count > 0 ? ctx.count : 1;
|
int repeat = ctx.count > 0 ? ctx.count : 1;
|
||||||
std::string killed_total;
|
std::string killed_total;
|
||||||
|
UndoGroupGuard guard(buf->Undo());
|
||||||
for (int i = 0; i < repeat; ++i) {
|
for (int i = 0; i < repeat; ++i) {
|
||||||
if (y >= rows.size())
|
if (y >= rows.size())
|
||||||
break;
|
break;
|
||||||
@@ -4346,7 +4466,7 @@ cmd_delete_word_next(CommandContext &ctx)
|
|||||||
}
|
}
|
||||||
// Now delete from (start_x, start_y) to (x, y) using PieceTable
|
// Now delete from (start_x, start_y) to (x, y) using PieceTable
|
||||||
std::string deleted = extract_region_text(*buf, start_x, start_y, x, y);
|
std::string deleted = extract_region_text(*buf, start_x, start_y, x, y);
|
||||||
delete_region(*buf, start_x, start_y, x, y);
|
delete_region(*buf, start_x, start_y, x, y, buf->Undo());
|
||||||
y = start_y;
|
y = start_y;
|
||||||
x = start_x;
|
x = start_x;
|
||||||
killed_total += deleted;
|
killed_total += deleted;
|
||||||
@@ -4380,8 +4500,16 @@ cmd_indent_region(CommandContext &ctx)
|
|||||||
ctx.editor.SetStatus("No region to indent");
|
ctx.editor.SetStatus("No region to indent");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
UndoSystem *u = buf->Undo();
|
||||||
|
UndoGroupGuard guard(u);
|
||||||
for (std::size_t y = sy; y <= ey && y < buf->Rows().size(); ++y) {
|
for (std::size_t y = sy; y <= ey && y < buf->Rows().size(); ++y) {
|
||||||
buf->insert_text(static_cast<int>(y), 0, std::string_view("\t"));
|
buf->insert_text(static_cast<int>(y), 0, std::string_view("\t"));
|
||||||
|
if (u) {
|
||||||
|
buf->SetCursor(0, y);
|
||||||
|
u->Begin(UndoType::Insert);
|
||||||
|
u->Append('\t');
|
||||||
|
u->commit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
buf->SetDirty(true);
|
buf->SetDirty(true);
|
||||||
buf->ClearMark();
|
buf->ClearMark();
|
||||||
@@ -4405,6 +4533,8 @@ cmd_unindent_region(CommandContext &ctx)
|
|||||||
ctx.editor.SetStatus("No region to unindent");
|
ctx.editor.SetStatus("No region to unindent");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
UndoSystem *u = buf->Undo();
|
||||||
|
UndoGroupGuard guard(u);
|
||||||
for (std::size_t y = sy; y <= ey && y < buf->Rows().size(); ++y) {
|
for (std::size_t y = sy; y <= ey && y < buf->Rows().size(); ++y) {
|
||||||
const auto &rows_view = buf->Rows();
|
const auto &rows_view = buf->Rows();
|
||||||
if (y >= rows_view.size())
|
if (y >= rows_view.size())
|
||||||
@@ -4413,13 +4543,26 @@ cmd_unindent_region(CommandContext &ctx)
|
|||||||
if (!line.empty()) {
|
if (!line.empty()) {
|
||||||
if (line[0] == '\t') {
|
if (line[0] == '\t') {
|
||||||
buf->delete_text(static_cast<int>(y), 0, 1);
|
buf->delete_text(static_cast<int>(y), 0, 1);
|
||||||
|
if (u) {
|
||||||
|
buf->SetCursor(0, y);
|
||||||
|
u->Begin(UndoType::Delete);
|
||||||
|
u->Append('\t');
|
||||||
|
u->commit();
|
||||||
|
}
|
||||||
} else if (line[0] == ' ') {
|
} else if (line[0] == ' ') {
|
||||||
std::size_t spaces = 0;
|
std::size_t spaces = 0;
|
||||||
while (spaces < line.size() && spaces < 8 && line[spaces] == ' ') {
|
while (spaces < line.size() && spaces < 8 && line[spaces] == ' ') {
|
||||||
++spaces;
|
++spaces;
|
||||||
}
|
}
|
||||||
if (spaces > 0)
|
if (spaces > 0) {
|
||||||
buf->delete_text(static_cast<int>(y), 0, spaces);
|
buf->delete_text(static_cast<int>(y), 0, spaces);
|
||||||
|
if (u) {
|
||||||
|
buf->SetCursor(0, y);
|
||||||
|
u->Begin(UndoType::Delete);
|
||||||
|
u->Append(line.substr(0, spaces));
|
||||||
|
u->commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4436,25 +4579,8 @@ cmd_reflow_paragraph(CommandContext &ctx)
|
|||||||
Buffer *buf = ctx.editor.CurrentBuffer();
|
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||||
if (!buf)
|
if (!buf)
|
||||||
return false;
|
return false;
|
||||||
struct GroupGuard {
|
|
||||||
UndoSystem *u;
|
|
||||||
|
|
||||||
|
|
||||||
explicit GroupGuard(UndoSystem *u_) : u(u_)
|
|
||||||
{
|
|
||||||
if (u)
|
|
||||||
u->BeginGroup();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
~GroupGuard()
|
|
||||||
{
|
|
||||||
if (u)
|
|
||||||
u->EndGroup();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// Reflow performs a multi-edit transformation; make it a single standalone undo/redo step.
|
// Reflow performs a multi-edit transformation; make it a single standalone undo/redo step.
|
||||||
GroupGuard guard(buf->Undo());
|
UndoGroupGuard guard(buf->Undo());
|
||||||
if (auto *u = buf->Undo())
|
if (auto *u = buf->Undo())
|
||||||
u->commit();
|
u->commit();
|
||||||
ensure_at_least_one_line(*buf);
|
ensure_at_least_one_line(*buf);
|
||||||
|
|||||||
41
Editor.cc
41
Editor.cc
@@ -165,9 +165,23 @@ std::size_t
|
|||||||
Editor::AddBuffer(const Buffer &buf)
|
Editor::AddBuffer(const Buffer &buf)
|
||||||
{
|
{
|
||||||
auto &bufs = Buffers();
|
auto &bufs = Buffers();
|
||||||
|
// push_back may reallocate the vector's storage, moving every existing
|
||||||
|
// Buffer to a new address. Drain any in-flight swap records first so the
|
||||||
|
// writer thread never dereferences an address that's about to move, then
|
||||||
|
// rehome each attached buffer that actually moved.
|
||||||
|
if (swap_ && !bufs.empty())
|
||||||
|
swap_->Flush();
|
||||||
|
std::vector<Buffer *> old_addrs;
|
||||||
|
old_addrs.reserve(bufs.size());
|
||||||
|
for (auto &b: bufs)
|
||||||
|
old_addrs.push_back(&b);
|
||||||
bufs.push_back(buf);
|
bufs.push_back(buf);
|
||||||
// Attach swap recorder
|
|
||||||
if (swap_) {
|
if (swap_) {
|
||||||
|
for (std::size_t i = 0; i < old_addrs.size(); ++i) {
|
||||||
|
Buffer *new_addr = &bufs[i];
|
||||||
|
if (new_addr != old_addrs[i])
|
||||||
|
bufs[i].SetSwapRecorder(swap_->Rehome(old_addrs[i], new_addr));
|
||||||
|
}
|
||||||
swap_->Attach(&bufs.back());
|
swap_->Attach(&bufs.back());
|
||||||
bufs.back().SetSwapRecorder(swap_->RecorderFor(&bufs.back()));
|
bufs.back().SetSwapRecorder(swap_->RecorderFor(&bufs.back()));
|
||||||
}
|
}
|
||||||
@@ -182,8 +196,19 @@ std::size_t
|
|||||||
Editor::AddBuffer(Buffer &&buf)
|
Editor::AddBuffer(Buffer &&buf)
|
||||||
{
|
{
|
||||||
auto &bufs = Buffers();
|
auto &bufs = Buffers();
|
||||||
|
if (swap_ && !bufs.empty())
|
||||||
|
swap_->Flush();
|
||||||
|
std::vector<Buffer *> old_addrs;
|
||||||
|
old_addrs.reserve(bufs.size());
|
||||||
|
for (auto &b: bufs)
|
||||||
|
old_addrs.push_back(&b);
|
||||||
bufs.push_back(std::move(buf));
|
bufs.push_back(std::move(buf));
|
||||||
if (swap_) {
|
if (swap_) {
|
||||||
|
for (std::size_t i = 0; i < old_addrs.size(); ++i) {
|
||||||
|
Buffer *new_addr = &bufs[i];
|
||||||
|
if (new_addr != old_addrs[i])
|
||||||
|
bufs[i].SetSwapRecorder(swap_->Rehome(old_addrs[i], new_addr));
|
||||||
|
}
|
||||||
swap_->Attach(&bufs.back());
|
swap_->Attach(&bufs.back());
|
||||||
bufs.back().SetSwapRecorder(swap_->RecorderFor(&bufs.back()));
|
bufs.back().SetSwapRecorder(swap_->RecorderFor(&bufs.back()));
|
||||||
}
|
}
|
||||||
@@ -495,8 +520,22 @@ Editor::CloseBuffer(std::size_t index)
|
|||||||
// This prevents stale swap files from accumulating (e.g., when used as git editor).
|
// This prevents stale swap files from accumulating (e.g., when used as git editor).
|
||||||
swap_->Detach(&bufs[index], true);
|
swap_->Detach(&bufs[index], true);
|
||||||
bufs[index].SetSwapRecorder(nullptr);
|
bufs[index].SetSwapRecorder(nullptr);
|
||||||
|
// Drain in-flight records before the erase-shift below moves other
|
||||||
|
// buffers to new addresses (vector::erase never reallocates, but it does
|
||||||
|
// move-assign each trailing buffer into the previous slot).
|
||||||
|
swap_->Flush();
|
||||||
}
|
}
|
||||||
bufs.erase(bufs.begin() + static_cast<std::ptrdiff_t>(index));
|
bufs.erase(bufs.begin() + static_cast<std::ptrdiff_t>(index));
|
||||||
|
if (swap_) {
|
||||||
|
// erase() shifts every buffer after `index` down by one slot in-place
|
||||||
|
// (no reallocation), so the buffer now at slot i used to live at slot
|
||||||
|
// i+1 (same underlying storage, since data() doesn't move on erase).
|
||||||
|
for (std::size_t i = index; i < bufs.size(); ++i) {
|
||||||
|
Buffer *new_addr = &bufs[i];
|
||||||
|
Buffer *old_addr = new_addr + 1;
|
||||||
|
bufs[i].SetSwapRecorder(swap_->Rehome(old_addr, new_addr));
|
||||||
|
}
|
||||||
|
}
|
||||||
if (bufs.empty()) {
|
if (bufs.empty()) {
|
||||||
curbuf_ = 0;
|
curbuf_ = 0;
|
||||||
} else if (curbuf_ >= bufs.size()) {
|
} else if (curbuf_ >= bufs.size()) {
|
||||||
|
|||||||
@@ -44,6 +44,12 @@ apply_syntax_to_buffer(Buffer *b, const GUIConfig &cfg)
|
|||||||
if (!b->EditModeDetected() && !b->Filename().empty())
|
if (!b->EditModeDetected() && !b->Filename().empty())
|
||||||
b->SetEditMode(DetectEditMode(b->Filename()));
|
b->SetEditMode(DetectEditMode(b->Filename()));
|
||||||
|
|
||||||
|
// If the user explicitly set syntax state via a command (:syntax on/off,
|
||||||
|
// :set filetype=...), leave it alone - otherwise this runs every frame
|
||||||
|
// and silently undoes a manual ":syntax off" on the very next frame.
|
||||||
|
if (b->SyntaxUserOverride())
|
||||||
|
return;
|
||||||
|
|
||||||
// Writing mode disables syntax; otherwise follow the global config.
|
// Writing mode disables syntax; otherwise follow the global config.
|
||||||
if (cfg.syntax && b->GetEditMode() != EditMode::Writing) {
|
if (cfg.syntax && b->GetEditMode() != EditMode::Writing) {
|
||||||
b->SetSyntaxEnabled(true);
|
b->SetSyntaxEnabled(true);
|
||||||
@@ -469,7 +475,19 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
|||||||
|
|
||||||
// Route input events to the correct window's input handler
|
// Route input events to the correct window's input handler
|
||||||
if (target) {
|
if (target) {
|
||||||
target->input.ProcessSDLEvent(e);
|
Editor &tgt_ed = (target_idx == 0) ? ed : target->editor;
|
||||||
|
if (tgt_ed.FilePickerVisible()) {
|
||||||
|
// Modal: don't let keystrokes fall through as edit commands
|
||||||
|
// to the buffer underneath. Escape closes the picker;
|
||||||
|
// everything else (navigation, filtering, double-click) is
|
||||||
|
// handled by the picker's own ImGui widgets via
|
||||||
|
// ImGui_ImplSDL2_ProcessEvent above.
|
||||||
|
if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_ESCAPE) {
|
||||||
|
tgt_ed.SetFilePickerVisible(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
target->input.ProcessSDLEvent(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,19 @@
|
|||||||
#include "KKeymap.h"
|
#include "KKeymap.h"
|
||||||
#include "Editor.h"
|
#include "Editor.h"
|
||||||
|
|
||||||
|
// Verbose k-prefix suffix logging for debugging macOS/SDL key translation
|
||||||
|
// issues. Default to off; enable by defining IMGUI_IH_DEBUG=1 at compile
|
||||||
|
// time. Mirrors QtInputHandler.cc's QT_IH_DEBUG gate.
|
||||||
|
#ifndef IMGUI_IH_DEBUG
|
||||||
|
#define IMGUI_IH_DEBUG 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if IMGUI_IH_DEBUG
|
||||||
|
#define IH_LOGF(...) do { std::fprintf(stderr, __VA_ARGS__); std::fflush(stderr); } while (0)
|
||||||
|
#else
|
||||||
|
#define IH_LOGF(...) ((void) 0)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
map_key(const SDL_Keycode key,
|
map_key(const SDL_Keycode key,
|
||||||
@@ -182,18 +195,19 @@ map_key(const SDL_Keycode key,
|
|||||||
k_ctrl_pending = false;
|
k_ctrl_pending = false;
|
||||||
CommandId id;
|
CommandId id;
|
||||||
bool mapped = KLookupKCommand(ascii_key, pass_ctrl, id);
|
bool mapped = KLookupKCommand(ascii_key, pass_ctrl, id);
|
||||||
|
#if IMGUI_IH_DEBUG
|
||||||
// Diagnostics for u/U
|
// Diagnostics for u/U
|
||||||
if (lower == 'u') {
|
if (lower == 'u') {
|
||||||
char disp = (ascii_key >= 0x20 && ascii_key <= 0x7e)
|
char disp = (ascii_key >= 0x20 && ascii_key <= 0x7e)
|
||||||
? static_cast<char>(ascii_key)
|
? static_cast<char>(ascii_key)
|
||||||
: '?';
|
: '?';
|
||||||
std::fprintf(stderr,
|
IH_LOGF(
|
||||||
"[kge] k-prefix suffix: sym=%d mods=0x%x ascii=%d '%c' ctrl2=%d pass_ctrl=%d mapped=%d id=%d\n",
|
"[kge] k-prefix suffix: sym=%d mods=0x%x ascii=%d '%c' ctrl2=%d pass_ctrl=%d mapped=%d id=%d\n",
|
||||||
static_cast<int>(key), static_cast<unsigned int>(mod), ascii_key, disp,
|
static_cast<int>(key), static_cast<unsigned int>(mod), ascii_key, disp,
|
||||||
ctrl2 ? 1 : 0, pass_ctrl ? 1 : 0, mapped ? 1 : 0,
|
ctrl2 ? 1 : 0, pass_ctrl ? 1 : 0, mapped ? 1 : 0,
|
||||||
mapped ? static_cast<int>(id) : -1);
|
mapped ? static_cast<int>(id) : -1);
|
||||||
std::fflush(stderr);
|
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
if (mapped) {
|
if (mapped) {
|
||||||
out = {true, id, "", 0};
|
out = {true, id, "", 0};
|
||||||
if (ed)
|
if (ed)
|
||||||
@@ -524,15 +538,17 @@ ImGuiInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
|||||||
bool pass_ctrl = k_ctrl_pending_;
|
bool pass_ctrl = k_ctrl_pending_;
|
||||||
k_ctrl_pending_ = false;
|
k_ctrl_pending_ = false;
|
||||||
bool mapped = KLookupKCommand(ascii_key, pass_ctrl, id);
|
bool mapped = KLookupKCommand(ascii_key, pass_ctrl, id);
|
||||||
|
#if IMGUI_IH_DEBUG
|
||||||
// Diagnostics: log any k-prefix TEXTINPUT suffix mapping
|
// Diagnostics: log any k-prefix TEXTINPUT suffix mapping
|
||||||
char disp = (ascii_key >= 0x20 && ascii_key <= 0x7e)
|
{
|
||||||
? static_cast<char>(ascii_key)
|
char disp = (ascii_key >= 0x20 && ascii_key <= 0x7e)
|
||||||
: '?';
|
? static_cast<char>(ascii_key)
|
||||||
std::fprintf(stderr,
|
: '?';
|
||||||
"[kge] k-prefix TEXTINPUT suffix: ascii=%d '%c' mapped=%d id=%d\n",
|
IH_LOGF("[kge] k-prefix TEXTINPUT suffix: ascii=%d '%c' mapped=%d id=%d\n",
|
||||||
ascii_key, disp, mapped ? 1 : 0,
|
ascii_key, disp, mapped ? 1 : 0,
|
||||||
mapped ? static_cast<int>(id) : -1);
|
mapped ? static_cast<int>(id) : -1);
|
||||||
std::fflush(stderr);
|
}
|
||||||
|
#endif
|
||||||
if (mapped) {
|
if (mapped) {
|
||||||
mi = {true, id, "", 0};
|
mi = {true, id, "", 0};
|
||||||
if (ed_)
|
if (ed_)
|
||||||
|
|||||||
14
KKeymap.cc
14
KKeymap.cc
@@ -1,6 +1,4 @@
|
|||||||
#include <iostream>
|
|
||||||
#include <ncurses.h>
|
#include <ncurses.h>
|
||||||
#include <ostream>
|
|
||||||
|
|
||||||
#include "KKeymap.h"
|
#include "KKeymap.h"
|
||||||
|
|
||||||
@@ -40,6 +38,15 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
|
|||||||
out = CommandId::ToggleReadOnly; // C-k ' (toggle read-only)
|
out = CommandId::ToggleReadOnly; // C-k ' (toggle read-only)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (ascii_key == 'E') {
|
||||||
|
// Explicitly rejected and kept distinct from 'e' (OpenFileStart): the
|
||||||
|
// switch below operates on the lowercased key, so it can't otherwise
|
||||||
|
// tell 'E' and 'e' apart. Return false and let the caller show its
|
||||||
|
// normal "unknown k-command" status-line message - curses already
|
||||||
|
// owns the terminal here, so writing to stderr would corrupt the
|
||||||
|
// screen.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
switch (k_lower) {
|
switch (k_lower) {
|
||||||
case 'a':
|
case 'a':
|
||||||
@@ -66,9 +73,6 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
|
|||||||
case 'e':
|
case 'e':
|
||||||
out = CommandId::OpenFileStart;
|
out = CommandId::OpenFileStart;
|
||||||
return true;
|
return true;
|
||||||
case 'E':
|
|
||||||
std::cerr << "E is not a valid command" << std::endl;
|
|
||||||
return false;
|
|
||||||
case 'f':
|
case 'f':
|
||||||
out = CommandId::FlushKillRing;
|
out = CommandId::FlushKillRing;
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
37
Swap.cc
37
Swap.cc
@@ -268,6 +268,43 @@ SwapManager::RecorderFor(Buffer *buf)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
SwapRecorder *
|
||||||
|
SwapManager::Rehome(Buffer *old_addr, Buffer *new_addr)
|
||||||
|
{
|
||||||
|
if (!old_addr || !new_addr || old_addr == new_addr)
|
||||||
|
return nullptr;
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
SwapRecorder *result = nullptr;
|
||||||
|
|
||||||
|
auto jit = journals_.find(old_addr);
|
||||||
|
if (jit != journals_.end()) {
|
||||||
|
JournalCtx ctx = std::move(jit->second);
|
||||||
|
journals_.erase(jit);
|
||||||
|
journals_[new_addr] = std::move(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto rit = recorders_.find(old_addr);
|
||||||
|
if (rit != recorders_.end()) {
|
||||||
|
recorders_.erase(rit);
|
||||||
|
// BufferRecorder binds a Buffer& at construction, so it can't be
|
||||||
|
// repointed in place; rebuild it against the buffer's new address.
|
||||||
|
auto rec = std::make_unique<BufferRecorder>(*this, *new_addr);
|
||||||
|
result = rec.get();
|
||||||
|
recorders_[new_addr] = std::move(rec);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defensive: any record still queued (not yet drained by the writer thread)
|
||||||
|
// for the old address must follow the buffer to its new location. Callers
|
||||||
|
// are expected to Flush() before rehoming so this should normally be a no-op.
|
||||||
|
for (auto &p: queue_) {
|
||||||
|
if (p.buf == old_addr)
|
||||||
|
p.buf = new_addr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
SwapManager::Attach(Buffer *buf)
|
SwapManager::Attach(Buffer *buf)
|
||||||
{
|
{
|
||||||
|
|||||||
9
Swap.h
9
Swap.h
@@ -94,6 +94,15 @@ public:
|
|||||||
// Detach(buf) or SwapManager destruction.
|
// Detach(buf) or SwapManager destruction.
|
||||||
SwapRecorder *RecorderFor(Buffer *buf);
|
SwapRecorder *RecorderFor(Buffer *buf);
|
||||||
|
|
||||||
|
// Re-key an attached buffer's journal/recorder entries after its Buffer object
|
||||||
|
// has moved to a new address (e.g. std::vector<Buffer> reallocation/erase-shift).
|
||||||
|
// Callers must ensure no swap records for old_addr are in flight (see Flush())
|
||||||
|
// before calling this, and must not call it with an address that isn't
|
||||||
|
// currently attached. Returns the recorder for new_addr (nullptr if old_addr
|
||||||
|
// wasn't attached); the caller is responsible for calling
|
||||||
|
// new_buf->SetSwapRecorder() with the result.
|
||||||
|
SwapRecorder *Rehome(Buffer *old_addr, Buffer *new_addr);
|
||||||
|
|
||||||
// Notify that the buffer's filename changed (e.g., SaveAs)
|
// Notify that the buffer's filename changed (e.g., SaveAs)
|
||||||
void NotifyFilenameChanged(Buffer &buf);
|
void NotifyFilenameChanged(Buffer &buf);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <climits>
|
||||||
|
#include <cwchar>
|
||||||
#include <ncurses.h>
|
#include <ncurses.h>
|
||||||
|
|
||||||
#include "TerminalInputHandler.h"
|
#include "TerminalInputHandler.h"
|
||||||
@@ -12,6 +14,22 @@ CTRL(char c)
|
|||||||
{
|
{
|
||||||
return c & 0x1F;
|
return c & 0x1F;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Encode a single wide character in the process locale's multibyte encoding
|
||||||
|
// (UTF-8, given main.cc's setlocale(LC_ALL, "")). Returns false if the
|
||||||
|
// codepoint can't be represented, leaving `out` untouched.
|
||||||
|
bool
|
||||||
|
wchar_to_mb(wchar_t wc, std::string &out)
|
||||||
|
{
|
||||||
|
std::mbstate_t state{};
|
||||||
|
char buf[MB_LEN_MAX];
|
||||||
|
std::size_t n = std::wcrtomb(buf, wc, &state);
|
||||||
|
if (n == static_cast<std::size_t>(-1))
|
||||||
|
return false;
|
||||||
|
out.assign(buf, n);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TerminalInputHandler::TerminalInputHandler() = default;
|
TerminalInputHandler::TerminalInputHandler() = default;
|
||||||
@@ -21,6 +39,7 @@ TerminalInputHandler::~TerminalInputHandler() = default;
|
|||||||
|
|
||||||
static bool
|
static bool
|
||||||
map_key_to_command(const int ch,
|
map_key_to_command(const int ch,
|
||||||
|
const bool is_keycode,
|
||||||
bool &k_prefix,
|
bool &k_prefix,
|
||||||
bool &esc_meta,
|
bool &esc_meta,
|
||||||
bool &k_ctrl_pending,
|
bool &k_ctrl_pending,
|
||||||
@@ -28,9 +47,13 @@ map_key_to_command(const int ch,
|
|||||||
Editor *ed,
|
Editor *ed,
|
||||||
MappedInput &out)
|
MappedInput &out)
|
||||||
{
|
{
|
||||||
// Handle special keys from ncurses
|
// Handle special keys from ncurses. These are only meaningful when
|
||||||
|
// get_wch() reported KEY_CODE_YES: a regular (possibly non-ASCII) wide
|
||||||
|
// character can numerically collide with a KEY_* constant otherwise
|
||||||
|
// (e.g. U+0107 'ć' equals KEY_BACKSPACE's value), which would wrongly
|
||||||
|
// swallow it as a special key instead of inserting it.
|
||||||
// These keys exit k-prefix mode if active (user pressed C-k then a special key).
|
// These keys exit k-prefix mode if active (user pressed C-k then a special key).
|
||||||
switch (ch) {
|
switch (is_keycode ? ch : -1) {
|
||||||
case KEY_ENTER:
|
case KEY_ENTER:
|
||||||
// Some terminals send KEY_ENTER distinct from '\n'/'\r'
|
// Some terminals send KEY_ENTER distinct from '\n'/'\r'
|
||||||
k_prefix = false;
|
k_prefix = false;
|
||||||
@@ -259,7 +282,7 @@ map_key_to_command(const int ch,
|
|||||||
esc_meta = false;
|
esc_meta = false;
|
||||||
int ascii_key = ch;
|
int ascii_key = ch;
|
||||||
// Handle ESC + BACKSPACE (meta-backspace, Alt-Backspace)
|
// Handle ESC + BACKSPACE (meta-backspace, Alt-Backspace)
|
||||||
if (ch == KEY_BACKSPACE || ch == 127 || ch == CTRL('H')) {
|
if ((is_keycode && ch == KEY_BACKSPACE) || ch == 127 || ch == CTRL('H')) {
|
||||||
ascii_key = KEY_BACKSPACE; // normalized value for lookup
|
ascii_key = KEY_BACKSPACE; // normalized value for lookup
|
||||||
} else if (ch == ',') {
|
} else if (ch == ',') {
|
||||||
// Some terminals emit ',' when Shift state is lost after ESC; treat as '<'
|
// Some terminals emit ',' when Shift state is lost after ESC; treat as '<'
|
||||||
@@ -281,7 +304,7 @@ map_key_to_command(const int ch,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Backspace in ncurses can be KEY_BACKSPACE or 127
|
// Backspace in ncurses can be KEY_BACKSPACE or 127
|
||||||
if (ch == KEY_BACKSPACE || ch == 127 || ch == CTRL('H')) {
|
if ((is_keycode && ch == KEY_BACKSPACE) || ch == 127 || ch == CTRL('H')) {
|
||||||
k_prefix = false;
|
k_prefix = false;
|
||||||
k_ctrl_pending = false;
|
k_ctrl_pending = false;
|
||||||
out = {true, CommandId::Backspace, "", 0};
|
out = {true, CommandId::Backspace, "", 0};
|
||||||
@@ -297,12 +320,21 @@ map_key_to_command(const int ch,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Printable ASCII
|
// Printable character: ASCII, or (for a regular decoded wide character,
|
||||||
if (ch >= 0x20 && ch <= 0x7E) {
|
// not a keycode) any other printable Unicode codepoint - e.g. accented
|
||||||
|
// Latin, Cyrillic, CJK, etc.
|
||||||
|
if (!is_keycode && ch >= 0x20 && ch != 0x7F) {
|
||||||
|
std::string mb;
|
||||||
|
if (ch <= 0x7E) {
|
||||||
|
mb.assign(1, static_cast<char>(ch));
|
||||||
|
} else if (!wchar_to_mb(static_cast<wchar_t>(ch), mb)) {
|
||||||
|
out.hasCommand = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
out.hasCommand = true;
|
out.hasCommand = true;
|
||||||
out.id = CommandId::InsertText;
|
out.id = CommandId::InsertText;
|
||||||
out.arg.assign(1, static_cast<char>(ch));
|
out.arg = mb;
|
||||||
out.count = 0;
|
out.count = 0;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,12 +346,15 @@ map_key_to_command(const int ch,
|
|||||||
bool
|
bool
|
||||||
TerminalInputHandler::decode_(MappedInput &out)
|
TerminalInputHandler::decode_(MappedInput &out)
|
||||||
{
|
{
|
||||||
int ch = getch();
|
wint_t wch;
|
||||||
if (ch == ERR) {
|
int ret = get_wch(&wch);
|
||||||
|
if (ret == ERR) {
|
||||||
return false; // no input
|
return false; // no input
|
||||||
}
|
}
|
||||||
|
const bool is_keycode = (ret == KEY_CODE_YES);
|
||||||
|
const int ch = static_cast<int>(wch);
|
||||||
bool consumed = map_key_to_command(
|
bool consumed = map_key_to_command(
|
||||||
ch,
|
ch, is_keycode,
|
||||||
k_prefix_, esc_meta_,
|
k_prefix_, esc_meta_,
|
||||||
k_ctrl_pending_,
|
k_ctrl_pending_,
|
||||||
mouse_selecting_,
|
mouse_selecting_,
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ enum class UndoType : std::uint8_t {
|
|||||||
Newline,
|
Newline,
|
||||||
DeleteRow,
|
DeleteRow,
|
||||||
InsertRow,
|
InsertRow,
|
||||||
|
// Inverse of Newline: forward = join_lines(row) [removes the newline at the
|
||||||
|
// end of `row`], backward = split_line(row, col) [recreates the original
|
||||||
|
// two lines]. Used by backspace-at-col-0 and delete-at-eol, whose forward
|
||||||
|
// action is a join, not a split — Newline's apply() semantics are the
|
||||||
|
// wrong direction for those.
|
||||||
|
JoinLines,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct UndoNode {
|
struct UndoNode {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ UndoSystem::Begin(UndoType type)
|
|||||||
|
|
||||||
// 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 || type ==
|
const bool always_standalone = (type == UndoType::Newline || type == UndoType::DeleteRow || type ==
|
||||||
UndoType::InsertRow);
|
UndoType::InsertRow || type == UndoType::JoinLines);
|
||||||
if (always_standalone) {
|
if (always_standalone) {
|
||||||
commit();
|
commit();
|
||||||
}
|
}
|
||||||
@@ -77,6 +77,7 @@ UndoSystem::Begin(UndoType type)
|
|||||||
case UndoType::Newline:
|
case UndoType::Newline:
|
||||||
case UndoType::DeleteRow:
|
case UndoType::DeleteRow:
|
||||||
case UndoType::InsertRow:
|
case UndoType::InsertRow:
|
||||||
|
case UndoType::JoinLines:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -325,6 +326,16 @@ 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::JoinLines:
|
||||||
|
// Mirror image of Newline: forward removes a newline, backward restores it.
|
||||||
|
if (direction > 0) {
|
||||||
|
buf_->join_lines(node->row);
|
||||||
|
buf_->SetCursor(static_cast<std::size_t>(node->col), static_cast<std::size_t>(node->row));
|
||||||
|
} else {
|
||||||
|
buf_->split_line(node->row, node->col);
|
||||||
|
buf_->SetCursor(0, static_cast<std::size_t>(node->row + 1));
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -424,6 +435,8 @@ UndoSystem::type_str(UndoType t)
|
|||||||
return "DeleteRow";
|
return "DeleteRow";
|
||||||
case UndoType::InsertRow:
|
case UndoType::InsertRow:
|
||||||
return "InsertRow";
|
return "InsertRow";
|
||||||
|
case UndoType::JoinLines:
|
||||||
|
return "JoinLines";
|
||||||
}
|
}
|
||||||
return "?";
|
return "?";
|
||||||
}
|
}
|
||||||
|
|||||||
25
UndoTree.cc
25
UndoTree.cc
@@ -1,3 +1,24 @@
|
|||||||
// Placeholder translation unit for UndoTree struct definition.
|
// Undo logic is implemented in UndoSystem; this file only owns node lifetime.
|
||||||
// Undo logic is implemented in UndoSystem.
|
|
||||||
#include "UndoTree.h"
|
#include "UndoTree.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
void
|
||||||
|
free_node_graph(UndoNode *node)
|
||||||
|
{
|
||||||
|
// Walk the sibling (redo-branch) list; for each node, recursively free its
|
||||||
|
// child subtree first, then the node itself.
|
||||||
|
while (node) {
|
||||||
|
UndoNode *next = node->next;
|
||||||
|
free_node_graph(node->child);
|
||||||
|
delete node;
|
||||||
|
node = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
UndoTree::~UndoTree()
|
||||||
|
{
|
||||||
|
free_node_graph(root);
|
||||||
|
delete pending;
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,4 +7,11 @@ struct UndoTree {
|
|||||||
UndoNode *current = nullptr; // current state of buffer
|
UndoNode *current = nullptr; // current state of buffer
|
||||||
UndoNode *saved = nullptr; // points to node matching last save (for dirty flag)
|
UndoNode *saved = nullptr; // points to node matching last save (for dirty flag)
|
||||||
UndoNode *pending = nullptr; // in-progress batch (detached)
|
UndoNode *pending = nullptr; // in-progress batch (detached)
|
||||||
|
|
||||||
|
// Frees the entire node graph (root's subtree/branches plus any detached
|
||||||
|
// pending node). `current`/`saved` are aliases into that same graph, not
|
||||||
|
// separately owned. Without this, closing/reloading/resetting a buffer
|
||||||
|
// (which replaces or destroys its UndoTree) leaks every UndoNode ever
|
||||||
|
// created for it.
|
||||||
|
~UndoTree();
|
||||||
};
|
};
|
||||||
|
|||||||
16
main.cc
16
main.cc
@@ -292,6 +292,20 @@ main(int argc, char *argv[])
|
|||||||
fe = std::make_unique<TerminalFrontend>();
|
fe = std::make_unique<TerminalFrontend>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Guarantee Shutdown() runs on every exit path from here on (normal
|
||||||
|
// return, early return, or exception unwinding e.g. from Init/Execute/
|
||||||
|
// Step) so a crash never leaves the terminal in raw/cbreak mode.
|
||||||
|
struct FrontendShutdownGuard {
|
||||||
|
Frontend *fe;
|
||||||
|
|
||||||
|
|
||||||
|
~FrontendShutdownGuard()
|
||||||
|
{
|
||||||
|
if (fe)
|
||||||
|
fe->Shutdown();
|
||||||
|
}
|
||||||
|
} shutdown_guard{fe.get()};
|
||||||
|
|
||||||
#if defined(KTE_BUILD_GUI) && defined(__APPLE__)
|
#if defined(KTE_BUILD_GUI) && defined(__APPLE__)
|
||||||
if (use_gui) {
|
if (use_gui) {
|
||||||
/* likely using the .app, so need to cd */
|
/* likely using the .app, so need to cd */
|
||||||
@@ -320,8 +334,6 @@ main(int argc, char *argv[])
|
|||||||
fe->Step(editor, running);
|
fe->Step(editor, running);
|
||||||
}
|
}
|
||||||
|
|
||||||
fe->Shutdown();
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
} catch (const std::exception &e) {
|
} catch (const std::exception &e) {
|
||||||
std::string msg = std::string("Unhandled exception: ") + e.what();
|
std::string msg = std::string("Unhandled exception: ") + e.what();
|
||||||
|
|||||||
@@ -46,14 +46,45 @@ GoHighlighter::GoHighlighter()
|
|||||||
void
|
void
|
||||||
GoHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
GoHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||||
{
|
{
|
||||||
|
StatefulHighlighter::LineState prev;
|
||||||
|
(void) HighlightLineStateful(buf, row, prev, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
StatefulHighlighter::LineState
|
||||||
|
GoHighlighter::HighlightLineStateful(const Buffer &buf,
|
||||||
|
int row,
|
||||||
|
const LineState &prev,
|
||||||
|
std::vector<HighlightSpan> &out) const
|
||||||
|
{
|
||||||
|
StatefulHighlighter::LineState state = prev;
|
||||||
if (row < 0 || static_cast<std::size_t>(row) >= buf.Nrows())
|
if (row < 0 || static_cast<std::size_t>(row) >= buf.Nrows())
|
||||||
return;
|
return state;
|
||||||
std::string s = buf.GetLineString(static_cast<std::size_t>(row));
|
std::string s = buf.GetLineString(static_cast<std::size_t>(row));
|
||||||
int n = static_cast<int>(s.size());
|
int n = static_cast<int>(s.size());
|
||||||
int i = 0;
|
int i = 0;
|
||||||
int bol = 0;
|
int bol = 0;
|
||||||
while (bol < n && (s[bol] == ' ' || s[bol] == '\t'))
|
while (bol < n && (s[bol] == ' ' || s[bol] == '\t'))
|
||||||
++bol;
|
++bol;
|
||||||
|
|
||||||
|
// Continue a multi-line block comment from the previous line.
|
||||||
|
if (state.in_block_comment) {
|
||||||
|
int j = i;
|
||||||
|
while (i + 1 < n) {
|
||||||
|
if (s[i] == '*' && s[i + 1] == '/') {
|
||||||
|
i += 2;
|
||||||
|
push(out, j, i, TokenKind::Comment);
|
||||||
|
state.in_block_comment = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
if (state.in_block_comment) {
|
||||||
|
push(out, j, n, TokenKind::Comment);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// line comment
|
// line comment
|
||||||
while (i < n) {
|
while (i < n) {
|
||||||
char c = s[i];
|
char c = s[i];
|
||||||
@@ -82,7 +113,8 @@ GoHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSp
|
|||||||
}
|
}
|
||||||
if (!closed) {
|
if (!closed) {
|
||||||
push(out, i, n, TokenKind::Comment);
|
push(out, i, n, TokenKind::Comment);
|
||||||
break;
|
state.in_block_comment = true;
|
||||||
|
return state;
|
||||||
} else {
|
} else {
|
||||||
push(out, i, j, TokenKind::Comment);
|
push(out, i, j, TokenKind::Comment);
|
||||||
i = j;
|
i = j;
|
||||||
@@ -152,5 +184,6 @@ GoHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSp
|
|||||||
push(out, i, i + 1, TokenKind::Default);
|
push(out, i, i + 1, TokenKind::Default);
|
||||||
++i;
|
++i;
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
} // namespace kte
|
} // namespace kte
|
||||||
@@ -5,12 +5,17 @@
|
|||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
|
||||||
namespace kte {
|
namespace kte {
|
||||||
class GoHighlighter final : public LanguageHighlighter {
|
class GoHighlighter final : public StatefulHighlighter {
|
||||||
public:
|
public:
|
||||||
GoHighlighter();
|
GoHighlighter();
|
||||||
|
|
||||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||||
|
|
||||||
|
LineState HighlightLineStateful(const Buffer &buf,
|
||||||
|
int row,
|
||||||
|
const LineState &prev,
|
||||||
|
std::vector<HighlightSpan> &out) const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::unordered_set<std::string> kws_;
|
std::unordered_set<std::string> kws_;
|
||||||
std::unordered_set<std::string> types_;
|
std::unordered_set<std::string> types_;
|
||||||
|
|||||||
@@ -74,7 +74,22 @@ HighlighterEngine::GetLine(const Buffer &buf, int row, std::uint64_t buf_version
|
|||||||
|
|
||||||
StatefulHighlighter::LineState prev_state;
|
StatefulHighlighter::LineState prev_state;
|
||||||
int start_row = -1;
|
int start_row = -1;
|
||||||
if (!state_cache_.empty()) {
|
|
||||||
|
// Fast path: state_last_contig_ tracks the highest row we've cached state
|
||||||
|
// for, per version. If that row is already below our target, it's
|
||||||
|
// necessarily the best anchor the O(n) scan below would have found, so we
|
||||||
|
// can skip the scan entirely. Re-validated against state_cache_ before
|
||||||
|
// use since InvalidateFrom()/SetHighlighter() may have raced/cleared it.
|
||||||
|
auto contig_it = state_last_contig_.find(buf_version);
|
||||||
|
if (contig_it != state_last_contig_.end() && contig_it->second < row) {
|
||||||
|
auto sc_it = state_cache_.find(contig_it->second);
|
||||||
|
if (sc_it != state_cache_.end() && sc_it->second.version == buf_version) {
|
||||||
|
start_row = contig_it->second;
|
||||||
|
prev_state = sc_it->second.state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start_row < 0 && !state_cache_.empty()) {
|
||||||
// linear search over map (unordered), track best candidate
|
// linear search over map (unordered), track best candidate
|
||||||
int best = -1;
|
int best = -1;
|
||||||
for (const auto &kv: state_cache_) {
|
for (const auto &kv: state_cache_) {
|
||||||
@@ -109,6 +124,9 @@ HighlighterEngine::GetLine(const Buffer &buf, int row, std::uint64_t buf_version
|
|||||||
se.state = next_state;
|
se.state = next_state;
|
||||||
state_cache_[r] = se;
|
state_cache_[r] = se;
|
||||||
cur_state = next_state;
|
cur_state = next_state;
|
||||||
|
int &contig = state_last_contig_[buf_version];
|
||||||
|
if (r > contig)
|
||||||
|
contig = r;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store in cache and return by value
|
// Store in cache and return by value
|
||||||
@@ -139,6 +157,14 @@ HighlighterEngine::InvalidateFrom(int row)
|
|||||||
++it;
|
++it;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// A version's tracked contiguous-state row is no longer valid once any
|
||||||
|
// row at or above it has been evicted from state_cache_ above.
|
||||||
|
for (auto it = state_last_contig_.begin(); it != state_last_contig_.end();) {
|
||||||
|
if (it->second >= row)
|
||||||
|
it = state_last_contig_.erase(it);
|
||||||
|
else
|
||||||
|
++it;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -55,31 +55,26 @@ PythonHighlighter::HighlightLineStateful(const Buffer &buf, int row, const LineS
|
|||||||
std::string s = buf.GetLineString(static_cast<std::size_t>(row));
|
std::string s = buf.GetLineString(static_cast<std::size_t>(row));
|
||||||
int n = static_cast<int>(s.size());
|
int n = static_cast<int>(s.size());
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
// Triple-quoted string continuation uses in_raw_string with raw_delim either "'''" or "\"\"\""
|
// Triple-quoted string continuation uses in_raw_string with raw_delim either "'''" or "\"\"\""
|
||||||
if (state.in_raw_string && (state.raw_delim == "'''" || state.raw_delim == "\"\"\"")) {
|
if (state.in_raw_string && (state.raw_delim == "'''" || state.raw_delim == "\"\"\"")) {
|
||||||
auto pos = s.find(state.raw_delim);
|
auto pos = s.find(state.raw_delim);
|
||||||
if (pos == std::string::npos) {
|
if (pos == std::string::npos) {
|
||||||
push(out, 0, n, TokenKind::String);
|
push(out, 0, n, TokenKind::String);
|
||||||
return state; // still inside
|
return state; // still inside
|
||||||
} else {
|
|
||||||
int end = static_cast<int>(pos + static_cast<int>(state.raw_delim.size()));
|
|
||||||
push(out, 0, end, TokenKind::String);
|
|
||||||
// remainder processed normally
|
|
||||||
s = s.substr(end);
|
|
||||||
n = static_cast<int>(s.size());
|
|
||||||
state.in_raw_string = false;
|
|
||||||
state.raw_delim.clear();
|
|
||||||
// Continue parsing remainder as a separate small loop
|
|
||||||
int base = end;
|
|
||||||
// original offset, but we already emitted to 'out' with base=0; following spans should be from 'end'
|
|
||||||
// For simplicity, mark rest as Default
|
|
||||||
if (n > 0)
|
|
||||||
push(out, base, base + n, TokenKind::Default);
|
|
||||||
return state;
|
|
||||||
}
|
}
|
||||||
|
int end = static_cast<int>(pos + static_cast<int>(state.raw_delim.size()));
|
||||||
|
push(out, 0, end, TokenKind::String);
|
||||||
|
state.in_raw_string = false;
|
||||||
|
state.raw_delim.clear();
|
||||||
|
// Resume the normal tokenizer at the closing delimiter's end, on the
|
||||||
|
// same (unmodified) `s`/`n`, so anything after it - including a new
|
||||||
|
// triple-quoted string opening on this same line - is re-scanned
|
||||||
|
// instead of being dumped into a single opaque Default span.
|
||||||
|
i = end;
|
||||||
}
|
}
|
||||||
|
|
||||||
int i = 0;
|
|
||||||
// Detect comment start '#', ignoring inside strings
|
// Detect comment start '#', ignoring inside strings
|
||||||
while (i < n) {
|
while (i < n) {
|
||||||
char c = s[i];
|
char c = s[i];
|
||||||
|
|||||||
@@ -47,11 +47,42 @@ RustHighlighter::RustHighlighter()
|
|||||||
void
|
void
|
||||||
RustHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
RustHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||||
{
|
{
|
||||||
|
StatefulHighlighter::LineState prev;
|
||||||
|
(void) HighlightLineStateful(buf, row, prev, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
StatefulHighlighter::LineState
|
||||||
|
RustHighlighter::HighlightLineStateful(const Buffer &buf,
|
||||||
|
int row,
|
||||||
|
const LineState &prev,
|
||||||
|
std::vector<HighlightSpan> &out) const
|
||||||
|
{
|
||||||
|
StatefulHighlighter::LineState state = prev;
|
||||||
if (row < 0 || static_cast<std::size_t>(row) >= buf.Nrows())
|
if (row < 0 || static_cast<std::size_t>(row) >= buf.Nrows())
|
||||||
return;
|
return state;
|
||||||
std::string s = buf.GetLineString(static_cast<std::size_t>(row));
|
std::string s = buf.GetLineString(static_cast<std::size_t>(row));
|
||||||
int n = static_cast<int>(s.size());
|
int n = static_cast<int>(s.size());
|
||||||
int i = 0;
|
int i = 0;
|
||||||
|
|
||||||
|
// Continue a multi-line block comment from the previous line.
|
||||||
|
if (state.in_block_comment) {
|
||||||
|
int j = i;
|
||||||
|
while (i + 1 < n) {
|
||||||
|
if (s[i] == '*' && s[i + 1] == '/') {
|
||||||
|
i += 2;
|
||||||
|
push(out, j, i, TokenKind::Comment);
|
||||||
|
state.in_block_comment = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
if (state.in_block_comment) {
|
||||||
|
push(out, j, n, TokenKind::Comment);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
while (i < n) {
|
while (i < n) {
|
||||||
char c = s[i];
|
char c = s[i];
|
||||||
if (c == ' ' || c == '\t') {
|
if (c == ' ' || c == '\t') {
|
||||||
@@ -79,7 +110,8 @@ RustHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<Highlight
|
|||||||
}
|
}
|
||||||
if (!closed) {
|
if (!closed) {
|
||||||
push(out, i, n, TokenKind::Comment);
|
push(out, i, n, TokenKind::Comment);
|
||||||
break;
|
state.in_block_comment = true;
|
||||||
|
return state;
|
||||||
} else {
|
} else {
|
||||||
push(out, i, j, TokenKind::Comment);
|
push(out, i, j, TokenKind::Comment);
|
||||||
i = j;
|
i = j;
|
||||||
@@ -140,5 +172,6 @@ RustHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<Highlight
|
|||||||
push(out, i, i + 1, TokenKind::Default);
|
push(out, i, i + 1, TokenKind::Default);
|
||||||
++i;
|
++i;
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
} // namespace kte
|
} // namespace kte
|
||||||
@@ -5,12 +5,17 @@
|
|||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
|
||||||
namespace kte {
|
namespace kte {
|
||||||
class RustHighlighter final : public LanguageHighlighter {
|
class RustHighlighter final : public StatefulHighlighter {
|
||||||
public:
|
public:
|
||||||
RustHighlighter();
|
RustHighlighter();
|
||||||
|
|
||||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||||
|
|
||||||
|
LineState HighlightLineStateful(const Buffer &buf,
|
||||||
|
int row,
|
||||||
|
const LineState &prev,
|
||||||
|
std::vector<HighlightSpan> &out) const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::unordered_set<std::string> kws_;
|
std::unordered_set<std::string> kws_;
|
||||||
std::unordered_set<std::string> types_;
|
std::unordered_set<std::string> types_;
|
||||||
|
|||||||
@@ -47,12 +47,42 @@ SqlHighlighter::SqlHighlighter()
|
|||||||
void
|
void
|
||||||
SqlHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
SqlHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||||
{
|
{
|
||||||
|
StatefulHighlighter::LineState prev;
|
||||||
|
(void) HighlightLineStateful(buf, row, prev, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
StatefulHighlighter::LineState
|
||||||
|
SqlHighlighter::HighlightLineStateful(const Buffer &buf,
|
||||||
|
int row,
|
||||||
|
const LineState &prev,
|
||||||
|
std::vector<HighlightSpan> &out) const
|
||||||
|
{
|
||||||
|
StatefulHighlighter::LineState state = prev;
|
||||||
if (row < 0 || static_cast<std::size_t>(row) >= buf.Nrows())
|
if (row < 0 || static_cast<std::size_t>(row) >= buf.Nrows())
|
||||||
return;
|
return state;
|
||||||
std::string s = buf.GetLineString(static_cast<std::size_t>(row));
|
std::string s = buf.GetLineString(static_cast<std::size_t>(row));
|
||||||
int n = static_cast<int>(s.size());
|
int n = static_cast<int>(s.size());
|
||||||
int i = 0;
|
int i = 0;
|
||||||
|
|
||||||
|
// Continue a multi-line block comment from the previous line.
|
||||||
|
if (state.in_block_comment) {
|
||||||
|
int j = i;
|
||||||
|
while (i + 1 < n) {
|
||||||
|
if (s[i] == '*' && s[i + 1] == '/') {
|
||||||
|
i += 2;
|
||||||
|
push(out, j, i, TokenKind::Comment);
|
||||||
|
state.in_block_comment = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
if (state.in_block_comment) {
|
||||||
|
push(out, j, n, TokenKind::Comment);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
while (i < n) {
|
while (i < n) {
|
||||||
char c = s[i];
|
char c = s[i];
|
||||||
if (c == ' ' || c == '\t') {
|
if (c == ' ' || c == '\t') {
|
||||||
@@ -68,7 +98,7 @@ SqlHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightS
|
|||||||
push(out, i, n, TokenKind::Comment);
|
push(out, i, n, TokenKind::Comment);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// simple block comment on same line: /* ... */
|
// block comment: /* ... */ (may span multiple lines)
|
||||||
if (c == '/' && i + 1 < n && s[i + 1] == '*') {
|
if (c == '/' && i + 1 < n && s[i + 1] == '*') {
|
||||||
int j = i + 2;
|
int j = i + 2;
|
||||||
bool closed = false;
|
bool closed = false;
|
||||||
@@ -82,7 +112,8 @@ SqlHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightS
|
|||||||
}
|
}
|
||||||
if (!closed) {
|
if (!closed) {
|
||||||
push(out, i, n, TokenKind::Comment);
|
push(out, i, n, TokenKind::Comment);
|
||||||
break;
|
state.in_block_comment = true;
|
||||||
|
return state;
|
||||||
} else {
|
} else {
|
||||||
push(out, i, j, TokenKind::Comment);
|
push(out, i, j, TokenKind::Comment);
|
||||||
i = j;
|
i = j;
|
||||||
@@ -151,5 +182,6 @@ SqlHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightS
|
|||||||
push(out, i, i + 1, TokenKind::Default);
|
push(out, i, i + 1, TokenKind::Default);
|
||||||
++i;
|
++i;
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
} // namespace kte
|
} // namespace kte
|
||||||
@@ -5,12 +5,17 @@
|
|||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
|
||||||
namespace kte {
|
namespace kte {
|
||||||
class SqlHighlighter final : public LanguageHighlighter {
|
class SqlHighlighter final : public StatefulHighlighter {
|
||||||
public:
|
public:
|
||||||
SqlHighlighter();
|
SqlHighlighter();
|
||||||
|
|
||||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||||
|
|
||||||
|
LineState HighlightLineStateful(const Buffer &buf,
|
||||||
|
int row,
|
||||||
|
const LineState &prev,
|
||||||
|
std::vector<HighlightSpan> &out) const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::unordered_set<std::string> kws_;
|
std::unordered_set<std::string> kws_;
|
||||||
std::unordered_set<std::string> types_;
|
std::unordered_set<std::string> types_;
|
||||||
|
|||||||
@@ -108,3 +108,26 @@ TEST(CommandSemantics_CopyRegion_And_KillRegion)
|
|||||||
ASSERT_EQ(b.MarkSet(), false);
|
ASSERT_EQ(b.MarkSet(), false);
|
||||||
ASSERT_EQ(h.Text(), std::string("hello "));
|
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)
|
TEST(KKeymap_CtrlChords_CanonicalChords)
|
||||||
{
|
{
|
||||||
CommandId id{};
|
CommandId id{};
|
||||||
|
|||||||
@@ -414,17 +414,21 @@ TEST (Migration_EmptyBufferCheck_Pattern)
|
|||||||
|
|
||||||
TEST (Migration_SyntaxHighlighter_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;
|
Buffer buf;
|
||||||
buf.insert_text(0, 0, std::string("int main() {\n return 0;\n}"));
|
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) {
|
for (std::size_t row = 0; row < buf.Nrows(); ++row) {
|
||||||
// This is the pattern used in all migrated highlighters
|
|
||||||
if (row >= buf.Nrows()) {
|
if (row >= buf.Nrows()) {
|
||||||
break; // Should never happen
|
break; // Should never happen
|
||||||
}
|
}
|
||||||
std::string line = buf.GetLineString(row);
|
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)
|
TEST(SearchFlow_RegexFind_InvalidPattern_FailsSafely_And_ClearsStateOnEnter)
|
||||||
{
|
{
|
||||||
TestHarness h;
|
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)
|
TEST (Undo_InsertRow_UndoDeletesRow)
|
||||||
{
|
{
|
||||||
Buffer b;
|
Buffer b;
|
||||||
|
|||||||
Reference in New Issue
Block a user