# Lumacs Architectural Review ## Executive Summary Lumacs is a well-structured C++20 text editor with a **Facade/Mediator** pattern centered on `EditorCore`. The architecture demonstrates solid separation of concerns through interface segregation and dependency injection. However, there are several areas where the design could be improved for scalability, testability, and maintainability. --- ## 1. Separation of Concerns ### Strengths **Interface Segregation is Well-Implemented** - Three core interfaces (`ICommandTarget`, `IEditorNotifier`, `IWindowManager`) effectively decouple components - `BufferManager` depends on interfaces (`i_editor_notifier.hpp:9-20`, `i_window_manager.hpp:11-24`), not concrete `EditorCore` - This enables dependency injection and breaks circular dependencies elegantly **Clear Delegation Pattern** - `EditorCore` (`editor_core.hpp:42-121`) delegates to specialized managers - Each operation routes to the appropriate manager (e.g., `kill_line()` → `KillRingManager`) ### Issues **EditorCore Is Becoming a God Object** - `EditorCore` implements 3 interfaces with ~80 public methods (`editor_core.hpp:32-177`) - It holds 15+ member pointers (`editor_core.hpp:190-206`) - Constructor is 119 lines with complex initialization dependencies (`editor_core.cpp:19-119`) **Circular Dependency Workaround Is Fragile** ```cpp // editor_core.cpp:105-106 buffer_manager_ = std::make_unique(*this, *this); window_manager_ = std::make_unique(*this, *buffer_manager_); ``` The extensive comments in `editor_core.cpp:36-103` reveal the complexity of this workaround. ### Recommendations 1. **Split EditorCore into Coordinator + OperationsDelegate** - `EditorCoordinator`: Manages lifecycle, initialization, and event routing - `EditorOperations`: Implements `ICommandTarget` operations 2. **Consider a Service Locator for Manager Access** - Reduce the number of explicit dependencies in constructors - Enable lazy initialization --- ## 2. Manager Proliferation ### Current State: 11+ Managers | Manager | Dependencies | Responsibilities | |---------|--------------|------------------| | BufferManager | IEditorNotifier, IWindowManager | Buffer lifecycle, file I/O | | WindowManager | IEditorNotifier, BufferManager | Window layout tree | | KillRingManager | None | Clipboard-like ring buffer | | RegisterManager | None | Named text registers | | MacroManager | EditorCore | Keyboard macro recording | | RectangleManager | EditorCore | Rectangular region operations | | CommandSystem | EditorCore, MinibufferManager | Command registration/execution | | MinibufferManager | EditorCore, LuaApi, CompletionSystem | Modal input handling | | CompletionSystem | EditorCore | Completion candidates | | PluginManager | EditorCore, LuaApi | Plugin lifecycle | | KeyBindingManager | CommandSystem* | Key sequence handling (Trie) | | ThemeManager | None | Theme management | | ModelineManager | None | Status line composition | ### Analysis **Well-Isolated Managers (Good)** - `KillRingManager`, `RegisterManager`, `ThemeManager`, `ModelineManager` have zero dependencies - These follow Single Responsibility Principle well **Over-Coupled Managers (Concern)** - `MacroManager`, `RectangleManager` take full `EditorCore&` reference - `MinibufferManager` depends on 3 systems (`minibuffer_manager.hpp:23`) **MinibufferManager Is Overloaded** (`minibuffer_manager.hpp:21-115`) - Handles multiple modes (Command, FilePath, BufferName, ThemeName, ISearch) - Manages history, completion, cursor position, search state - 115 lines of header with mixed responsibilities ### Recommendations 1. **Extract ISearchManager from MinibufferManager** - `isearch_*` members and methods (`minibuffer_manager.hpp:100-111`) should be separate 2. **Create MacroContext/RectangleContext Instead of EditorCore Reference** - Define narrow interfaces for what these managers actually need 3. **Consider Consolidation**: - Merge `KillRingManager` and `RegisterManager` into a unified `ClipboardSystem` - Both deal with text storage/retrieval --- ## 3. Lua Integration ### Architecture ``` LuaApi (lua_api.hpp:23-101) ├── sol::state lua_ (single global state) ├── EditorCore* core_ (set via set_core()) ├── register_types() → exposes C++ usertypes └── register_functions() → global Lua functions ``` ### Strengths **Comprehensive Type Exposure** (`lua_api.cpp:390-898`) - Position, Range, Buffer, Window, EditorCore usertypes - Enums for ThemeElement, FontWeight, MinibufferMode - Callback support for buffer events (`lua_api.cpp:662-670`) **Command System Integration** ```cpp // lua_api.cpp:764-823 "bind_key", [this](EditorCore& core, std::string key, sol::object callback_or_cmd, ...) ``` - Keys can bind to either command names or Lua functions - Transparent interop between C++ commands and Lua callbacks ### Issues **Single Global Lua State - Plugin Isolation Risk** ```cpp // lua_api.hpp:89 sol::state lua_; ``` - All plugins share the same Lua environment - One misbehaving plugin can corrupt global state - No sandboxing for untrusted plugins **LuaApi Constructor Before Core Assignment** (`lua_api.cpp:58-69`, `lua_api.cpp:71-74`) ```cpp LuaApi::LuaApi() { lua_.open_libraries(...); } void LuaApi::set_core(EditorCore& core) { core_ = &core; setup_api(); } ``` - `set_core()` must be called before API is usable - No enforcement at compile time **Error Handling Is Log-Only** (`lua_api.cpp:83-86`) ```cpp } catch (const sol::error& e) { spdlog::error("Lua error loading {}: {}", path.string(), e.what()); return false; } ``` - Errors are logged but not propagated to UI - Users may not see why their config failed **Duplicated Callback Wrapper Code** - `lua_api.cpp:143-182`, `lua_api.cpp:777-810`, `lua_api.cpp:834-868` contain nearly identical callback wrapping logic ### Recommendations 1. **Plugin Sandboxing with Separate Environments** ```cpp sol::environment plugin_env(lua_, sol::create, lua_.globals()); ``` 2. **Compile-Time Core Requirement** - Make `EditorCore&` a constructor parameter, or use RAII pattern 3. **Extract Callback Wrapper Helper** ```cpp CommandResult wrap_lua_callback(sol::function& callback, CommandContext& context); ``` 4. **User-Visible Error Reporting** - Push Lua errors to `*Messages*` buffer or minibuffer --- ## 4. UI Layer Abstraction ### Interface Design (`ui_interface.hpp:38-59`) ```cpp class IEditorView { virtual void init() = 0; virtual void run() = 0; virtual void handle_editor_event(EditorEvent event) = 0; virtual void set_core(EditorCore* core) = 0; }; ``` ### Strengths **Clean Factory Pattern** (`gtk_editor.hpp:81`, `tui_editor.cpp:761-763`) ```cpp std::unique_ptr create_gtk_editor(); std::unique_ptr create_tui_editor(); ``` **Conditional Compilation** (`main.cpp:52-66`) ```cpp #ifdef LUMACS_WITH_GTK if (!force_tui) { view = create_gtk_editor(); } #endif ``` ### Issues **Event Handling Has Redundant Dispatching** Both `TuiEditor::handle_editor_event()` (`tui_editor.cpp:159-256`) and `GtkEditor::handle_editor_event()` have nearly identical switch statements for mode activation: ```cpp // tui_editor.cpp:171-183 } else if (event == EditorEvent::CommandMode) { core_->minibuffer_manager().activate_minibuffer( MinibufferMode::Command, ":", [this](const std::string& input) { ... }, [this]() { core_->set_message("Cancelled"); } ); } ``` This is duplicated for BufferSwitchMode, KillBufferMode, FindFileMode, etc. **Raw Pointer to Core** ```cpp // gtk_editor.hpp:43, tui_editor.cpp:35 EditorCore* core_ = nullptr; ``` - No ownership semantics expressed - `set_core()` must be called before `init()` **TUI Implementation In .cpp** - `TuiEditor` class is defined entirely in `tui_editor.cpp:20-60` - Inconsistent with `GtkEditor` which has a header ### Recommendations 1. **Extract Common Mode Activation Logic** ```cpp class MinibufferModeHandler { void handle_mode_event(EditorEvent event, EditorCore& core, ...); }; ``` 2. **Use Non-Owning Smart Pointer** ```cpp std::reference_wrapper core_; ``` 3. **Create TUI Header for Consistency** - Move class definition to `tui_editor.hpp` --- ## 5. Event System ### Current Implementation (`ui_interface.hpp:12-32`, `editor_core.hpp:143-145`) ```cpp enum class EditorEvent { BufferModified, CursorMoved, ViewportChanged, WindowLayoutChanged, WindowFocused, Message, CommandMode, BufferSwitchMode, ... }; using EventCallback = std::function; void on_event(EventCallback callback); ``` ### Strengths - Simple, synchronous event dispatch - Clean separation via callback registration ### Issues **No Event Data Payload** ```cpp void EditorCore::emit_event(EditorEvent event) { for (const auto& callback : event_callbacks_) { callback(event); // Only event type, no context } } ``` - Observers must query `EditorCore` for related data - Race conditions possible in multi-threaded scenarios **O(n) Callback Iteration** ```cpp std::vector event_callbacks_; ``` - Fine for current use (typically 1 UI), but doesn't scale **Modal Events Mix Control and Data** - `CommandMode`, `ISearchMode` are events that trigger UI state changes - Different semantic level from `BufferModified`, `CursorMoved` ### Recommendations 1. **Add Event Payload** ```cpp struct EditorEventData { EditorEvent type; std::variant payload; }; ``` 2. **Consider Observer Pattern with Typed Handlers** ```cpp void on_buffer_modified(std::function handler); void on_cursor_moved(std::function handler); ``` 3. **Separate Modal Events into Different Mechanism** - Use direct method calls for mode transitions - Reserve events for state change notifications --- ## 6. Plugin/Extension System ### Current Implementation (`plugin_manager.hpp:22-59`) ```cpp class PluginManager { void discover_plugins(const std::vector& plugin_dirs); bool load_plugin(const std::string& name); bool unload_plugin(const std::string& name); }; ``` ### Strengths - Simple file-based plugin discovery - Optional `on_load()`/`on_unload()` lifecycle hooks ### Issues **No Plugin Metadata** ```cpp struct Plugin { std::string name; std::filesystem::path path; // No: version, dependencies, compatibility info }; ``` **No Plugin Isolation** - All plugins run in shared `sol::state` - No capability restrictions - Plugins can access filesystem, network via Lua stdlib **No Plugin Dependency Resolution** - Plugins loaded in discovery order - No mechanism for plugin A to require plugin B **Unload Is Incomplete** ```cpp void execute_on_unload(const Plugin& plugin); ``` - Calls Lua `on_unload()` but doesn't clean up: - Registered commands - Key bindings - Event handlers ### Recommendations 1. **Plugin Manifest** ```lua -- plugin.lua return { name = "my-plugin", version = "1.0.0", requires = {"core >= 0.1.0"}, on_load = function() ... end } ``` 2. **Capability-Based Sandboxing** ```cpp sol::environment safe_env(lua_, sol::create); safe_env["editor"] = core_subset; // Limited API ``` 3. **Plugin Registry for Cleanup** - Track commands/bindings registered by each plugin - Auto-cleanup on unload --- ## 7. State Management ### Current State Distribution | Component | State Owned | |-----------|-------------| | BufferManager | `std::list>` | | WindowManager | `shared_ptr`, `shared_ptr` | | Buffer | Content, undo stack, mark, styles | | Window | Cursor, viewport, buffer reference | | KillRingManager | `std::deque` | | MinibufferManager | Mode, input, history, completion | | MacroManager | Recording state, macro content | ### Issues **Shared Mutable State via shared_ptr** ```cpp // buffer_manager.hpp:78 std::list> buffers_; ``` - Multiple windows can reference same Buffer - No synchronization primitives - Buffer modification from any path **Undo Is Buffer-Local Only** - No transaction semantics for multi-manager operations - Kill-then-yank is two unrelated operations from undo perspective **YankState in EditorCore** (`editor_core.hpp:187-188`) ```cpp std::optional last_yank_start_; std::optional last_yank_end_; ``` - Implementation detail leaking into core class - Should be managed by KillRingManager or a YankContext ### Recommendations 1. **Consider Copy-on-Write for Buffer Content** - Enables efficient snapshotting - Safer for potential future threading 2. **Centralized Undo Stack** ```cpp class UndoManager { void begin_transaction(); void commit_transaction(); void rollback_transaction(); }; ``` 3. **Move Yank State to KillRingManager** - Keep yank position tracking with kill ring logic --- ## 8. Testability ### Current Test Structure (`tests/test_editor_core.cpp`) ```cpp struct EditorCoreTest : public ::testing::Test { lumacs::EditorCore core; // Full system initialization }; ``` ### Strengths - Comprehensive test files for each manager - Integration tests in `test_integration.cpp` - Fixture-based setup with GoogleTest ### Issues **Tests Create Full EditorCore** ```cpp lumacs::EditorCore core; ``` - Every test initializes: - All 11+ managers - Lua VM - Theme system - Loads embedded defaults - Slow and couples tests to entire system **No Mock Implementations** - Interfaces exist (`ICommandTarget`, `IEditorNotifier`) but no mocks - Hard to test managers in isolation **LuaApi Test Depends on Disk** (`test_lua_api.cpp`) - Tests that load files can fail based on environment - No embedded test scripts **Comments Acknowledge Limitations** (`test_editor_core.cpp:103-106`) ```cpp // Executing the macro will depend on process_key being called // which is hard to test without mocking keybinding_manager directly. // For now, just test recording state. ``` ### Recommendations 1. **Create Mock Implementations** ```cpp class MockEditorNotifier : public IEditorNotifier { ... }; class MockCommandTarget : public ICommandTarget { ... }; ``` 2. **Manager-Level Test Fixtures** ```cpp struct KillRingManagerTest : public ::testing::Test { MockEditorNotifier notifier; KillRingManager manager; // No EditorCore needed! }; ``` 3. **Embedded Lua Test Scripts** ```cpp static const char* TEST_LUA_SCRIPT = R"lua(...)lua"; ``` --- ## 9. Performance Considerations ### Current Bottlenecks **Key Processing Trie Lookup** (`keybinding.hpp:185-210`) ```cpp struct TrieNode { std::map> children; }; ``` - `std::map` means O(log n) per key in sequence - Fine for typical Emacs sequences, but `std::unordered_map` would be O(1) **Syntax Highlighting Per-Render** (`tui_editor.cpp:521-572`) ```cpp const auto& styles = buffer.get_line_styles(buffer_line_idx); ``` - Styles retrieved and iterated per line, per frame - For large files, this could be expensive **Event Callback Vector Copy** (`editor_core.cpp:757-760`) ```cpp for (const auto& callback : event_callbacks_) { callback(event); } ``` - Safe iteration, but callbacks could modify the vector **No Line Caching in Buffer** - `buffer.line(idx)` constructs string view each time - No memoization for frequently accessed lines ### Recommendations 1. **Use `std::unordered_map` for Trie Nodes** - With custom hash for `Key` 2. **Cache Visible Lines' Styles** - Invalidate on buffer modification in visible range 3. **Consider Rope or Gap Buffer for Large Files** - Current `std::vector` is O(n) for mid-file insertions --- ## 10. Maintainability ### Strengths - Consistent naming conventions - Doxygen-style comments on public interfaces - Clear header/implementation separation - Logical file organization ### Issues **Large Files** - `lua_api.cpp`: 1031 lines - `editor_core.cpp`: 763 lines - `tui_editor.cpp`: 765 lines **Constructor Comments Reveal Design Debt** (`editor_core.cpp:27-103`) - 76 lines of comments explaining circular dependency workaround - Indicates architectural complexity **Inconsistent Header Guards** - Mix of `#pragma once` (good) throughout **Debug Code in Production** ```cpp // tui_editor.cpp:14-15 extern std::ofstream debug_log; std::ofstream debug_log("lumacs_debug.log"); ``` - Creates log file unconditionally ### Recommendations 1. **Split Large Files** - `lua_api.cpp` → `lua_types.cpp`, `lua_functions.cpp`, `lua_api.cpp` - `editor_core.cpp` → core + operations 2. **Remove Production Debug Logging** - Use spdlog with runtime level control 3. **Address Design Debt** - Refactor circular dependency if comments exceed code --- ## Summary of Key Recommendations ### High Priority 1. **Split EditorCore** into Coordinator + Operations to address God Object anti-pattern 2. **Extract ISearchManager** from MinibufferManager 3. **Add Plugin Sandboxing** using Lua environments 4. **Create Mock Implementations** for testability ### Medium Priority 5. **Unified Event Data** payloads for type-safe event handling 6. **Common Mode Activation** handler shared between TUI/GTK 7. **Plugin Manifest** system with dependency resolution 8. **Extract Lua Callback Wrapper** to eliminate duplication ### Low Priority (Technical Debt) 9. **Performance optimizations** (Trie hash map, style caching) 10. **Split large implementation files** 11. **Production debug log cleanup** --- ## Conclusion Lumacs demonstrates a thoughtful architecture with proper interface segregation and dependency injection patterns. The main concerns are: 1. **EditorCore complexity** - trending toward a God Object 2. **Lua integration security** - shared state with no sandboxing 3. **Test isolation** - full system initialization for each test 4. **MinibufferManager overload** - too many responsibilities The foundation is solid, and the suggested improvements would enhance scalability without requiring a complete rewrite. The interface-based design already in place makes incremental refactoring feasible.