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.
Interface Segregation is Well-Implemented
ICommandTarget, IEditorNotifier, IWindowManager) effectively decouple componentsBufferManager depends on interfaces (i_editor_notifier.hpp:9-20, i_window_manager.hpp:11-24), not concrete EditorCoreClear Delegation Pattern
EditorCore (editor_core.hpp:42-121) delegates to specialized managerskill_line() → KillRingManager)EditorCore Is Becoming a God Object
EditorCore implements 3 interfaces with ~80 public methods (editor_core.hpp:32-177)editor_core.hpp:190-206)editor_core.cpp:19-119)Circular Dependency Workaround Is Fragile
// 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.
Split EditorCore into Coordinator + OperationsDelegate
EditorCoordinator: Manages lifecycle, initialization, and event routingEditorOperations: Implements ICommandTarget operationsConsider a Service Locator for Manager Access
| 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 |
Well-Isolated Managers (Good)
KillRingManager, RegisterManager, ThemeManager, ModelineManager have zero dependenciesOver-Coupled Managers (Concern)
MacroManager, RectangleManager take full EditorCore& referenceMinibufferManager depends on 3 systems (minibuffer_manager.hpp:23)MinibufferManager Is Overloaded (minibuffer_manager.hpp:21-115)
Extract ISearchManager from MinibufferManager
isearch_* members and methods (minibuffer_manager.hpp:100-111) should be separateCreate MacroContext/RectangleContext Instead of EditorCore Reference
Consider Consolidation:
KillRingManager and RegisterManager into a unified ClipboardSystemLuaApi (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
Comprehensive Type Exposure (lua_api.cpp:390-898)
lua_api.cpp:662-670)Command System Integration
// lua_api.cpp:764-823
"bind_key", [this](EditorCore& core, std::string key, sol::object callback_or_cmd, ...)
Single Global Lua State - Plugin Isolation Risk
// lua_api.hpp:89
sol::state lua_;
LuaApi Constructor Before Core Assignment (lua_api.cpp:58-69, lua_api.cpp:71-74)
LuaApi::LuaApi() { lua_.open_libraries(...); }
void LuaApi::set_core(EditorCore& core) { core_ = &core; setup_api(); }
set_core() must be called before API is usableError Handling Is Log-Only (lua_api.cpp:83-86)
} catch (const sol::error& e) {
spdlog::error("Lua error loading {}: {}", path.string(), e.what());
return false;
}
Duplicated Callback Wrapper Code
lua_api.cpp:143-182, lua_api.cpp:777-810, lua_api.cpp:834-868 contain nearly identical callback wrapping logicPlugin Sandboxing with Separate Environments
sol::environment plugin_env(lua_, sol::create, lua_.globals());
Compile-Time Core Requirement
EditorCore& a constructor parameter, or use RAII patternExtract Callback Wrapper Helper
CommandResult wrap_lua_callback(sol::function& callback, CommandContext& context);
User-Visible Error Reporting
*Messages* buffer or minibufferui_interface.hpp:38-59)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;
};
Clean Factory Pattern (gtk_editor.hpp:81, tui_editor.cpp:761-763)
std::unique_ptr<IEditorView> create_gtk_editor();
std::unique_ptr<IEditorView> create_tui_editor();
Conditional Compilation (main.cpp:52-66)
#ifdef LUMACS_WITH_GTK
if (!force_tui) { view = create_gtk_editor(); }
#endif
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:
// 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
// gtk_editor.hpp:43, tui_editor.cpp:35
EditorCore* core_ = nullptr;
set_core() must be called before init()TUI Implementation In .cpp
TuiEditor class is defined entirely in tui_editor.cpp:20-60GtkEditor which has a headerExtract Common Mode Activation Logic
class MinibufferModeHandler {
void handle_mode_event(EditorEvent event, EditorCore& core, ...);
};
Use Non-Owning Smart Pointer
std::reference_wrapper<EditorCore> core_;
Create TUI Header for Consistency
tui_editor.hppui_interface.hpp:12-32, editor_core.hpp:143-145)enum class EditorEvent {
BufferModified, CursorMoved, ViewportChanged, WindowLayoutChanged,
WindowFocused, Message, CommandMode, BufferSwitchMode, ...
};
using EventCallback = std::function<void(EditorEvent)>;
void on_event(EventCallback callback);
No Event Data Payload
void EditorCore::emit_event(EditorEvent event) {
for (const auto& callback : event_callbacks_) {
callback(event); // Only event type, no context
}
}
EditorCore for related dataO(n) Callback Iteration
std::vector<EventCallback> event_callbacks_;
Modal Events Mix Control and Data
CommandMode, ISearchMode are events that trigger UI state changesBufferModified, CursorMovedAdd Event Payload
struct EditorEventData {
EditorEvent type;
std::variant<BufferModifiedData, CursorMovedData, ...> payload;
};
Consider Observer Pattern with Typed Handlers
void on_buffer_modified(std::function<void(const BufferModifiedData&)> handler);
void on_cursor_moved(std::function<void(Position)> handler);
Separate Modal Events into Different Mechanism
plugin_manager.hpp:22-59)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);
};
on_load()/on_unload() lifecycle hooksNo Plugin Metadata
struct Plugin {
std::string name;
std::filesystem::path path;
// No: version, dependencies, compatibility info
};
No Plugin Isolation
sol::stateNo Plugin Dependency Resolution
Unload Is Incomplete
void execute_on_unload(const Plugin& plugin);
on_unload() but doesn't clean up:
Plugin Manifest
-- plugin.lua
return {
name = "my-plugin",
version = "1.0.0",
requires = {"core >= 0.1.0"},
on_load = function() ... end
}
Capability-Based Sandboxing
sol::environment safe_env(lua_, sol::create);
safe_env["editor"] = core_subset; // Limited API
Plugin Registry for Cleanup
| 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 |
Shared Mutable State via shared_ptr
// buffer_manager.hpp:78
std::list<std::shared_ptr<Buffer>> buffers_;
Undo Is Buffer-Local Only
YankState in EditorCore (editor_core.hpp:187-188)
std::optional<Position> last_yank_start_;
std::optional<Position> last_yank_end_;
Consider Copy-on-Write for Buffer Content
Centralized Undo Stack
class UndoManager {
void begin_transaction();
void commit_transaction();
void rollback_transaction();
};
Move Yank State to KillRingManager
tests/test_editor_core.cpp)struct EditorCoreTest : public ::testing::Test {
lumacs::EditorCore core; // Full system initialization
};
test_integration.cppTests Create Full EditorCore
lumacs::EditorCore core;
No Mock Implementations
ICommandTarget, IEditorNotifier) but no mocksLuaApi Test Depends on Disk (test_lua_api.cpp)
Comments Acknowledge Limitations (test_editor_core.cpp:103-106)
// 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.
Create Mock Implementations
class MockEditorNotifier : public IEditorNotifier { ... };
class MockCommandTarget : public ICommandTarget { ... };
Manager-Level Test Fixtures
struct KillRingManagerTest : public ::testing::Test {
MockEditorNotifier notifier;
KillRingManager manager; // No EditorCore needed!
};
Embedded Lua Test Scripts
static const char* TEST_LUA_SCRIPT = R"lua(...)lua";
Key Processing Trie Lookup (keybinding.hpp:185-210)
struct TrieNode {
std::map<Key, std::unique_ptr<TrieNode>> children;
};
std::map means O(log n) per key in sequencestd::unordered_map would be O(1)Syntax Highlighting Per-Render (tui_editor.cpp:521-572)
const auto& styles = buffer.get_line_styles(buffer_line_idx);
Event Callback Vector Copy (editor_core.cpp:757-760)
for (const auto& callback : event_callbacks_) {
callback(event);
}
No Line Caching in Buffer
buffer.line(idx) constructs string view each timeUse std::unordered_map for Trie Nodes
KeyCache Visible Lines' Styles
Consider Rope or Gap Buffer for Large Files
std::vector<std::string> is O(n) for mid-file insertionsLarge Files
lua_api.cpp: 1031 lineseditor_core.cpp: 763 linestui_editor.cpp: 765 linesConstructor Comments Reveal Design Debt (editor_core.cpp:27-103)
Inconsistent Header Guards
#pragma once (good) throughoutDebug Code in Production
// tui_editor.cpp:14-15
extern std::ofstream debug_log;
std::ofstream debug_log("lumacs_debug.log");
Split Large Files
lua_api.cpp → lua_types.cpp, lua_functions.cpp, lua_api.cppeditor_core.cpp → core + operationsRemove Production Debug Logging
Address Design Debt
Lumacs demonstrates a thoughtful architecture with proper interface segregation and dependency injection patterns. The main concerns are:
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.