Эх сурвалжийг харах

Implement M-d (kill-word) and M-Backspace (backward-kill-word)

- Add kill_word and backward_kill_word to EditorCore
- Update move_forward_word logic to match Emacs behavior (move to end of word)
- Expose new methods to Lua
- Bind M-d and M-Backspace in init.lua
- Add unit tests for word kill operations
Bernardo Magri 1 сар өмнө
parent
commit
d71a98ae83

+ 10 - 0
include/lumacs/editor_core.hpp

@@ -185,10 +185,20 @@ public:
     /// Yank and pop to previous kill ring entry
     void yank_pop();
 
+    /// Kill word forward
+    void kill_word();
+
+    /// Kill word backward
+    void backward_kill_word();
+
 private:
     // All open buffers
     std::list<std::shared_ptr<Buffer>> buffers_;
 
+    // Word movement helpers
+    Position calculate_forward_word_pos(Position start_pos);
+    Position calculate_backward_word_pos(Position start_pos);
+
     // Window layout
     std::shared_ptr<LayoutNode> root_node_;
     std::shared_ptr<Window> active_window_;

+ 10 - 0
init.lua

@@ -617,6 +617,16 @@ bind_key("M-y", function()
     editor:yank_pop()
 end)
 
+-- M-d (kill-word) - Kill word forward
+bind_key("M-d", function()
+    editor:kill_word()
+end)
+
+-- M-Backspace (backward-kill-word) - Kill word backward
+bind_key("M-Backspace", function()
+    editor:backward_kill_word()
+end)
+
 -- ============================================================================
 -- UNDO/REDO
 -- ============================================================================

+ 74 - 27
src/editor_core.cpp

@@ -385,49 +385,61 @@ static bool is_word_char(char c) {
 }
 
 void EditorCore::move_forward_word() {
-    auto& buf = active_window_->buffer();
-    auto cursor = active_window_->cursor();
-    const auto& line = buf.line(cursor.line);
+    auto new_pos = calculate_forward_word_pos(active_window_->cursor());
+    active_window_->set_cursor(new_pos);
+    emit_event(EditorEvent::CursorMoved);
+}
 
-    // Skip current word if we're in one
-    while (cursor.column < line.size() && is_word_char(line[cursor.column])) {
-        cursor.column++;
-    }
+void EditorCore::move_backward_word() {
+    auto new_pos = calculate_backward_word_pos(active_window_->cursor());
+    active_window_->set_cursor(new_pos);
+    emit_event(EditorEvent::CursorMoved);
+}
 
-    // Skip whitespace and punctuation
+Position EditorCore::calculate_forward_word_pos(Position start_pos) {
+    auto& buf = active_window_->buffer();
+    auto cursor = start_pos;
+    
+    // Check if we are at the end of buffer
+    if (cursor.line >= buf.line_count()) {
+        return cursor;
+    }
+    
+    // 1. Skip non-word chars (whitespace/punctuation)
     while (true) {
-        // Skip non-word chars on current line
+        const auto& line = buf.line(cursor.line);
+        
         while (cursor.column < line.size() && !is_word_char(line[cursor.column])) {
             cursor.column++;
         }
-
-        // If we found a word char, we're done
+        
+        // If we found a word char, we're done with step 1
         if (cursor.column < line.size()) {
             break;
         }
-
+        
         // Move to next line
         if (cursor.line < buf.line_count() - 1) {
             cursor.line++;
             cursor.column = 0;
-            const auto& new_line = buf.line(cursor.line);
-            // Update line reference
-            if (!new_line.empty()) {
-                continue; // Check this new line
-            }
         } else {
             // At end of buffer
-            break;
+            return cursor;
         }
     }
 
-    active_window_->set_cursor(cursor);
-    emit_event(EditorEvent::CursorMoved);
+    // 2. Skip word chars
+    const auto& line = buf.line(cursor.line);
+    while (cursor.column < line.size() && is_word_char(line[cursor.column])) {
+        cursor.column++;
+    }
+    
+    return cursor;
 }
 
-void EditorCore::move_backward_word() {
+Position EditorCore::calculate_backward_word_pos(Position start_pos) {
     auto& buf = active_window_->buffer();
-    auto cursor = active_window_->cursor();
+    auto cursor = start_pos;
 
     // Skip whitespace and punctuation backwards
     while (true) {
@@ -454,12 +466,8 @@ void EditorCore::move_backward_word() {
             }
             break;
         }
-
-        // Otherwise continue skipping non-word chars
     }
-
-    active_window_->set_cursor(cursor);
-    emit_event(EditorEvent::CursorMoved);
+    return cursor;
 }
 
 void EditorCore::page_up() {
@@ -660,6 +668,45 @@ void EditorCore::kill_region() {
     }
 }
 
