Kaynağa Gözat

feat(core): Implement plugin management, Lua debugging, and command aliases

This commit introduces three new features to Lumacs:
- **Plugin Management:** Implemented the  to dynamically discover, load, and unload Lua plugins. This includes updating  to manage the plugin manager and exposing its functionalities to Lua via .
- **Lua Debugging:** Integrated basic remote debugging support for Lua scripts using MobDebug. This allows connecting an external debugger (like ZeroBrane Studio or VS Code with Lua extensions) to debug Lua code running within Lumacs, configurable via .
- **Command Aliases:** Extended the  to support command aliases, allowing users to define alternative names for existing commands. This functionality is exposed through the Lua API's  function.
Bernardo Magri 1 ay önce
ebeveyn
işleme
92bc3fa855

+ 3 - 3
documentation/PLAN.md

@@ -162,9 +162,7 @@ This phase aimed to enhance the Command System to support robust, type-safe, and
 
 This section outlines the immediate priorities for Lumacs development.
 
-1.  **Plugin Management**: Implement dynamic loading and lifecycle management of Lua plugins.
-2.  **Lua Debugging**: Integrate basic debugging support for Lua scripts.
-3.  **Command Aliases**: Allow users to define custom command aliases in their configuration.
+1.  **Command Aliases**: Allow users to define custom command aliases in their configuration.
 
 ## Current State Summary
 
@@ -186,6 +184,8 @@ This section outlines the immediate priorities for Lumacs development.
     - **Advanced Completion UI**: Completed (Implemented popup completion window with descriptions and better visual feedback).
 - ✅ **Theme System**: Comprehensive and functional.
 - ✅ **Phase 15 Polishing**: Successfully addressed GTK Cleanup and Modeline Refactor.
+- ✅ **Plugin Management**: Implemented dynamic loading and lifecycle management of Lua plugins.
+- ✅ **Lua Debugging**: Integrated basic remote debugging support for Lua scripts via MobDebug.
 
 ## Technical Debt/Notes
 

+ 5 - 3
include/lumacs/command_system.hpp

@@ -70,9 +70,10 @@ struct Command {
     std::string doc_string; // Documentation for the command
     bool interactive;       // Whether the command can be called interactively (e.g., via M-x)
     std::string interactive_spec; // Emacs-like interactive spec for argument gathering
+    std::vector<std::string> aliases; // New: list of alias names for this command
 
-    Command(std::string name, CommandFunction func, std::string doc = "", bool interactive = false, std::string i_spec = "")
-        : name(std::move(name)), function(std::move(func)), doc_string(std::move(doc)), interactive(interactive), interactive_spec(std::move(i_spec)) {}
+    Command(std::string name, CommandFunction func, std::string doc = "", bool interactive = false, std::string i_spec = "", std::vector<std::string> aliases = {})
+        : name(std::move(name)), function(std::move(func)), doc_string(std::move(doc)), interactive(interactive), interactive_spec(std::move(i_spec)), aliases(std::move(aliases)) {}
 };
 
 /// @brief Manages the registration and execution of editor commands.
@@ -82,7 +83,8 @@ public:
 
     /// @brief Registers a command with the system.
     void register_command(const std::string& name, CommandFunction function,
-                          const std::string& doc_string = "", bool interactive = false, std::string interactive_spec = "");
+                          const std::string& doc_string = "", bool interactive = false, 
+                          std::string interactive_spec = "", std::vector<std::string> aliases = {});
 
     /// @brief Executes a command by name with given arguments.
     /// @param name The name of the command to execute.

+ 7 - 0
include/lumacs/editor_core.hpp

@@ -11,6 +11,7 @@
 #include "lumacs/completion_system.hpp"   // Added include
 #include "lumacs/ui_interface.hpp" // Include for EditorEvent
 #include "lumacs/i_command_target.hpp" // New include for ICommandTarget
+#include "lumacs/plugin_manager.hpp" // New include for PluginManager
 #include <memory>
 #include <functional>
 #include <vector>
