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.gzAdd vim y p w .. With Claude
src/states/game_state.cpp | 180 +++++++++++----------- src/states/game_state.hpp | 11 +- src/systems/compiler_system.cpp | 4 +- src/systems/input_system.cpp | 20 ++- src/systems/input_system.hpp | 5 +- src/text/handler.cpp | 7 +- src/text/vim_buffer.cpp | 331 ++++++++++++++++++++++++++++++++++++++-- src/text/vim_buffer.hpp | 40 ++++- 8 files changed, 466 insertions(+), 132 deletions(-) diff --git a/src/states/game_state.cpp b/src/states/game_state.cpp index 1a3c77a..c4d579c 100644 --- a/src/states/game_state.cpp +++ b/src/states/game_state.cpp @@ -23,8 +23,8 @@ #include "wall/label.hpp" #include "wall/wall.hpp" -GameState::GameState(size_t starting_level) +GameState::GameState(size_t starting_level) { _ctx.player = cpputils::make_unique<Player>(); _ctx.world = cpputils::make_unique<World> (); @@ -97,8 +97,8 @@ GameState::GameState(size_t starting_level) if (starting_level >= Level::get_Count()) starting_level = 0; _level_director = cpputils::make_unique<Level::Director>(Level::get_Data(starting_level)); - _level_director->enter_Step(_ctx); + _word_ai_sys->set_MaxActiveAi(_level_director->get_MaxAiForStep()); _hint_sys = cpputils::make_unique<HintSystem>(); @@ -190,6 +190,7 @@ GameState::pult_DispatchAction(PultAction act) } } + void GameState::on_Click() { @@ -244,8 +245,6 @@ GameState::on_DropLast() if (!word) return; revert_ToSingle(word.get()); - //infowall_Clear(); - word->body.mode = PhysicsMode::Dynamic; word->body.vel = { _ctx.camera->fwd.x * 1.5, 0.8, _ctx.camera->fwd.z * 1.5 }; @@ -271,8 +270,6 @@ GameState::on_ShootOne() if (!word) return false; revert_ToSingle(word.get()); - //infowall_Clear(); - _freelook_sys->state.active = false; _freelook_sys->state.lerping = false; @@ -299,8 +296,6 @@ GameState::on_ShootAll() { if (_carried.empty()) return; - //infowall_Clear(); - _freelook_sys->state.active = false; _freelook_sys->state.lerping = false; @@ -571,27 +566,47 @@ GameState::handle_Event(Game& game, const SDL_Event& event) _input_sys->handle_Event(game, event, _frame_input); } + void -GameState::handle_Input(FrameInput& input, double dt) { +GameState::update(double dt, const GlRenderer* gl) +{ + FrameInput& input = _frame_input; + + { FP_ZONE("update.input" ); update_Input (input, dt);} + { FP_ZONE("update.world_sim" ); update_Physics (dt); } + { FP_ZONE("update.present" ); update_Objects (dt, gl); } + { FP_ZONE("update.subsystems" ); update_Compiler (); } + { FP_ZONE("update.progression"); update_Progression (dt); } + { FP_ZONE("update.highlight" ); update_Highlighting(); } + + input.look_x = 0; + input.look_y = 0; + input.commands.clear(); +} + + +void +GameState::update_Input(FrameInput& input, double dt) +{ for (InputCommand cmd : input.commands) { switch (cmd) { - case InputCommand::Jump: { FP_ZONE("on_Jump"); on_Jump();} break; - case InputCommand::Click: { FP_ZONE("on_Click"); on_Click();} break; - case InputCommand::Drop: { FP_ZONE("on_DropLast"); on_DropLast();} break; - case InputCommand::ShootAll: { FP_ZONE("on_ShootAll"); on_ShootAll();} break; - case InputCommand::Compile: { FP_ZONE("on_Compile"); on_Compile();} break; - case InputCommand::Run: { FP_ZONE("on_Run"); on_Run();} break; - case InputCommand::DebugPrint: { FP_ZONE("on_DebugPrint"); on_DebugPrint();} break; - case InputCommand::FreelookStart: { FP_ZONE("on_FreelookStart"); on_FreelookStart();} break; - case InputCommand::FreelookEnd: { FP_ZONE("on_FreelookEnd"); on_FreelookEnd();} break; - case InputCommand::NumberInc: { FP_ZONE("on_NumberAdjust"); on_NumberAdjust(+1);} break; - case InputCommand::NumberDec: { FP_ZONE("on_NumberAdjust"); on_NumberAdjust(-1);} break; - case InputCommand::TypingStart: { FP_ZONE("on_TypingStart "); on_TypingStart (input);} break; - case InputCommand::TypingCommit: { FP_ZONE("on_TypingCommit"); on_TypingCommit(input);} break; - case InputCommand::TypingCancel: { FP_ZONE("on_TypingCancel"); on_TypingCancel(input);} break; - case InputCommand::SandboxRestore: { FP_ZONE("on_TypingCancel"); on_SandboxRestore();} break; - case InputCommand::ConnectPultSubmit: { FP_ZONE("on_WallConnectSubmit");on_WallConnectSubmit();} break; - case InputCommand::ConnectPultExit: { FP_ZONE("on_WallConnectExit"); on_WallConnectExit();} break; + case InputCommand::Jump: on_Jump(); break; + case InputCommand::Click: on_Click(); break; + case InputCommand::Drop: on_DropLast(); break; + case InputCommand::ShootAll: on_ShootAll(); break; + case InputCommand::Compile: on_Compile(); break; + case InputCommand::Run: on_Run(); break; + case InputCommand::DebugPrint: on_DebugPrint(); break; + case InputCommand::FreelookStart: on_FreelookStart(); break; + case InputCommand::FreelookEnd: on_FreelookEnd(); break; + case InputCommand::NumberInc: on_NumberAdjust(+1); break; + case InputCommand::NumberDec: on_NumberAdjust(-1); break; + case InputCommand::TypingStart: on_TypingStart (input); break; + case InputCommand::TypingCommit: on_TypingCommit(input); break; + case InputCommand::TypingCancel: on_TypingCancel(input); break; + case InputCommand::SandboxRestore: on_SandboxRestore(); break; + case InputCommand::ConnectPultSubmit: on_WallConnectSubmit(); break; + case InputCommand::ConnectPultExit: on_WallConnectExit(); break; } } @@ -639,12 +654,7 @@ GameState::handle_Input(FrameInput& input, double dt) { if (_shoot_cooldown > 0.0) _shoot_cooldown -= dt; if (input.shoot && _shoot_cooldown <= 0.0) { FP_ZONE("handle_input.onShootO"); if (on_ShootOne()) _shoot_cooldown = 0.15; } -} - -void -GameState::update_PlayerCamera(FrameInput& input, double dt) -{ if (!_freelook_sys->state.active) _ctx.player->add_Look(input.look_x, input.look_y); @@ -653,61 +663,49 @@ GameState::update_PlayerCamera(FrameInput& input, double dt) _ctx.camera->sync_FromPlayer(*_ctx.player); } + void -GameState::update(double dt, const GlRenderer* gl) +GameState::update_Objects(double dt, const GlRenderer* gl) { - FrameInput& input = _frame_input; + for (Wall::Label& lbl : _wall_labels) + lbl.update(dt, *_ctx.camera); - { FP_ZONE("update.input_dispatch"); - handle_Input(input, dt); - } + carriedline_Update(); - { FP_ZONE("update.player_cam"); - update_PlayerCamera(input, dt); - } + _word_anim_sys->update(_anims_add, _anims_drop, _carried, _ctx.words, dt); - { FP_ZONE("update.world_simulation"); - update_WorldSimulation(dt); + if (_carried.size != _carried_last_size) { + if (_carried.size > 0) + lex_CarriedLine(_carried); + _carried_last_size = _carried.size; } - { FP_ZONE("update.post_step"); + _pult_sys ->update(dt, *_ctx.camera); + _plate_sys->update(dt, *_ctx.player, _level_director->get_StepPlates()); - for (Wall::Label& lbl : _wall_labels) - lbl.update(dt, *_ctx.camera); + for (Text::Handler* h : _text_walls) + if (h) h->update(dt, *_ctx.camera); - carriedline_Update(); + _wall_connection.update(dt, *_ctx.camera, gl->get_Aspect(), gl->get_Fov_y()); - _word_anim_sys->update(_anims_add, _anims_drop, _carried, _ctx.words, dt); - if (_carried.size != _carried_last_size) { - if (_carried.size > 0) - lex_CarriedLine(_carried); - _carried_last_size = _carried.size; - } - - _pult_sys-> update(dt, *_ctx.camera); - _plate_sys-> update(dt, *_ctx.player, _level_director->get_StepPlates()); - - for (Text::Handler* h : _text_walls) - if (h) h->update(dt, *_ctx.camera); - - _wall_connection.update(dt, *_ctx.camera, gl->get_Aspect(), gl->get_Fov_y()); - } + DynArray<GameObject*> particle_colliders; + particle_colliders.reserve(_ctx.scenery.size()); + for (const GameObjectPtr& s : _ctx.scenery) + if (s) particle_colliders.push_back(s.get()); + _particle_sys->update(dt, particle_colliders, World::get_BoxScale()); +} - { FP_ZONE("update.particles"); - DynArray<GameObject*> particle_colliders; - particle_colliders.reserve(_ctx.scenery.size()); - for (const GameObjectPtr& s : _ctx.scenery) - if (s) particle_colliders.push_back(s.get()); - _particle_sys->update(dt, particle_colliders, World::get_BoxScale()); - } - +void +GameState::update_Compiler() +{ _compiler_sys->update(_compiler); if (_compiler_sys->last_compile_ok) { _compiler_sys->last_compile_ok = false; _level_director->record_CompileOk(); } + // TODO: the dyn path may still blocks the frame in dyn_Apply _dyn_loader_sys->update(_compiler); if (_dyn_loader_sys->last_build_ok) { _dyn_loader_sys->last_build_ok = false; @@ -715,28 +713,28 @@ GameState::update(double dt, const GlRenderer* gl) SourceTokens::copy_File(sandbox_CanonicalPath(), sandbox_BackupPath()); } - { FP_ZONE("update._program_sys"); - _program_sys->update(_program); - } + _program_sys->update(_program); +} - { FP_ZONE("update.goals"); - _celebration_sys->tick(dt, *_particle_sys); - level_TryAdvance(dt); - } - { FP_ZONE("update.highlight"); - if (_editor.is_Dirty()) - _highlight_sys->request(_editor.render_Source(), _editor.generation()); +void +GameState::update_Highlighting() +{ + if (_editor.is_Dirty()) + _highlight_sys->request(_editor.render_Source(), _editor.generation()); + + DynArray<Text::HiSpan> spans; + uint64_t gen; + if (_highlight_sys->poll(spans, gen)) + _editor.apply_Highlight(spans, gen); +} - DynArray<Text::HiSpan> spans; - uint64_t gen; - if (_highlight_sys->poll(spans, gen)) - _editor.apply_Highlight(spans, gen); - } - input.look_x = 0; - input.look_y = 0; - input.commands.clear(); +void +GameState::update_Progression(double dt) +{ + _celebration_sys->tick(dt, *_particle_sys); + level_TryAdvance(dt); } @@ -818,14 +816,7 @@ GameState::render(SDL_Window& window, GlRenderer& renderer) void -GameState::infowall_Clear() -{ - _info.clear(); -} - - -void -GameState::update_WorldSimulation(double dt) +GameState::update_Physics(double dt) { _ctx.player->prev_pos = _ctx.player->pos; @@ -1201,6 +1192,7 @@ GameState::on_EditorClear() _editor.clear_ToFloor(_ctx.words); } + void GameState::on_EditorScroll(int8_t dt) { diff --git a/src/states/game_state.hpp b/src/states/game_state.hpp index 318d00d..4f98762 100644 --- a/src/states/game_state.hpp +++ b/src/states/game_state.hpp @@ -115,9 +115,6 @@ class GameState : public IState void typing_ClearPreview (); void typing_RebuildPreview (const std::string& s); - void update_WorldSimulation(double dt); - - void infowall_Clear (); void projectile_FeedProgramInput(ProjectileResult&); void projectile_LandOnFloor (ProjectileResult&); @@ -128,8 +125,12 @@ class GameState : public IState void debug_SpawnWords (size_t n); - void handle_Input (FrameInput&, double dt); - void update_PlayerCamera(FrameInput& input, double dt); + void update_Input (FrameInput& input, double dt); + void update_Physics (double dt); + void update_Objects (double dt, const GlRenderer* gl); + void update_Compiler (); + void update_Highlighting(); + void update_Progression (double dt); // dynamic-lib sandbox START void dyn_Clear (); diff --git a/src/systems/compiler_system.cpp b/src/systems/compiler_system.cpp index 29399f7..2e5f628 100644 --- a/src/systems/compiler_system.cpp +++ b/src/systems/compiler_system.cpp @@ -38,7 +38,7 @@ CompilerSystem::compile(Text::Handler& out, const std::string& src) void CompilerSystem::run_Compile(CompilerSystem* self, std::string src) { - FILE* f = fopen("/tmp/game_prog.c", "w"); + FILE* f = fopen("/tmp/game_prog.cpp", "w"); if (!f) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "CompilerSystem: could not write temp file"); @@ -48,7 +48,7 @@ CompilerSystem::run_Compile(CompilerSystem* self, std::string src) fputs(src.c_str(), f); fclose(f); - FILE* pipe = popen("LC_ALL=C gcc /tmp/game_prog.c -o /tmp/game_prog 2>&1", "r"); + FILE* pipe = popen("LC_ALL=C g++ /tmp/game_prog.cpp -o /tmp/game_prog 2>&1", "r"); if (!pipe) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "CompilerSystem: popen failed"); self->_running.store(false); diff --git a/src/systems/input_system.cpp b/src/systems/input_system.cpp index 03a50c6..ceebf0b 100644 --- a/src/systems/input_system.cpp +++ b/src/systems/input_system.cpp @@ -30,19 +30,15 @@ InputSystem::handle_Event(Game& game, const SDL_Event& e, FrameInput& input) case SDL_EVENT_KEY_DOWN: { SDL_Keycode sym = e.key.key; - if (input.capture_vim) { + if (input.capture_vim && !input.normal_keys) { const bool ctrl = (e.key.mod & SDL_KMOD_CTRL); - if (ctrl) { - switch (sym) { - case SDLK_W: input.forward = true; break; - case SDLK_A: input.left = true; break; - case SDLK_S: input.backward = true; break; - case SDLK_D: input.right = true; break; - } - } - else if (sym == SDLK_RETURN) input.vim_keys.push_back(-1); + + if (ctrl) { input.normal_keys = true; break; } + + if (sym == SDLK_RETURN) input.vim_keys.push_back(-1); else if (sym == SDLK_ESCAPE) input.vim_keys.push_back(-2); else if (sym == SDLK_BACKSPACE) input.vim_keys.push_back(-3); + else if (sym == SDLK_TAB) input.vim_keys.push_back(-4); break; } if (input.capture_typing) { @@ -117,6 +113,8 @@ InputSystem::handle_Event(Game& game, const SDL_Event& e, FrameInput& input) case SDLK_D: input.right = false; break; case SDLK_E: input.pick = false; break; + case SDLK_LCTRL: input.normal_keys = false; break; + case SDLK_LSHIFT: case SDLK_RSHIFT: if (!input.capture_typing && !input.capture_vim) @@ -127,7 +125,7 @@ InputSystem::handle_Event(Game& game, const SDL_Event& e, FrameInput& input) } case SDL_EVENT_TEXT_INPUT: - if (input.capture_vim) { + if (input.capture_vim && !input.normal_keys) { for (const char* p = e.text.text; *p; ++p) input.vim_keys.push_back((unsigned char)*p); } diff --git a/src/systems/input_system.hpp b/src/systems/input_system.hpp index 11fcea2..4fcf1a8 100644 --- a/src/systems/input_system.hpp +++ b/src/systems/input_system.hpp @@ -35,8 +35,11 @@ struct FrameInput bool right = false; bool pick = false; bool shoot = false; + + bool normal_keys = false; + bool capture_typing = false; - bool capture_vim = false; + bool capture_vim = false; bool typing_mode = false; bool typing_commit = false; diff --git a/src/text/handler.cpp b/src/text/handler.cpp index 33fb39a..4a2d349 100644 --- a/src/text/handler.cpp +++ b/src/text/handler.cpp @@ -989,6 +989,8 @@ Handler::feed_Keys(DynArray<int>& keys, double eye_y) if (k == -1) _vim->key_enter(); else if (k == -2) _vim->key_escape(); else if (k == -3) _vim->key_backspace(); + else if (k == -4) _vim->key_tab(); + else if (k == -5) _vim->key_redo(); else if (k > 0) _vim->key_char(static_cast<char>(k)); } @@ -1001,8 +1003,9 @@ Handler::feed_Keys(DynArray<int>& keys, double eye_y) case VimBuffer::Signal::Help: _status = { "-- VIM HELP --", - "i a A o O insert Esc normal h j k l move", - "0 $ line ends gg G :N jump line x dd delete", + "i a A o O insert Esc normal h j k l move", + "0 $ line ends gg G :N jump x dd delete", + "yy yw dw cw cc yank/change p P paste >> << indent", ":w write :q quit :wq write+quit :q! discard", }; break; diff --git a/src/text/vim_buffer.cpp b/src/text/vim_buffer.cpp index dbef74e..c4509d6 100644 --- a/src/text/vim_buffer.cpp +++ b/src/text/vim_buffer.cpp @@ -15,6 +15,9 @@ VimBuffer::load(const std::vector<std::string>& src) _pending.clear(); _cmd .clear(); _status .clear(); + + _undo.clear(); + _redo.clear(); } @@ -50,6 +53,26 @@ void VimBuffer::key_escape() } +void VimBuffer::key_tab() +{ + if (_mode != Mode::Insert) return; + constexpr int SW = 4; + std::string& L = _lines[(size_t)_row]; + if (_col > (int)L.size()) _col = (int)L.size(); + L.insert((size_t)_col, (size_t)SW, ' '); + _col += 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; } @@ -69,6 +92,8 @@ VimBuffer::insert_Word_At(int row, int col, const std::string& word) if (word.empty()) return; if (_lines.empty()) _lines.push_back(""); + push_Undo(); + if (row < 0) row = 0; if (row >= (int)_lines.size()) { _lines.resize((size_t)row + 1); // pad with blank rows (at most one, by caller) @@ -134,9 +159,32 @@ VimBuffer::normal_Key(char c) } if (_pending == "d") { _pending.clear(); - if (c == 'd') { delete_Lines(get_Eff()); _count = 0; - touch(); return; } - _count = 0; return; + 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); @@ -151,15 +199,25 @@ VimBuffer::normal_Key(char c) 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 'x': delete_Chars(get_Eff()); break; - case 'i': _mode = Mode::Insert; break; - case 'a': _col = (get_LineLen() > 0 ? _col + 1 : 0); _mode = Mode::Insert; break; - case 'A': _col = get_LineLen(); _mode = Mode::Insert; break; - case 'o': open_Below(); _mode = Mode::Insert; break; - case 'O': open_Above(); _mode = Mode::Insert; 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 : (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(); @@ -263,3 +321,258 @@ VimBuffer::cancel_Command() bool VimBuffer::is_Dirty() const { return _dirty; } + + +// ── word motion (whitespace-delimited) ─────────────────────────────────────── + +// Exclusive end column of the word the cursor is in (skips any leading spaces +// first), advanced `times` words. Used by cw — stops at the word end, does NOT +// consume trailing whitespace (matches vim's cw == ce behaviour). +int +VimBuffer::word_EndCol(int times) const +{ + const std::string& L = _lines[(size_t)_row]; + const int n = (int)L.size(); + int c = _col; + for (int k = 0; k < times; ++k) { + while (c < n && L[(size_t)c] == ' ') ++c; // skip spaces + while (c < n && L[(size_t)c] != ' ') ++c; // to end of word + } + return c; +} + +// Start column of the `times`-th next word (end-of-line if none). Used by dw/yw: +// delete/yank spans [_col, next_WordCol), which includes trailing spaces up to +// the next word (matches vim's dw). +int +VimBuffer::next_WordCol(int times) const +{ + const std::string& L = _lines[(size_t)_row]; + const int n = (int)L.size(); + int c = _col; + for (int k = 0; k < times; ++k) { + while (c < n && L[(size_t)c] != ' ') ++c; // skip current word + while (c < n && L[(size_t)c] == ' ') ++c; // skip spaces to next word + } + return c; +} + + +// ── register fill: linewise ────────────────────────────────────────────────── +void +VimBuffer::yank_Lines(int n) +{ + const int end = (_row + n < (int)_lines.size()) ? _row + n : (int)_lines.size(); + std::string reg; + for (int i = _row; i < end; ++i) { + reg += _lines[(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& L = _lines[(size_t)_row]; + const int from = _col; + int to = next_WordCol(times); + if (to <= from) return; + + if (yank) { _register = L.substr((size_t)from, (size_t)(to - from)); _reg_linewise = false; } + if (del) { L.erase((size_t)from, (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& L = _lines[(size_t)_row]; + const int from = _col; + const int to = word_EndCol(times); + if (to > from) { + _register = L.substr((size_t)from, (size_t)(to - from)); + _reg_linewise = false; + L.erase((size_t)from, (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[(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& L = _lines[(size_t)_row]; + int at = _col + (L.empty() ? 0 : 1); // after the cursor char + if (at > (int)L.size()) at = (int)L.size(); + L.insert((size_t)at, _register); + _col = at + (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& L = _lines[(size_t)_row]; + const int at = _col; + L.insert((size_t)at, _register); + _col = at + (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 < (int)_lines.size()) ? _row + n : (int)_lines.size(); + + for (int i = _row; i < end; ++i) { + std::string& L = _lines[(size_t)i]; + if (dir > 0) { + L.insert(0, (size_t)SW, ' '); + if (i == _row) _col += SW; + } else { + int rm = 0; + while (rm < SW && rm < (int)L.size() && L[(size_t)rm] == ' ') ++rm; + L.erase(0, (size_t)rm); + if (i == _row) _col = (_col > rm) ? _col - rm : 0; + } + } + _dirty = true; + clamp_Col(); +} + + +// ── e: last char of the n-th word forward (always advances at least one) ───── +int +VimBuffer::end_WordCol(int times) const +{ + const std::string& L = _lines[(size_t)_row]; + const int n = (int)L.size(); + if (n == 0) return 0; + int c = _col; + for (int k = 0; k < times; ++k) { + if (c < n) ++c; // step off current char + while (c < n && L[(size_t)c] == ' ') ++c; // skip spaces + while (c + 1 < n && L[(size_t)(c + 1)] != ' ') ++c; // to last char of word + } + return (c < n) ? c : n - 1; +} + + +// ── b: start of the n-th word backward (always retreats at least one) ──────── +int +VimBuffer::prev_WordCol(int times) const +{ + const std::string& L = _lines[(size_t)_row]; + int c = _col; + for (int k = 0; k < times; ++k) { + if (c > 0) --c; // step off current char + while (c > 0 && L[(size_t)c] == ' ') --c; // skip spaces backward + while (c > 0 && L[(size_t)(c - 1)] != ' ') --c; // to first char of word + } + 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(); +} diff --git a/src/text/vim_buffer.hpp b/src/text/vim_buffer.hpp index f8919b0..2856784 100644 --- a/src/text/vim_buffer.hpp +++ b/src/text/vim_buffer.hpp @@ -21,15 +21,14 @@ public: void key_enter (); void key_backspace(); void key_escape (); + void key_tab (); + void key_redo (); void load (const std::vector<std::string>& src); Signal take_Signal(); void mark_Saved (); - // Shoot-merge: splice a whole word into line `row` at character `col`, - // adding surrounding spaces so it tokenizes back out as its own token. - // `col` is expected pre-snapped to an inter-token gap by the caller - // (Text::Handler::attach). Clamps row/col, bumps version, marks dirty. + void insert_Word_At(int row, int col, const std::string& word); bool is_Empty() const { return _lines.size() == 1 && _lines[0].empty(); } @@ -48,15 +47,22 @@ public: void start_Insert() { _mode = Mode::Insert; _col = 0; touch(); } private: - bool _dirty = false; - unsigned _version = 0; - int _row = 0, _col = 0; - int _count = 0; + bool _dirty = false; + unsigned _version= 0; + int _row = 0, _col = 0; + int _count = 0; std::string _pending; std::string _cmd; std::string _status; + std::string _register; + bool _reg_linewise = false; + + struct Snapshot { std::vector<std::string> lines; int row; int col; }; + std::vector<Snapshot> _undo; + std::vector<Snapshot> _redo; + Mode _mode = Mode::Normal; Signal _sig = Signal::None; std::vector<std::string> _saved; @@ -75,6 +81,24 @@ private: void run_Command (); void cancel_Command (); + int word_EndCol (int times) const; + int next_WordCol (int times) const; + int end_WordCol (int times) const; + int prev_WordCol (int times) const; + + void push_Undo (); + void undo (); + void redo (); + + void yank_Lines (int n); + void op_ToNextWord (int times, bool del, bool yank); + void op_ChangeWord (int times); + void change_Line (); + void paste_After (); + void paste_Before (); + + void indent_Lines (int n, int dir); + bool is_Dirty () const; int get_LineLen () const; int get_Eff () const;