#include "vim_buffer.hpp" namespace Nimbin { void VimBuffer::load(const std::vector& 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; } // 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(_row)]; if (_col > static_cast(line.size())) _col = static_cast(line.size()); line.insert(static_cast(_col), SW, ' '); _col += static_cast(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(_lines.size())) { _lines.resize(static_cast(row) + 1); } else { _lines.insert(_lines.begin() + row, std::string()); } } else { if (row < 0) row = 0; if (row >= static_cast(_lines.size())) { _lines.resize(static_cast(row) + 1); col = 0; } } std::string& line = _lines[static_cast(row)]; if (col < 0) col = 0; if (col > static_cast(line.size())) line.append(static_cast(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(col - 1)] != ' ') ins += ' '; ins += word; if ( col < static_cast(line.size()) && line[static_cast(col)] != ' ') ins += ' '; line.insert(static_cast(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(ins.size()); _dirty = true; clamp_Col(); touch(); } void VimBuffer::touch() { ++_version; } int VimBuffer::get_LineLen() const { return static_cast(_lines[static_cast(_row)].size()); } int VimBuffer::get_ClampRow(int r) const { int n=static_cast(_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(_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(_row)]; if (_col > static_cast(line.size())) _col = static_cast(line.size()); line.insert(static_cast(_col), 1, c); ++_col; _dirty = true; touch(); } void VimBuffer::split_Line() { std::string& line = _lines[static_cast(_row)]; std::string tail = line.substr(static_cast(_col)); line.erase(static_cast(_col)); _lines.insert(_lines.begin() + _row + 1, tail); ++_row; _col = 0; _dirty = true; touch(); } void VimBuffer::backspace_Insert() { if (_col > 0) { _lines[static_cast(_row)].erase(static_cast(_col-1), 1); --_col; _dirty = true; } else if (_row > 0) { int prevlen = static_cast(_lines[static_cast(_row-1)].size()); _lines[static_cast(_row-1)] += _lines[static_cast(_row)]; _lines.erase(_lines.begin() + _row); --_row; _col = prevlen; _dirty = true; } touch(); } void VimBuffer::delete_Chars(int n) { std::string& line = _lines[static_cast(_row)]; for (int i = 0; i < n && _col < static_cast(line.size()); i++) { line.erase(static_cast(_col), 1); _dirty = true; } clamp_Col(); } void VimBuffer::delete_Lines(int n) { for (int i = 0; i < n && static_cast(_lines.size()) > 0; i++) { if (_lines.size() == 1) { _lines[0].clear(); _dirty = true; break; } _lines.erase(_lines.begin() + _row); if (_row >= static_cast(_lines.size())) _row = static_cast(_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(s.size()); if (p >= n) return p; const CharClass c = char_Class(s[static_cast(p)]); if (c == CC_PUNCT) return p + 1; while (p < n && char_Class(s[static_cast(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(_row)]; const int n = static_cast(line.size()); int c = _col; for (int k = 0; k < times; ++k) { while (c < n && char_Class(line[static_cast(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(_row)]; const int n = static_cast(line.size()); int c = _col; for (int k = 0; k < times; ++k) { if (c >= n) break; if (char_Class(line[static_cast(c)]) != CC_SPACE) c = token_End(line, c); // past this token while (c < n && char_Class(line[static_cast(c)]) == CC_SPACE) ++c; } return c; } // ── register fill: linewise ────────────────────────────────────────────────── void VimBuffer::yank_Lines(int n) { const int end = (_row + n < static_cast(_lines.size())) ? _row + n : static_cast(_lines.size()); std::string reg; for (int i = _row; i < end; i++) { reg += _lines[static_cast(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(_row)]; const int from = _col; int to = next_WordCol(times); if (to <= from) return; if (yank) { _register = line.substr(static_cast(from), static_cast(to - from)); _reg_linewise = false; } if (del) { line.erase(static_cast(from), static_cast(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(_row)]; const int from = _col; const int to = word_EndCol(times); if (to > from) { _register = line.substr(static_cast(from), static_cast(to - from)); _reg_linewise = false; line.erase(static_cast(from), static_cast(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(_row)].clear(); _col = 0; _mode = Mode::Insert; _dirty = true; } // ── paste ───────────────────────────────────────────────────────────────── namespace { std::vector split_Lines(const std::string& s) { std::vector 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 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(_row)]; int at = _col + (line.empty() ? 0 : 1); // after the cursor char if (at > static_cast(line.size())) at = static_cast(line.size()); line.insert(static_cast(at), _register); _col = at + static_cast(_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 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(_row)]; const int at = _col; line.insert(static_cast(at), _register); _col = at + static_cast(_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(_lines.size())) ? _row + n : static_cast(_lines.size()); for (int i = _row; i < end; i++) { std::string& line = _lines[static_cast(i)]; if (dir > 0) { line.insert(0, static_cast(SW), ' '); if (i == _row) _col += SW; } else { int rm = 0; while (rm < SW && rm < static_cast(line.size()) && line[static_cast(rm)] == ' ') ++rm; line.erase(0, static_cast(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(_row)]; const int n = static_cast(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(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(_row)]; int c = _col; for (int k = 0; k < times; ++k) { --c; // step off current char while (c >= 0 && char_Class(line[static_cast(c)]) == CC_SPACE) --c; if (c < 0) break; const CharClass cl = char_Class(line[static_cast(c)]); if (cl != CC_PUNCT) // word/op run → its start while (c - 1 >= 0 && char_Class(line[static_cast(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