+void EditorCore::kill_word() {
+    auto cursor = active_window_->cursor();
+    auto end_pos = calculate_forward_word_pos(cursor);
+    
+    if (cursor == end_pos) return;
+    
+    Range range = {cursor, end_pos};
+    auto& buf = active_window_->buffer();
+    std::string text = buf.get_text_in_range(range);
+    
+    if (!text.empty()) {
+        kill_ring_.push(text);
+        buf.erase(range);
+        emit_event(EditorEvent::BufferModified);
+        std::cerr << "[DEBUG] Killed word: '" << text << "'" << std::endl;
+    }
+}
+
+void EditorCore::backward_kill_word() {
+    auto cursor = active_window_->cursor();
+    auto start_pos = calculate_backward_word_pos(cursor);
+    
+    if (cursor == start_pos) return;
+    
+    Range range = {start_pos, cursor};
+    auto& buf = active_window_->buffer();
+    std::string text = buf.get_text_in_range(range);
+    
+    if (!text.empty()) {
+        kill_ring_.push(text);
+        buf.erase(range);
+        active_window_->set_cursor(start_pos);
+        emit_event(EditorEvent::BufferModified);
+        emit_event(EditorEvent::CursorMoved);
+        std::cerr << "[DEBUG] Backward killed word: '" << text << "'" << std::endl;
+    }
+}
+
+
 void EditorCore::copy_region_as_kill() {
     auto& buf = active_window_->buffer();
     auto cursor = active_window_->cursor();

+ 2 - 0
src/lua_api.cpp

@@ -283,6 +283,8 @@ void LuaApi::register_types() {
         // Kill ring
         "kill_line", &EditorCore::kill_line,
         "kill_region", &EditorCore::kill_region,
+        "kill_word", &EditorCore::kill_word,
+        "backward_kill_word", &EditorCore::backward_kill_word,
         "copy_region_as_kill", &EditorCore::copy_region_as_kill,
         "yank", &EditorCore::yank,
         "yank_pop", &EditorCore::yank_pop,

+ 2 - 1
tests/CMakeLists.txt

@@ -9,7 +9,8 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
 include_directories(${CMAKE_SOURCE_DIR}/include)
 
 # Build tests
-add_executable(run_tests test_buffer.cpp ../src/buffer.cpp)
+add_executable(run_tests test_buffer.cpp test_editor_core.cpp)
+target_link_libraries(run_tests PRIVATE lumacs_core)
 
 # Add test target
 enable_testing()

+ 48 - 0
tests/test_editor_core.cpp

@@ -0,0 +1,48 @@
+#include "test_framework.hpp"
+#include "lumacs/editor_core.hpp"
+
+using namespace lumacs;
+
+TEST(EditorCore_KillWord) {
+    EditorCore core;
+    core.buffer().insert({0, 0}, "Hello World");
+    core.set_cursor({0, 0});
+    
+    core.kill_word();
+    
+    ASSERT_EQ(std::string(" World"), core.buffer().content());
+    ASSERT_EQ(std::string("Hello"), core.kill_ring().current());
+}
+
+TEST(EditorCore_BackwardKillWord) {
+    EditorCore core;
+    core.buffer().insert({0, 0}, "Hello World");
+    core.set_cursor({0, 5}); // After "Hello"
+    
+    core.backward_kill_word();
+    
+    ASSERT_EQ(std::string(" World"), core.buffer().content());
+    ASSERT_EQ(std::string("Hello"), core.kill_ring().current());
+}
+
+TEST(EditorCore_KillWord_Middle) {
+    EditorCore core;
+    core.buffer().insert({0, 0}, "Hello World");
+    core.set_cursor({0, 2}); // "He|llo"
+    
+    core.kill_word(); // Should kill "llo"
+    
+    ASSERT_EQ(std::string("He World"), core.buffer().content());
+    ASSERT_EQ(std::string("llo"), core.kill_ring().current());
+}
+
+TEST(EditorCore_BackwardKillWord_Middle) {
+    EditorCore core;
+    core.buffer().insert({0, 0}, "Hello World");
+    core.set_cursor({0, 3}); // "Hel|lo"
+    
+    core.backward_kill_word(); // Should kill "Hel"
+    
+    ASSERT_EQ(std::string("lo World"), core.buffer().content());
+    ASSERT_EQ(std::string("Hel"), core.kill_ring().current());
+}