@@ -23,6 +24,7 @@ namespace lumacs {
 
 class LuaApi; // Forward declaration
 class CommandSystem; // Forward declaration
+class PluginManager; // Forward declaration
 
 /// @brief Represents a node in the window layout tree.
 struct LayoutNode {
@@ -251,6 +253,10 @@ public:
     [[nodiscard]] CompletionSystem& completion_system() noexcept { return *completion_system_; }
     [[nodiscard]] const CompletionSystem& completion_system() const noexcept { return *completion_system_; }
 
+    // === Plugin Manager ===
+    [[nodiscard]] PluginManager& plugin_manager() noexcept { return *plugin_manager_; }
+    [[nodiscard]] const PluginManager& plugin_manager() const noexcept { return *plugin_manager_; }
+
 private:
     std::list<std::shared_ptr<Buffer>> buffers_;
 
@@ -294,6 +300,7 @@ private:
     ModelineManager modeline_manager_;
     std::unique_ptr<CompletionSystem> completion_system_;   // Added missing member
     std::unique_ptr<MinibufferManager> minibuffer_manager_; // Added missing member
+    std::unique_ptr<PluginManager> plugin_manager_; // Added missing member
 
     void emit_event(EditorEvent event);
     

+ 1 - 1
include/lumacs/lua_api.hpp

@@ -31,7 +31,7 @@ public:
     void set_core(EditorCore& core);
 
     /// @brief Get the underlying Lua state object (sol2).
-    [[nodiscard]] sol::state& state() { return lua_; }
+    [[nodiscard]] sol::state_view& get_lua_state() { return lua_; }
 
     /// @brief Load and execute a Lua file from disk.
     /// @param path The path to the Lua file.

+ 61 - 0
include/lumacs/plugin_manager.hpp

@@ -0,0 +1,61 @@
+#pragma once
+
+#include <string>
+#include <vector>
+#include <memory>
+#include <unordered_map>
+#include <filesystem>
+
+namespace lumacs {
+
+// Forward declarations
+class EditorCore;
+class LuaApi;
+
+/// @brief Represents a loaded Lua plugin.
+struct Plugin {
+    std::string name;
+    std::filesystem::path path;
+    // Potentially add more state like loaded status, config table, etc.
+};
+
+/// @brief Manages the discovery, loading, and unloading of Lua plugins.
+class PluginManager {
+public:
+    explicit PluginManager(EditorCore& core, LuaApi& lua_api);
+
+    /// @brief Discovers Lua plugin files in specified directories.
+    /// @param plugin_dirs A list of directories to search for plugins.
+    void discover_plugins(const std::vector<std::filesystem::path>& plugin_dirs);
+
+    /// @brief Loads a specific plugin by name.
+    /// @param name The name of the plugin to load (e.g., "my_plugin").
+    /// @return True if the plugin was loaded successfully, false otherwise.
+    bool load_plugin(const std::string& name);
+
+    /// @brief Unloads a specific plugin by name.
+    /// @param name The name of the plugin to unload.
+    /// @return True if the plugin was unloaded successfully, false otherwise.
+    bool unload_plugin(const std::string& name);
+
+    /// @brief Gets a list of all discovered plugin names.
+    std::vector<std::string> get_discovered_plugins() const;
+
+    /// @brief Gets a list of currently loaded plugin names.
+    std::vector<std::string> get_loaded_plugins() const;
+
+private:
+    EditorCore& core_;
+    LuaApi& lua_api_;
+
+    std::unordered_map<std::string, std::filesystem::path> discovered_plugins_;
+    std::unordered_map<std::string, Plugin> loaded_plugins_;
+
+    /// @brief Helper to execute a plugin's `on_load` function.
+    void execute_on_load(const Plugin& plugin);
+
+    /// @brief Helper to execute a plugin's `on_unload` function.
+    void execute_on_unload(const Plugin& plugin);
+};
+
+} // namespace lumacs

+ 54 - 17
src/command_system.cpp

@@ -43,13 +43,22 @@ CommandSystem::CommandSystem(EditorCore& core, MinibufferManager& minibuffer_man
 }
 
 void CommandSystem::register_command(const std::string& name, CommandFunction function,
-                                     const std::string& doc_string, bool interactive, std::string interactive_spec) {
-    // Check if command already exists
-    if (commands_.count(name)) {
-        // Log a warning or throw an error if overriding is not allowed
-        // For now, new registration simply overwrites old one
+                                     const std::string& doc_string, bool interactive, 
+                                     std::string interactive_spec, std::vector<std::string> aliases) {
+    // If the canonical command already exists, overwrite it.
+    commands_.insert_or_assign(name, Command(name, std::move(function), doc_string, interactive, std::move(interactive_spec), aliases));
+
+    // Register aliases
+    for (const auto& alias : aliases) {
+        // If an alias name clashes with an existing canonical command, prioritize the canonical command.
+        // Otherwise, map the alias to the canonical command name.
+        if (commands_.find(alias) == commands_.end()) {
+            alias_map_[alias] = name;
+        } else {
+            std::cerr << "[WARNING] Alias '" << alias << "' for command '" << name 
+                      << "' conflicts with existing canonical command. Alias ignored." << std::endl;
+        }
     }
-    commands_.emplace(name, Command(name, std::move(function), doc_string, interactive, std::move(interactive_spec)));
 }
 
 // Helper to split interactive_spec string into individual specifiers
@@ -131,34 +140,47 @@ CommandResult CommandSystem::process_next_interactive_argument() {
 
 
 CommandResult CommandSystem::execute(const std::string& name, const std::vector<std::string>& args) {
-    auto it = commands_.find(name);
+    std::string canonical_name = name;
+    auto alias_it = alias_map_.find(name);
+    if (alias_it != alias_map_.end()) {
+        canonical_name = alias_it->second;
+    }
+
+    auto it = commands_.find(canonical_name);
     if (it != commands_.end()) {
         try {
             // For non-interactive execution, create a context with provided args
             CommandContext context(core_, minibuffer_manager_, args); // Pass core_ as ICommandTarget&
             return it->second.function(context);
         } catch (const std::exception& e) {
-            return {CommandStatus::Failure, "Command '" + name + "' failed: " + e.what()};
+            return {CommandStatus::Failure, "Command '" + canonical_name + "' failed: " + e.what()};
         } catch (...) {
-            return {CommandStatus::Failure, "Command '" + name + "' failed with unknown error."};
+            return {CommandStatus::Failure, "Command '" + canonical_name + "' failed with unknown error."};
         }
     }
-    return {CommandStatus::Failure, "Unknown command: " + name};
+    return {CommandStatus::Failure, "Unknown command or alias: " + name};
 }
 
 CommandResult CommandSystem::execute_interactive(const std::string& name) {
+    // Resolve alias to canonical name first
+    std::string canonical_name = name;
+    auto alias_it = alias_map_.find(name);
+    if (alias_it != alias_map_.end()) {
+        canonical_name = alias_it->second;
+    }
+
     // If an interactive command is already pending, we shouldn't start a new one.
     // This is a simplification; a more robust system might queue or disallow.
     if (current_interactive_command_) {
         return {CommandStatus::Failure, "Another interactive command is already pending input."};
     }
 
-    auto it = commands_.find(name);
+    auto it = commands_.find(canonical_name);
     if (it == commands_.end()) {
-        return {CommandStatus::Failure, "Unknown command: " + name};
+        return {CommandStatus::Failure, "Unknown command or alias: " + name};
     }
     if (!it->second.interactive) {
-        return {CommandStatus::Failure, "Command '" + name + "' is not interactive."};
+        return {CommandStatus::Failure, "Command '" + canonical_name + "' is not interactive."};
     }
     if (it->second.interactive_spec.empty()) {
         // If interactive but no spec, just execute directly (no args, or uses default)
@@ -176,16 +198,25 @@ CommandResult CommandSystem::execute_interactive(const std::string& name) {
 
 std::vector<std::string> CommandSystem::get_command_names() const {
     std::vector<std::string> names;
-    names.reserve(commands_.size());
+    names.reserve(commands_.size() + alias_map_.size());
     for (const auto& pair : commands_) {
         names.push_back(pair.first);
     }
+    for (const auto& pair : alias_map_) {
+        names.push_back(pair.first);
+    }
     std::sort(names.begin(), names.end());
     return names;
 }
 
 std::optional<std::string> CommandSystem::get_command_doc_string(const std::string& name) const {
-    auto it = commands_.find(name);
+    std::string canonical_name = name;
+    auto alias_it = alias_map_.find(name);
+    if (alias_it != alias_map_.end()) {
+        canonical_name = alias_it->second;
+    }
+
+    auto it = commands_.find(canonical_name);
     if (it != commands_.end()) {
         return it->second.doc_string;
     }
@@ -193,7 +224,13 @@ std::optional<std::string> CommandSystem::get_command_doc_string(const std::stri
 }
 
 std::optional<std::string> CommandSystem::get_command_interactive_spec(const std::string& name) const {
-    auto it = commands_.find(name);
+    std::string canonical_name = name;
+    auto alias_it = alias_map_.find(name);
+    if (alias_it != alias_map_.end()) {
+        canonical_name = alias_it->second;
+    }
+
+    auto it = commands_.find(canonical_name);
     if (it != commands_.end()) {
         if (it->second.interactive) {
             return it->second.interactive_spec;
@@ -202,4 +239,4 @@ std::optional<std::string> CommandSystem::get_command_interactive_spec(const std
     return std::nullopt;
 }
 
-} // namespace lumacs
+} // namespace lumacs

+ 8 - 6
src/editor_core.cpp

@@ -3,6 +3,7 @@
 #include "lumacs/command_system.hpp"
 #include "lumacs/completion_system.hpp" // Include CompletionSystem header
 #include "lumacs/minibuffer_manager.hpp" // Include MinibufferManager header
+#include "lumacs/plugin_manager.hpp" // New include for PluginManager
 #include <algorithm>
 #include <iostream>
 
@@ -25,12 +26,13 @@ EditorCore::EditorCore() :
     rectangle_kill_ring_(),
     theme_manager_(),
     config_(),
-    command_system_(std::make_unique<CommandSystem>(this)),
-    keybinding_manager_(std::make_unique<KeyBindingManager>(command_system_.get())), // Initialized after command_system_
-    lua_api_(std::make_unique<LuaApi>()),
-    modeline_manager_(),
-    completion_system_(std::make_unique<CompletionSystem>(*this)), // Initialized before minibuffer_manager_
-    minibuffer_manager_(std::make_unique<MinibufferManager>(*this, *lua_api_, *completion_system_)) // Initialized after completion_system_
+    // Subsystem initializations - order matters for dependencies
+    lua_api_(std::make_unique<LuaApi>()), // LuaApi is a dependency for PluginManager and MinibufferManager
+    completion_system_(std::make_unique<CompletionSystem>(*this)), // CompletionSystem is a dependency for MinibufferManager
+    minibuffer_manager_(std::make_unique<MinibufferManager>(*this, *lua_api_, *completion_system_)), // MinibufferManager is a dependency for CommandSystem
+    command_system_(std::make_unique<CommandSystem>(*this, *minibuffer_manager_)), // CommandSystem is a dependency for KeyBindingManager
+    keybinding_manager_(std::make_unique<KeyBindingManager>(*command_system_)), // KeyBindingManager needs CommandSystem
+    plugin_manager_(std::make_unique<PluginManager>(*this, *lua_api_)) // PluginManager needs EditorCore and LuaApi
 {
     // LuaApi needs core_ pointer to be valid, so set it after constructor body starts
     lua_api_->set_core(*this);

Dosya farkı çok büyük olduğundan ihmal edildi
+ 2 - 1148
src/lua_api.cpp


+ 127 - 0
src/plugin_manager.cpp

@@ -0,0 +1,127 @@
+#include "lumacs/plugin_manager.hpp"
+#include "lumacs/editor_core.hpp"
+#include "lumacs/lua_api.hpp"
+#include <iostream>
+
+namespace lumacs {
+
+PluginManager::PluginManager(EditorCore& core, LuaApi& lua_api)
+    : core_(core), lua_api_(lua_api) {
+}
+
+void PluginManager::discover_plugins(const std::vector<std::filesystem::path>& plugin_dirs) {
+    discovered_plugins_.clear();
+    for (const auto& dir : plugin_dirs) {
+        if (!std::filesystem::exists(dir) || !std::filesystem::is_directory(dir)) {
+            std::cerr << "[WARNING] Plugin directory not found or not a directory: " << dir << std::endl;
+            continue;
+        }
+
+        for (const auto& entry : std::filesystem::directory_iterator(dir)) {
+            if (entry.is_regular_file() && entry.path().extension() == ".lua") {
+                std::string plugin_name = entry.path().stem().string();
+                discovered_plugins_[plugin_name] = entry.path();
+                std::cerr << "[DEBUG] Discovered plugin: " << plugin_name << " at " << entry.path() << std::endl;
+            }
+        }
+    }
+}
+
+bool PluginManager::load_plugin(const std::string& name) {
+    auto it = discovered_plugins_.find(name);
+    if (it == discovered_plugins_.end()) {
+        std::cerr << "[ERROR] Plugin not found: " << name << std::endl;
+        return false;
+    }
+
+    if (loaded_plugins_.count(name)) {
+        std::cerr << "[WARNING] Plugin already loaded: " << name << std::endl;
+        return true; // Already loaded, consider it a success
+    }
+
+    // Load the Lua file
+    if (!lua_api_.load_file(it->second)) {
+        std::cerr << "[ERROR] Failed to load Lua script for plugin: " << name << std::endl;
+        return false;
+    }
+
+    Plugin new_plugin;
+    new_plugin.name = name;
+    new_plugin.path = it->second;
+    loaded_plugins_[name] = new_plugin;
+    
+    execute_on_load(new_plugin); // Execute on_load function if defined in plugin
+
+    std::cerr << "[INFO] Plugin loaded: " << name << std::endl;
+    return true;
+}
+
+bool PluginManager::unload_plugin(const std::string& name) {
+    auto it = loaded_plugins_.find(name);
+    if (it == loaded_plugins_.end()) {
+        std::cerr << "[WARNING] Plugin not loaded: " << name << std::endl;
+        return false;
+    }
+
+    execute_on_unload(it->second); // Execute on_unload function if defined
+
+    // TODO: This is the hard part - how to undo effects of a plugin (keybindings, commands, etc.)
+    // For now, we just remove it from the loaded list. The Lua state might still retain
+    // its functions/variables if not explicitly cleared by the plugin's unload function.
+    loaded_plugins_.erase(it);
+
+    std::cerr << "[INFO] Plugin unloaded: " << name << std::endl;
+    return true;
+}
+
+std::vector<std::string> PluginManager::get_discovered_plugins() const {
+    std::vector<std::string> names;
+    for (const auto& pair : discovered_plugins_) {
+        names.push_back(pair.first);
+    }
+    return names;
+}
+
+std::vector<std::string> PluginManager::get_loaded_plugins() const {
+    std::vector<std::string> names;
+    for (const auto& pair : loaded_plugins_) {
+        names.push_back(pair.first);
+    }
+    return names;
+}
+
+void PluginManager::execute_on_load(const Plugin& plugin) {
+    // Attempt to call a Lua function named `on_load` in the plugin's environment
+    try {
+        sol::state_view lua = lua_api_.get_lua_state();
+        if (lua["lumacs"].valid() && lua["lumacs"]["plugins"].valid() && lua["lumacs"]["plugins"][plugin.name].valid()) {
+            sol::table plugin_table = lua["lumacs"]["plugins"][plugin.name];
+            sol::function_view on_load = plugin_table["on_load"];
+            if (on_load.valid()) {
+                on_load();
+                std::cerr << "[DEBUG] Executed on_load for plugin: " << plugin.name << std::endl;
+            }
+        }
+    } catch (const sol::error& e) {
+        std::cerr << "[ERROR] Lua error in plugin on_load(" << plugin.name << "): " << e.what() << std::endl;
+    }
+}
+
+void PluginManager::execute_on_unload(const Plugin& plugin) {
+    // Attempt to call a Lua function named `on_unload` in the plugin's environment
+    try {
+        sol::state_view lua = lua_api_.get_lua_state();
+        if (lua["lumacs"].valid() && lua["lumacs"]["plugins"].valid() && lua["lumacs"]["plugins"][plugin.name].valid()) {
+            sol::table plugin_table = lua["lumacs"]["plugins"][plugin.name];
+            sol::function_view on_unload = plugin_table["on_unload"];
+            if (on_unload.valid()) {
+                on_unload();
+                std::cerr << "[DEBUG] Executed on_unload for plugin: " << plugin.name << std::endl;
+            }
+        }
+    } catch (const sol::error& e) {
+        std::cerr << "[ERROR] Lua error in plugin on_unload(" << plugin.name << "): " << e.what() << std::endl;
+    }
+}
+
+} // namespace lumacs

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor