Add visual-line mode support with tests and UI integration.
- Introduced visual-line mode for multi-line selection and edits. - Implemented commands, rendering, and keyboard shortcuts. - Added tests for broadcast operations in visual-line mode.
This commit is contained in:
51
Buffer.h
51
Buffer.h
@@ -370,6 +370,54 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Visual-line selection support (multicursor/visual mode)
|
||||||
|
void VisualLineClear()
|
||||||
|
{
|
||||||
|
visual_line_active_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void VisualLineStart()
|
||||||
|
{
|
||||||
|
visual_line_active_ = true;
|
||||||
|
visual_line_anchor_y_ = cury_;
|
||||||
|
visual_line_active_y_ = cury_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void VisualLineToggle()
|
||||||
|
{
|
||||||
|
if (visual_line_active_)
|
||||||
|
VisualLineClear();
|
||||||
|
else
|
||||||
|
VisualLineStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[[nodiscard]] bool VisualLineActive() const
|
||||||
|
{
|
||||||
|
return visual_line_active_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void VisualLineSetActiveY(std::size_t y)
|
||||||
|
{
|
||||||
|
visual_line_active_y_ = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[[nodiscard]] std::size_t VisualLineStartY() const
|
||||||
|
{
|
||||||
|
return visual_line_anchor_y_ < visual_line_active_y_ ? visual_line_anchor_y_ : visual_line_active_y_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[[nodiscard]] std::size_t VisualLineEndY() const
|
||||||
|
{
|
||||||
|
return visual_line_anchor_y_ < visual_line_active_y_ ? visual_line_active_y_ : visual_line_anchor_y_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
[[nodiscard]] std::string AsString() const;
|
[[nodiscard]] std::string AsString() const;
|
||||||
|
|
||||||
// Syntax highlighting integration (per-buffer)
|
// Syntax highlighting integration (per-buffer)
|
||||||
@@ -471,6 +519,9 @@ private:
|
|||||||
bool read_only_ = false;
|
bool read_only_ = false;
|
||||||
bool mark_set_ = false;
|
bool mark_set_ = false;
|
||||||
std::size_t mark_curx_ = 0, mark_cury_ = 0;
|
std::size_t mark_curx_ = 0, mark_cury_ = 0;
|
||||||
|
bool visual_line_active_ = false;
|
||||||
|
std::size_t visual_line_anchor_y_ = 0;
|
||||||
|
std::size_t visual_line_active_y_ = 0;
|
||||||
|
|
||||||
// Per-buffer undo state
|
// Per-buffer undo state
|
||||||
std::unique_ptr<struct UndoTree> undo_tree_;
|
std::unique_ptr<struct UndoTree> undo_tree_;
|
||||||
|
|||||||
@@ -302,6 +302,7 @@ if (BUILD_TESTS)
|
|||||||
tests/test_piece_table.cc
|
tests/test_piece_table.cc
|
||||||
tests/test_search.cc
|
tests/test_search.cc
|
||||||
tests/test_reflow_paragraph.cc
|
tests/test_reflow_paragraph.cc
|
||||||
|
tests/test_visual_line_mode.cc
|
||||||
|
|
||||||
# minimal engine sources required by Buffer
|
# minimal engine sources required by Buffer
|
||||||
PieceTable.cc
|
PieceTable.cc
|
||||||
|
|||||||
156
Command.cc
156
Command.cc
@@ -761,6 +761,15 @@ cmd_quit_now(CommandContext &ctx)
|
|||||||
static bool
|
static bool
|
||||||
cmd_refresh(CommandContext &ctx)
|
cmd_refresh(CommandContext &ctx)
|
||||||
{
|
{
|
||||||
|
// C-g is mapped to Refresh and acts as a general cancel key.
|
||||||
|
// Cancel visual-line (multicursor) mode if active.
|
||||||
|
if (Buffer *buf = ctx.editor.CurrentBuffer()) {
|
||||||
|
if (buf->VisualLineActive()) {
|
||||||
|
buf->VisualLineClear();
|
||||||
|
ctx.editor.SetStatus("Visual line: OFF");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
// If a generic prompt is active, cancel it
|
// If a generic prompt is active, cancel it
|
||||||
if (ctx.editor.PromptActive()) {
|
if (ctx.editor.PromptActive()) {
|
||||||
// If also in search mode, restore state
|
// If also in search mode, restore state
|
||||||
@@ -1970,9 +1979,38 @@ cmd_insert_text(CommandContext &ctx)
|
|||||||
ensure_at_least_one_line(*buf);
|
ensure_at_least_one_line(*buf);
|
||||||
std::size_t y = buf->Cury();
|
std::size_t y = buf->Cury();
|
||||||
std::size_t x = buf->Curx();
|
std::size_t x = buf->Curx();
|
||||||
|
int repeat = ctx.count > 0 ? ctx.count : 1;
|
||||||
|
std::size_t cx = x;
|
||||||
|
std::size_t cy = y;
|
||||||
|
|
||||||
|
// Visual-line mode: broadcast inserts to each selected line at the same column.
|
||||||
|
if (buf->VisualLineActive()) {
|
||||||
|
const std::size_t sy = buf->VisualLineStartY();
|
||||||
|
const std::size_t ey = buf->VisualLineEndY();
|
||||||
|
const auto &rows = buf->Rows();
|
||||||
|
for (std::size_t yy = sy; yy <= ey; ++yy) {
|
||||||
|
if (yy >= rows.size())
|
||||||
|
break;
|
||||||
|
std::size_t xx = x;
|
||||||
|
if (xx > rows[yy].size())
|
||||||
|
xx = rows[yy].size();
|
||||||
|
for (int i = 0; i < repeat; ++i) {
|
||||||
|
buf->insert_text(static_cast<int>(yy), static_cast<int>(xx), std::string_view(ctx.arg));
|
||||||
|
xx += ctx.arg.size();
|
||||||
|
}
|
||||||
|
if (yy == y) {
|
||||||
|
cx = xx;
|
||||||
|
cy = yy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf->SetDirty(true);
|
||||||
|
buf->SetCursor(cx, cy);
|
||||||
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
std::size_t ins_y = y;
|
std::size_t ins_y = y;
|
||||||
std::size_t ins_x = x; // remember insertion start for undo positioning
|
std::size_t ins_x = x; // remember insertion start for undo positioning
|
||||||
int repeat = ctx.count > 0 ? ctx.count : 1;
|
|
||||||
|
|
||||||
// Apply edits to the underlying PieceTable through Buffer::insert_text,
|
// Apply edits to the underlying PieceTable through Buffer::insert_text,
|
||||||
// not directly to the legacy rows_ cache. This ensures Save() persists text.
|
// not directly to the legacy rows_ cache. This ensures Save() persists text.
|
||||||
@@ -2784,6 +2822,41 @@ cmd_newline(CommandContext &ctx)
|
|||||||
std::size_t y = buf->Cury();
|
std::size_t y = buf->Cury();
|
||||||
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;
|
||||||
|
|
||||||
|
// Visual-line mode: broadcast newline splits across selected lines.
|
||||||
|
if (buf->VisualLineActive()) {
|
||||||
|
const std::size_t sy = buf->VisualLineStartY();
|
||||||
|
const std::size_t ey = buf->VisualLineEndY();
|
||||||
|
const auto &rows = buf->Rows();
|
||||||
|
if (rows.empty())
|
||||||
|
return true;
|
||||||
|
std::size_t splits_above = 0;
|
||||||
|
if (sy < y)
|
||||||
|
splits_above = std::min(ey, y - 1) - sy + 1;
|
||||||
|
|
||||||
|
// Iterate bottom-up to keep row indices stable while splitting.
|
||||||
|
for (std::size_t yy = ey + 1; yy-- > sy;) {
|
||||||
|
const auto &rows_view = buf->Rows();
|
||||||
|
if (yy >= rows_view.size())
|
||||||
|
continue;
|
||||||
|
std::size_t xx = x;
|
||||||
|
if (xx > rows_view[yy].size())
|
||||||
|
xx = rows_view[yy].size();
|
||||||
|
// First split at the cursor column; subsequent splits create blank lines.
|
||||||
|
buf->split_line(static_cast<int>(yy), static_cast<int>(xx));
|
||||||
|
for (int i = 1; i < repeat; ++i) {
|
||||||
|
buf->split_line(static_cast<int>(yy + i), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buf->SetDirty(true);
|
||||||
|
// Cursor: end up on the final inserted line for the original cursor line.
|
||||||
|
std::size_t new_y = y + static_cast<std::size_t>(repeat);
|
||||||
|
new_y += splits_above;
|
||||||
|
buf->SetCursor(0, new_y);
|
||||||
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
for (int i = 0; i < repeat; ++i) {
|
for (int i = 0; i < repeat; ++i) {
|
||||||
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
|
||||||
@@ -2858,6 +2931,35 @@ cmd_backspace(CommandContext &ctx)
|
|||||||
std::size_t x = buf->Curx();
|
std::size_t x = buf->Curx();
|
||||||
UndoSystem *u = buf->Undo();
|
UndoSystem *u = buf->Undo();
|
||||||
int repeat = ctx.count > 0 ? ctx.count : 1;
|
int repeat = ctx.count > 0 ? ctx.count : 1;
|
||||||
|
|
||||||
|
// Visual-line mode: broadcast backspace deletes within each selected line.
|
||||||
|
// For now, we do NOT join lines when at column 0 (too ambiguous across multiple lines).
|
||||||
|
if (buf->VisualLineActive()) {
|
||||||
|
const std::size_t sy = buf->VisualLineStartY();
|
||||||
|
const std::size_t ey = buf->VisualLineEndY();
|
||||||
|
const auto &rows = buf->Rows();
|
||||||
|
std::size_t cx = x;
|
||||||
|
for (std::size_t yy = sy; yy <= ey; ++yy) {
|
||||||
|
if (yy >= rows.size())
|
||||||
|
break;
|
||||||
|
std::size_t xx = x;
|
||||||
|
if (xx > rows[yy].size())
|
||||||
|
xx = rows[yy].size();
|
||||||
|
for (int i = 0; i < repeat; ++i) {
|
||||||
|
if (xx == 0)
|
||||||
|
break;
|
||||||
|
buf->delete_text(static_cast<int>(yy), static_cast<int>(xx - 1), 1);
|
||||||
|
--xx;
|
||||||
|
}
|
||||||
|
if (yy == y)
|
||||||
|
cx = xx;
|
||||||
|
}
|
||||||
|
buf->SetDirty(true);
|
||||||
|
buf->SetCursor(cx, y);
|
||||||
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
|
(void) u;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
for (int i = 0; i < repeat; ++i) {
|
for (int i = 0; i < repeat; ++i) {
|
||||||
// Refresh a read-only view of lines for char capture/lengths
|
// Refresh a read-only view of lines for char capture/lengths
|
||||||
const auto &rows_view = buf->Rows();
|
const auto &rows_view = buf->Rows();
|
||||||
@@ -2910,6 +3012,30 @@ cmd_delete_char(CommandContext &ctx)
|
|||||||
std::size_t x = buf->Curx();
|
std::size_t x = buf->Curx();
|
||||||
UndoSystem *u = buf->Undo();
|
UndoSystem *u = buf->Undo();
|
||||||
int repeat = ctx.count > 0 ? ctx.count : 1;
|
int repeat = ctx.count > 0 ? ctx.count : 1;
|
||||||
|
|
||||||
|
// Visual-line mode: broadcast delete-char within each selected line.
|
||||||
|
// For now, we do NOT join lines when at end-of-line.
|
||||||
|
if (buf->VisualLineActive()) {
|
||||||
|
const std::size_t sy = buf->VisualLineStartY();
|
||||||
|
const std::size_t ey = buf->VisualLineEndY();
|
||||||
|
const auto &rows = buf->Rows();
|
||||||
|
for (std::size_t yy = sy; yy <= ey; ++yy) {
|
||||||
|
if (yy >= rows.size())
|
||||||
|
break;
|
||||||
|
std::size_t xx = x;
|
||||||
|
if (xx > rows[yy].size())
|
||||||
|
xx = rows[yy].size();
|
||||||
|
for (int i = 0; i < repeat; ++i) {
|
||||||
|
if (xx >= buf->Rows()[yy].size())
|
||||||
|
break;
|
||||||
|
buf->delete_text(static_cast<int>(yy), static_cast<int>(xx), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf->SetDirty(true);
|
||||||
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
|
(void) u;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
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())
|
||||||
@@ -3107,6 +3233,8 @@ cmd_move_file_start(CommandContext &ctx)
|
|||||||
return false;
|
return false;
|
||||||
ensure_at_least_one_line(*buf);
|
ensure_at_least_one_line(*buf);
|
||||||
buf->SetCursor(0, 0);
|
buf->SetCursor(0, 0);
|
||||||
|
if (buf->VisualLineActive())
|
||||||
|
buf->VisualLineSetActiveY(buf->Cury());
|
||||||
ensure_cursor_visible(ctx.editor, *buf);
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -3123,6 +3251,8 @@ cmd_move_file_end(CommandContext &ctx)
|
|||||||
std::size_t y = rows.empty() ? 0 : rows.size() - 1;
|
std::size_t y = rows.empty() ? 0 : rows.size() - 1;
|
||||||
std::size_t x = rows.empty() ? 0 : rows[y].size();
|
std::size_t x = rows.empty() ? 0 : rows[y].size();
|
||||||
buf->SetCursor(x, y);
|
buf->SetCursor(x, y);
|
||||||
|
if (buf->VisualLineActive())
|
||||||
|
buf->VisualLineSetActiveY(buf->Cury());
|
||||||
ensure_cursor_visible(ctx.editor, *buf);
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -3145,6 +3275,18 @@ cmd_toggle_mark(CommandContext &ctx)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
cmd_visual_line_mode_toggle(CommandContext &ctx)
|
||||||
|
{
|
||||||
|
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||||
|
if (!buf)
|
||||||
|
return false;
|
||||||
|
buf->VisualLineToggle();
|
||||||
|
ctx.editor.SetStatus(std::string("Visual line: ") + (buf->VisualLineActive() ? "ON" : "OFF"));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
cmd_jump_to_mark(CommandContext &ctx)
|
cmd_jump_to_mark(CommandContext &ctx)
|
||||||
{
|
{
|
||||||
@@ -3306,6 +3448,8 @@ cmd_move_left(CommandContext &ctx)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
buf->SetCursor(x, y);
|
buf->SetCursor(x, y);
|
||||||
|
if (buf->VisualLineActive())
|
||||||
|
buf->VisualLineSetActiveY(buf->Cury());
|
||||||
ensure_cursor_visible(ctx.editor, *buf);
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -3383,6 +3527,8 @@ cmd_move_right(CommandContext &ctx)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
buf->SetCursor(x, y);
|
buf->SetCursor(x, y);
|
||||||
|
if (buf->VisualLineActive())
|
||||||
|
buf->VisualLineSetActiveY(buf->Cury());
|
||||||
ensure_cursor_visible(ctx.editor, *buf);
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -3423,6 +3569,8 @@ cmd_move_up(CommandContext &ctx)
|
|||||||
if (x > rows[y].size())
|
if (x > rows[y].size())
|
||||||
x = rows[y].size();
|
x = rows[y].size();
|
||||||
buf->SetCursor(x, y);
|
buf->SetCursor(x, y);
|
||||||
|
if (buf->VisualLineActive())
|
||||||
|
buf->VisualLineSetActiveY(buf->Cury());
|
||||||
ensure_cursor_visible(ctx.editor, *buf);
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -3464,6 +3612,8 @@ cmd_move_down(CommandContext &ctx)
|
|||||||
if (x > rows[y].size())
|
if (x > rows[y].size())
|
||||||
x = rows[y].size();
|
x = rows[y].size();
|
||||||
buf->SetCursor(x, y);
|
buf->SetCursor(x, y);
|
||||||
|
if (buf->VisualLineActive())
|
||||||
|
buf->VisualLineSetActiveY(buf->Cury());
|
||||||
ensure_cursor_visible(ctx.editor, *buf);
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -4463,6 +4613,10 @@ InstallDefaultCommands()
|
|||||||
});
|
});
|
||||||
CommandRegistry::Register({CommandId::MoveFileEnd, "file-end", "Move to end of file", cmd_move_file_end});
|
CommandRegistry::Register({CommandId::MoveFileEnd, "file-end", "Move to end of file", cmd_move_file_end});
|
||||||
CommandRegistry::Register({CommandId::ToggleMark, "toggle-mark", "Toggle mark at cursor", cmd_toggle_mark});
|
CommandRegistry::Register({CommandId::ToggleMark, "toggle-mark", "Toggle mark at cursor", cmd_toggle_mark});
|
||||||
|
CommandRegistry::Register({
|
||||||
|
CommandId::VisualLineModeToggle, "visual-line-toggle", "Toggle visual-line (multicursor) mode",
|
||||||
|
cmd_visual_line_mode_toggle, false, false
|
||||||
|
});
|
||||||
CommandRegistry::Register({
|
CommandRegistry::Register({
|
||||||
CommandId::JumpToMark, "jump-to-mark", "Jump to mark (swap mark)", cmd_jump_to_mark
|
CommandId::JumpToMark, "jump-to-mark", "Jump to mark (swap mark)", cmd_jump_to_mark
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ enum class CommandId {
|
|||||||
MoveFileStart, // move to beginning of file
|
MoveFileStart, // move to beginning of file
|
||||||
MoveFileEnd, // move to end of file
|
MoveFileEnd, // move to end of file
|
||||||
ToggleMark, // toggle mark at cursor
|
ToggleMark, // toggle mark at cursor
|
||||||
|
VisualLineModeToggle, // toggle visual-line (multicursor) mode (C-k /)
|
||||||
JumpToMark, // jump to mark, set mark to previous cursor
|
JumpToMark, // jump to mark, set mark to previous cursor
|
||||||
KillRegion, // kill region between mark and cursor (to kill ring)
|
KillRegion, // kill region between mark and cursor (to kill ring)
|
||||||
CopyRegion, // copy region to kill ring (Alt-w)
|
CopyRegion, // copy region to kill ring (Alt-w)
|
||||||
|
|||||||
130
ImGuiRenderer.cc
130
ImGuiRenderer.cc
@@ -150,73 +150,84 @@ ImGuiRenderer::Draw(Editor &ed)
|
|||||||
// Cache current horizontal offset in rendered columns for click handling
|
// Cache current horizontal offset in rendered columns for click handling
|
||||||
const std::size_t coloffs_now = buf->Coloffs();
|
const std::size_t coloffs_now = buf->Coloffs();
|
||||||
|
|
||||||
// Handle mouse click before rendering to avoid dependent on drawn items
|
// Mark selection state (mark -> cursor), in source coordinates
|
||||||
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
bool sel_active = false;
|
||||||
|
std::size_t sel_sy = 0, sel_sx = 0, sel_ey = 0, sel_ex = 0;
|
||||||
|
if (buf->MarkSet()) {
|
||||||
|
sel_sy = buf->MarkCury();
|
||||||
|
sel_sx = buf->MarkCurx();
|
||||||
|
sel_ey = buf->Cury();
|
||||||
|
sel_ex = buf->Curx();
|
||||||
|
if (sel_sy > sel_ey || (sel_sy == sel_ey && sel_sx > sel_ex)) {
|
||||||
|
std::swap(sel_sy, sel_ey);
|
||||||
|
std::swap(sel_sx, sel_ex);
|
||||||
|
}
|
||||||
|
sel_active = !(sel_sy == sel_ey && sel_sx == sel_ex);
|
||||||
|
}
|
||||||
|
// Visual-line selection: full-line highlight range
|
||||||
|
const bool vsel_active = buf->VisualLineActive();
|
||||||
|
const std::size_t vsel_sy = vsel_active ? buf->VisualLineStartY() : 0;
|
||||||
|
const std::size_t vsel_ey = vsel_active ? buf->VisualLineEndY() : 0;
|
||||||
|
|
||||||
|
static bool mouse_selecting = false;
|
||||||
|
auto mouse_pos_to_buf = [&]() -> std::pair<std::size_t, std::size_t> {
|
||||||
ImVec2 mp = ImGui::GetIO().MousePos;
|
ImVec2 mp = ImGui::GetIO().MousePos;
|
||||||
// Compute content-relative position accounting for scroll
|
// Convert mouse pos to buffer row
|
||||||
// mp.y - child_window_pos.y gives us pixels from top of child window
|
|
||||||
// Adding scroll_y gives us pixels from top of content (buffer row 0)
|
|
||||||
float content_y = (mp.y - child_window_pos.y) + scroll_y;
|
float content_y = (mp.y - child_window_pos.y) + scroll_y;
|
||||||
long by_l = static_cast<long>(content_y / row_h);
|
long by_l = static_cast<long>(content_y / row_h);
|
||||||
if (by_l < 0)
|
if (by_l < 0)
|
||||||
by_l = 0;
|
by_l = 0;
|
||||||
|
|
||||||
// Convert to buffer row
|
|
||||||
std::size_t by = static_cast<std::size_t>(by_l);
|
std::size_t by = static_cast<std::size_t>(by_l);
|
||||||
if (by >= lines.size()) {
|
if (by >= lines.size())
|
||||||
if (!lines.empty())
|
by = lines.empty() ? 0 : (lines.size() - 1);
|
||||||
by = lines.size() - 1;
|
|
||||||
else
|
|
||||||
by = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute click X position relative to left edge of child window (in pixels)
|
// Convert mouse pos to rendered x
|
||||||
// This gives us the visual offset from the start of displayed content
|
|
||||||
float visual_x = mp.x - child_window_pos.x;
|
float visual_x = mp.x - child_window_pos.x;
|
||||||
if (visual_x < 0.0f)
|
if (visual_x < 0.0f)
|
||||||
visual_x = 0.0f;
|
visual_x = 0.0f;
|
||||||
|
|
||||||
// Convert visual pixel offset to rendered column, then add coloffs_now
|
|
||||||
// to get the absolute rendered column in the buffer
|
|
||||||
std::size_t clicked_rx = static_cast<std::size_t>(visual_x / space_w) + coloffs_now;
|
std::size_t clicked_rx = static_cast<std::size_t>(visual_x / space_w) + coloffs_now;
|
||||||
|
|
||||||
// Empty buffer guard: if there are no lines yet, just move to 0:0
|
// Convert rendered column to source column
|
||||||
if (lines.empty()) {
|
if (lines.empty())
|
||||||
Execute(ed, CommandId::MoveCursorTo, std::string("0:0"));
|
return {0, 0};
|
||||||
} else {
|
|
||||||
// Convert rendered column (clicked_rx) to source column accounting for tabs
|
|
||||||
std::string line_clicked = static_cast<std::string>(lines[by]);
|
std::string line_clicked = static_cast<std::string>(lines[by]);
|
||||||
const std::size_t tabw = 8;
|
const std::size_t tabw = 8;
|
||||||
|
std::size_t rx = 0;
|
||||||
// Iterate through source columns, computing rendered position, to find closest match
|
|
||||||
std::size_t rx = 0; // rendered column position
|
|
||||||
std::size_t best_col = 0;
|
std::size_t best_col = 0;
|
||||||
float best_dist = std::numeric_limits<float>::infinity();
|
float best_dist = std::numeric_limits<float>::infinity();
|
||||||
float clicked_rx_f = static_cast<float>(clicked_rx);
|
float clicked_rx_f = static_cast<float>(clicked_rx);
|
||||||
|
|
||||||
for (std::size_t i = 0; i <= line_clicked.size(); ++i) {
|
for (std::size_t i = 0; i <= line_clicked.size(); ++i) {
|
||||||
// Check current position
|
|
||||||
float dist = std::fabs(clicked_rx_f - static_cast<float>(rx));
|
float dist = std::fabs(clicked_rx_f - static_cast<float>(rx));
|
||||||
if (dist < best_dist) {
|
if (dist < best_dist) {
|
||||||
best_dist = dist;
|
best_dist = dist;
|
||||||
best_col = i;
|
best_col = i;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Advance to next position if not at end
|
|
||||||
if (i < line_clicked.size()) {
|
if (i < line_clicked.size()) {
|
||||||
if (line_clicked[i] == '\t') {
|
rx += (line_clicked[i] == '\t') ? (tabw - (rx % tabw)) : 1;
|
||||||
rx += (tabw - (rx % tabw));
|
|
||||||
} else {
|
|
||||||
rx += 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return {by, best_col};
|
||||||
|
};
|
||||||
|
|
||||||
// Dispatch absolute buffer coordinates (row:col)
|
// Mouse-driven selection: set mark on press, update cursor on drag
|
||||||
|
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
||||||
|
mouse_selecting = true;
|
||||||
|
auto [by, bx] = mouse_pos_to_buf();
|
||||||
char tmp[64];
|
char tmp[64];
|
||||||
std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, best_col);
|
std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, bx);
|
||||||
|
Execute(ed, CommandId::MoveCursorTo, std::string(tmp));
|
||||||
|
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
|
||||||
|
mbuf->SetMark(bx, by);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mouse_selecting && ImGui::IsWindowHovered() && ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
|
||||||
|
auto [by, bx] = mouse_pos_to_buf();
|
||||||
|
char tmp[64];
|
||||||
|
std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, bx);
|
||||||
Execute(ed, CommandId::MoveCursorTo, std::string(tmp));
|
Execute(ed, CommandId::MoveCursorTo, std::string(tmp));
|
||||||
}
|
}
|
||||||
|
if (mouse_selecting && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
|
||||||
|
mouse_selecting = false;
|
||||||
}
|
}
|
||||||
for (std::size_t i = rowoffs; i < lines.size(); ++i) {
|
for (std::size_t i = rowoffs; i < lines.size(); ++i) {
|
||||||
// Capture the screen position before drawing the line
|
// Capture the screen position before drawing the line
|
||||||
@@ -295,6 +306,51 @@ ImGuiRenderer::Draw(Editor &ed)
|
|||||||
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
|
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw selection background (over search highlight; under text)
|
||||||
|
if (sel_active || vsel_active) {
|
||||||
|
bool line_has = false;
|
||||||
|
std::size_t sx = 0, ex = 0;
|
||||||
|
if (vsel_active && i >= vsel_sy && i <= vsel_ey) {
|
||||||
|
sx = 0;
|
||||||
|
ex = line.size();
|
||||||
|
line_has = ex > sx;
|
||||||
|
} else if (i < sel_sy || i > sel_ey) {
|
||||||
|
line_has = false;
|
||||||
|
} else if (sel_sy == sel_ey) {
|
||||||
|
sx = sel_sx;
|
||||||
|
ex = sel_ex;
|
||||||
|
line_has = ex > sx;
|
||||||
|
} else if (i == sel_sy) {
|
||||||
|
sx = sel_sx;
|
||||||
|
ex = line.size();
|
||||||
|
line_has = ex > sx;
|
||||||
|
} else if (i == sel_ey) {
|
||||||
|
sx = 0;
|
||||||
|
ex = std::min(sel_ex, line.size());
|
||||||
|
line_has = ex > sx;
|
||||||
|
} else {
|
||||||
|
sx = 0;
|
||||||
|
ex = line.size();
|
||||||
|
line_has = ex > sx;
|
||||||
|
}
|
||||||
|
if (line_has) {
|
||||||
|
std::size_t rx_start = src_to_rx(sx);
|
||||||
|
std::size_t rx_end = src_to_rx(ex);
|
||||||
|
if (rx_end > coloffs_now) {
|
||||||
|
std::size_t vx0 = (rx_start > coloffs_now)
|
||||||
|
? (rx_start - coloffs_now)
|
||||||
|
: 0;
|
||||||
|
std::size_t vx1 = rx_end - coloffs_now;
|
||||||
|
ImVec2 p0 = ImVec2(line_pos.x + static_cast<float>(vx0) * space_w,
|
||||||
|
line_pos.y);
|
||||||
|
ImVec2 p1 = ImVec2(line_pos.x + static_cast<float>(vx1) * space_w,
|
||||||
|
line_pos.y + line_h);
|
||||||
|
ImU32 col = ImGui::GetColorU32(ImGuiCol_TextSelectedBg);
|
||||||
|
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// Emit entire line to an expanded buffer (tabs -> spaces)
|
// Emit entire line to an expanded buffer (tabs -> spaces)
|
||||||
for (std::size_t src = 0; src < line.size(); ++src) {
|
for (std::size_t src = 0; src < line.size(); ++src) {
|
||||||
char c = line[src];
|
char c = line[src];
|
||||||
|
|||||||
@@ -114,6 +114,9 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
|
|||||||
case '=':
|
case '=':
|
||||||
out = CommandId::IndentRegion;
|
out = CommandId::IndentRegion;
|
||||||
return true;
|
return true;
|
||||||
|
case '/':
|
||||||
|
out = CommandId::VisualLineModeToggle;
|
||||||
|
return true;
|
||||||
case ';':
|
case ';':
|
||||||
out = CommandId::CommandPromptStart; // C-k ; : generic command prompt
|
out = CommandId::CommandPromptStart; // C-k ; : generic command prompt
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include "TerminalInputHandler.h"
|
#include "TerminalInputHandler.h"
|
||||||
#include "KKeymap.h"
|
#include "KKeymap.h"
|
||||||
|
#include "Command.h"
|
||||||
#include "Editor.h"
|
#include "Editor.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -23,6 +24,7 @@ map_key_to_command(const int ch,
|
|||||||
bool &k_prefix,
|
bool &k_prefix,
|
||||||
bool &esc_meta,
|
bool &esc_meta,
|
||||||
bool &k_ctrl_pending,
|
bool &k_ctrl_pending,
|
||||||
|
bool &mouse_selecting,
|
||||||
Editor *ed,
|
Editor *ed,
|
||||||
MappedInput &out)
|
MappedInput &out)
|
||||||
{
|
{
|
||||||
@@ -54,13 +56,34 @@ map_key_to_command(const int ch,
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
// React to left button click/press
|
// React to left button click/press
|
||||||
if (ev.bstate & (BUTTON1_CLICKED | BUTTON1_PRESSED | BUTTON1_RELEASED)) {
|
if (ed && (ev.bstate & (BUTTON1_CLICKED | BUTTON1_PRESSED | BUTTON1_RELEASED |
|
||||||
|
REPORT_MOUSE_POSITION))) {
|
||||||
char buf[64];
|
char buf[64];
|
||||||
// Use screen coordinates; command handler will translate via offsets
|
// Use screen coordinates; command handler will translate via offsets
|
||||||
std::snprintf(buf, sizeof(buf), "@%d:%d", ev.y, ev.x);
|
std::snprintf(buf, sizeof(buf), "@%d:%d", ev.y, ev.x);
|
||||||
out = {true, CommandId::MoveCursorTo, std::string(buf), 0};
|
const bool pressed = (ev.bstate & (BUTTON1_PRESSED | BUTTON1_CLICKED)) != 0;
|
||||||
|
const bool released = (ev.bstate & BUTTON1_RELEASED) != 0;
|
||||||
|
const bool moved = (ev.bstate & REPORT_MOUSE_POSITION) != 0;
|
||||||
|
if (pressed) {
|
||||||
|
mouse_selecting = true;
|
||||||
|
Execute(*ed, CommandId::MoveCursorTo, std::string(buf));
|
||||||
|
if (Buffer *b = ed->CurrentBuffer()) {
|
||||||
|
b->SetMark(b->Curx(), b->Cury());
|
||||||
|
}
|
||||||
|
out.hasCommand = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (mouse_selecting && moved) {
|
||||||
|
Execute(*ed, CommandId::MoveCursorTo, std::string(buf));
|
||||||
|
out.hasCommand = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (released) {
|
||||||
|
mouse_selecting = false;
|
||||||
|
out.hasCommand = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// No actionable mouse event
|
// No actionable mouse event
|
||||||
out.hasCommand = false;
|
out.hasCommand = false;
|
||||||
@@ -292,6 +315,7 @@ TerminalInputHandler::decode_(MappedInput &out)
|
|||||||
ch,
|
ch,
|
||||||
k_prefix_, esc_meta_,
|
k_prefix_, esc_meta_,
|
||||||
k_ctrl_pending_,
|
k_ctrl_pending_,
|
||||||
|
mouse_selecting_,
|
||||||
ed_,
|
ed_,
|
||||||
out);
|
out);
|
||||||
if (!consumed)
|
if (!consumed)
|
||||||
|
|||||||
@@ -30,5 +30,8 @@ private:
|
|||||||
// Simple meta (ESC) state for ESC sequences like ESC b/f
|
// Simple meta (ESC) state for ESC sequences like ESC b/f
|
||||||
bool esc_meta_ = false;
|
bool esc_meta_ = false;
|
||||||
|
|
||||||
|
// Mouse drag selection state
|
||||||
|
bool mouse_selecting_ = false;
|
||||||
|
|
||||||
Editor *ed_ = nullptr; // attached editor for uarg handling
|
Editor *ed_ = nullptr; // attached editor for uarg handling
|
||||||
};
|
};
|
||||||
@@ -107,8 +107,43 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
const std::size_t cur_mx = has_current ? ed.SearchMatchX() : 0;
|
const std::size_t cur_mx = has_current ? ed.SearchMatchX() : 0;
|
||||||
const std::size_t cur_my = has_current ? ed.SearchMatchY() : 0;
|
const std::size_t cur_my = has_current ? ed.SearchMatchY() : 0;
|
||||||
const std::size_t cur_mend = has_current ? (ed.SearchMatchX() + ed.SearchMatchLen()) : 0;
|
const std::size_t cur_mend = has_current ? (ed.SearchMatchX() + ed.SearchMatchLen()) : 0;
|
||||||
bool hl_on = false;
|
|
||||||
bool cur_on = false;
|
// Mark selection (mark -> cursor), in source coordinates
|
||||||
|
bool sel_active = false;
|
||||||
|
std::size_t sel_sy = 0, sel_sx = 0, sel_ey = 0, sel_ex = 0;
|
||||||
|
if (buf->MarkSet()) {
|
||||||
|
sel_sy = buf->MarkCury();
|
||||||
|
sel_sx = buf->MarkCurx();
|
||||||
|
sel_ey = buf->Cury();
|
||||||
|
sel_ex = buf->Curx();
|
||||||
|
if (sel_sy > sel_ey || (sel_sy == sel_ey && sel_sx > sel_ex)) {
|
||||||
|
std::swap(sel_sy, sel_ey);
|
||||||
|
std::swap(sel_sx, sel_ex);
|
||||||
|
}
|
||||||
|
sel_active = !(sel_sy == sel_ey && sel_sx == sel_ex);
|
||||||
|
}
|
||||||
|
// Visual-line selection: full-line selection range
|
||||||
|
const bool vsel_active = buf->VisualLineActive();
|
||||||
|
const std::size_t vsel_sy = vsel_active ? buf->VisualLineStartY() : 0;
|
||||||
|
const std::size_t vsel_ey = vsel_active ? buf->VisualLineEndY() : 0;
|
||||||
|
auto is_src_in_sel = [&](std::size_t y, std::size_t sx) -> bool {
|
||||||
|
(void) sx;
|
||||||
|
if (vsel_active) {
|
||||||
|
if (y >= vsel_sy && y <= vsel_ey)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!sel_active)
|
||||||
|
return false;
|
||||||
|
if (y < sel_sy || y > sel_ey)
|
||||||
|
return false;
|
||||||
|
if (sel_sy == sel_ey)
|
||||||
|
return sx >= sel_sx && sx < sel_ex;
|
||||||
|
if (y == sel_sy)
|
||||||
|
return sx >= sel_sx;
|
||||||
|
if (y == sel_ey)
|
||||||
|
return sx < sel_ex;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
int written = 0;
|
int written = 0;
|
||||||
if (li < lines.size()) {
|
if (li < lines.size()) {
|
||||||
std::string line = static_cast<std::string>(lines[li]);
|
std::string line = static_cast<std::string>(lines[li]);
|
||||||
@@ -156,27 +191,21 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
}
|
}
|
||||||
return kte::TokenKind::Default;
|
return kte::TokenKind::Default;
|
||||||
};
|
};
|
||||||
auto apply_token_attr = [&](kte::TokenKind k) {
|
auto token_attr = [&](kte::TokenKind k) -> attr_t {
|
||||||
// Map to simple attributes; search highlight uses A_STANDOUT which takes precedence below
|
|
||||||
attrset(A_NORMAL);
|
|
||||||
switch (k) {
|
switch (k) {
|
||||||
case kte::TokenKind::Keyword:
|
case kte::TokenKind::Keyword:
|
||||||
case kte::TokenKind::Type:
|
case kte::TokenKind::Type:
|
||||||
case kte::TokenKind::Constant:
|
case kte::TokenKind::Constant:
|
||||||
case kte::TokenKind::Function:
|
case kte::TokenKind::Function:
|
||||||
attron(A_BOLD);
|
return A_BOLD;
|
||||||
break;
|
|
||||||
case kte::TokenKind::Comment:
|
case kte::TokenKind::Comment:
|
||||||
attron(A_DIM);
|
return A_DIM;
|
||||||
break;
|
|
||||||
case kte::TokenKind::String:
|
case kte::TokenKind::String:
|
||||||
case kte::TokenKind::Char:
|
case kte::TokenKind::Char:
|
||||||
case kte::TokenKind::Number:
|
case kte::TokenKind::Number:
|
||||||
// standout a bit using A_UNDERLINE if available
|
return A_UNDERLINE;
|
||||||
attron(A_UNDERLINE);
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
break;
|
return 0;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
while (written < cols) {
|
while (written < cols) {
|
||||||
@@ -218,36 +247,23 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
}
|
}
|
||||||
// Now render visible spaces
|
// Now render visible spaces
|
||||||
while (next_tab > 0 && written < cols) {
|
while (next_tab > 0 && written < cols) {
|
||||||
|
bool in_sel = is_src_in_sel(li, src_i);
|
||||||
bool in_hl = search_mode && is_src_in_hl(src_i);
|
bool in_hl = search_mode && is_src_in_hl(src_i);
|
||||||
bool in_cur =
|
bool in_cur =
|
||||||
has_current && li == cur_my && src_i >= cur_mx
|
has_current && li == cur_my && src_i >= cur_mx
|
||||||
&& src_i < cur_mend;
|
&&
|
||||||
// Toggle highlight attributes
|
src_i < cur_mend;
|
||||||
int attr = 0;
|
attr_t a = A_NORMAL;
|
||||||
|
a |= token_attr(token_at(src_i));
|
||||||
|
if (in_sel) {
|
||||||
|
a |= A_REVERSE;
|
||||||
|
} else {
|
||||||
if (in_hl)
|
if (in_hl)
|
||||||
attr |= A_STANDOUT;
|
a |= A_STANDOUT;
|
||||||
if (in_cur)
|
if (in_cur)
|
||||||
attr |= A_BOLD;
|
a |= A_BOLD;
|
||||||
if ((attr & A_STANDOUT) && !hl_on) {
|
|
||||||
attron(A_STANDOUT);
|
|
||||||
hl_on = true;
|
|
||||||
}
|
|
||||||
if (!(attr & A_STANDOUT) && hl_on) {
|
|
||||||
attroff(A_STANDOUT);
|
|
||||||
hl_on = false;
|
|
||||||
}
|
|
||||||
if ((attr & A_BOLD) && !cur_on) {
|
|
||||||
attron(A_BOLD);
|
|
||||||
cur_on = true;
|
|
||||||
}
|
|
||||||
if (!(attr & A_BOLD) && cur_on) {
|
|
||||||
attroff(A_BOLD);
|
|
||||||
cur_on = false;
|
|
||||||
}
|
|
||||||
// Apply syntax attribute only if not in search highlight
|
|
||||||
if (!in_hl) {
|
|
||||||
apply_token_attr(token_at(src_i));
|
|
||||||
}
|
}
|
||||||
|
attrset(a);
|
||||||
addch(' ');
|
addch(' ');
|
||||||
++written;
|
++written;
|
||||||
++render_col;
|
++render_col;
|
||||||
@@ -281,34 +297,27 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool in_sel = from_src && is_src_in_sel(li, src_i);
|
||||||
bool in_hl = search_mode && from_src && is_src_in_hl(src_i);
|
bool in_hl = search_mode && from_src && is_src_in_hl(src_i);
|
||||||
bool in_cur =
|
bool in_cur = has_current && li == cur_my && from_src && src_i >= cur_mx &&
|
||||||
has_current && li == cur_my && from_src && src_i >= cur_mx && src_i <
|
src_i < cur_mend;
|
||||||
cur_mend;
|
attr_t a = A_NORMAL;
|
||||||
if (in_hl && !hl_on) {
|
if (from_src)
|
||||||
attron(A_STANDOUT);
|
a |= token_attr(token_at(src_i));
|
||||||
hl_on = true;
|
if (in_sel) {
|
||||||
}
|
a |= A_REVERSE;
|
||||||
if (!in_hl && hl_on) {
|
} else {
|
||||||
attroff(A_STANDOUT);
|
if (in_hl)
|
||||||
hl_on = false;
|
a |= A_STANDOUT;
|
||||||
}
|
if (in_cur)
|
||||||
if (in_cur && !cur_on) {
|
a |= A_BOLD;
|
||||||
attron(A_BOLD);
|
|
||||||
cur_on = true;
|
|
||||||
}
|
|
||||||
if (!in_cur && cur_on) {
|
|
||||||
attroff(A_BOLD);
|
|
||||||
cur_on = false;
|
|
||||||
}
|
|
||||||
if (!in_hl && from_src) {
|
|
||||||
apply_token_attr(token_at(src_i));
|
|
||||||
}
|
}
|
||||||
|
attrset(a);
|
||||||
|
|
||||||
if (from_src) {
|
if (from_src) {
|
||||||
cchar_t cch;
|
cchar_t cch;
|
||||||
wchar_t warr[2] = {wch, L'\0'};
|
wchar_t warr[2] = {wch, L'\0'};
|
||||||
setcchar(&cch, warr, A_NORMAL, 0, nullptr);
|
setcchar(&cch, warr, 0, 0, nullptr);
|
||||||
add_wch(&cch);
|
add_wch(&cch);
|
||||||
} else {
|
} else {
|
||||||
addch(' ');
|
addch(' ');
|
||||||
@@ -322,14 +331,6 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (hl_on) {
|
|
||||||
attroff(A_STANDOUT);
|
|
||||||
hl_on = false;
|
|
||||||
}
|
|
||||||
if (cur_on) {
|
|
||||||
attroff(A_BOLD);
|
|
||||||
cur_on = false;
|
|
||||||
}
|
|
||||||
attrset(A_NORMAL);
|
attrset(A_NORMAL);
|
||||||
clrtoeol();
|
clrtoeol();
|
||||||
}
|
}
|
||||||
|
|||||||
125
tests/test_visual_line_mode.cc
Normal file
125
tests/test_visual_line_mode.cc
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
#include "Test.h"
|
||||||
|
|
||||||
|
#include "Buffer.h"
|
||||||
|
#include "Command.h"
|
||||||
|
#include "Editor.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
|
||||||
|
static std::string
|
||||||
|
dump_buf(const Buffer &buf)
|
||||||
|
{
|
||||||
|
std::string out;
|
||||||
|
for (const auto &r: buf.Rows()) {
|
||||||
|
out += static_cast<std::string>(r);
|
||||||
|
out.push_back('\n');
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static std::string
|
||||||
|
dump_bytes(const std::string &s)
|
||||||
|
{
|
||||||
|
static const char *hex = "0123456789abcdef";
|
||||||
|
std::string out;
|
||||||
|
for (unsigned char c: s) {
|
||||||
|
out.push_back(hex[(c >> 4) & 0xF]);
|
||||||
|
out.push_back(hex[c & 0xF]);
|
||||||
|
out.push_back(' ');
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (VisualLineMode_BroadcastInsert)
|
||||||
|
{
|
||||||
|
InstallDefaultCommands();
|
||||||
|
|
||||||
|
Editor ed;
|
||||||
|
ed.SetDimensions(24, 80);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
b.insert_text(0, 0, "foo\nfoo\nfoo\n");
|
||||||
|
b.SetCursor(1, 0); // fo|o
|
||||||
|
ed.AddBuffer(std::move(b));
|
||||||
|
|
||||||
|
ASSERT_TRUE(ed.CurrentBuffer() != nullptr);
|
||||||
|
|
||||||
|
// Enter visual-line mode and extend selection to 3 lines
|
||||||
|
ASSERT_TRUE(Execute(ed, std::string("visual-line-toggle")));
|
||||||
|
ASSERT_TRUE(Execute(ed, std::string("down"), std::string(), 2));
|
||||||
|
|
||||||
|
// Broadcast insert to all selected lines
|
||||||
|
ASSERT_TRUE(Execute(ed, std::string("insert"), std::string("X")));
|
||||||
|
|
||||||
|
const std::string got = dump_buf(*ed.CurrentBuffer());
|
||||||
|
// Note: buffers that end with a trailing '\n' have an extra empty row.
|
||||||
|
const std::string exp = "fXoo\nfXoo\nfXoo\n\n";
|
||||||
|
if (got != exp) {
|
||||||
|
std::cerr << "Expected (len=" << exp.size() << ") bytes: " << dump_bytes(exp) << "\n";
|
||||||
|
std::cerr << "Got (len=" << got.size() << ") bytes: " << dump_bytes(got) << "\n";
|
||||||
|
}
|
||||||
|
ASSERT_TRUE(got == exp);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (VisualLineMode_BroadcastBackspace)
|
||||||
|
{
|
||||||
|
InstallDefaultCommands();
|
||||||
|
|
||||||
|
Editor ed;
|
||||||
|
ed.SetDimensions(24, 80);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
b.insert_text(0, 0, "abcd\nabcd\nabcd\n");
|
||||||
|
b.SetCursor(2, 0); // ab|cd
|
||||||
|
ed.AddBuffer(std::move(b));
|
||||||
|
|
||||||
|
ASSERT_TRUE(Execute(ed, std::string("visual-line-toggle")));
|
||||||
|
ASSERT_TRUE(Execute(ed, std::string("down"), std::string(), 2));
|
||||||
|
|
||||||
|
ASSERT_TRUE(Execute(ed, std::string("backspace")));
|
||||||
|
const std::string got = dump_buf(*ed.CurrentBuffer());
|
||||||
|
// Note: buffers that end with a trailing '\n' have an extra empty row.
|
||||||
|
const std::string exp = "acd\nacd\nacd\n\n";
|
||||||
|
if (got != exp) {
|
||||||
|
std::cerr << "Expected (len=" << exp.size() << ") bytes: " << dump_bytes(exp) << "\n";
|
||||||
|
std::cerr << "Got (len=" << got.size() << ") bytes: " << dump_bytes(got) << "\n";
|
||||||
|
}
|
||||||
|
ASSERT_TRUE(got == exp);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (VisualLineMode_CancelWithCtrlG)
|
||||||
|
{
|
||||||
|
InstallDefaultCommands();
|
||||||
|
|
||||||
|
Editor ed;
|
||||||
|
ed.SetDimensions(24, 80);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
b.insert_text(0, 0, "foo\nfoo\nfoo\n");
|
||||||
|
b.SetCursor(1, 0);
|
||||||
|
ed.AddBuffer(std::move(b));
|
||||||
|
|
||||||
|
ASSERT_TRUE(Execute(ed, std::string("visual-line-toggle")));
|
||||||
|
ASSERT_TRUE(Execute(ed, std::string("down"), std::string(), 2));
|
||||||
|
|
||||||
|
// C-g is mapped to "refresh" and should cancel visual-line mode.
|
||||||
|
ASSERT_TRUE(Execute(ed, std::string("refresh")));
|
||||||
|
ASSERT_TRUE(ed.CurrentBuffer() != nullptr);
|
||||||
|
ASSERT_TRUE(!ed.CurrentBuffer()->VisualLineActive());
|
||||||
|
|
||||||
|
// After cancel, edits should only affect the primary cursor line.
|
||||||
|
ASSERT_TRUE(Execute(ed, std::string("insert"), std::string("X")));
|
||||||
|
const std::string got = dump_buf(*ed.CurrentBuffer());
|
||||||
|
// Cursor is still on the last line we moved to (down, down).
|
||||||
|
const std::string exp = "foo\nfoo\nfXoo\n\n";
|
||||||
|
if (got != exp) {
|
||||||
|
std::cerr << "Expected (len=" << exp.size() << ") bytes: " << dump_bytes(exp) << "\n";
|
||||||
|
std::cerr << "Got (len=" << got.size() << ") bytes: " << dump_bytes(got) << "\n";
|
||||||
|
}
|
||||||
|
ASSERT_TRUE(got == exp);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user