Bernardo Magri преди 1 месец
родител
ревизия
305eb8fc34
променени са 11 файла, в които са добавени 1737 реда и са изтрити 308 реда
  1. 2 0
      CMakeLists.txt
  2. 64 41
      LUA_API.md
  3. 7 5
      README.md
  4. 149 0
      include/lumacs/buffer.hpp
  5. 92 37
      include/lumacs/editor_core.hpp
  6. 252 6
      init.lua
  7. 283 6
      src/buffer.cpp
  8. 271 81
      src/editor_core.cpp
  9. 118 8
      src/lua_api.cpp
  10. 483 122
      src/main.cpp
  11. 16 2
      tests/CMakeLists.txt

+ 2 - 0
CMakeLists.txt

@@ -50,12 +50,14 @@ FetchContent_MakeAvailable(sol2)
 # Core library (UI-independent)
 add_library(lumacs_core STATIC
     src/buffer.cpp
+    src/window.cpp
     src/editor_core.cpp
     src/lua_api.cpp
 )
 
 target_include_directories(lumacs_core PUBLIC
     ${CMAKE_CURRENT_SOURCE_DIR}/include
+    ${LUA_INCLUDE_DIR}
 )
 
 target_link_libraries(lumacs_core PUBLIC

+ 64 - 41
LUA_API.md

@@ -27,13 +27,28 @@ The main editor instance.
 - `editor:move_to_line_start()` - Move to start of line
 - `editor:move_to_line_end()` - Move to end of line
 - `editor:load_file(path)` - Load a file
+- `editor:undo()` - Undo last change
+- `editor:redo()` - Redo last change
 - `editor:quit()` - Quit the editor
 
+**Window Management:**
+- `editor:split_horizontally()` - Split current window (Top/Bottom)
+- `editor:split_vertically()` - Split current window (Left/Right)
+- `editor:close_window()` - Close the active window
+- `editor:next_window()` - Cycle focus to the next window
+
 ### `lumacs` (module)
 Module namespace.
 
 **Classes:**
 - `lumacs.Position(line, column)` - Create a cursor position
+- `lumacs.Range(start_pos, end_pos)` - Create a range
+- `lumacs.TextAttribute(color, style)` - Create a text attribute
+
+**Enums:**
+- `lumacs.ColorType` - Colors (Keyword, String, Comment, etc.)
+- `lumacs.Style` - Styles (Normal, Bold, Italic, Underline)
+- `lumacs.BufferEvent` - Events (Loaded, AfterChange, etc.)
 
 ## Buffer Object
 
@@ -44,15 +59,24 @@ Returned by `editor:buffer()`.
 - `buffer:line_count()` - Get number of lines
 - `buffer:is_modified()` - Check if modified
 - `buffer:content()` - Get entire content as string
+- `buffer.language` - Get/Set language string (e.g., "lua", "cpp")
 
 **Methods:**
 - `buffer:line(index)` - Get line by index (0-based)
 - `buffer:insert(pos, text)` - Insert text at position
 - `buffer:insert_char(pos, char)` - Insert single character
 - `buffer:insert_newline(pos)` - Insert newline
+- `buffer:erase(range)` - Delete a range of text
 - `buffer:erase_char(pos)` - Delete character (backspace)
+- `buffer:replace(range, text)` - Replace text in a range
+- `buffer:find(query, start_pos)` - Find text starting from position. Returns `Range` or `nil`.
 - `buffer:save()` - Save buffer to file
 
+**Styling & Events:**
+- `buffer:set_style(range, attr)` - Apply syntax highlighting
+- `buffer:clear_styles()` - Clear all styles
+- `buffer:on_buffer_event(callback)` - Register an event listener
+
 ## Position Object
 
 Represents a cursor position.
@@ -66,6 +90,19 @@ local pos = lumacs.Position(line, column)
 - `pos.line` - Line number (0-based)
 - `pos.column` - Column number (0-based)
 
+## Range Object
+
+Represents a text range.
+
+**Constructor:**
+```lua
+local range = lumacs.Range(start_pos, end_pos)
+```
+
+**Fields:**
+- `range.start` - Start Position
+- `range.end` - End Position
+
 ## Global Functions
 
 ### `bind_key(key, callback)`
@@ -75,6 +112,7 @@ Bind a key to a Lua function.
 - Single characters: `"a"`, `"b"`, `"1"`, etc.
 - Special keys: `"Escape"`, `"ArrowUp"`, `"ArrowDown"`, `"ArrowLeft"`, `"ArrowRight"`, `"Home"`, `"End"`
 - Control keys: `"C-a"`, `"C-s"`, `"C-c"`, etc.
+- Meta/Alt keys: `"M-a"`, `"M-2"`, `"M-ArrowUp"`, etc.
 
 **Example:**
 ```lua
@@ -85,7 +123,7 @@ end)
 ```
 
 ### `message(text)`
-Display a message to the user (appears in stderr).
+Display a message to the user (appears in the minibuffer/status line).
 
 ```lua
 message("Hello from Lua!")
@@ -101,49 +139,34 @@ print("Debug info:", value)
 ## Example Configuration
 
 ```lua
--- Emacs-style navigation
-bind_key("C-n", function() editor:move_down() end)
-bind_key("C-p", function() editor:move_up() end)
-bind_key("C-f", function() editor:move_right() end)
-bind_key("C-b", function() editor:move_left() end)
-bind_key("C-a", function() editor:move_to_line_start() end)
-bind_key("C-e", function() editor:move_to_line_end() end)
-
--- Save buffer
-bind_key("C-s", function()
-    if editor:buffer():save() then
-        message("Saved!")
-    else
-        message("Failed to save")
-    end
+-- Window Management
+bind_key("M-2", function() editor:split_horizontally() end)
+bind_key("M-3", function() editor:split_vertically() end)
+bind_key("M-0", function() editor:close_window() end)
+bind_key("C-w", function() editor:next_window() end)
+
+-- Search
+bind_key("C-f", function()
+    -- Implementation using buffer:find()
 end)
-
--- Insert timestamp
-bind_key("C-t", function()
-    local timestamp = os.date("%Y-%m-%d %H:%M:%S")
-    editor:buffer():insert(editor:cursor(), timestamp)
-end)
-
--- Custom functions
-function goto_line(n)
-    editor:set_cursor(lumacs.Position(n - 1, 0))
-end
-
-message("Lumacs configured!")
 ```
 
-## Future Features
+## Interactive Command Buffer
+
+Press `:` to enter the command buffer. You can type Lua commands or editor shortcuts.
+- `:w` - Save
+- `:q` - Quit
+- `:wq` - Save and Quit
+- `:print(1+1)` - Execute Lua code
 
-Coming soon:
-- **Command buffer** - Execute Lua code interactively (like M-x eval in Emacs)
-- **Multiple buffers** - Switch between open files
-- **Window splits** - Split panes with different buffers
-- **Hooks** - Run code on events (file open, save, etc.)
-- **More buffer operations** - Search, replace, undo/redo
+## Events System
 
-## Tips
+Plugins can react to editor events:
 
-- Use `print()` for debugging (output goes to stderr)
-- Check the example `init.lua` for more ideas
-- Lua has full access to the editor state
-- Key bindings override default bindings
+```lua
+editor.buffer:on_buffer_event(function(event)
+    if event.event == lumacs.BufferEvent.Loaded then
+        print("File loaded: " .. event.language)
+    end
+end)
+```

+ 7 - 5
README.md

@@ -53,7 +53,7 @@ lumacs/
 ├── src/               # Implementation
 │   ├── main.cpp       # Entry point
 │   └── buffer.cpp     # Buffer implementation
-├── tests/             # Unit tests (TODO)
+├── tests/             # Unit tests
 ├── CMakeLists.txt     # Build configuration
 └── shell.nix          # Nix development environment
 ```
@@ -65,10 +65,12 @@ lumacs/
 - [x] Buffer implementation with basic operations
 - [x] Minimal FTXUI integration
 - [x] Lua runtime initialized
-- [ ] Keyboard input handling
-- [ ] Window splits
-- [ ] Lua configuration loading
-- [ ] Basic editing commands
+- [x] Keyboard input handling
+- [x] Window splits (M-2 horizontal, M-3 vertical, M-0 close, C-w next)
+- [x] Lua configuration loading
+- [x] Basic editing commands (Search/Replace API added)
+- [x] Unit tests
+- [x] Command Buffer (Minibuffer)
 
 ## Development
 

+ 149 - 0
include/lumacs/buffer.hpp

@@ -24,6 +24,83 @@ struct Range {
     auto operator<=>(const Range&) const = default;
 };
 
+/// Buffer events for hooks/callbacks
+enum class BufferEvent {
+    // Lifecycle events
+    Created,        // Buffer was created
+    Loaded,         // File was loaded into buffer
+    Closed,         // Buffer is being closed
+
+    // Modification events
+    BeforeChange,   // About to modify buffer (can be used to save state for undo)
+    AfterChange,    // Buffer content was modified
+    LineChanged,    // Specific line was modified
+
+    // File operations
+    BeforeSave,     // About to save
+    AfterSave,      // File was saved
+
+    // Language/mode
+    LanguageChanged // Buffer language/mode changed
+};
+
+/// Event data passed to callbacks
+struct BufferEventData {
+    BufferEvent event;
+    size_t line = 0;           // Line number for LineChanged events
+    std::string language = ""; // For LanguageChanged events
+};
+
+/// Text styling attributes for syntax highlighting
+struct TextAttribute {
+    enum class Style {
+        Normal = 0,
+        Bold = 1,
+        Italic = 2,
+        Underline = 4,
+        // Can combine with bitwise OR
+    };
+
+    // Common semantic colors for syntax highlighting
+    enum class ColorType {
+        Default,
+        Keyword,      // if, for, while, etc.
+        String,       // String literals
+        Comment,      // Comments
+        Function,     // Function names
+        Type,         // Type names, classes
+        Number,       // Numeric literals
+        Operator,     // +, -, *, etc.
+        Variable,     // Variable names
+        Constant,     // Constants, enums
+        Error,        // Error highlighting
+    };
+
+    ColorType color = ColorType::Default;
+    int style_flags = 0; // Combination of Style flags
+
+    TextAttribute() = default;
+    TextAttribute(ColorType c, int s = 0) : color(c), style_flags(s) {}
+};
+
+/// A range with associated styling
+struct StyledRange {
+    Range range;
+    TextAttribute attr;
+
+    StyledRange() = default;
+    StyledRange(Range r, TextAttribute a) : range(r), attr(a) {}
+};
+
+/// Undo/Redo state snapshot
+struct UndoState {
+    std::vector<std::string> lines;
+    Position cursor;
+
+    UndoState() = default;
+    UndoState(const std::vector<std::string>& l, Position c) : lines(l), cursor(c) {}
+};
+
 /// A text buffer that manages the content of a file or scratch buffer
 class Buffer {
 public:
@@ -78,6 +155,9 @@ public:
     /// Replace text in a range
     void replace(Range range, std::string_view text);
 
+    /// Find text starting from a position
+    [[nodiscard]] std::optional<Range> find(const std::string& query, Position start_pos) const;
+
     /// Clear the entire buffer
     void clear();
 
@@ -109,17 +189,86 @@ public:
     /// Clamp a position to valid bounds
     [[nodiscard]] Position clamp_position(Position pos) const noexcept;
 
+    // === Syntax Highlighting / Styling ===
+
+    /// Set styling for a range of text
+    void set_style(Range range, TextAttribute attr);
+
+    /// Get all styled ranges for a specific line
+    [[nodiscard]] const std::vector<StyledRange>& get_line_styles(size_t line) const;
+
+    /// Clear all styling information
+    void clear_styles();
+
+    /// Clear styling for a specific line
+    void clear_line_styles(size_t line);
+
+    // === Events & Hooks ===
+
+    using BufferEventCallback = std::function<void(const BufferEventData&)>;
+
+    /// Register a callback for buffer events
+    void on_buffer_event(BufferEventCallback callback);
+
+    /// Get the language/file type of this buffer
+    [[nodiscard]] const std::string& language() const noexcept { return language_; }
+
+    /// Set the language/file type (triggers LanguageChanged event)
+    void set_language(std::string lang);
+
+    /// Auto-detect language from file path
+    static std::string detect_language(const std::filesystem::path& path);
+
+    // === Undo/Redo ===
+
+    /// Undo the last change
+    bool undo(Position& out_cursor);
+
+    /// Redo the last undone change
+    bool redo(Position& out_cursor);
+
+    /// Check if undo is available
+    [[nodiscard]] bool can_undo() const noexcept { return !undo_stack_.empty(); }
+
+    /// Check if redo is available
+    [[nodiscard]] bool can_redo() const noexcept { return !redo_stack_.empty(); }
+
+    /// Save current state to undo stack (called automatically before changes)
+    void save_undo_state(Position cursor);
+
+    /// Clear redo stack (called when new change happens)
+    void clear_redo_stack();
+
 private:
     std::string name_;
     std::vector<std::string> lines_;
     std::optional<std::filesystem::path> file_path_;
     bool modified_;
+    std::string language_;
+
+    // Styling information: one vector of styled ranges per line
+    std::vector<std::vector<StyledRange>> line_styles_;
+
+    // Event callbacks
+    std::vector<BufferEventCallback> event_callbacks_;
+
+    // Undo/Redo stacks
+    std::vector<UndoState> undo_stack_;
+    std::vector<UndoState> redo_stack_;
+    static constexpr size_t MAX_UNDO_LEVELS = 100;
+    bool in_undo_redo_ = false; // Prevent saving state during undo/redo
 
     /// Ensure the buffer has at least one line
     void ensure_min_lines();
 
     /// Mark the buffer as modified
     void mark_modified();
+
+    /// Ensure styles vector matches lines vector size
+    void ensure_styles_size();
+
+    /// Emit a buffer event to all registered callbacks
+    void emit_event(BufferEvent event, size_t line = 0);
 };
 
 } // namespace lumacs

+ 92 - 37
include/lumacs/editor_core.hpp

@@ -1,27 +1,27 @@
 #pragma once
 
 #include "lumacs/buffer.hpp"
+#include "lumacs/window.hpp"
 #include <memory>
 #include <functional>
 #include <vector>
+#include <list>
 
 namespace lumacs {
 
-/// Viewport information for rendering
-struct Viewport {
-    int scroll_offset = 0;
-    int height = 20;
-    int width = 80;
-};
-
 /// Editor state change events
 enum class EditorEvent {
     BufferModified,
     CursorMoved,
     ViewportChanged,
+    WindowLayoutChanged, // New event
+    Message, // New event
+    CommandMode, // Trigger command mode (minibuffer)
     Quit
 };
 
+struct LayoutNode;
+
 /// Core editor logic, independent of UI framework
 class EditorCore {
 public:
@@ -34,27 +34,57 @@ public:
     EditorCore(EditorCore&&) noexcept = default;
     EditorCore& operator=(EditorCore&&) noexcept = default;
 
-    // === Buffer Management ===
+    // === Message System ===
+    void set_message(std::string msg) {
+        last_message_ = std::move(msg);
+        emit_event(EditorEvent::Message);
+    }
+    
+    const std::string& last_message() const { return last_message_; }
 
-    /// Get the current buffer
-    [[nodiscard]] const Buffer& buffer() const noexcept { return *buffer_; }
-    [[nodiscard]] Buffer& buffer() noexcept { return *buffer_; }
+    // === Actions ===
+    void enter_command_mode() {
+        emit_event(EditorEvent::CommandMode);
+    }
 
-    /// Load a file into the buffer
+    // === Buffer Management ===
+    
+    /// Get the current buffer (of the active window)
+    [[nodiscard]] const Buffer& buffer() const noexcept;
+    [[nodiscard]] Buffer& buffer() noexcept;
+
+    /// Load a file into the current window
     bool load_file(const std::filesystem::path& path);
 
-    /// Create a new empty buffer
+    /// Create a new empty buffer in current window
     void new_buffer(std::string name = "*scratch*");
 
-    // === Cursor Management ===
+    // === Window Management ===
+
+    /// Split the current window horizontally (active window becomes top, new one bottom)
+    void split_horizontally();
+
+    /// Split the current window vertically (active window becomes left, new one right)
+    void split_vertically();
+
+    /// Close the current window
+    void close_active_window();
+
+    /// Move focus to the next window
+    void next_window();
 
-    /// Get the current cursor position
-    [[nodiscard]] Position cursor() const noexcept { return cursor_; }
+    /// Get the active window
+    std::shared_ptr<Window> active_window() const { return active_window_; }
 
-    /// Set the cursor position (will be clamped to valid bounds)
+    /// Get the root of the layout tree (for rendering)
+    std::shared_ptr<LayoutNode> root_layout() const { return root_node_; }
+
+    // === Cursor Management (Proxies to active window) ===
+
+    [[nodiscard]] Position cursor() const noexcept;
     void set_cursor(Position pos);
 
-    // === Cursor Movement ===
+    // === Cursor Movement (Proxies to active window) ===
 
     void move_up();
     void move_down();
@@ -63,25 +93,17 @@ public:
     void move_to_line_start();
     void move_to_line_end();
 
-    // === Viewport Management ===
-
-    /// Get the current viewport
-    [[nodiscard]] const Viewport& viewport() const noexcept { return viewport_; }
-
-    /// Update viewport dimensions (typically called on window resize)
+    // === Viewport Management (Proxies to active window) ===
+    
+    const Viewport& viewport() const noexcept;
     void set_viewport_size(int width, int height);
-
-    /// Adjust scroll to ensure cursor is visible
     void adjust_scroll();
-
-    /// Get the range of visible lines [start, end)
-    [[nodiscard]] std::pair<size_t, size_t> visible_line_range() const;
+    std::pair<size_t, size_t> visible_line_range() const;
 
     // === Event Callbacks ===
 
     using EventCallback = std::function<void(EditorEvent)>;
 
-    /// Register a callback for editor events
     void on_event(EventCallback callback) {
         event_callbacks_.push_back(std::move(callback));
     }
@@ -92,16 +114,49 @@ public:
         emit_event(EditorEvent::Quit);
     }
 
+    // === Undo/Redo ===
+
+    bool undo();
+    bool redo();
+    bool can_undo() const;
+    bool can_redo() const;
+
 private:
-    std::unique_ptr<Buffer> buffer_;
-    Position cursor_;
-    Viewport viewport_;
-    std::vector<EventCallback> event_callbacks_;
+    // All open buffers
+    std::list<std::shared_ptr<Buffer>> buffers_;
+    
+    // Window layout
+    std::shared_ptr<LayoutNode> root_node_;
+    std::shared_ptr<Window> active_window_;
+    
+    std::string last_message_;
 
-    static constexpr int SCROLL_MARGIN = 3;
+    std::vector<EventCallback> event_callbacks_;
 
-    void clamp_cursor();
     void emit_event(EditorEvent event);
+    
+    // Helper to find a node containing the active window
+    LayoutNode* find_parent_node(LayoutNode* root, std::shared_ptr<Window> target);
+    
+    // Helper to collect all windows in traversal order
+    void collect_windows(LayoutNode* node, std::vector<std::shared_ptr<Window>>& windows);
+};
+
+struct LayoutNode {
+    enum class Type { Leaf, HorizontalSplit, VerticalSplit };
+    Type type;
+    
+    // If Leaf
+    std::shared_ptr<Window> window;
+    
+    // If Split
+    std::shared_ptr<LayoutNode> child1;
+    std::shared_ptr<LayoutNode> child2;
+    float ratio = 0.5f; // For future resizing
+    
+    LayoutNode(std::shared_ptr<Window> w) : type(Type::Leaf), window(w) {}
+    LayoutNode(Type t, std::shared_ptr<LayoutNode> c1, std::shared_ptr<LayoutNode> c2)
+        : type(t), child1(c1), child2(c2) {}
 };
 
-} // namespace lumacs
+} // namespace lumacs

+ 252 - 6
init.lua

@@ -37,7 +37,7 @@ end)
 
 -- Custom command: Save buffer
 bind_key("C-s", function()
-    local buf = editor:buffer()
+    local buf = editor.buffer
     if buf:save() then
         message("Buffer saved: " .. buf:name())
     else
@@ -47,9 +47,9 @@ end)
 
 -- Custom command: Insert timestamp
 bind_key("C-t", function()
-    local cursor_pos = editor:cursor()
+    local cursor_pos = editor.cursor
     local timestamp = os.date("%Y-%m-%d %H:%M:%S")
-    editor:buffer():insert(cursor_pos, timestamp)
+    editor.buffer:insert(cursor_pos, timestamp)
     message("Inserted timestamp")
 end)
 
@@ -62,8 +62,8 @@ end
 
 -- Example: Buffer inspection
 function buffer_info()
-    local buf = editor:buffer()
-    local cursor = editor:cursor()
+    local buf = editor.buffer
+    local cursor = editor.cursor
 
     message(string.format(
         "Buffer: %s | Lines: %d | Cursor: %d,%d | Modified: %s",
@@ -78,5 +78,251 @@ end
 -- Bind to show buffer info
 bind_key("C-i", buffer_info)
 
+-- Search helper function
+function find_next(query)
+    local buf = editor.buffer
+    local cursor = editor.cursor
+    -- Start searching AFTER the current cursor position to find the next occurrence
+    -- Otherwise we might find the same one if we are sitting on it.
+    -- A simple way is to advance column by 1 for the search start.
+    local search_start = lumacs.Position(cursor.line, cursor.column + 1)
+    
+    -- If at end of line, search from start of next line is handled by find() implementation? 
+    -- Buffer::find currently implements simple linear search from a position.
+    -- If column is beyond end, it should handle it. Let's trust the C++ impl or adjust.
+    
+    local res = buf:find(query, search_start)
+    if res then
+        editor.cursor = res.start
+        message("Found '" .. query .. "' at " .. res.start.line .. ":" .. res.start.column)
+        -- Optional: Highlight the found range?
+    else
+        message("'" .. query .. "' not found")
+    end
+end
+
+-- Example binding: Find "TODO"
+bind_key("C-o", function() find_next("TODO") end)
+
+-- Line swapping functions (like Emacs M-up/down or VS Code Alt+arrows)
+function swap_line_up()
+    local buf = editor.buffer
+    local cursor = editor.cursor
+
+    -- Can't move first line up
+    if cursor.line == 0 then
+        message("Already at first line")
+        return
+    end
+
+    message("DEBUG: Starting swap_line_up, cursor at line " .. cursor.line)
+
+    -- Get the current line and above line text
+    local current_line = buf:line(cursor.line)
+    local above_line = buf:line(cursor.line - 1)
+
+    -- Strategy: Replace both lines with them swapped
+    -- Delete from start of line above to end of current line (not including next line)
+    local delete_start = lumacs.Position(cursor.line - 1, 0)
+    local delete_end = lumacs.Position(cursor.line, string.len(current_line))
+
+    local range = lumacs.Range(delete_start, delete_end)
+    buf:erase(range)
+
+    -- Insert them back in swapped order
+    -- Add extra newline if above_line is empty to preserve it
+    local insert_pos = lumacs.Position(cursor.line - 1, 0)
+    local text = current_line .. "\n" .. above_line
+    if above_line == "" then
+        text = text .. "\n"
+    end
+    buf:insert(insert_pos, text)
+
+    -- Move cursor up to follow the line
+    editor.cursor = lumacs.Position(cursor.line - 1, cursor.column)
+    message("Swapped line up")
+end
+
+function swap_line_down()
+    local buf = editor.buffer
+    local cursor = editor.cursor
+
+    -- Can't move last line down
+    if cursor.line >= buf:line_count() - 1 then
+        message("Already at last line")
+        return
+    end
+
+    -- Get the current line and the line below
+    local current_line = buf:line(cursor.line)
+    local below_line = buf:line(cursor.line + 1)
+
+    -- Strategy: Replace both lines with them swapped
+    -- Delete from start of current line to end of line below (not including line after)
+    local delete_start = lumacs.Position(cursor.line, 0)
+    local delete_end = lumacs.Position(cursor.line + 1, string.len(below_line))
+    local range = lumacs.Range(delete_start, delete_end)
+    buf:erase(range)
+
+    -- Insert them back in swapped order
+    -- Add extra newline if current_line is empty to preserve it
+    local insert_pos = lumacs.Position(cursor.line, 0)
+    local text = below_line .. "\n" .. current_line
+    if current_line == "" then
+        text = text .. "\n"
+    end
+    buf:insert(insert_pos, text)
+
+    -- Move cursor down to follow the line
+    editor.cursor = lumacs.Position(cursor.line + 1, cursor.column)
+    message("Swapped line down")
+end
+
+-- Bind to M-ArrowUp and M-ArrowDown (Meta/Alt + arrows)
+bind_key("M-ArrowUp", swap_line_up)
+bind_key("M-ArrowDown", swap_line_down)
+
+-- Window Management Bindings
+bind_key("C-w", function()
+    editor:next_window()
+    -- message("Switched window")
+end)
+
+-- Split horizontal (like Emacs C-x 2, simplified to M-2)
+bind_key("M-2", function()
+    editor:split_horizontally()
+    message("Split horizontally")
+end)
+
+-- Split vertical (like Emacs C-x 3, simplified to M-3)
+bind_key("M-3", function()
+    editor:split_vertically()
+    message("Split vertically")
+end)
+
+-- Close window (like Emacs C-x 0, simplified to M-0)
+bind_key("M-0", function()
+    editor:close_window()
+    message("Closed window")
+end)
+
+-- Command Mode (Minibuffer)
+bind_key("M-x", function()
+    editor:command_mode()
+end)
+
+-- Undo/Redo
+bind_key("C-z", function()
+    if editor:undo() then
+        message("Undid change")
+    else
+        message("Nothing to undo")
+    end
+end)
+
+bind_key("C-y", function()
+    if editor:redo() then
+        message("Redid change")
+    else
+        message("Nothing to redo")
+    end
+end)
+
+-- Simple syntax highlighter for demonstration
+function highlight_buffer()
+    local buf = editor.buffer
+    buf:clear_styles()  -- Clear existing styles
+
+    -- Keywords to highlight
+    local keywords = {
+        "function", "local", "end", "if", "then", "else", "elseif",
+        "for", "while", "do", "return", "break", "and", "or", "not"
+    }
+
+    -- Highlight each line
+    for line_num = 0, buf:line_count() - 1 do
+        local line_text = buf:line(line_num)
+
+        -- Highlight keywords
+        for _, keyword in ipairs(keywords) do
+            local start_pos = 1
+            while true do
+                -- Find keyword with word boundaries
+                local pattern = "%f[%w]" .. keyword .. "%f[%W]"
+                local pos = string.find(line_text, pattern, start_pos)
+                if not pos then break end
+
+                local range = lumacs.Range(
+                    lumacs.Position(line_num, pos - 1),
+                    lumacs.Position(line_num, pos + #keyword - 1)
+                )
+
+                buf:set_style(range, lumacs.TextAttribute(lumacs.ColorType.Keyword, 0))
+                start_pos = pos + #keyword
+            end
+        end
+
+        -- Highlight strings (simple version - just finds quoted text)
+        local start_pos = 1
+        while true do
+            local quote_start = string.find(line_text, '"', start_pos, true)
+            if not quote_start then break end
+
+            local quote_end = string.find(line_text, '"', quote_start + 1, true)
+            if not quote_end then break end
+
+            local range = lumacs.Range(
+                lumacs.Position(line_num, quote_start - 1),
+                lumacs.Position(line_num, quote_end)
+            )
+
+            buf:set_style(range, lumacs.TextAttribute(lumacs.ColorType.String, 0))
+            start_pos = quote_end + 1
+        end
+
+        -- Highlight comments
+        local comment_pos = string.find(line_text, "%-%-", 1, true)
+        if comment_pos then
+            local range = lumacs.Range(
+                lumacs.Position(line_num, comment_pos - 1),
+                lumacs.Position(line_num, #line_text)
+            )
+
+            buf:set_style(range, lumacs.TextAttribute(lumacs.ColorType.Comment, 0))
+        end
+    end
+end
+
+-- Auto-highlighting using events!
+-- Register event handler to automatically highlight when buffer loads or changes
+editor.buffer:on_buffer_event(function(event_data)
+    local buf = editor.buffer
+
+    -- Auto-highlight on these events
+    if event_data.event == lumacs.BufferEvent.Loaded or
+       event_data.event == lumacs.BufferEvent.LanguageChanged then
+        -- Only highlight Lua files automatically
+        if buf.language == "lua" then
+            highlight_buffer()
+            print(string.format("[Auto-highlight] Applied to %s buffer", buf.language))
+        end
+    end
+end)
+
+-- Manual highlight key (C-l) for any language
+bind_key("C-l", function()
+    highlight_buffer()
+
+    -- Debug: Count applied styles
+    local buf = editor.buffer
+    local styles_count = 0
+    for line = 0, buf:line_count() - 1 do
+        local styles = buf:get_line_styles(line)
+        styles_count = styles_count + #styles
+    end
+
+    message(string.format("Highlighted! Applied %d styled ranges", styles_count))
+end)
+
 -- Welcome message
-message("Lumacs initialized! Press C-i for buffer info, C-s to save.")
+message("Lumacs ready! C-s=save, C-l=highlight, M-arrows=swap lines, C-i=info, Esc/Q=quit")

+ 283 - 6
src/buffer.cpp

@@ -5,10 +5,16 @@
 
 namespace lumacs {
 
-Buffer::Buffer() : name_("*scratch*"), lines_{""}, modified_(false) {}
+Buffer::Buffer() : name_("*scratch*"), lines_{""}, modified_(false), language_("text") {
+    line_styles_.resize(1); // Match lines_ size
+    emit_event(BufferEvent::Created);
+}
 
 Buffer::Buffer(std::string name)
-    : name_(std::move(name)), lines_{""}, modified_(false) {}
+    : name_(std::move(name)), lines_{""}, modified_(false), language_("text") {
+    line_styles_.resize(1); // Match lines_ size
+    emit_event(BufferEvent::Created);
+}
 
 std::optional<Buffer> Buffer::from_file(const std::filesystem::path& path) {
     std::ifstream file(path);
@@ -20,6 +26,7 @@ std::optional<Buffer> Buffer::from_file(const std::filesystem::path& path) {
     buffer.file_path_ = path;
     buffer.name_ = path.filename().string();
     buffer.lines_.clear();
+    buffer.line_styles_.clear();
     buffer.modified_ = false;
 
     std::string line;
@@ -32,6 +39,15 @@ std::optional<Buffer> Buffer::from_file(const std::filesystem::path& path) {
         buffer.lines_.push_back("");
     }
 
+    // Initialize styles to match lines
+    buffer.line_styles_.resize(buffer.lines_.size());
+
+    // Auto-detect language
+    buffer.language_ = detect_language(path);
+
+    // Emit loaded event
+    buffer.emit_event(BufferEvent::Loaded);
+
     return buffer;
 }
 
@@ -71,6 +87,7 @@ void Buffer::insert(Position pos, std::string_view text) {
         return;
     }
 
+    emit_event(BufferEvent::BeforeChange, pos.line);
     pos = clamp_position(pos);
 
     // Handle multiline text insertion
@@ -96,37 +113,49 @@ void Buffer::insert(Position pos, std::string_view text) {
         std::string after = current_line.substr(pos.column);
         current_line = current_line.substr(0, pos.column) + new_lines[0];
 
+        ensure_styles_size();
+
         // Insert middle lines
         for (size_t i = 1; i < new_lines.size() - 1; ++i) {
             lines_.insert(lines_.begin() + pos.line + i, new_lines[i]);
+            line_styles_.insert(line_styles_.begin() + pos.line + i, {});
         }
 
         // Insert last line with remaining text
         lines_.insert(lines_.begin() + pos.line + new_lines.size() - 1,
                      new_lines.back() + after);
+        line_styles_.insert(line_styles_.begin() + pos.line + new_lines.size() - 1, {});
     }
 
     mark_modified();
+    emit_event(BufferEvent::AfterChange, pos.line);
 }
 
 void Buffer::insert_char(Position pos, char c) {
+    emit_event(BufferEvent::BeforeChange, pos.line);
     pos = clamp_position(pos);
     lines_[pos.line].insert(pos.column, 1, c);
     mark_modified();
+    emit_event(BufferEvent::AfterChange, pos.line);
 }
 
 void Buffer::insert_newline(Position pos) {
+    emit_event(BufferEvent::BeforeChange, pos.line);
     pos = clamp_position(pos);
 
     auto& current_line = lines_[pos.line];
     std::string after = current_line.substr(pos.column);
     current_line = current_line.substr(0, pos.column);
 
+    ensure_styles_size();
     lines_.insert(lines_.begin() + pos.line + 1, std::move(after));
+    line_styles_.insert(line_styles_.begin() + pos.line + 1, {});
     mark_modified();
+    emit_event(BufferEvent::AfterChange, pos.line);
 }
 
 void Buffer::erase(Range range) {
+    emit_event(BufferEvent::BeforeChange, range.start.line);
     if (range.start >= range.end) {
         return;
     }
@@ -135,27 +164,39 @@ void Buffer::erase(Range range) {
     range.end = clamp_position(range.end);
 
     if (range.start.line == range.end.line) {
-        // Single line deletion
+        // Single line deletion - styles on this line are invalidated
         auto& line = lines_[range.start.line];
         line.erase(range.start.column, range.end.column - range.start.column);
+
+        // Clear styles for this line since positions have changed
+        if (range.start.line < line_styles_.size()) {
+            line_styles_[range.start.line].clear();
+        }
     } else {
         // Multiline deletion
         std::string prefix = lines_[range.start.line].substr(0, range.start.column);
         std::string suffix = lines_[range.end.line].substr(range.end.column);
 
-        // Remove lines in between
+        ensure_styles_size();
+
+        // Remove lines in between (and their styles)
         lines_.erase(lines_.begin() + range.start.line,
                     lines_.begin() + range.end.line + 1);
+        line_styles_.erase(line_styles_.begin() + range.start.line,
+                          line_styles_.begin() + range.end.line + 1);
 
         // Insert merged line
         lines_.insert(lines_.begin() + range.start.line, prefix + suffix);
+        line_styles_.insert(line_styles_.begin() + range.start.line, {});
     }
 
     ensure_min_lines();
     mark_modified();
+    emit_event(BufferEvent::AfterChange, range.start.line);
 }
 
 void Buffer::erase_char(Position pos) {
+    emit_event(BufferEvent::BeforeChange, pos.line);
     if (pos.column == 0 && pos.line == 0) {
         return; // Nothing to delete
     }
@@ -163,16 +204,28 @@ void Buffer::erase_char(Position pos) {
     pos = clamp_position(pos);
 
     if (pos.column > 0) {
-        // Delete character before cursor
+        // Delete character before cursor - clear styles for this line
         lines_[pos.line].erase(pos.column - 1, 1);
+        if (pos.line < line_styles_.size()) {
+            line_styles_[pos.line].clear();
+        }
     } else if (pos.line > 0) {
         // Join with previous line
         std::string current = std::move(lines_[pos.line]);
+
+        ensure_styles_size();
         lines_.erase(lines_.begin() + pos.line);
+        line_styles_.erase(line_styles_.begin() + pos.line);
+
         lines_[pos.line - 1] += current;
+        // Clear styles for merged line
+        if (pos.line - 1 < line_styles_.size()) {
+            line_styles_[pos.line - 1].clear();
+        }
     }
 
     mark_modified();
+    emit_event(BufferEvent::AfterChange, pos.line);
 }
 
 void Buffer::replace(Range range, std::string_view text) {
@@ -180,9 +233,37 @@ void Buffer::replace(Range range, std::string_view text) {
     insert(range.start, text);
 }
 
+std::optional<Range> Buffer::find(const std::string& query, Position start_pos) const {
+    if (query.empty()) {
+        return std::nullopt;
+    }
+
+    // Simple linear search for now
+    // Start from the start_pos line and column
+    
+    start_pos = clamp_position(start_pos);
+    
+    for (size_t i = start_pos.line; i < lines_.size(); ++i) {
+        const auto& line_text = lines_[i];
+        size_t start_col = (i == start_pos.line) ? start_pos.column : 0;
+        
+        size_t found = line_text.find(query, start_col);
+        if (found != std::string::npos) {
+            Position start = {i, found};
+            Position end = {i, found + query.length()};
+            return Range{start, end};
+        }
+    }
+    
+    // Note: This only searches within lines (no multi-line search yet)
+    return std::nullopt;
+}
+
 void Buffer::clear() {
     lines_.clear();
     lines_.push_back("");
+    line_styles_.clear();
+    line_styles_.resize(1);
     mark_modified();
 }
 
@@ -196,6 +277,8 @@ bool Buffer::save() {
 }
 
 bool Buffer::save_as(const std::filesystem::path& path) {
+    emit_event(BufferEvent::BeforeSave);
+
     std::ofstream file(path);
     if (!file.is_open()) {
         return false;
@@ -215,6 +298,8 @@ bool Buffer::save_as(const std::filesystem::path& path) {
     file_path_ = path;
     name_ = path.filename().string();
     modified_ = false;
+
+    emit_event(BufferEvent::AfterSave);
     return true;
 }
 
@@ -258,6 +343,7 @@ Position Buffer::clamp_position(Position pos) const noexcept {
 void Buffer::ensure_min_lines() {
     if (lines_.empty()) {
         lines_.push_back("");
+        line_styles_.resize(1);
     }
 }
 
@@ -265,4 +351,195 @@ void Buffer::mark_modified() {
     modified_ = true;
 }
 
-} // namespace lumacs
+void Buffer::ensure_styles_size() {
+    if (line_styles_.size() != lines_.size()) {
+        line_styles_.resize(lines_.size());
+    }
+}
+
+// === Syntax Highlighting / Styling ===
+
+void Buffer::set_style(Range range, TextAttribute attr) {
+    ensure_styles_size();
+
+    // Clamp the range to valid positions
+    range.start = clamp_position(range.start);
+    range.end = clamp_position(range.end);
+
+    if (range.start >= range.end) {
+        return; // Invalid range
+    }
+
+    // For simplicity, only handle single-line styles for now
+    // Multi-line styles can be added later if needed
+    if (range.start.line != range.end.line) {
+        // Could split into multiple single-line ranges
+        // For now, just handle the first line
+        Range single_line = range;
+        single_line.end.line = range.start.line;
+        single_line.end.column = lines_[range.start.line].size();
+        line_styles_[range.start.line].push_back({single_line, attr});
+        return;
+    }
+
+    // Add the styled range to the appropriate line
+    line_styles_[range.start.line].push_back({range, attr});
+}
+
+const std::vector<StyledRange>& Buffer::get_line_styles(size_t line) const {
+    static const std::vector<StyledRange> empty;
+
+    if (line >= line_styles_.size()) {
+        return empty;
+    }
+
+    return line_styles_[line];
+}
+
+void Buffer::clear_styles() {
+    line_styles_.clear();
+    line_styles_.resize(lines_.size());
+}
+
+void Buffer::clear_line_styles(size_t line) {
+    ensure_styles_size();
+
+    if (line < line_styles_.size()) {
+        line_styles_[line].clear();
+    }
+}
+
+// === Events & Hooks ===
+
+void Buffer::on_buffer_event(BufferEventCallback callback) {
+    event_callbacks_.push_back(std::move(callback));
+}
+
+void Buffer::emit_event(BufferEvent event, size_t line) {
+    BufferEventData data;
+    data.event = event;
+    data.line = line;
+    data.language = language_;
+
+    for (const auto& callback : event_callbacks_) {
+        callback(data);
+    }
+}
+
+void Buffer::set_language(std::string lang) {
+    if (language_ != lang) {
+        language_ = std::move(lang);
+        emit_event(BufferEvent::LanguageChanged);
+    }
+}
+
+std::string Buffer::detect_language(const std::filesystem::path& path) {
+    auto ext = path.extension().string();
+
+    // Convert to lowercase
+    std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
+
+    // Map extensions to language names
+    if (ext == ".lua") return "lua";
+    if (ext == ".cpp" || ext == ".cc" || ext == ".cxx") return "cpp";
+    if (ext == ".c") return "c";
+    if (ext == ".h" || ext == ".hpp" || ext == ".hxx") return "cpp";
+    if (ext == ".py") return "python";
+    if (ext == ".js") return "javascript";
+    if (ext == ".ts") return "typescript";
+    if (ext == ".rs") return "rust";
+    if (ext == ".go") return "go";
+    if (ext == ".java") return "java";
+    if (ext == ".rb") return "ruby";
+    if (ext == ".sh") return "bash";
+    if (ext == ".md") return "markdown";
+    if (ext == ".json") return "json";
+    if (ext == ".xml") return "xml";
+    if (ext == ".html" || ext == ".htm") return "html";
+    if (ext == ".css") return "css";
+
+    return "text"; // Default fallback
+}
+
+// === Undo/Redo ===
+
+void Buffer::save_undo_state(Position cursor) {
+    // Don't save state during undo/redo operations
+    if (in_undo_redo_) {
+        return;
+    }
+
+    // Save current state
+    undo_stack_.emplace_back(lines_, cursor);
+
+    // Limit undo stack size
+    if (undo_stack_.size() > MAX_UNDO_LEVELS) {
+        undo_stack_.erase(undo_stack_.begin());
+    }
+}
+
+void Buffer::clear_redo_stack() {
+    if (!in_undo_redo_) {
+        redo_stack_.clear();
+    }
+}
+
+bool Buffer::undo(Position& out_cursor) {
+    if (undo_stack_.empty()) {
+        return false;
+    }
+
+    in_undo_redo_ = true;
+
+    // Save current state to redo stack
+    redo_stack_.emplace_back(lines_, out_cursor);
+
+    // Restore previous state
+    auto& state = undo_stack_.back();
+    lines_ = state.lines;
+    out_cursor = state.cursor;
+
+    // Remove from undo stack
+    undo_stack_.pop_back();
+
+    // Update styles size
+    line_styles_.clear();
+    line_styles_.resize(lines_.size());
+
+    mark_modified();
+    in_undo_redo_ = false;
+
+    emit_event(BufferEvent::AfterChange);
+    return true;
+}
+
+bool Buffer::redo(Position& out_cursor) {
+    if (redo_stack_.empty()) {
+        return false;
+    }
+
+    in_undo_redo_ = true;
+
+    // Save current state to undo stack
+    undo_stack_.emplace_back(lines_, out_cursor);
+
+    // Restore next state
+    auto& state = redo_stack_.back();
+    lines_ = state.lines;
+    out_cursor = state.cursor;
+
+    // Remove from redo stack
+    redo_stack_.pop_back();
+
+    // Update styles size
+    line_styles_.clear();
+    line_styles_.resize(lines_.size());
+
+    mark_modified();
+    in_undo_redo_ = false;
+
+    emit_event(BufferEvent::AfterChange);
+    return true;
+}
+
+} // namespace lumacs

+ 271 - 81
src/editor_core.cpp

@@ -1,25 +1,40 @@
 #include "lumacs/editor_core.hpp"
 #include <algorithm>
+#include <iostream>
 
 namespace lumacs {
 
-EditorCore::EditorCore()
-    : buffer_(std::make_unique<Buffer>())
-    , cursor_{0, 0}
-{
+EditorCore::EditorCore() {
+    // Create initial buffer
+    auto buffer = std::make_shared<Buffer>();
+    buffers_.push_back(buffer);
+    
+    // Create initial window
+    active_window_ = std::make_shared<Window>(buffer);
+    root_node_ = std::make_shared<LayoutNode>(active_window_);
 }
 
 // === Buffer Management ===
 
+const Buffer& EditorCore::buffer() const noexcept {
+    return active_window_->buffer();
+}
+
+Buffer& EditorCore::buffer() noexcept {
+    return active_window_->buffer();
+}
+
 bool EditorCore::load_file(const std::filesystem::path& path) {
-    auto new_buffer = Buffer::from_file(path);
-    if (!new_buffer) {
+    auto new_buffer_opt = Buffer::from_file(path);
+    if (!new_buffer_opt) {
         return false;
     }
-
-    buffer_ = std::make_unique<Buffer>(std::move(*new_buffer));
-    cursor_ = {0, 0};
-    viewport_.scroll_offset = 0;
+    
+    auto new_buffer = std::make_shared<Buffer>(std::move(*new_buffer_opt));
+    buffers_.push_back(new_buffer);
+    
+    active_window_->set_buffer(new_buffer);
+    
     emit_event(EditorEvent::BufferModified);
     emit_event(EditorEvent::CursorMoved);
     emit_event(EditorEvent::ViewportChanged);
@@ -27,125 +42,300 @@ bool EditorCore::load_file(const std::filesystem::path& path) {
 }
 
 void EditorCore::new_buffer(std::string name) {
-    buffer_ = std::make_unique<Buffer>(std::move(name));
-    cursor_ = {0, 0};
-    viewport_.scroll_offset = 0;
+    auto new_buffer = std::make_shared<Buffer>(std::move(name));
+    buffers_.push_back(new_buffer);
+    active_window_->set_buffer(new_buffer);
+    
     emit_event(EditorEvent::BufferModified);
     emit_event(EditorEvent::CursorMoved);
     emit_event(EditorEvent::ViewportChanged);
 }
 
-// === Cursor Management ===
+// === Window Management ===
 
-void EditorCore::set_cursor(Position pos) {
-    cursor_ = buffer_->clamp_position(pos);
-    adjust_scroll();
-    emit_event(EditorEvent::CursorMoved);
+// Helper to recursively replace a window in the tree
+bool replace_window_node(std::shared_ptr<LayoutNode> node, 
+                        std::shared_ptr<Window> target, 
+                        std::shared_ptr<LayoutNode> replacement) {
+    if (node->type == LayoutNode::Type::Leaf) {
+        return false;
+    }
+    
+    // Check immediate children
+    if (node->child1->type == LayoutNode::Type::Leaf) {
+        if (node->child1->window == target) {
+            node->child1 = replacement;
+            return true;
+        }
+    }
+    
+    if (node->child2->type == LayoutNode::Type::Leaf) {
+        if (node->child2->window == target) {
+            node->child2 = replacement;
+            return true;
+        }
+    }
+    
+    // Recurse
+    bool found = false;
+    if (node->child1->type != LayoutNode::Type::Leaf) {
+        found = replace_window_node(node->child1, target, replacement);
+    }
+    
+    if (!found && node->child2->type != LayoutNode::Type::Leaf) {
+        found = replace_window_node(node->child2, target, replacement);
+    }
+    
+    return found;
 }
 
-// === Cursor Movement ===
+void EditorCore::split_horizontally() {
+    // New window sharing same buffer
+    auto new_window = std::make_shared<Window>(active_window_->buffer_ptr());
+    new_window->set_cursor(active_window_->cursor()); // Start at same position
+    
+    // Create split node
+    auto new_leaf = std::make_shared<LayoutNode>(new_window);
+    auto current_leaf = std::make_shared<LayoutNode>(active_window_);
+    
+    auto split = std::make_shared<LayoutNode>(
+        LayoutNode::Type::HorizontalSplit,
+        current_leaf, // Top
+        new_leaf      // Bottom
+    );
+    
+    if (root_node_->type == LayoutNode::Type::Leaf && root_node_->window == active_window_) {
+        root_node_ = split;
+    } else {
+        replace_window_node(root_node_, active_window_, split);
+    }
+    
+    active_window_ = new_window; // Focus new window
+    emit_event(EditorEvent::WindowLayoutChanged);
+}
 
-void EditorCore::move_up() {
-    if (cursor_.line > 0) {
-        cursor_.line--;
-        clamp_cursor();
-        adjust_scroll();
-        emit_event(EditorEvent::CursorMoved);
+void EditorCore::split_vertically() {
+    // New window sharing same buffer
+    auto new_window = std::make_shared<Window>(active_window_->buffer_ptr());
+    new_window->set_cursor(active_window_->cursor());
+    
+    // Create split node
+    auto new_leaf = std::make_shared<LayoutNode>(new_window);
+    auto current_leaf = std::make_shared<LayoutNode>(active_window_);
+    
+    // Vertical Split = Left/Right division
+    auto split = std::make_shared<LayoutNode>(
+        LayoutNode::Type::VerticalSplit,
+        current_leaf, // Left
+        new_leaf      // Right
+    );
+    
+    if (root_node_->type == LayoutNode::Type::Leaf && root_node_->window == active_window_) {
+        root_node_ = split;
+    } else {
+        replace_window_node(root_node_, active_window_, split);
     }
+    
+    active_window_ = new_window;
+    emit_event(EditorEvent::WindowLayoutChanged);
 }
 
-void EditorCore::move_down() {
-    if (cursor_.line < buffer_->line_count() - 1) {
-        cursor_.line++;
-        clamp_cursor();
-        adjust_scroll();
-        emit_event(EditorEvent::CursorMoved);
+// Recursive parent finder
+LayoutNode* find_parent_of_node(LayoutNode* current, LayoutNode* child_target) {
+    if (current->type == LayoutNode::Type::Leaf) return nullptr;
+    
+    if (current->child1.get() == child_target || current->child2.get() == child_target) {
+        return current;
     }
+    
+    auto left = find_parent_of_node(current->child1.get(), child_target);
+    if (left) return left;
+    
+    return find_parent_of_node(current->child2.get(), child_target);
 }
 
-void EditorCore::move_left() {
-    if (cursor_.column > 0) {
-        cursor_.column--;
-    } else if (cursor_.line > 0) {
-        // Move to end of previous line
-        cursor_.line--;
-        cursor_.column = buffer_->line(cursor_.line).size();
-        adjust_scroll();
+// Recursive leaf finder
+LayoutNode* find_node_with_window(LayoutNode* current, std::shared_ptr<Window> target) {
+    if (current->type == LayoutNode::Type::Leaf) {
+        return (current->window == target) ? current : nullptr;
     }
-    emit_event(EditorEvent::CursorMoved);
+    auto left = find_node_with_window(current->child1.get(), target);
+    if (left) return left;
+    return find_node_with_window(current->child2.get(), target);
 }
 
-void EditorCore::move_right() {
-    const auto& line = buffer_->line(cursor_.line);
-    if (cursor_.column < line.size()) {
-        cursor_.column++;
-    } else if (cursor_.line < buffer_->line_count() - 1) {
-        // Move to start of next line
-        cursor_.line++;
-        cursor_.column = 0;
-        adjust_scroll();
+
+void EditorCore::close_active_window() {
+    // Cannot close last window
+    if (root_node_->type == LayoutNode::Type::Leaf) {
+        return;
     }
+    
+    // Find the node containing active_window
+    LayoutNode* target_node = find_node_with_window(root_node_.get(), active_window_);
+    if (!target_node) return; // Should not happen
+    
+    LayoutNode* parent = find_parent_of_node(root_node_.get(), target_node);
+    if (!parent) return; // Should not happen if not root
+    
+    // Identify sibling
+    std::shared_ptr<LayoutNode> sibling;
+    if (parent->child1.get() == target_node) {
+        sibling = parent->child2;
+    } else {
+        sibling = parent->child1;
+    }
+    
+    // Replace parent with sibling
+    // If parent is root, root becomes sibling
+    if (parent == root_node_.get()) {
+        root_node_ = sibling;
+    } else {
+        // Find grandparent
+        LayoutNode* grandparent = find_parent_of_node(root_node_.get(), parent);
+        if (grandparent->child1.get() == parent) {
+            grandparent->child1 = sibling;
+        } else {
+            grandparent->child2 = sibling;
+        }
+    }
+    
+    // Focus a new window (first leaf in sibling)
+    std::vector<std::shared_ptr<Window>> windows;
+    collect_windows(sibling.get(), windows);
+    if (!windows.empty()) {
+        active_window_ = windows[0];
+    }
+    
+    emit_event(EditorEvent::WindowLayoutChanged);
+}
+
+void EditorCore::collect_windows(LayoutNode* node, std::vector<std::shared_ptr<Window>>& windows) {
+    if (node->type == LayoutNode::Type::Leaf) {
+        windows.push_back(node->window);
+    } else {
+        collect_windows(node->child1.get(), windows);
+        collect_windows(node->child2.get(), windows);
+    }
+}
+
+void EditorCore::next_window() {
+    std::vector<std::shared_ptr<Window>> windows;
+    collect_windows(root_node_.get(), windows);
+    
+    if (windows.size() <= 1) return;
+    
+    auto it = std::find(windows.begin(), windows.end(), active_window_);
+    if (it != windows.end()) {
+        auto next = it + 1;
+        if (next == windows.end()) {
+            active_window_ = windows[0];
+        } else {
+            active_window_ = *next;
+        }
+        emit_event(EditorEvent::CursorMoved); // Focus change is like cursor move
+        emit_event(EditorEvent::ViewportChanged);
+    }
+}
+
+// === Cursor Proxies ===
+
+Position EditorCore::cursor() const noexcept {
+    return active_window_->cursor();
+}
+
+void EditorCore::set_cursor(Position pos) {
+    active_window_->set_cursor(pos);
     emit_event(EditorEvent::CursorMoved);
 }
 
+void EditorCore::move_up() {
+    active_window_->move_up();
+    emit_event(EditorEvent::CursorMoved);
+}
+void EditorCore::move_down() {
+    active_window_->move_down();
+    emit_event(EditorEvent::CursorMoved);
+}
+void EditorCore::move_left() {
+    active_window_->move_left();
+    emit_event(EditorEvent::CursorMoved);
+}
+void EditorCore::move_right() {
+    active_window_->move_right();
+    emit_event(EditorEvent::CursorMoved);
+}
 void EditorCore::move_to_line_start() {
-    cursor_.column = 0;
+    active_window_->move_to_line_start();
     emit_event(EditorEvent::CursorMoved);
 }
-
 void EditorCore::move_to_line_end() {
-    cursor_.column = buffer_->line(cursor_.line).size();
+    active_window_->move_to_line_end();
     emit_event(EditorEvent::CursorMoved);
 }
 
-// === Viewport Management ===
+// === Viewport Proxies ===
+
+const Viewport& EditorCore::viewport() const noexcept {
+    return active_window_->viewport();
+}
 
 void EditorCore::set_viewport_size(int width, int height) {
-    viewport_.width = std::max(1, width);
-    viewport_.height = std::max(1, height);
-    adjust_scroll();
+    active_window_->set_viewport_size(width, height);
     emit_event(EditorEvent::ViewportChanged);
 }
 
 void EditorCore::adjust_scroll() {
-    // Ensure cursor is visible with a margin from edges
-    viewport_.height = std::max(10, viewport_.height);
+    active_window_->adjust_scroll();
+    emit_event(EditorEvent::ViewportChanged);
+}
 
-    // Scroll down if cursor is below visible area
-    if (static_cast<int>(cursor_.line) >= viewport_.scroll_offset + viewport_.height - SCROLL_MARGIN) {
-        viewport_.scroll_offset = static_cast<int>(cursor_.line) - viewport_.height + SCROLL_MARGIN + 1;
-        emit_event(EditorEvent::ViewportChanged);
-    }
+std::pair<size_t, size_t> EditorCore::visible_line_range() const {
+    return active_window_->visible_line_range();
+}
 
-    // Scroll up if cursor is above visible area
-    if (static_cast<int>(cursor_.line) < viewport_.scroll_offset + SCROLL_MARGIN) {
-        viewport_.scroll_offset = (cursor_.line >= static_cast<size_t>(SCROLL_MARGIN))
-                                ? static_cast<int>(cursor_.line) - SCROLL_MARGIN
-                                : 0;
-        emit_event(EditorEvent::ViewportChanged);
-    }
+// === Undo/Redo ===
 
-    // Ensure scroll offset is valid
-    viewport_.scroll_offset = std::max(0, viewport_.scroll_offset);
+bool EditorCore::undo() {
+    auto& buf = active_window_->buffer();
+    buf.save_undo_state(active_window_->cursor()); // Save state before
+
+    Position new_cursor = active_window_->cursor();
+    if (buf.undo(new_cursor)) {
+        active_window_->set_cursor(new_cursor);
+        emit_event(EditorEvent::CursorMoved);
+        emit_event(EditorEvent::BufferModified);
+        return true;
+    }
+    return false;
 }
 
-std::pair<size_t, size_t> EditorCore::visible_line_range() const {
-    size_t start = viewport_.scroll_offset;
-    size_t end = std::min(buffer_->line_count(),
-                         static_cast<size_t>(viewport_.scroll_offset + viewport_.height));
-    return {start, end};
+bool EditorCore::redo() {
+    auto& buf = active_window_->buffer();
+    Position new_cursor = active_window_->cursor();
+    if (buf.redo(new_cursor)) {
+        active_window_->set_cursor(new_cursor);
+        emit_event(EditorEvent::CursorMoved);
+        emit_event(EditorEvent::BufferModified);
+        return true;
+    }
+    return false;
 }
 
-// === Private Methods ===
+bool EditorCore::can_undo() const {
+    return active_window_->buffer().can_undo();
+}
 
-void EditorCore::clamp_cursor() {
-    cursor_ = buffer_->clamp_position(cursor_);
+bool EditorCore::can_redo() const {
+    return active_window_->buffer().can_redo();
 }
 
+// === Private ===
+
 void EditorCore::emit_event(EditorEvent event) {
     for (const auto& callback : event_callbacks_) {
         callback(event);
     }
 }
 
-} // namespace lumacs
+} // namespace lumacs

+ 118 - 8
src/lua_api.cpp

@@ -88,11 +88,31 @@ void LuaApi::setup_api() {
     register_types();
     register_functions();
 
-    // Set version info
-    lua_["lumacs"] = lua_.create_table_with(
-        "version", "0.1.0",
-        "name", "Lumacs"
-    );
+    // Set version info and add constructor functions to lumacs namespace
+    auto lumacs_table = lua_.create_table();
+    lumacs_table["version"] = "0.1.0";
+    lumacs_table["name"] = "Lumacs";
+
+    // Add constructor functions
+    lumacs_table["Position"] = [](size_t line, size_t column) {
+        return Position{line, column};
+    };
+    lumacs_table["Range"] = [](Position start, Position end) {
+        return Range{start, end};
+    };
+    lumacs_table["TextAttribute"] = [](TextAttribute::ColorType color, int style) {
+        return TextAttribute{color, style};
+    };
+    lumacs_table["StyledRange"] = [](Range range, TextAttribute attr) {
+        return StyledRange{range, attr};
+    };
+
+    // Add enums to lumacs namespace for convenience
+    lumacs_table["ColorType"] = lua_["ColorType"];
+    lumacs_table["Style"] = lua_["Style"];
+    lumacs_table["BufferEvent"] = lua_["BufferEvent"];
+
+    lua_["lumacs"] = lumacs_table;
 }
 
 void LuaApi::register_types() {
@@ -103,6 +123,77 @@ void LuaApi::register_types() {
         "column", &Position::column
     );
 
+    // Range type
+    lua_.new_usertype<Range>("Range",
+        sol::constructors<Range(), Range(Position, Position)>(),
+        "start", &Range::start,
+        "end", &Range::end
+    );
+
+    // TextAttribute type
+    lua_.new_usertype<TextAttribute>("TextAttribute",
+        sol::constructors<TextAttribute(), TextAttribute(TextAttribute::ColorType, int)>(),
+        "color", &TextAttribute::color,
+        "style_flags", &TextAttribute::style_flags
+    );
+
+    // TextAttribute::ColorType enum
+    lua_.new_enum<TextAttribute::ColorType>("ColorType",
+        {
+            {"Default", TextAttribute::ColorType::Default},
+            {"Keyword", TextAttribute::ColorType::Keyword},
+            {"String", TextAttribute::ColorType::String},
+            {"Comment", TextAttribute::ColorType::Comment},
+            {"Function", TextAttribute::ColorType::Function},
+            {"Type", TextAttribute::ColorType::Type},
+            {"Number", TextAttribute::ColorType::Number},
+            {"Operator", TextAttribute::ColorType::Operator},
+            {"Variable", TextAttribute::ColorType::Variable},
+            {"Constant", TextAttribute::ColorType::Constant},
+            {"Error", TextAttribute::ColorType::Error}
+        }
+    );
+
+    // TextAttribute::Style enum
+    lua_.new_enum<TextAttribute::Style>("Style",
+        {
+            {"Normal", TextAttribute::Style::Normal},
+            {"Bold", TextAttribute::Style::Bold},
+            {"Italic", TextAttribute::Style::Italic},
+            {"Underline", TextAttribute::Style::Underline}
+        }
+    );
+
+    // BufferEvent enum
+    lua_.new_enum<BufferEvent>("BufferEvent",
+        {
+            {"Created", BufferEvent::Created},
+            {"Loaded", BufferEvent::Loaded},
+            {"Closed", BufferEvent::Closed},
+            {"BeforeChange", BufferEvent::BeforeChange},
+            {"AfterChange", BufferEvent::AfterChange},
+            {"LineChanged", BufferEvent::LineChanged},
+            {"BeforeSave", BufferEvent::BeforeSave},
+            {"AfterSave", BufferEvent::AfterSave},
+            {"LanguageChanged", BufferEvent::LanguageChanged}
+        }
+    );
+
+    // BufferEventData type
+    lua_.new_usertype<BufferEventData>("BufferEventData",
+        sol::no_constructor,
+        "event", &BufferEventData::event,
+        "line", &BufferEventData::line,
+        "language", &BufferEventData::language
+    );
+
+    // StyledRange type
+    lua_.new_usertype<StyledRange>("StyledRange",
+        sol::constructors<StyledRange(), StyledRange(Range, TextAttribute)>(),
+        "range", &StyledRange::range,
+        "attr", &StyledRange::attr
+    );
+
     // Buffer type
     lua_.new_usertype<Buffer>("Buffer",
         sol::no_constructor,
@@ -114,8 +205,20 @@ void LuaApi::register_types() {
         "insert", &Buffer::insert,
         "insert_char", &Buffer::insert_char,
         "insert_newline", &Buffer::insert_newline,
+        "erase", &Buffer::erase,
         "erase_char", &Buffer::erase_char,
-        "save", &Buffer::save
+        "replace", &Buffer::replace,
+        "find", &Buffer::find,
+        "save", &Buffer::save,
+        // Styling methods
+        "set_style", &Buffer::set_style,
+        "get_line_styles", &Buffer::get_line_styles,
+        "clear_styles", &Buffer::clear_styles,
+        "clear_line_styles", &Buffer::clear_line_styles,
+        // Event system
+        "on_buffer_event", &Buffer::on_buffer_event,
+        // Language
+        "language", sol::property(&Buffer::language, &Buffer::set_language)
     );
 
     // EditorCore type
@@ -130,6 +233,13 @@ void LuaApi::register_types() {
         "move_to_line_start", &EditorCore::move_to_line_start,
         "move_to_line_end", &EditorCore::move_to_line_end,
         "load_file", &EditorCore::load_file,
+        "split_horizontally", &EditorCore::split_horizontally,
+        "split_vertically", &EditorCore::split_vertically,
+        "close_window", &EditorCore::close_active_window,
+        "next_window", &EditorCore::next_window,
+        "undo", &EditorCore::undo,
+        "redo", &EditorCore::redo,
+        "command_mode", &EditorCore::enter_command_mode,
         "quit", &EditorCore::request_quit
     );
 }
@@ -152,8 +262,8 @@ void LuaApi::register_functions() {
     };
 
     // Message function for user feedback
-    lua_["message"] = [](std::string msg) {
-        std::cerr << "[Lumacs] " << msg << std::endl;
+    lua_["message"] = [this](std::string msg) {
+        core_.set_message(std::move(msg));
     };
 }
 

+ 483 - 122
src/main.cpp

@@ -4,26 +4,86 @@
 #include <ftxui/component/screen_interactive.hpp>
 #include <ftxui/dom/elements.hpp>
 #include <iostream>
+#include <fstream>
 #include <memory>
 
+// Global debug log
+std::ofstream debug_log("lumacs_debug.log");
+
 using namespace ftxui;
 using namespace lumacs;
 
+/// Convert TextAttribute::ColorType to FTXUI Color
+Color color_type_to_ftxui(TextAttribute::ColorType color) {
+    switch (color) {
+        case TextAttribute::ColorType::Keyword:
+            return Color::Blue;
+        case TextAttribute::ColorType::String:
+            return Color::Green;
+        case TextAttribute::ColorType::Comment:
+            return Color::GrayDark;
+        case TextAttribute::ColorType::Function:
+            return Color::Cyan;
+        case TextAttribute::ColorType::Type:
+            return Color::Yellow;
+        case TextAttribute::ColorType::Number:
+            return Color::Magenta;
+        case TextAttribute::ColorType::Operator:
+            return Color::White;
+        case TextAttribute::ColorType::Variable:
+            return Color::White;
+        case TextAttribute::ColorType::Constant:
+            return Color::Magenta;
+        case TextAttribute::ColorType::Error:
+            return Color::Red;
+        case TextAttribute::ColorType::Default:
+        default:
+            return Color::Default;
+    }
+}
+
+/// Apply style flags to an FTXUI Element
+Element apply_style_flags(Element elem, int style_flags) {
+    if (style_flags & static_cast<int>(TextAttribute::Style::Bold)) {
+        elem = elem | bold;
+    }
+    if (style_flags & static_cast<int>(TextAttribute::Style::Italic)) {
+        // FTXUI doesn't have italic, use dim as approximation
+        elem = elem | dim;
+    }
+    if (style_flags & static_cast<int>(TextAttribute::Style::Underline)) {
+        elem = elem | underlined;
+    }
+    return elem;
+}
+
 /// FTXUI-based TUI frontend for Lumacs
 class TuiEditor {
 public:
-    TuiEditor() : lua_api_(core_) {
+    TuiEditor() {
+        // Initialize Core first
+        core_ = std::make_unique<EditorCore>();
+        // Then LuaApi, which depends on Core
+        lua_api_ = std::make_unique<LuaApi>(*core_);
+
         // Listen to editor events
-        core_.on_event([this](EditorEvent event) {
+        core_->on_event([this](EditorEvent event) {
             handle_editor_event(event);
         });
 
         // Load init.lua configuration
-        lua_api_.load_init_file();
+        lua_api_->load_init_file();
+    }
+    
+    ~TuiEditor() {
+        // Explicitly destroy Core (and its Buffers/Callbacks) BEFORE LuaApi
+        // This prevents callbacks from trying to access a closed Lua state
+        core_.reset();
+        lua_api_.reset();
     }
 
     void load_file(const std::filesystem::path& path) {
-        if (!core_.load_file(path)) {
+        if (!core_->load_file(path)) {
             std::cerr << "Failed to load file: " << path << std::endl;
         }
     }
@@ -41,11 +101,21 @@ public:
     }
 
 private:
-    EditorCore core_;
-    LuaApi lua_api_;
+    enum class Mode {
+        Normal,
+        Command // Minibuffer entry
+    };
+
+    std::unique_ptr<EditorCore> core_;
+    std::unique_ptr<LuaApi> lua_api_;
     ScreenInteractive* screen_ = nullptr;
     bool should_quit_ = false;
     std::string message_line_;  // For displaying messages in the UI
+    
+    // Command mode state
+    Mode mode_ = Mode::Normal;
+    std::string command_buffer_;
+    bool meta_pending_ = false; // Emacs-style ESC prefix support
 
     void handle_editor_event(EditorEvent event) {
         if (event == EditorEvent::Quit) {
@@ -53,52 +123,194 @@ private:
             if (screen_) {
                 screen_->Exit();
             }
+        } else if (event == EditorEvent::Message) {
+            message_line_ = core_->last_message();
+            // Trigger re-render? The main loop usually handles it if it came from input.
+            // But if it came from elsewhere, we might need screen_->Post(Event::Custom)
+        } else if (event == EditorEvent::CommandMode) {
+            mode_ = Mode::Command;
+            command_buffer_.clear();
         }
     }
+    
+    /// Render a line with syntax highlighting
+    Element render_styled_line(lumacs::Window& window, const std::string& line_text, size_t line_num,
+                                 size_t cursor_col, bool has_cursor) {
+        const auto& styles = window.buffer().get_line_styles(line_num);
+
+        // If no styles, render as plain text
+        if (styles.empty()) {
+            if (has_cursor) {
+                return render_line_with_cursor(line_text, cursor_col);
+            }
+            return text(line_text);
+        }
 
-    bool handle_input(Event event) {
-        // Convert FTXUI event to string key for Lua bindings
-        std::string key_name;
+        // Build segments with styles
+        Elements segments;
+        size_t pos = 0;
+
+        for (const auto& styled : styles) {
+            size_t start = styled.range.start.column;
+            size_t end = styled.range.end.column;
+
+            // Add unstyled text before this styled range
+            if (pos < start) {
+                std::string unstyled = line_text.substr(pos, start - pos);
+                if (has_cursor && cursor_col >= pos && cursor_col < start) {
+                    // Cursor is in unstyled part
+                    segments.push_back(render_line_with_cursor(unstyled, cursor_col - pos));
+                } else {
+                    segments.push_back(text(unstyled));
+                }
+                pos = start;
+            }
+
+            // Add styled text
+            if (pos < end && pos < line_text.size()) {
+                size_t segment_end = std::min(end, line_text.size());
+                std::string styled_text = line_text.substr(pos, segment_end - pos);
 
-        // Handle control characters first (they're not is_character())
-        if (!event.input().empty()) {
-            int code = static_cast<int>(static_cast<unsigned char>(event.input()[0]));
+                Element elem;
+                if (has_cursor && cursor_col >= pos && cursor_col < segment_end) {
+                    // Cursor is in styled part
+                    elem = render_line_with_cursor(styled_text, cursor_col - pos);
+                } else {
+                    elem = text(styled_text);
+                }
 
-            // Control characters (Ctrl+A through Ctrl+Z)
-            if (code > 0 && code < 27 && code != 9 && code != 10 && code != 13) {
-                // Skip tab(9), newline(10), return(13) - handle them specially
-                char letter = 'a' + (code - 1);
-                key_name = "C-" + std::string(1, letter);
+                // Apply color and style
+                auto color = color_type_to_ftxui(styled.attr.color);
+                elem = elem | ftxui::color(color);
+                elem = apply_style_flags(elem, styled.attr.style_flags);
+
+                segments.push_back(elem);
+                pos = segment_end;
             }
         }
 
-        // Detect Meta/Alt modifier by checking raw input first
-        // Alt+ArrowUp sends ESC[1;3A (bytes: 27 91 49 59 51 65)
+        // Add remaining unstyled text
+        if (pos < line_text.size()) {
+            std::string remaining = line_text.substr(pos);
+            if (has_cursor && cursor_col >= pos) {
+                segments.push_back(render_line_with_cursor(remaining, cursor_col - pos));
+            } else {
+                segments.push_back(text(remaining));
+            }
+        } else if (has_cursor && cursor_col >= line_text.size()) {
+            // Cursor at end of line
+            segments.push_back(text(" ") | inverted);
+        }
+
+        return hbox(segments);
+    }
+
+    /// Render a line segment with cursor highlighting
+    Element render_line_with_cursor(const std::string& text_str, size_t cursor_pos) {
+        if (cursor_pos >= text_str.size()) {
+            return hbox({text(text_str), text(" ") | inverted});
+        }
+
+        std::string before = text_str.substr(0, cursor_pos);
+        std::string at_cursor = text_str.substr(cursor_pos, 1);
+        std::string after = text_str.substr(cursor_pos + 1);
+
+        return hbox({
+            text(before),
+            text(at_cursor) | inverted,
+            text(after)
+        });
+    }
+
+    bool handle_input(Event event) {
+        // Debug input in hex
+        std::string input_debug = event.input();
+        debug_log << "Input Event: is_char=" << event.is_character() << " size=" << input_debug.size() << " bytes=[";
+        for(unsigned char c : input_debug) debug_log << std::hex << (int)c << " ";
+        debug_log << std::dec << "]" << std::endl;
+
+        // Handle Command Mode Input
+        if (mode_ == Mode::Command) {
+            if (event == Event::Escape) {
+                mode_ = Mode::Normal;
+                command_buffer_.clear();
+                message_line_ = "Cancelled";
+                return true;
+            }
+            if (event == Event::Return) {
+                execute_command(command_buffer_);
+                mode_ = Mode::Normal;
+                command_buffer_.clear();
+                return true;
+            }
+            if (event == Event::Backspace) {
+                if (!command_buffer_.empty()) {
+                    command_buffer_.pop_back();
+                } else {
+                    // Backspace on empty buffer exits command mode
+                    mode_ = Mode::Normal;
+                }
+                return true;
+            }
+            if (event.is_character()) {
+                command_buffer_ += event.input();
+                return true;
+            }
+            // Allow arrows to fall through to default navigation or specific handling if we added history?
+            // For now, just consume
+            return true; 
+        }
+
+        // Convert FTXUI event to string key for Lua bindings
+        std::string key_name;
         std::string input_str = event.input();
 
-        // Check for Alt+Arrow sequences before FTXUI event processing
-        if (input_str.size() >= 6 &&
-            input_str[0] == 27 && input_str[1] == 91 &&
-            input_str[2] == 49 && input_str[3] == 59 && input_str[4] == 51) {
-            // Alt modifier detected (ESC[1;3X pattern)
-            char arrow_key = input_str[5];
-            switch (arrow_key) {
-                case 65: key_name = "M-ArrowUp"; break;     // A
-                case 66: key_name = "M-ArrowDown"; break;   // B
-                case 67: key_name = "M-ArrowRight"; break;  // C
-                case 68: key_name = "M-ArrowLeft"; break;   // D
+        // 1. Handle Meta Pending state (previous key was ESC)
+        if (meta_pending_) {
+            meta_pending_ = false; // One-shot prefix
+            
+            if (event == Event::Escape) {
+                // ESC ESC -> treat as single Escape (Cancel/Quit)
+                key_name = "Escape";
+            } else if (event.is_character()) {
+                key_name = "M-" + event.input();
+            } else if (event == Event::ArrowUp) key_name = "M-ArrowUp";
+            else if (event == Event::ArrowDown) key_name = "M-ArrowDown";
+            else if (event == Event::ArrowLeft) key_name = "M-ArrowLeft";
+            else if (event == Event::ArrowRight) key_name = "M-ArrowRight";
+            else {
+                // Unknown special key with meta... ignore or try best effort?
+                // Fallback to normal processing effectively ignoring the previous ESC
+            }
+        }
+        
+        // 2. Check for Raw Meta sequences (Alt+Key sending \x1b + char in one event)
+        if (key_name.empty()) {
+            if (input_str.size() >= 2 && input_str[0] == 27 && input_str[1] >= 32 && input_str[1] < 127) {
+                 // Atomic Alt+Char
+                 key_name = "M-" + std::string(1, input_str[1]);
+            } else if (input_str.size() >= 6 && input_str.substr(0,5) == "\x1b[1;3") {
+                 // Atomic Alt+Arrow (simplified check)
+                 if (event == Event::ArrowUp) key_name = "M-ArrowUp";
+                 if (event == Event::ArrowDown) key_name = "M-ArrowDown";
+                 if (event == Event::ArrowRight) key_name = "M-ArrowRight";
+                 if (event == Event::ArrowLeft) key_name = "M-ArrowLeft";
             }
         }
 
-        // Handle special keys
+        // 3. Handle standard Control/Special keys if not yet resolved
         if (key_name.empty()) {
             if (event == Event::Escape) {
-                key_name = "Escape";
+                // Don't act immediately, set pending
+                meta_pending_ = true;
+                message_line_ = "M- (Press key)";
+                return true; 
             } else if (event == Event::ArrowUp) {
                 key_name = "ArrowUp";
             } else if (event == Event::ArrowDown) {
                 key_name = "ArrowDown";
-            } else if (event == Event::ArrowLeft) {
+            }
+            else if (event == Event::ArrowLeft) {
                 key_name = "ArrowLeft";
             } else if (event == Event::ArrowRight) {
                 key_name = "ArrowRight";
@@ -111,170 +323,318 @@ private:
             } else if (event == Event::Tab) {
                 key_name = "Tab";
             } else if (event.is_character()) {
-                // Regular printable characters
-                key_name = event.input();
+                // Check control chars
+                if (!input_str.empty()) {
+                    int code = static_cast<int>(static_cast<unsigned char>(input_str[0]));
+                    if (code > 0 && code < 27 && code != 9 && code != 10 && code != 13) {
+                        char letter = 'a' + (code - 1);
+                        key_name = "C-" + std::string(1, letter);
+                    } else {
+                        key_name = input_str;
+                    }
+                }
             }
         }
 
         // Debug: show what key was pressed
         if (!key_name.empty()) {
             message_line_ = "Key: " + key_name;
+            debug_log << "Resolved Key: " << key_name << std::endl;
         }
 
         // Try Lua key binding first
-        if (!key_name.empty() && lua_api_.execute_key_binding(key_name)) {
+        if (!key_name.empty() && lua_api_->execute_key_binding(key_name)) {
             message_line_ = "Executed: " + key_name;
             return true;
         }
 
         // Fallback to default bindings
-        // Quit commands
-        if (event == Event::Escape || event == Event::Character('Q')) {
-            core_.request_quit();
-            return true;
+        
+        // Quit commands (Explicit Q binding if generic quit failed)
+        // Note: Escape is now handled by meta_pending logic. To quit via Escape, press ESC ESC.
+        // Or we can check if generic "Escape" key_name was produced (from double esc).
+        if (key_name == "Escape") {
+             core_->request_quit();
+             return true;
         }
 
         // Ctrl+C to quit
         if (event.is_character()) {
             auto input = event.input();
             if (!input.empty() && input[0] == 3) {  // Ctrl+C
-                core_.request_quit();
+                core_->request_quit();
                 return true;
             }
         }
 
         // Arrow keys for cursor movement
         if (event == Event::ArrowUp) {
-            core_.move_up();
+            core_->move_up();
             return true;
         }
         if (event == Event::ArrowDown) {
-            core_.move_down();
+            core_->move_down();
             return true;
         }
         if (event == Event::ArrowLeft) {
-            core_.move_left();
+            core_->move_left();
             return true;
         }
         if (event == Event::ArrowRight) {
-            core_.move_right();
+            core_->move_right();
             return true;
         }
 
-        // Vim-style movement (h, j, k, l)
-        if (event == Event::Character('h')) {
-            core_.move_left();
+        // Home/End
+        if (event == Event::Home) {
+            core_->move_to_line_start();
             return true;
         }
-        if (event == Event::Character('j')) {
-            core_.move_down();
+        if (event == Event::End) {
+            core_->move_to_line_end();
             return true;
         }
-        if (event == Event::Character('k')) {
-            core_.move_up();
+
+        // Backspace
+        if (event == Event::Backspace) {
+            auto cursor = core_->cursor();
+            core_->buffer().erase_char(cursor);
+            // Move cursor back if we deleted a character
+            if (cursor.column > 0) {
+                core_->set_cursor({cursor.line, cursor.column - 1});
+            } else if (cursor.line > 0) {
+                // Moved to end of previous line
+                size_t prev_line_len = core_->buffer().line(cursor.line - 1).size();
+                core_->set_cursor({cursor.line - 1, prev_line_len});
+            }
             return true;
         }
-        if (event == Event::Character('l')) {
-            core_.move_right();
+
+        // Delete key (delete character at cursor)
+        if (event == Event::Delete) {
+            auto cursor = core_->cursor();
+            // Delete the character after cursor by erasing at cursor+1
+            if (cursor.column < core_->buffer().line(cursor.line).size()) {
+                core_->buffer().erase_char({cursor.line, cursor.column + 1});
+            } else if (cursor.line < core_->buffer().line_count() - 1) {
+                // At end of line, join with next line
+                core_->buffer().erase_char({cursor.line + 1, 0});
+            }
             return true;
         }
 
-        // Home/End
-        if (event == Event::Home) {
-            core_.move_to_line_start();
+        // Return/Enter - insert newline
+        if (event == Event::Return) {
+            auto cursor = core_->cursor();
+            core_->buffer().insert_newline(cursor);
+            // Move cursor to start of next line
+            core_->set_cursor({cursor.line + 1, 0});
             return true;
         }
-        if (event == Event::End) {
-            core_.move_to_line_end();
+
+        // Tab - insert tab character or spaces
+        if (event == Event::Tab) {
+            auto cursor = core_->cursor();
+            // Insert 4 spaces (or you could insert '\t')
+            core_->buffer().insert(cursor, "    ");
+            core_->set_cursor({cursor.line, cursor.column + 4});
             return true;
         }
 
+        // Regular printable characters
+        if (event.is_character()) {
+            std::string input = event.input();
+            if (!input.empty() && input[0] >= 32 && input[0] < 127) {
+                // Printable ASCII character
+                auto cursor = core_->cursor();
+                core_->buffer().insert_char(cursor, input[0]);
+                // Move cursor forward
+                core_->set_cursor({cursor.line, cursor.column + 1});
+                return true;
+            }
+        }
+
         return false;
     }
+    
+    void execute_command(const std::string& cmd) {
+        if (cmd.empty()) return; 
+        
+        if (cmd == "q" || cmd == "quit") {
+            core_->request_quit();
+            return;
+        }
+        
+        if (cmd == "w" || cmd == "write") {
+            core_->buffer().save();
+            message_line_ = "Saved";
+            return;
+        }
+        
+        if (cmd == "wq") {
+            core_->buffer().save();
+            core_->request_quit();
+            return;
+        }
+        
+        // Try executing as Lua
+        if (lua_api_->execute(cmd)) {
+             message_line_ = "Lua Executed";
+        }
+        else {
+             message_line_ = "Lua Error (check stderr)";
+        }
+    }
 
-    Element render() {
-        const auto& buffer = core_.buffer();
-        const auto& cursor = core_.cursor();
-
-        // Get the current terminal dimensions
-        auto dimensions = Terminal::Size();
-        // Account for border (2 lines) + status line (2 lines including separator)
-        int viewport_height = std::max(1, dimensions.dimy - 4);
-        core_.set_viewport_size(dimensions.dimx, viewport_height);
-
+    // Helper to render a single window
+    Element render_window(lumacs::Window& window, int width, int height) {
+        debug_log << "render_window: " << width << "x" << height << std::endl;
+        
+        const auto& buffer = window.buffer();
+        const auto& cursor = window.cursor();
+        bool is_active = (core_->active_window().get() == &window);
+        
+        // Calculate available height for text lines
+        // Total height - 2 (border) - 1 (status)
+        int lines_height = std::max(0, height - 3);
+        
+        // Get visible range for this window
+        auto [start_line, end_line] = window.visible_line_range();
         const auto& buffer_lines = buffer.lines();
-        auto [start_line, end_line] = core_.visible_line_range();
-
+        
         Elements lines;
-
+        
         // Render visible lines
         for (size_t i = start_line; i < end_line; ++i) {
             const auto& line_text = buffer_lines[i];
 
-            // Build line with cursor highlighting
+            // Build line
             Elements line_elements;
-
-            // Add line number
+            
+            // Line number
             line_elements.push_back(text(std::to_string(i + 1) + " │ "));
 
-            // Render line with cursor on the current line
-            if (i == cursor.line) {
-                // Split line into: before cursor | at cursor | after cursor
-                std::string before = line_text.substr(0, cursor.column);
-                std::string at_cursor;
-                std::string after;
-
-                if (cursor.column < line_text.size()) {
-                    // Cursor is on a character
-                    at_cursor = line_text.substr(cursor.column, 1);
-                    after = line_text.substr(cursor.column + 1);
-                } else {
-                    // Cursor is at end of line (show as space)
-                    at_cursor = " ";
-                }
-
-                line_elements.push_back(text(before));
-                line_elements.push_back(text(at_cursor) | inverted);  // Highlight cursor
-                line_elements.push_back(text(after));
-            } else {
-                // Regular line without cursor
-                line_elements.push_back(text(line_text));
-            }
+            // Styled line
+            bool has_cursor = (is_active && i == cursor.line && mode_ == Mode::Normal);
+            line_elements.push_back(render_styled_line(window, line_text, i, cursor.column, has_cursor));
 
             lines.push_back(hbox(line_elements));
         }
-
-        // Fill remaining space if viewport is larger than content
-        while (lines.size() < static_cast<size_t>(viewport_height)) {
+        
+        // Fill remaining space
+        while (lines.size() < static_cast<size_t>(lines_height)) {
             lines.push_back(text("~"));
         }
-
+        
         // Status line
         std::string status = buffer.name() +
                            (buffer.is_modified() ? " [+]" : "") +
-                           " | Line " + std::to_string(cursor.line + 1) +
-                           "/" + std::to_string(buffer.line_count()) +
-                           " Col " + std::to_string(cursor.column + 1);
+                           " | " + std::to_string(cursor.line + 1) +
+                           ":" + std::to_string(cursor.column + 1);
+        
+        if (is_active && mode_ == Mode::Command) status += " [CMD]";
+        
+        auto status_elem = text(status) | inverted;
+        if (!is_active) status_elem = status_elem | dim;
+        
+        return vbox({
+            vbox(lines) | flex,
+            status_elem
+        }) | border | size(WIDTH, EQUAL, width) | size(HEIGHT, EQUAL, height);
+    }
 
-        // Show scroll indicator if there's more content
-        if (buffer_lines.size() > static_cast<size_t>(viewport_height)) {
-            int percent = (cursor.line * 100) / (buffer_lines.size() - 1);
-            status += " " + std::to_string(percent) + "%";
+    void update_layout_sizes(LayoutNode* node, int w, int h) {
+        debug_log << "update_layout_sizes: node=" << (int)node->type << " w=" << w << " h=" << h << std::endl;
+        
+        if (node->type == LayoutNode::Type::Leaf) {
+            // Account for border (2 lines) and status (1 line)
+            // render_window produces an element of height: viewport.height + 1 (status) + 2 (border)
+            // We want this to be <= h.
+            // viewport.height + 3 <= h  =>  viewport.height <= h - 3
+            int vp_h = std::max(1, h - 3);
+            int vp_w = std::max(1, w - 2);
+            debug_log << "  Leaf: setting viewport to " << vp_w << "x" << vp_h << std::endl;
+            node->window->set_viewport_size(vp_w, vp_h);
+        } else if (node->type == LayoutNode::Type::HorizontalSplit) {
+            // Account for separator (1 line)
+            int available_h = std::max(0, h - 1);
+            int h1 = available_h / 2;
+            int h2 = available_h - h1;
+            debug_log << "  HSplit: h1=" << h1 << " h2=" << h2 << std::endl;
+            update_layout_sizes(node->child1.get(), w, h1);
+            update_layout_sizes(node->child2.get(), w, h2);
+        } else { // Vertical
+            // Account for separator (1 column)
+            int available_w = std::max(0, w - 1);
+            int w1 = available_w / 2;
+            int w2 = available_w - w1;
+            debug_log << "  VSplit: w1=" << w1 << " w2=" << w2 << std::endl;
+            update_layout_sizes(node->child1.get(), w1, h);
+            update_layout_sizes(node->child2.get(), w2, h);
         }
+    }
 
-        // Message line (minibuffer)
-        Elements ui_elements = {
-            vbox(lines) | flex,
-            separator(),
-            text(status) | inverted
-        };
-
-        // Show message line if there's a message
-        if (!message_line_.empty()) {
-            ui_elements.push_back(text(message_line_) | dim);
+    Element render_node(LayoutNode* node) {
+        if (node->type == LayoutNode::Type::Leaf) {
+            // dimensions are updated in update_layout_sizes
+            const auto& vp = node->window->viewport();
+            // Reconstruct approximated outer size for render_window helper
+            // content height = vp.height. outer = height + 3
+            return render_window(*node->window, vp.width + 2, vp.height + 3);
+        } else if (node->type == LayoutNode::Type::HorizontalSplit) {
+            return vbox({
+                render_node(node->child1.get()) | flex,
+                separator(),
+                render_node(node->child2.get()) | flex
+            });
+        } else { // Vertical
+            return hbox({
+                render_node(node->child1.get()) | flex,
+                separator(),
+                render_node(node->child2.get()) | flex
+            });
         }
+    }
 
-        return vbox(ui_elements) | border;
+    Element render() {
+        // Dimensions
+        auto term_size = Terminal::Size();
+        debug_log << "Render Frame. Term Size: " << term_size.dimx << "x" << term_size.dimy << std::endl;
+
+        int height = std::max(1, term_size.dimy - 1); // -1 for minibuffer/message line
+        
+        // Calculate layout sizes
+        update_layout_sizes(core_->root_layout().get(), term_size.dimx, height);
+        
+        // Render Tree
+        Element editor_area = render_node(core_->root_layout().get());
+        
+        // Minibuffer / Message Line
+        Element bottom_bar;
+        
+        if (mode_ == Mode::Command) {
+             debug_log << "  Mode: Command" << std::endl;
+             bottom_bar = hbox({
+                 text(":") | bold | color(Color::Yellow),
+                 text(command_buffer_), 
+                 text(" ") | inverted // Fake cursor
+             });
+        } else {
+            debug_log << "  Mode: Normal" << std::endl;
+            if (!message_line_.empty()) {
+                bottom_bar = text(message_line_) | dim;
+            } else {
+                bottom_bar = text(""); 
+            }
+        }
+        
+        // Force bottom bar to take exactly 1 line
+        bottom_bar = bottom_bar | size(HEIGHT, EQUAL, 1);
+        
+        return vbox({
+            editor_area | flex,
+            bottom_bar
+        });
     }
 
 };
@@ -290,8 +650,9 @@ int main(int argc, char* argv[]) {
 
     auto ui = editor.create_ui(&screen);
 
-    std::cout << "Lumacs - A modern text editor" << std::endl;
-    std::cout << "Controls: Arrow keys or hjkl to move, Q or Esc to quit" << std::endl;
+    std::cout << "Lumacs - A modern text editor with Lua scripting" << std::endl;
+    std::cout << "Navigation: Arrow keys | Editing: Type, Backspace, Enter, Tab" << std::endl;
+    std::cout << "Commands: Ctrl+S=save, Ctrl+L=highlight, Esc/Q=quit" << std::endl;
     std::cout << std::endl;
 
     screen.Loop(ui);

+ 16 - 2
tests/CMakeLists.txt

@@ -1,2 +1,16 @@
-# Tests will go here
-# For now, just a placeholder
+cmake_minimum_required(VERSION 3.20)
+project(lumacs_tests)
+
+# Set C++ standard
+set(CMAKE_CXX_STANDARD 20)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+# Add includes
+include_directories(${CMAKE_SOURCE_DIR}/include)
+
+# Build tests
+add_executable(run_tests test_buffer.cpp ../src/buffer.cpp)
+
+# Add test target
+enable_testing()
+add_test(NAME buffer_tests COMMAND run_tests)