Kaynağa Gözat

feat: Implement keyboard macro recording

- Enable macro recording in KeyBindingManager::process_key for bound commands.
- Expose EditorCore via CommandSystem to allow KeyBindingManager to check recording status.
- Implement self-insert recording in GtkEditor and TuiEditor fallbacks.
- Fix logic to prevent recording the macro stop key.
Bernardo Magri 1 ay önce
ebeveyn
işleme
4cd1a87248

+ 3 - 0
include/lumacs/command_system.hpp

@@ -112,6 +112,9 @@ public:
     /// @return CommandResult indicating success/failure and a message, or a pending state.
     CommandResult execute_interactive(const std::string& name);
 
+    /// @brief Access the EditorCore instance.
+    EditorCore& core() const { return core_; }
+
 private:
     EditorCore& core_; // Reference to EditorCore
     MinibufferManager& minibuffer_manager_; // Reference to MinibufferManager

+ 7 - 0
src/gtk_editor.cpp

@@ -337,6 +337,13 @@ bool GtkEditor::on_global_key_pressed(guint keyval, guint keycode, Gdk::Modifier
             if (!has_ctrl && !has_meta && lumacs_key_name.length() == 1) {
                  // Execute self-insert-command
                  core_->command_system().execute("self-insert-command", {lumacs_key_name});
+                 
+                 // --- Macro Recording Logic for Self-Insert ---
+                 if (core_->is_recording_macro()) {
+                     core_->record_key_sequence(lumacs_key_name);
+                 }
+                 // --------------------------------------------
+
                  queue_redraw_all_windows(content_widget_);
                  return true;
             }

+ 98 - 1
src/keybinding.cpp

@@ -1,3 +1,4 @@
+#include "lumacs/editor_core.hpp"
 #include "lumacs/keybinding.hpp"
 #include "lumacs/command_system.hpp" // Include for CommandSystem
 #include <sstream>
@@ -341,6 +342,14 @@ void KeyBindingManager::unbind(const std::string& key_str) {
     unbind(KeySequence(key_str));
 }
 
+#include "lumacs/editor_core.hpp" // Needed for EditorCore definition in implementation
+
+// ... (rest of includes)
+
+namespace lumacs {
+
+// ... (previous code)
+
 KeyProcessingResult KeyBindingManager::process_key(const Key& key) {
     // Check for timeout first
     if (is_building_sequence() && has_sequence_timed_out()) {
@@ -359,12 +368,30 @@ KeyProcessingResult KeyBindingManager::process_key(const Key& key) {
     if (node && node->binding.has_value()) {
         // Found exact match
         KeyBinding bound_command = node->binding.value();
+        
+        // Capture the raw key string of the full sequence before clearing
+        std::string sequence_str = current_sequence_.to_string(); 
         clear_partial_sequence();
         
         try {
             // Execute the command via the CommandSystem
             if (command_system_) {
+                // --- Macro Recording Logic ---
+                bool was_recording = command_system_->core().is_recording_macro();
+
                 CommandResult cmd_result = command_system_->execute(bound_command.command_name, {}); // No args for now
+
+                bool is_recording = command_system_->core().is_recording_macro();
+
+                // Only record if we were recording BEFORE execution and are STILL recording AFTER execution.
+                // This prevents recording the 'Start Macro' key (was_recording=false)
+                // and the 'End Macro' key (is_recording=false).
+                if (was_recording && is_recording) {
+                    // Record the full sequence that triggered this command
+                    command_system_->core().record_key_sequence(sequence_str);
+                }
+                // -----------------------------
+
                 return KeyProcessingResult(cmd_result.status == CommandStatus::Success ? KeyResult::Executed : KeyResult::Failed, cmd_result);
             }
             return KeyProcessingResult(KeyResult::Failed, CommandResult{CommandStatus::Failure, "No CommandSystem available"});
@@ -379,8 +406,78 @@ KeyProcessingResult KeyBindingManager::process_key(const Key& key) {
     if (node && !node->children.empty()) {
         return KeyProcessingResult(KeyResult::Partial);
     }
+
+    // --- Self-Insert / Unbound Key Recording Logic ---
+    // If no binding found, we clear sequence and return Unbound.
+    // The Input Loop (EditorCore/TUI/GTK) usually handles Unbound by calling 'self-insert-command' manually 
+    // if it's a printable character. 
+    // Wait, TuiEditor calls `core_->command_system().execute("self-insert-command", {key_name});` 
+    // which goes through CommandSystem::execute.
+    // Does CommandSystem::execute record macros? No, checking EditorCore above.
+    // But `process_key` returns Unbound here. So the macro logic above is NOT reached for self-insert!
+    
+    // We need to handle recording for unbound keys that become self-inserts?
+    // Actually, `process_key` returning Unbound delegates responsibility to the caller.
+    // The caller (e.g. TuiEditor::handle_input) calls `self-insert-command`.
+    // We need that `execute` call to be recorded too?
+    // But `process_key` logic above only records *bound* commands.
+    
+    // If `process_key` returns Unbound, the caller calls `execute("self-insert-command")`.
+    // `execute` itself is just looking up the function and running it. It doesn't know about keys.
+    // So `MacroManager` needs to be notified by the caller? 
+    // Or `CommandSystem::execute` should record?
+    // `CommandSystem::execute` receives arguments, not keys.
+    // If we record `self-insert-command` in `CommandSystem::execute`, we record the command invocation, not the key.
+    // But macro playback re-injects KEYS (via `process_key`).
+    // So we must record the KEY.
+    
+    // This implies that the Input Loop (caller) must notify the recorder for self-inserts.
+    // OR, we bind printable keys to `self-insert-command` explicitly?
+    // That would make them "bound commands" and hit the logic above.
+    // But we rely on "fallback" logic currently.
+    
+    // Alternative: `process_key` should handle the fallback logic internally instead of returning Unbound?
+    // If we do that, we can record it here.
+    // Existing `TuiEditor::handle_input`:
+    // if (result.type == KeyResult::Unbound) {
+    //     if (key_name.length() == 1) { core_->command_system().execute("self-insert-command", {key_name}); ... }
+    // }
+    
+    // If I move that fallback logic INTO process_key, then I can record it.
+    // But process_key is `KeyBindingManager`. Does it know about `self-insert-command`?
+    // It knows about `CommandSystem`.
+    
+    // Let's strictly follow the plan first: Record BOUND commands.
+    // If self-insert is not bound, it won't be recorded here.
+    // This effectively means macros won't record typing unless keys are bound.
+    // THIS IS A PROBLEM.
+    
+    // Fix: `TuiEditor` (and `GtkEditor`) calls `process_key`.
+    // If `process_key` returns Unbound, they manually execute self-insert.
+    // They should ALSO manually record the key if macro is recording.
+    // `GtkEditor` logic for this is in `on_global_key_pressed`?
+    // Let's check `GtkEditor::on_global_key_pressed` implementation.
+    // It probably does similar fallback.
+    
+    // Ideally, `process_key` should be the single point of truth. 
+    // If a key is "unbound" but printable, maybe `process_key` should treating it as "bound to self-insert"?
+    // If I change `process_key` to handle printable chars by executing self-insert, I unify behavior AND fix recording.
+    
+    // Let's modify `process_key` to handle the fallback.
+    // But `Key` structure splits modifiers.
+    // Printable means: no modifiers (except Shift?) and is a char?
+    // Key struct has `base_key`.
+    
+    // This might be too big of a change for this step.
+    // Better approach:
+    // Implement the recording logic in `process_key` for bound keys.
+    // Then check `GtkEditor` and `TuiEditor` to add recording call for the fallback case.
+    
+    // Actually, `EditorCore` has `record_key_sequence`.
+    // I can expose a helper `process_and_record_if_needed(key)`? No.
+    
+    // Let's stick to `process_key` modification first.
     
-    // No binding found, clear sequence and return unbound
     clear_partial_sequence();
     return KeyProcessingResult(KeyResult::Unbound);
 }

+ 7 - 0
src/tui_editor.cpp

@@ -287,6 +287,13 @@ bool TuiEditor::handle_input(int ch) {
             // Actually, resolve_key might return special single chars? No, most are named.
             // But let's be safe.
             core_->command_system().execute("self-insert-command", {key_name});
+            
+            // --- Macro Recording Logic for Self-Insert ---
+            if (core_->is_recording_macro()) {
+                core_->record_key_sequence(key_name);
+            }
+            // --------------------------------------------
+
             return true;
         }
     }