sdl_runtime_compiler git · main
SDL3 game for running and compiling code at runtime
C++ 72.3% C 26.2%git clone https://git.christianimmanuel.de/sdl-graphics/sdl_runtime_compiler.gitwget https://git.christianimmanuel.de/sdl-graphics/sdl_runtime_compiler/archive/sdl_runtime_compiler.tar.gzsrc/text/vim_buffer.cpp raw
#include "vim_buffer.hpp"
namespace
Nimbin
{
void
VimBuffer::load(const std::vector<std::string>& src)
{
_lines = src;
if (_lines.empty()) _lines.push_back("");
_row = _col = 0; _mode = Mode::Normal;
_count = 0;
_sig = Signal::None; ++_version;
_saved = _lines;
_dirty = false;
_pending.clear();
_cmd .clear();
_status .clear();
_undo.clear();
_redo.clear();
}
void
VimBuffer::key_char(char c)
{
if (_mode == Mode::Insert) { insert_Char(c); }
else if (_mode == Mode::Command) { _cmd.push_back(c); touch(); }
else { normal_Key(c); }
}
void
VimBuffer::key_enter()
{
if (_mode == Mode::Insert) { split_Line(); }
else if (_mode == Mode::Command) { run_Command(); }
else { _row = get_ClampRow(_row + 1); clamp_Col(); touch(); }
}
void VimBuffer::key_backspace()
{
if (_mode == Mode::Insert) { backspace_Insert(); }
else if (_mode == Mode::Command) { _cmd.size() > 1 ? _cmd.pop_back() : cancel_Command(); touch(); }
else { _col = (_col > 0 ? _col - 1 : 0); touch(); }
}
void VimBuffer::key_escape()
{
if (_mode == Mode::Command)
{ cancel_Command(); return; }
// <Esc> cancels any half-typed normal-mode command — a pending count ("12")
// and/or operator ("d", "c", "y", "g", ">", "<") — so it disappears from the
// showcmd, matching vim. The cursor only nudges left when actually leaving
// insert mode, not when cancelling a count in normal mode.
const bool was_insert = (_mode == Mode::Insert);
_mode = Mode::Normal;
_count = 0;
_pending.clear();
if (was_insert && _col > 0) --_col;
clamp_Col(); touch();
}
void VimBuffer::key_tab()
{
if (_mode != Mode::Insert) return;
constexpr size_t SW = 4;
std::string& line = _lines[static_cast<size_t>(_row)];
if (_col > static_cast<int>(line.size()))
_col = static_cast<int>(line.size());
line.insert(static_cast<size_t>(_col), SW, ' ');
_col += static_cast<int>(SW);
_dirty = true;
touch();
}
void VimBuffer::key_redo()
{
if (_mode != Mode::Normal) return;
redo();
}
VimBuffer::Signal
VimBuffer::take_Signal()
{ Signal s = _sig; _sig = Signal::None; return s; }
void
VimBuffer::mark_Saved()
{ _saved = _lines; _dirty = false; touch(); }
void
VimBuffer::insert_Word_At(int row, int col, const std::string& word, bool new_line)
{
if (word.empty()) return;
if (_lines.empty()) _lines.push_back("");
push_Undo();
if (new_line) {
if (row < 0) {
_lines.insert(_lines.begin(), (size_t)(-row), std::string());
row = 0;
} else if (row >= static_cast<int>(_lines.size())) {
_lines.resize(static_cast<size_t>(row) + 1);
} else {
_lines.insert(_lines.begin() + row, std::string());
}
} else {
if (row < 0) row = 0;
if (row >= static_cast<int>(_lines.size())) {
_lines.resize(static_cast<size_t>(row) + 1);
col = 0;
}
}
std::string& line = _lines[static_cast<size_t>(row)];
if (col < 0) col = 0;
if (col > static_cast<int>(line.size()))
line.append(static_cast<size_t>(col) - line.size(), ' ');
// Add separating spaces only where the splice would otherwise fuse with
// neighbouring text, so the new word stays a standalone token.
std::string ins;
if (col > 0 && line[static_cast<size_t>(col - 1)] != ' ')
ins += ' ';
ins += word;
if ( col < static_cast<int>(line.size())
&& line[static_cast<size_t>(col)] != ' ')
ins += ' ';
line.insert(static_cast<size_t>(col), ins);
// Keep the cursor on the same character if the splice landed at/left of it.
if (row == _row && col <= _col)
_col += static_cast<int>(ins.size());
_dirty = true;
clamp_Col();
touch();
}
void
VimBuffer::touch()
{ ++_version; }
int
VimBuffer::get_LineLen() const
{ return static_cast<int>(_lines[static_cast<size_t>(_row)].size()); }
int
VimBuffer::get_ClampRow(int r) const
{ int n=static_cast<int>(_lines.size()); return r<0?0:(r>=n?n-1:r); }
void
VimBuffer::clamp_Col()
{
int maxc = (_mode==Mode::Insert)
? get_LineLen()
: (get_LineLen()>0
? get_LineLen()-1
: 0);
if (_col < 0) _col = 0;
if (_col > maxc) _col = maxc;
}
int
VimBuffer::get_Eff() const
{ return _count > 0 ? _count : 1; }
void
VimBuffer::normal_Key(char c)
{
if (c == ':') {
_mode = Mode::Command; _cmd = ":";
_status.clear(); touch();
return;
}
if (std::isdigit((unsigned char)c)) {
if (c == '0' && _count == 0 && _pending.empty()) {
_col = 0;
touch();
return;
}
_count = _count * 10 + (c - '0');
return;
}
if (_pending == "d") { _pending.clear();
int eff = get_Eff(); _count = 0;
if (c == 'd') { push_Undo(); yank_Lines(eff); delete_Lines(eff); }
else if (c == 'w') { push_Undo(); op_ToNextWord(eff, /*del=*/true, /*yank=*/true); }
touch(); return;
}
if (_pending == "y") { _pending.clear();
int eff = get_Eff(); _count = 0;
if (c == 'y') { yank_Lines(eff); }
else if (c == 'w') { op_ToNextWord(eff, /*del=*/false, /*yank=*/true); }
touch(); return;
}
if (_pending == "c") { _pending.clear();
int eff = get_Eff(); _count = 0;
if (c == 'c') { push_Undo(); yank_Lines(1); change_Line(); }
else if (c == 'w') { push_Undo(); op_ChangeWord(eff); }
touch(); return;
}
if (_pending == ">") { _pending.clear();
int eff = get_Eff(); _count = 0;
if (c == '>') { push_Undo(); indent_Lines(eff, +1); }
touch(); return;
}
if (_pending == "<") { _pending.clear();
int eff = get_Eff(); _count = 0;
if (c == '<') { push_Undo(); indent_Lines(eff, -1); }
touch(); return;
}
if (_pending == "g") { _pending.clear();
if (c == 'g') { _row = get_ClampRow(_count > 0 ? _count - 1 : 0);
_count = 0; clamp_Col(); touch(); return; }
_count = 0; return;
}
switch (c) {
case 'h': _col -= get_Eff(); break;
case 'l': _col += get_Eff(); break;
case 'j': _row = get_ClampRow(_row + get_Eff()); break;
case 'k': _row = get_ClampRow(_row - get_Eff()); break;
case '0': _col = 0; break;
case '$': _col = get_LineLen() > 0 ? get_LineLen() - 1 : 0; break;
case 'w': _col = next_WordCol(get_Eff()); break;
case 'e': _col = end_WordCol (get_Eff()); break;
case 'b': _col = prev_WordCol(get_Eff()); break;
case 'x': push_Undo(); delete_Chars(get_Eff()); break;
case 'i': push_Undo(); _mode = Mode::Insert; break;
case 'a': push_Undo(); _col = (get_LineLen() > 0 ? _col + 1 : 0); _mode = Mode::Insert; break;
case 'A': push_Undo(); _col = get_LineLen(); _mode = Mode::Insert; break;
case 'o': push_Undo(); open_Below(); _mode = Mode::Insert; break;
case 'O': push_Undo(); open_Above(); _mode = Mode::Insert; break;
case 'G': _row = get_ClampRow(_count > 0 ? _count - 1 : static_cast<int>(_lines.size()) - 1); break;
case 'd': _pending = "d"; return;
case 'y': _pending = "y"; return;
case 'c': _pending = "c"; return;
case 'g': _pending = "g"; return;
case '>': _pending = ">"; return;
case '<': _pending = "<"; return;
case 'p': push_Undo(); paste_After(); break;
case 'P': push_Undo(); paste_Before(); break;
case 'u': undo(); break;
default: break;
}
_count = 0; clamp_Col(); touch();
}
void
VimBuffer::insert_Char(char c)
{
std::string& line = _lines[static_cast<size_t>(_row)];
if (_col > static_cast<int>(line.size()))
_col = static_cast<int>(line.size());
line.insert(static_cast<size_t>(_col), 1, c);
++_col;
_dirty = true;
touch();
}
void
VimBuffer::split_Line()
{
std::string& line = _lines[static_cast<size_t>(_row)];
std::string tail = line.substr(static_cast<size_t>(_col));
line.erase(static_cast<size_t>(_col));
_lines.insert(_lines.begin() + _row + 1, tail);
++_row; _col = 0; _dirty = true; touch();
}
void
VimBuffer::backspace_Insert()
{
if (_col > 0) {
_lines[static_cast<size_t>(_row)].erase(static_cast<size_t>(_col-1), 1);
--_col;
_dirty = true;
}
else if (_row > 0) {
int prevlen = static_cast<int>(_lines[static_cast<size_t>(_row-1)].size());
_lines[static_cast<size_t>(_row-1)] += _lines[static_cast<size_t>(_row)];
_lines.erase(_lines.begin() + _row);
--_row;
_col = prevlen;
_dirty = true;
}
touch();
}
void
VimBuffer::delete_Chars(int n)
{
std::string& line = _lines[static_cast<size_t>(_row)];
for (int i = 0; i < n && _col < static_cast<int>(line.size()); i++) {
line.erase(static_cast<size_t>(_col), 1);
_dirty = true;
}
clamp_Col();
}
void
VimBuffer::delete_Lines(int n)
{
for (int i = 0; i < n && static_cast<int>(_lines.size()) > 0; i++) {
if (_lines.size() == 1) {
_lines[0].clear();
_dirty = true;
break;
}
_lines.erase(_lines.begin() + _row);
if (_row >= static_cast<int>(_lines.size()))
_row = static_cast<int>(_lines.size()) - 1;
_dirty = true;
}
clamp_Col();
}
void
VimBuffer::open_Below()
{
_lines.insert(_lines.begin() + _row + 1, "");
++_row;
_col = 0;
_dirty = true;
}
void
VimBuffer::open_Above()
{
_lines.insert(_lines.begin() + _row, "");
_col = 0;
_dirty = true;
}
void
VimBuffer::run_Command()
{
std::string c = _cmd.size() ? _cmd.substr(1) : "";
_cmd.clear(); _mode = Mode::Normal;
if (c == "w" || c == "write") {
_sig = Signal::Write;
}
else if (c == "wq" || c == "x") {
_sig = Signal::WriteQuit;
}
else if (c == "q" || c == "quit") {
if (_dirty) {
_status = "No write since last change (add ! to override)";
} else {
_sig = Signal::Exit;
}
}
else if (c == "q!" || c == "quit!") {
_lines = _saved;
_row = get_ClampRow(_row); clamp_Col();
_dirty = false;
_sig = Signal::QuitDiscard;
}
else if (!c.empty() && std::all_of(c.begin(), c.end(),
[](char ch){ return std::isdigit((unsigned char)ch); })) {
_row = get_ClampRow(std::stoi(c) - 1); clamp_Col();
}
else _status = "not a command: " + c;
touch();
}
void
VimBuffer::cancel_Command()
{ _cmd.clear(); _mode = Mode::Normal; touch(); }
bool
VimBuffer::is_Dirty() const
{ return _dirty; }
// ── word motion treats each TOKEN as a vim word ──────────────────────────────
// The classes mirror the game tokenizer (handler.cpp relayout/chunker): a word
// run [A-Za-z0-9_], an operator run from the op set, single-char punctuation,
// or a whitespace run. So dw/cw/yw/w/e/b stop on token boundaries exactly where
// the rendered tokens split — e.g. dw on "fuu(buu)" with the cursor on 'f'
// deletes just "fuu".
namespace {
enum CharClass : uint8_t { CC_SPACE, CC_WORD, CC_OP, CC_PUNCT };
inline CharClass
char_Class(char ch)
{
if (ch == ' ' || ch == '\t') return CC_SPACE;
if ((ch >= 'A' && ch <= 'Z') ||
(ch >= 'a' && ch <= 'z') ||
(ch >= '0' && ch <= '9') || ch == '_') return CC_WORD;
for (const char* o = "+-*/%=<>!&|^~.:?"; *o; ++o) // op set
if (ch == *o) return CC_OP;
return CC_PUNCT;
}
// Exclusive end of the token that starts at p: word/op/space form runs, a punct
// char is a token of its own (matching how the renderer splits tokens).
inline int
token_End(const std::string& s, int p)
{
const int n = static_cast<int>(s.size());
if (p >= n) return p;
const CharClass c = char_Class(s[static_cast<size_t>(p)]);
if (c == CC_PUNCT) return p + 1;
while (p < n && char_Class(s[static_cast<size_t>(p)]) == c) ++p;
return p;
}
} // namespace
int
VimBuffer::word_EndCol(int times) const // cw: exclusive end of token(s)
{
const std::string&
line = _lines[static_cast<size_t>(_row)];
const int n = static_cast<int>(line.size());
int c = _col;
for (int k = 0; k < times; ++k) {
while (c < n && char_Class(line[static_cast<size_t>(c)]) == CC_SPACE) ++c;
if (c < n) c = token_End(line, c);
}
return c;
}
int
VimBuffer::next_WordCol(int times) const // w: start of the next token
{
const std::string&
line = _lines[static_cast<size_t>(_row)];
const int n = static_cast<int>(line.size());
int c = _col;
for (int k = 0; k < times; ++k) {
if (c >= n) break;
if (char_Class(line[static_cast<size_t>(c)]) != CC_SPACE)
c = token_End(line, c); // past this token
while (c < n && char_Class(line[static_cast<size_t>(c)]) == CC_SPACE) ++c;
}
return c;
}
// ── register fill: linewise ──────────────────────────────────────────────────
void
VimBuffer::yank_Lines(int n)
{
const int end = (_row + n < static_cast<int>(_lines.size()))
? _row + n
: static_cast<int>(_lines.size());
std::string reg;
for (int i = _row; i < end; i++) {
reg += _lines[static_cast<size_t>(i)];
if (i + 1 < end) reg += '\n';
}
_register = reg;
_reg_linewise = true;
}
// ── dw / yw: operate on [_col, next_WordCol) charwise ────────────────────────
void
VimBuffer::op_ToNextWord(int times, bool del, bool yank)
{
std::string& line = _lines[static_cast<size_t>(_row)];
const int from = _col;
int to = next_WordCol(times);
if (to <= from) return;
if (yank) {
_register = line.substr(static_cast<size_t>(from), static_cast<size_t>(to - from));
_reg_linewise = false;
}
if (del) {
line.erase(static_cast<size_t>(from), static_cast<size_t>(to - from));
_dirty = true;
clamp_Col();
}
}
// ── cw: change to end of word (charwise), then enter insert ──────────────────
void
VimBuffer::op_ChangeWord(int times)
{
std::string& line = _lines[static_cast<size_t>(_row)];
const int from = _col;
const int to = word_EndCol(times);
if (to > from) {
_register = line.substr(static_cast<size_t>(from), static_cast<size_t>(to - from));
_reg_linewise = false;
line.erase(static_cast<size_t>(from), static_cast<size_t>(to - from));
_dirty = true;
}
_mode = Mode::Insert;
clamp_Col();
}
// ── cc: clear the current line, keep it, enter insert (register pre-filled) ──
void
VimBuffer::change_Line()
{
_lines[static_cast<size_t>(_row)].clear();
_col = 0;
_mode = Mode::Insert;
_dirty = true;
}
// ── paste ─────────────────────────────────────────────────────────────────
namespace {
std::vector<std::string> split_Lines(const std::string& s)
{
std::vector<std::string> out;
std::string cur;
for (char c : s) {
if (c == '\n') { out.push_back(cur); cur.clear(); }
else cur.push_back(c);
}
out.push_back(cur);
return out;
}
} // namespace
void
VimBuffer::paste_After()
{
if (_register.empty()) return;
if (_reg_linewise) {
std::vector<std::string> rl = split_Lines(_register);
const int at = _row + 1;
_lines.insert(_lines.begin() + at, rl.begin(), rl.end());
_row = at; // land on first pasted line
_col = 0;
} else {
std::string& line = _lines[static_cast<size_t>(_row)];
int at = _col + (line.empty() ? 0 : 1); // after the cursor char
if (at > static_cast<int>(line.size()))
at = static_cast<int>(line.size());
line.insert(static_cast<size_t>(at), _register);
_col = at + static_cast<int>(_register.size()) - 1; // cursor on last pasted char
}
_dirty = true;
clamp_Col();
touch();
}
void
VimBuffer::paste_Before()
{
if (_register.empty()) return;
if (_reg_linewise) {
std::vector<std::string> rl = split_Lines(_register);
_lines.insert(_lines.begin() + _row, rl.begin(), rl.end());
_col = 0; // _row now points at the first pasted line
} else {
std::string& line = _lines[static_cast<size_t>(_row)];
const int at = _col;
line.insert(static_cast<size_t>(at), _register);
_col = at + static_cast<int>(_register.size()) - 1;
}
_dirty = true;
clamp_Col();
touch();
}
// ── indentation: shift line(s) by one shiftwidth ─────────────────────────────
void
VimBuffer::indent_Lines(int n, int dir)
{
constexpr int SW = 4;
const int end = (_row + n < static_cast<int>(_lines.size()))
? _row + n
: static_cast<int>(_lines.size());
for (int i = _row; i < end; i++) {
std::string& line = _lines[static_cast<size_t>(i)];
if (dir > 0) {
line.insert(0, static_cast<size_t>(SW), ' ');
if (i == _row) _col += SW;
} else {
int rm = 0;
while (rm < SW && rm < static_cast<int>(line.size()) && line[static_cast<size_t>(rm)] == ' ')
++rm;
line.erase(0, static_cast<size_t>(rm));
if (i == _row)
_col = (_col > rm)
? _col - rm
: 0;
}
}
_dirty = true;
clamp_Col();
}
// ── e: last char of the n-th token forward (always advances at least one) ────
int
VimBuffer::end_WordCol(int times) const
{
const std::string& line = _lines[static_cast<size_t>(_row)];
const int n = static_cast<int>(line.size());
if (n == 0) return 0;
int c = _col;
for (int k = 0; k < times; ++k) {
++c; // step off current char
while (c < n && char_Class(line[static_cast<size_t>(c)]) == CC_SPACE) ++c;
if (c < n) {
const int e = token_End(line, c); // exclusive end
c = (e > c) ? e - 1 : c; // → last char of token
}
}
return (c < n) ? c : n - 1;
}
// ── b: start of the n-th token backward (always retreats at least one) ───────
int
VimBuffer::prev_WordCol(int times) const
{
const std::string& line = _lines[static_cast<size_t>(_row)];
int c = _col;
for (int k = 0; k < times; ++k) {
--c; // step off current char
while (c >= 0 && char_Class(line[static_cast<size_t>(c)]) == CC_SPACE) --c;
if (c < 0) break;
const CharClass cl = char_Class(line[static_cast<size_t>(c)]);
if (cl != CC_PUNCT) // word/op run → its start
while (c - 1 >= 0 && char_Class(line[static_cast<size_t>(c - 1)]) == cl) --c;
// CC_PUNCT is a single-char token: c already sits at its start
}
return (c > 0) ? c : 0;
}
// ── undo/redo: full-state snapshot stacks ────────────────────────────────────
void
VimBuffer::push_Undo()
{
_undo.push_back({ _lines, _row, _col });
_redo.clear(); // a new edit invalidates the redo branch
constexpr size_t LIMIT = 200; // bound memory on a long session
if (_undo.size() > LIMIT)
_undo.erase(_undo.begin());
}
void
VimBuffer::undo()
{
if (_undo.empty()) { _status = "Already at oldest change"; return; }
_redo.push_back({ _lines, _row, _col });
Snapshot s = _undo.back(); _undo.pop_back();
_lines = std::move(s.lines);
_row = get_ClampRow(s.row);
_col = s.col;
_mode = Mode::Normal;
_dirty = true;
clamp_Col();
touch();
}
void
VimBuffer::redo()
{
if (_redo.empty()) { _status = "Already at newest change"; return; }
_undo.push_back({ _lines, _row, _col });
Snapshot s = _redo.back(); _redo.pop_back();
_lines = std::move(s.lines);
_row = get_ClampRow(s.row);
_col = s.col;
_mode = Mode::Normal;
_dirty = true;
clamp_Col();
touch();
}
} // namespace Nimbin