Files
kte/Command.cpp
2025-11-29 17:54:55 -08:00

467 lines
13 KiB
C++

#include "Command.h"
#include <algorithm>
#include "Editor.h"
#include "Buffer.h"
// Keep buffer viewport offsets so that the cursor stays within the visible
// window based on the editor's current dimensions. The bottom row is reserved
// for the status line.
static void ensure_cursor_visible(Editor &ed, Buffer &buf)
{
std::size_t rows = ed.Rows();
std::size_t cols = ed.Cols();
if (rows == 0 || cols == 0) return;
std::size_t content_rows = rows > 0 ? rows - 1 : 0; // last row = status
std::size_t cury = buf.Cury();
std::size_t curx = buf.Curx();
std::size_t rowoffs = buf.Rowoffs();
std::size_t coloffs = buf.Coloffs();
// Vertical scrolling
if (cury < rowoffs) {
rowoffs = cury;
} else if (content_rows > 0 && cury >= rowoffs + content_rows) {
rowoffs = cury - content_rows + 1;
}
// Clamp vertical offset to available content
const auto total_rows = buf.Rows().size();
if (content_rows < total_rows) {
std::size_t max_rowoffs = total_rows - content_rows;
if (rowoffs > max_rowoffs) rowoffs = max_rowoffs;
} else {
rowoffs = 0;
}
// Horizontal scrolling
if (curx < coloffs) {
coloffs = curx;
} else if (curx >= coloffs + cols) {
coloffs = curx - cols + 1;
}
buf.SetOffsets(rowoffs, coloffs);
}
static void ensure_at_least_one_line(Buffer &buf)
{
if (buf.Rows().empty()) {
buf.Rows().push_back("");
buf.SetDirty(true);
}
}
// (helper removed)
// --- File/Session commands ---
static bool cmd_save(CommandContext &ctx)
{
Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) {
ctx.editor.SetStatus("No buffer to save");
return false;
}
std::string err;
if (!buf->IsFileBacked()) {
ctx.editor.SetStatus("Buffer is not file-backed; use save-as");
return false;
}
if (!buf->Save(err)) {
ctx.editor.SetStatus(err);
return false;
}
buf->SetDirty(false);
ctx.editor.SetStatus("Saved " + buf->Filename());
return true;
}
static bool cmd_save_as(CommandContext &ctx)
{
Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) {
ctx.editor.SetStatus("No buffer to save");
return false;
}
if (ctx.arg.empty()) {
ctx.editor.SetStatus("save-as requires a filename");
return false;
}
std::string err;
if (!buf->SaveAs(ctx.arg, err)) {
ctx.editor.SetStatus(err);
return false;
}
ctx.editor.SetStatus("Saved as " + ctx.arg);
return true;
}
static bool cmd_quit(CommandContext &ctx)
{
// Placeholder: actual app loop should react to this status or a future flag
ctx.editor.SetStatus("Quit requested");
return true;
}
static bool cmd_save_and_quit(CommandContext &ctx)
{
// Try save current buffer (if any), then mark quit requested.
Buffer *buf = ctx.editor.CurrentBuffer();
if (buf && buf->Dirty()) {
std::string err;
if (buf->IsFileBacked()) {
if (buf->Save(err)) {
buf->SetDirty(false);
} else {
ctx.editor.SetStatus(err);
return false;
}
} else {
ctx.editor.SetStatus("Buffer not file-backed; use save-as before quitting");
return false;
}
}
ctx.editor.SetStatus("Save and quit requested");
return true;
}
static bool cmd_refresh(CommandContext &ctx)
{
// Placeholder: renderer will handle this in Milestone 3
ctx.editor.SetStatus("Refresh requested");
return true;
}
static bool cmd_find_start(CommandContext &ctx)
{
// Placeholder for incremental search start
ctx.editor.SetStatus("Find (incremental) start");
return true;
}
// --- Editing ---
static bool cmd_insert_text(CommandContext &ctx)
{
Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) {
ctx.editor.SetStatus("No buffer to edit");
return false;
}
// Disallow newlines in InsertText; they should come via Newline
if (ctx.arg.find('\n') != std::string::npos || ctx.arg.find('\r') != std::string::npos) {
ctx.editor.SetStatus("InsertText arg must not contain newlines");
return false;
}
ensure_at_least_one_line(*buf);
auto &rows = buf->Rows();
std::size_t y = buf->Cury();
std::size_t x = buf->Curx();
if (y >= rows.size()) {
rows.resize(y + 1);
}
int repeat = ctx.count > 0 ? ctx.count : 1;
for (int i = 0; i < repeat; ++i) {
rows[y].insert(x, ctx.arg);
x += ctx.arg.size();
}
buf->SetCursor(x, y);
buf->SetDirty(true);
ensure_cursor_visible(ctx.editor, *buf);
return true;
}
static bool cmd_newline(CommandContext &ctx)
{
Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) {
ctx.editor.SetStatus("No buffer to edit");
return false;
}
ensure_at_least_one_line(*buf);
auto &rows = buf->Rows();
std::size_t y = buf->Cury();
std::size_t x = buf->Curx();
int repeat = ctx.count > 0 ? ctx.count : 1;
for (int i = 0; i < repeat; ++i) {
if (y >= rows.size()) rows.resize(y + 1);
std::string &line = rows[y];
std::string tail;
if (x < line.size()) {
tail = line.substr(x);
line.erase(x);
}
rows.insert(rows.begin() + static_cast<std::ptrdiff_t>(y + 1), tail);
y += 1;
x = 0;
}
buf->SetCursor(x, y);
buf->SetDirty(true);
ensure_cursor_visible(ctx.editor, *buf);
return true;
}
static bool cmd_backspace(CommandContext &ctx)
{
Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) {
ctx.editor.SetStatus("No buffer to edit");
return false;
}
ensure_at_least_one_line(*buf);
auto &rows = buf->Rows();
std::size_t y = buf->Cury();
std::size_t x = buf->Curx();
int repeat = ctx.count > 0 ? ctx.count : 1;
for (int i = 0; i < repeat; ++i) {
if (x > 0) {
rows[y].erase(x - 1, 1);
--x;
} else if (y > 0) {
// join with previous line
std::size_t prev_len = rows[y - 1].size();
rows[y - 1] += rows[y];
rows.erase(rows.begin() + static_cast<std::ptrdiff_t>(y));
y = y - 1;
x = prev_len;
} else {
// at very start; nothing to do
break;
}
}
buf->SetCursor(x, y);
buf->SetDirty(true);
ensure_cursor_visible(ctx.editor, *buf);
return true;
}
static bool cmd_delete_char(CommandContext &ctx)
{
Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) {
ctx.editor.SetStatus("No buffer to edit");
return false;
}
ensure_at_least_one_line(*buf);
auto &rows = buf->Rows();
std::size_t y = buf->Cury();
std::size_t x = buf->Curx();
int repeat = ctx.count > 0 ? ctx.count : 1;
for (int i = 0; i < repeat; ++i) {
if (y >= rows.size()) break;
if (x < rows[y].size()) {
rows[y].erase(x, 1);
} else if (y + 1 < rows.size()) {
// join next line
rows[y] += rows[y + 1];
rows.erase(rows.begin() + static_cast<std::ptrdiff_t>(y + 1));
} else {
break;
}
}
buf->SetDirty(true);
ensure_cursor_visible(ctx.editor, *buf);
return true;
}
// --- Navigation ---
// (helper removed)
static bool cmd_move_left(CommandContext &ctx)
{
Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) return false;
ensure_at_least_one_line(*buf);
auto &rows = buf->Rows();
std::size_t y = buf->Cury();
std::size_t x = buf->Curx();
int repeat = ctx.count > 0 ? ctx.count : 1;
while (repeat-- > 0) {
if (x > 0) {
--x;
} else if (y > 0) {
--y;
x = rows[y].size();
}
}
buf->SetCursor(x, y);
ensure_cursor_visible(ctx.editor, *buf);
return true;
}
static bool cmd_move_right(CommandContext &ctx)
{
Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) return false;
ensure_at_least_one_line(*buf);
auto &rows = buf->Rows();
std::size_t y = buf->Cury();
std::size_t x = buf->Curx();
int repeat = ctx.count > 0 ? ctx.count : 1;
while (repeat-- > 0) {
if (y < rows.size() && x < rows[y].size()) {
++x;
} else if (y + 1 < rows.size()) {
++y;
x = 0;
}
}
buf->SetCursor(x, y);
ensure_cursor_visible(ctx.editor, *buf);
return true;
}
static bool cmd_move_up(CommandContext &ctx)
{
Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) return false;
ensure_at_least_one_line(*buf);
auto &rows = buf->Rows();
std::size_t y = buf->Cury();
std::size_t x = buf->Curx();
int repeat = ctx.count > 0 ? ctx.count : 1;
if (repeat > static_cast<int>(y)) repeat = static_cast<int>(y);
y -= static_cast<std::size_t>(repeat);
if (x > rows[y].size()) x = rows[y].size();
buf->SetCursor(x, y);
ensure_cursor_visible(ctx.editor, *buf);
return true;
}
static bool cmd_move_down(CommandContext &ctx)
{
Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) return false;
ensure_at_least_one_line(*buf);
auto &rows = buf->Rows();
std::size_t y = buf->Cury();
std::size_t x = buf->Curx();
int repeat = ctx.count > 0 ? ctx.count : 1;
std::size_t max_down = rows.size() - 1 - y;
if (repeat > static_cast<int>(max_down)) repeat = static_cast<int>(max_down);
y += static_cast<std::size_t>(repeat);
if (x > rows[y].size()) x = rows[y].size();
buf->SetCursor(x, y);
ensure_cursor_visible(ctx.editor, *buf);
return true;
}
static bool cmd_move_home(CommandContext &ctx)
{
Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) return false;
ensure_at_least_one_line(*buf);
std::size_t y = buf->Cury();
buf->SetCursor(0, y);
ensure_cursor_visible(ctx.editor, *buf);
return true;
}
static bool cmd_move_end(CommandContext &ctx)
{
Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) return false;
ensure_at_least_one_line(*buf);
auto &rows = buf->Rows();
std::size_t y = buf->Cury();
std::size_t x = (y < rows.size()) ? rows[y].size() : 0;
buf->SetCursor(x, y);
ensure_cursor_visible(ctx.editor, *buf);
return true;
}
std::vector<Command> &
CommandRegistry::storage_()
{
static std::vector<Command> cmds;
return cmds;
}
void
CommandRegistry::Register(const Command &cmd)
{
auto &v = storage_();
// Replace existing with same id or name
auto it = std::find_if(v.begin(), v.end(), [&](const Command &c) {
return c.id == cmd.id || c.name == cmd.name;
});
if (it != v.end()) {
*it = cmd;
} else {
v.push_back(cmd);
}
}
const Command *
CommandRegistry::FindById(CommandId id)
{
auto &v = storage_();
auto it = std::find_if(v.begin(), v.end(), [&](const Command &c) { return c.id == id; });
return it == v.end() ? nullptr : &*it;
}
const Command *
CommandRegistry::FindByName(const std::string &name)
{
auto &v = storage_();
auto it = std::find_if(v.begin(), v.end(), [&](const Command &c) { return c.name == name; });
return it == v.end() ? nullptr : &*it;
}
const std::vector<Command> &
CommandRegistry::All()
{
return storage_();
}
void
InstallDefaultCommands()
{
CommandRegistry::Register({CommandId::Save, "save", "Save current buffer", cmd_save});
CommandRegistry::Register({CommandId::SaveAs, "save-as", "Save current buffer as...", cmd_save_as});
CommandRegistry::Register({CommandId::Quit, "quit", "Quit editor (request)", cmd_quit});
CommandRegistry::Register({CommandId::SaveAndQuit, "save-quit", "Save and quit (request)", cmd_save_and_quit});
CommandRegistry::Register({CommandId::Refresh, "refresh", "Force redraw", cmd_refresh});
CommandRegistry::Register({CommandId::FindStart, "find-start", "Begin incremental search", cmd_find_start});
// Editing
CommandRegistry::Register({CommandId::InsertText, "insert", "Insert text at cursor (no newlines)", cmd_insert_text});
CommandRegistry::Register({CommandId::Newline, "newline", "Insert newline at cursor", cmd_newline});
CommandRegistry::Register({CommandId::Backspace, "backspace", "Delete char before cursor", cmd_backspace});
CommandRegistry::Register({CommandId::DeleteChar, "delete-char", "Delete char at cursor", cmd_delete_char});
// Navigation
CommandRegistry::Register({CommandId::MoveLeft, "left", "Move cursor left", cmd_move_left});
CommandRegistry::Register({CommandId::MoveRight, "right", "Move cursor right", cmd_move_right});
CommandRegistry::Register({CommandId::MoveUp, "up", "Move cursor up", cmd_move_up});
CommandRegistry::Register({CommandId::MoveDown, "down", "Move cursor down", cmd_move_down});
CommandRegistry::Register({CommandId::MoveHome, "home", "Move to beginning of line", cmd_move_home});
CommandRegistry::Register({CommandId::MoveEnd, "end", "Move to end of line", cmd_move_end});
}
bool Execute(Editor &ed, CommandId id, const std::string &arg, int count)
{
const Command *cmd = CommandRegistry::FindById(id);
if (!cmd) return false;
CommandContext ctx{ed, arg, count};
return cmd->handler ? cmd->handler(ctx) : false;
}
bool Execute(Editor &ed, const std::string &name, const std::string &arg, int count)
{
const Command *cmd = CommandRegistry::FindByName(name);
if (!cmd) return false;
CommandContext ctx{ed, arg, count};
return cmd->handler ? cmd->handler(ctx) : false;
}