Nimbin[12]?SDL & Graphics / sdl_runtime_compiler / src/text/vim_buffer.cpp

sdl_runtime_compiler git · main

SDL3 game for running and compiling code at runtime

sdl3 c++ compiler dlopen cmake · first commit 2026-04-19 · last commit 2026-07-03 (3 months ago) · synced 3 days ago · upstream: git.ide3.de/hsnr/sdl-runtime-compiler

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.gz
src/text/vim_buffer.cpp 21.1 KB · 722 lines 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