# 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<BufferManager>(*this, *this);
window_manager_ = std::make_unique<WindowManager>(*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<IEditorView> create_gtk_editor();
std::unique_ptr<IEditorView> 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<EditorCore> 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(EditorEvent)>;
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<EventCallback> 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<BufferModifiedData, CursorMovedData, ...> payload;
   };
   ```

2. **Consider Observer Pattern with Typed Handlers**
   ```cpp
   void on_buffer_modified(std::function<void(const BufferModifiedData&)> handler);
   void on_cursor_moved(std::function<void(Position)> 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<std::filesystem::path>& 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<shared_ptr<Buffer>>` |
| WindowManager | `shared_ptr<LayoutNode>`, `shared_ptr<Window>` |
| Buffer | Content, undo stack, mark, styles |
| Window | Cursor, viewport, buffer reference |
| KillRingManager | `std::deque<std::string>` |
| MinibufferManager | Mode, input, history, completion |
| MacroManager | Recording state, macro content |

### Issues

**Shared Mutable State via shared_ptr**
```cpp
// buffer_manager.hpp:78
std::list<std::shared_ptr<Buffer>> 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<Position> last_yank_start_;
std::optional<Position> 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<Key, std::unique_ptr<TrieNode>> 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<std::string>` 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.
