#include "lumacs/editor_core.hpp" #include "lumacs/lua_api.hpp" // Include LuaApi header #include "lumacs/command_system.hpp" #include "lumacs/completion_system.hpp" // Include CompletionSystem header #include "lumacs/minibuffer_manager.hpp" // Include MinibufferManager header #include #include namespace lumacs { EditorCore::EditorCore() : buffers_(), root_node_(), active_window_(), last_message_(), message_clear_time_(), event_callbacks_(), kill_ring_(), last_yank_start_(), last_yank_end_(), registers_(), current_macro_(), last_macro_(), recording_macro_(false), rectangle_kill_ring_(), theme_manager_(), config_(), command_system_(std::make_unique(this)), keybinding_manager_(std::make_unique(command_system_.get())), // Initialized after command_system_ lua_api_(std::make_unique()), modeline_manager_(), completion_system_(std::make_unique(*this)), // Initialized before minibuffer_manager_ minibuffer_manager_(std::make_unique(*this, *lua_api_, *completion_system_)) // Initialized after completion_system_ { // LuaApi needs core_ pointer to be valid, so set it after constructor body starts lua_api_->set_core(*this); // Create initial buffer auto buffer = std::make_shared(); buffers_.push_back(buffer); // Create initial window active_window_ = std::make_shared(buffer); root_node_ = std::make_shared(active_window_); // Initialize themes theme_manager_.create_default_themes(); theme_manager_.set_active_theme("everforest-dark"); // LuaApi will load init.lua, which relies on `editor` global being set via set_core(). lua_api_->load_init_file(); } EditorCore::~EditorCore() = default; // === 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) { std::filesystem::path abs_path = std::filesystem::absolute(path); // Check if already open for (const auto& buf : buffers_) { if (buf->file_path() && std::filesystem::equivalent(*buf->file_path(), abs_path)) { active_window_->set_buffer(buf); emit_event(EditorEvent::BufferModified); emit_event(EditorEvent::CursorMoved); emit_event(EditorEvent::ViewportChanged); return true; } } auto new_buffer_opt = Buffer::from_file(abs_path); if (!new_buffer_opt) { return false; } auto new_buffer = std::make_shared(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); return true; } void EditorCore::new_buffer(std::string name) { auto new_buffer = std::make_shared(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); } std::vector EditorCore::get_buffer_names() const { std::vector names; names.reserve(buffers_.size()); for (const auto& buf : buffers_) { names.push_back(buf->name()); } return names; } std::shared_ptr EditorCore::get_buffer_by_name(const std::string& name) { for (const auto& buf : buffers_) { if (buf->name() == name) { return buf; } } return nullptr; } bool EditorCore::switch_buffer_in_window(const std::string& name) { auto buf = get_buffer_by_name(name); if (!buf) { return false; } active_window_->set_buffer(buf); emit_event(EditorEvent::BufferModified); emit_event(EditorEvent::CursorMoved); emit_event(EditorEvent::ViewportChanged); return true; } bool EditorCore::close_buffer(const std::string& name) { auto buf = get_buffer_by_name(name); if (!buf) { return false; } // Cannot close buffer if it's the only one if (buffers_.size() <= 1) { set_message("Cannot close last buffer"); return false; } // Check if buffer is displayed in any window std::vector> windows; collect_windows(root_node_.get(), windows); for (const auto& win : windows) { if (win->buffer_ptr() == buf) { // Buffer is displayed, switch to another buffer first // Find another buffer auto other_buf = buffers_.front() == buf ? *(++buffers_.begin()) : buffers_.front(); win->set_buffer(other_buf); } } // Remove buffer from list buffers_.remove(buf); emit_event(EditorEvent::BufferModified); emit_event(EditorEvent::CursorMoved); emit_event(EditorEvent::ViewportChanged); return true; } std::vector EditorCore::get_all_buffer_info() const { std::vector info; info.reserve(buffers_.size()); for (const auto& buf : buffers_) { BufferInfo bi; bi.name = buf->name(); bi.size = buf->line_count(); bi.modified = buf->is_modified(); bi.mode = "fundamental-mode"; // TODO: Get actual mode from buffer bi.filepath = buf->file_path(); info.push_back(bi); } return info; } // === Window Management === // Helper to recursively replace a window in the tree bool replace_window_node(std::shared_ptr node, std::shared_ptr target, std::shared_ptr 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; } void EditorCore::split_horizontally() { std::cerr << "[DEBUG] split_horizontally() called" << std::endl; // New window sharing same buffer auto new_window = std::make_shared(active_window_->buffer_ptr()); new_window->set_cursor(active_window_->cursor()); // Start at same position // Create split node auto new_leaf = std::make_shared(new_window); auto current_leaf = std::make_shared(active_window_); auto split = std::make_shared( LayoutNode::Type::HorizontalSplit, current_leaf, // Top new_leaf // Bottom ); if (root_node_->type == LayoutNode::Type::Leaf && root_node_->window == active_window_) { std::cerr << "[DEBUG] Replacing root node" << std::endl; root_node_ = split; } else { std::cerr << "[DEBUG] Replacing window node in tree" << std::endl; replace_window_node(root_node_, active_window_, split); } active_window_ = new_window; // Focus new window emit_event(EditorEvent::WindowLayoutChanged); std::cerr << "[DEBUG] split_horizontally() completed" << std::endl; } void EditorCore::split_vertically() { std::cerr << "[DEBUG] split_vertically() called" << std::endl; // New window sharing same buffer auto new_window = std::make_shared(active_window_->buffer_ptr()); new_window->set_cursor(active_window_->cursor()); // Create split node auto new_leaf = std::make_shared(new_window); auto current_leaf = std::make_shared(active_window_); // Vertical Split = Left/Right division auto split = std::make_shared( LayoutNode::Type::VerticalSplit, current_leaf, // Left new_leaf // Right ); if (root_node_->type == LayoutNode::Type::Leaf && root_node_->window == active_window_) { std::cerr << "[DEBUG] Replacing root node" << std::endl; root_node_ = split; } else { std::cerr << "[DEBUG] Replacing window node in tree" << std::endl; replace_window_node(root_node_, active_window_, split); } active_window_ = new_window; emit_event(EditorEvent::WindowLayoutChanged); std::cerr << "[DEBUG] split_vertically() completed" << std::endl; } // 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); } // Recursive leaf finder LayoutNode* find_node_with_window(LayoutNode* current, std::shared_ptr target) { if (current->type == LayoutNode::Type::Leaf) { return (current->window == target) ? current : nullptr; } 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::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 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> 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>& 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() { // Cycle to the next window in the window tree // Note: Focus jumping bug was fixed in GTK frontend by caching active_window during redraws std::vector> 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::WindowFocused); } } void EditorCore::next_window_safe() { // Deprecated: Use next_window() instead. Kept for backwards compatibility. next_window(); } bool EditorCore::set_active_window(std::shared_ptr window) { if (!window) return false; // Verify that the window exists in the current window tree std::vector> windows; collect_windows(root_node_.get(), windows); auto it = std::find(windows.begin(), windows.end(), window); if (it != windows.end()) { if (active_window_ != window) { active_window_ = window; emit_event(EditorEvent::WindowFocused); } return true; } return false; // Window not found in tree } // === 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() { active_window_->move_to_line_start(); emit_event(EditorEvent::CursorMoved); } void EditorCore::move_to_line_end() { active_window_->move_to_line_end(); emit_event(EditorEvent::CursorMoved); } // Helper: Check if character is a word constituent static bool is_word_char(char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'; } void EditorCore::move_forward_word() { auto new_pos = calculate_forward_word_pos(active_window_->cursor()); active_window_->set_cursor(new_pos); emit_event(EditorEvent::CursorMoved); } void EditorCore::move_backward_word() { auto new_pos = calculate_backward_word_pos(active_window_->cursor()); active_window_->set_cursor(new_pos); emit_event(EditorEvent::CursorMoved); } Position EditorCore::calculate_forward_word_pos(Position start_pos) { auto& buf = active_window_->buffer(); auto cursor = start_pos; // Check if we are at the end of buffer if (cursor.line >= buf.line_count()) { return cursor; } // 1. Skip non-word chars (whitespace/punctuation) while (true) { const auto& line = buf.line(cursor.line); while (cursor.column < line.size() && !is_word_char(line[cursor.column])) { cursor.column++; } // If we found a word char, we're done with step 1 if (cursor.column < line.size()) { break; } // Move to next line if (cursor.line < buf.line_count() - 1) { cursor.line++; cursor.column = 0; } else { // At end of buffer return cursor; } } // 2. Skip word chars const auto& line = buf.line(cursor.line); while (cursor.column < line.size() && is_word_char(line[cursor.column])) { cursor.column++; } return cursor; } Position EditorCore::calculate_backward_word_pos(Position start_pos) { auto& buf = active_window_->buffer(); auto cursor = start_pos; // Skip whitespace and punctuation backwards while (true) { // If at start of line, go to previous line if (cursor.column == 0) { if (cursor.line == 0) { // At start of buffer break; } cursor.line--; cursor.column = buf.line(cursor.line).size(); continue; } // Move back one char cursor.column--; const auto& line = buf.line(cursor.line); // If we hit a word char, keep going back through the word if (is_word_char(line[cursor.column])) { // Move to start of word while (cursor.column > 0 && is_word_char(line[cursor.column - 1])) { cursor.column--; } break; } } return cursor; } void EditorCore::page_up() { auto& viewport = active_window_->viewport(); auto cursor = active_window_->cursor(); // Move up by viewport height (minus 2 for overlap) int page_size = std::max(1, viewport.height - 2); if (cursor.line >= static_cast(page_size)) { cursor.line -= page_size; } else { cursor.line = 0; } // Keep column position if possible auto& buf = active_window_->buffer(); const auto& line = buf.line(cursor.line); cursor.column = std::min(cursor.column, line.size()); active_window_->set_cursor(cursor); active_window_->adjust_scroll(); emit_event(EditorEvent::CursorMoved); emit_event(EditorEvent::ViewportChanged); } void EditorCore::page_down() { auto& viewport = active_window_->viewport(); auto cursor = active_window_->cursor(); auto& buf = active_window_->buffer(); // Move down by viewport height (minus 2 for overlap) int page_size = std::max(1, viewport.height - 2); cursor.line = std::min(cursor.line + page_size, buf.line_count() - 1); // Keep column position if possible const auto& line = buf.line(cursor.line); cursor.column = std::min(cursor.column, line.size()); active_window_->set_cursor(cursor); active_window_->adjust_scroll(); emit_event(EditorEvent::CursorMoved); emit_event(EditorEvent::ViewportChanged); } void EditorCore::goto_beginning() { Position pos = {0, 0}; active_window_->set_cursor(pos); active_window_->adjust_scroll(); emit_event(EditorEvent::CursorMoved); emit_event(EditorEvent::ViewportChanged); } void EditorCore::goto_end() { auto& buf = active_window_->buffer(); size_t last_line = buf.line_count() > 0 ? buf.line_count() - 1 : 0; size_t last_col = buf.line(last_line).size(); Position pos = {last_line, last_col}; active_window_->set_cursor(pos); active_window_->adjust_scroll(); emit_event(EditorEvent::CursorMoved); emit_event(EditorEvent::ViewportChanged); } void EditorCore::goto_line(size_t line) { auto& buf = active_window_->buffer(); line = std::min(line, buf.line_count() > 0 ? buf.line_count() - 1 : 0); // Clamp to max line index Position pos = {line, 0}; active_window_->set_cursor(pos); active_window_->adjust_scroll(); emit_event(EditorEvent::CursorMoved); emit_event(EditorEvent::ViewportChanged); } // === Viewport Proxies === const Viewport& EditorCore::viewport() const noexcept { return active_window_->viewport(); } void EditorCore::set_viewport_size(int width, int height) { active_window_->set_viewport_size(width, height); emit_event(EditorEvent::ViewportChanged); } void EditorCore::adjust_scroll() { active_window_->adjust_scroll(); emit_event(EditorEvent::ViewportChanged); } std::pair EditorCore::visible_line_range() const { return active_window_->visible_line_range(); } // === Undo/Redo === 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; } 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; } bool EditorCore::can_undo() const { return active_window_->buffer().can_undo(); } bool EditorCore::can_redo() const { return active_window_->buffer().can_redo(); } // === Kill Ring === void EditorCore::kill_line() { auto& buf = active_window_->buffer(); auto cursor = active_window_->cursor(); const auto& line = buf.line(cursor.line); // If at end of line, kill the newline (join with next line) if (cursor.column >= line.size()) { if (cursor.line < buf.line_count() - 1) { // Kill the newline character Position start = {cursor.line, line.size()}; Position end = {cursor.line + 1, 0}; Range range = {start, end}; std::string killed_text = "\n"; kill_ring_.push(killed_text); buf.erase(range); emit_event(EditorEvent::BufferModified); std::cerr << "[DEBUG] Killed newline at end of line " << cursor.line << std::endl; } return; } // Kill from cursor to end of line Position start = cursor; Position end = {cursor.line, line.size()}; Range range = {start, end}; std::string killed_text = buf.get_text_in_range(range); if (!killed_text.empty()) { kill_ring_.push(killed_text); buf.erase(range); emit_event(EditorEvent::BufferModified); std::cerr << "[DEBUG] Killed text: '" << killed_text << "'" << std::endl; } } void EditorCore::kill_region() { auto& buf = active_window_->buffer(); auto cursor = active_window_->cursor(); auto region = buf.get_region(cursor); if (!region.has_value()) { set_message("No active region"); return; } std::string killed_text = buf.get_text_in_range(region.value()); if (!killed_text.empty()) { kill_ring_.push(killed_text); buf.deactivate_mark(); // Move cursor to start of killed region active_window_->set_cursor(region.value().start); emit_event(EditorEvent::BufferModified); emit_event(EditorEvent::CursorMoved); std::cerr << "[DEBUG] Killed region: '" << killed_text << "'" << std::endl; } } void EditorCore::kill_word() { auto cursor = active_window_->cursor(); auto end_pos = calculate_forward_word_pos(cursor); if (cursor == end_pos) return; Range range = {cursor, end_pos}; auto& buf = active_window_->buffer(); std::string text = buf.get_text_in_range(range); if (!text.empty()) { kill_ring_.push(text); buf.erase(range); emit_event(EditorEvent::BufferModified); std::cerr << "[DEBUG] Killed word: '" << text << "'" << std::endl; } } void EditorCore::backward_kill_word() { auto cursor = active_window_->cursor(); auto start_pos = calculate_backward_word_pos(cursor); if (cursor == start_pos) return; Range range = {start_pos, cursor}; auto& buf = active_window_->buffer(); std::string text = buf.get_text_in_range(range); if (!text.empty()) { kill_ring_.push(text); buf.erase(range); active_window_->set_cursor(start_pos); emit_event(EditorEvent::BufferModified); emit_event(EditorEvent::CursorMoved); std::cerr << "[DEBUG] Backward killed word: '" << text << "'" << std::endl; } } void EditorCore::copy_region_as_kill() { auto& buf = active_window_->buffer(); auto cursor = active_window_->cursor(); auto region = buf.get_region(cursor); if (!region.has_value()) { set_message("No active region"); return; } std::string copied_text = buf.get_text_in_range(region.value()); if (!copied_text.empty()) { kill_ring_.push(copied_text); buf.deactivate_mark(); set_message("Region copied"); std::cerr << "[DEBUG] Copied region: '" << copied_text << "'" << std::endl; } } void EditorCore::yank() { if (kill_ring_.empty()) { set_message("Kill ring is empty"); return; } std::string text = kill_ring_.current(); if (text.empty()) { return; } auto& buf = active_window_->buffer(); auto cursor = active_window_->cursor(); // Save yank start position last_yank_start_ = cursor; // Insert the text buf.insert(cursor, text); // Calculate new cursor position after insertion Position new_cursor = cursor; // Count newlines in text size_t newline_count = std::count(text.begin(), text.end(), '\n'); if (newline_count > 0) { // Multi-line yank: cursor goes to end of inserted text new_cursor.line += newline_count; size_t last_newline = text.rfind('\n'); new_cursor.column = (last_newline != std::string::npos) ? (text.size() - last_newline - 1) : 0; } else { // Single-line yank: advance column new_cursor.column += text.size(); } last_yank_end_ = new_cursor; active_window_->set_cursor(new_cursor); emit_event(EditorEvent::BufferModified); emit_event(EditorEvent::CursorMoved); std::cerr << "[DEBUG] Yanked: '" << text << "'" << std::endl; } void EditorCore::yank_pop() { if (kill_ring_.empty()) { set_message("Kill ring is empty"); return; } if (!last_yank_start_.has_value() || !last_yank_end_.has_value()) { set_message("Previous command was not a yank"); return; } // Delete the previously yanked text auto& buf = active_window_->buffer(); Range yank_range = {last_yank_start_.value(), last_yank_end_.value()}; buf.erase(yank_range); // Get previous entry in kill ring std::string text = kill_ring_.previous(); // Restore cursor to yank start auto cursor = last_yank_start_.value(); active_window_->set_cursor(cursor); // Insert new text buf.insert(cursor, text); // Calculate new end position Position new_cursor = cursor; size_t newline_count = std::count(text.begin(), text.end(), '\n'); if (newline_count > 0) { new_cursor.line += newline_count; size_t last_newline = text.rfind('\n'); new_cursor.column = (last_newline != std::string::npos) ? (text.size() - last_newline - 1) : 0; } else { new_cursor.column += text.size(); } last_yank_end_ = new_cursor; active_window_->set_cursor(new_cursor); emit_event(EditorEvent::BufferModified); emit_event(EditorEvent::CursorMoved); std::cerr << "[DEBUG] Yank-pop: '" << text << "'" << std::endl; } // === Registers === void EditorCore::copy_to_register(char register_name, const std::string& text) { // Validate register name (a-z, A-Z, 0-9) if (!std::isalnum(register_name)) { set_message("Invalid register name"); return; } registers_[register_name] = text; set_message(std::string("Saved text to register ") + register_name); } bool EditorCore::insert_register(char register_name) { // Validate register name if (!std::isalnum(register_name)) { set_message("Invalid register name"); return false; } auto it = registers_.find(register_name); if (it == registers_.end()) { set_message(std::string("Register ") + register_name + " is empty"); return false; } auto& buf = buffer(); Position cursor = active_window_->cursor(); buf.insert(cursor, it->second); // Move cursor to end of inserted text size_t newline_count = std::count(it->second.begin(), it->second.end(), '\n'); if (newline_count > 0) { cursor.line += newline_count; size_t last_newline = it->second.rfind('\n'); cursor.column = (last_newline != std::string::npos) ? (it->second.size() - last_newline - 1) : 0; } else { cursor.column += it->second.size(); } active_window_->set_cursor(cursor); emit_event(EditorEvent::BufferModified); emit_event(EditorEvent::CursorMoved); set_message(std::string("Inserted register ") + register_name); return true; } void EditorCore::copy_region_to_register(char register_name) { auto& buf = buffer(); Position cursor = active_window_->cursor(); auto region = buf.get_region(cursor); if (!region) { set_message("No active region"); return; } std::string text = buf.get_text_in_range(*region); copy_to_register(register_name, text); } bool EditorCore::yank_from_register(char register_name) { return insert_register(register_name); } // === Keyboard Macros === void EditorCore::start_kbd_macro() { if (recording_macro_) { set_message("Already recording macro"); return; } recording_macro_ = true; current_macro_.clear(); set_message("Recording macro..."); } void EditorCore::end_kbd_macro_or_call() { if (recording_macro_) { // End recording recording_macro_ = false; last_macro_ = current_macro_; current_macro_.clear(); if (last_macro_.empty()) { set_message("Macro recorded (empty)"); } else { set_message(std::string("Macro recorded (") + std::to_string(last_macro_.size()) + " keys)"); } } else { // Call last macro if (last_macro_.empty()) { set_message("No macro recorded"); return; } set_message(std::string("Executing macro (") + std::to_string(last_macro_.size()) + " keys)"); // This is a simplified approach - in a real implementation, you'd need // to replay the actual commands through the key binding system // For now, we just show that the macro system is set up for (const auto& key : last_macro_) { // TODO: Execute the actual key binding for this key // This would require access to the LuaApi from EditorCore std::cerr << "[MACRO] Would execute key: " << key << std::endl; } } } void EditorCore::record_key_sequence(const std::string& key_sequence) { if (recording_macro_) { current_macro_.push_back(key_sequence); } } // === Rectangles === void EditorCore::kill_rectangle() { auto& buf = buffer(); Position cursor = active_window_->cursor(); auto region = buf.get_region(cursor); if (!region) { set_message("No active region"); return; } // Ensure start is top-left, end is bottom-right Position start = region->start; Position end = region->end; if (start.line > end.line || (start.line == end.line && start.column > end.column)) { std::swap(start, end); } rectangle_kill_ring_.clear(); // Extract rectangle line by line for (size_t line = start.line; line <= end.line; ++line) { if (line >= buf.line_count()) break; std::string line_text = buf.line(line); size_t start_col = (line == start.line) ? start.column : std::min(start.column, end.column); size_t end_col = (line == end.line) ? end.column : std::max(start.column, end.column); // Ensure we don't go beyond the line length start_col = std::min(start_col, line_text.size()); end_col = std::min(end_col, line_text.size()); if (start_col < end_col) { rectangle_kill_ring_.push_back(line_text.substr(start_col, end_col - start_col)); } else { rectangle_kill_ring_.push_back(""); } } // Now delete the rectangle content (from bottom to top to preserve line indices) for (int line = static_cast(end.line); line >= static_cast(start.line); --line) { if (line >= static_cast(buf.line_count())) continue; std::string line_text = buf.line(line); size_t start_col = (line == static_cast(start.line)) ? start.column : std::min(start.column, end.column); size_t end_col = (line == static_cast(end.line)) ? end.column : std::max(start.column, end.column); start_col = std::min(start_col, line_text.size()); end_col = std::min(end_col, line_text.size()); if (start_col < end_col) { Range del_range{Position(line, start_col), Position(line, end_col)}; buf.erase(del_range); } } buf.deactivate_mark(); emit_event(EditorEvent::BufferModified); set_message(std::string("Rectangle killed (") + std::to_string(rectangle_kill_ring_.size()) + " lines)"); } void EditorCore::yank_rectangle() { if (rectangle_kill_ring_.empty()) { set_message("No rectangle in kill ring"); return; } auto& buf = buffer(); Position cursor = active_window_->cursor(); // Insert rectangle starting at cursor position for (size_t i = 0; i < rectangle_kill_ring_.size(); ++i) { Position insert_pos{cursor.line + i, cursor.column}; // Ensure we have enough lines while (buf.line_count() <= insert_pos.line) { buf.insert_newline(Position{buf.line_count() - 1, buf.line(buf.line_count() - 1).size()}); } // Pad line with spaces if necessary std::string current_line = buf.line(insert_pos.line); if (current_line.size() < insert_pos.column) { std::string padding(insert_pos.column - current_line.size(), ' '); buf.insert(Position{insert_pos.line, current_line.size()}, padding); } buf.insert(insert_pos, rectangle_kill_ring_[i]); } emit_event(EditorEvent::BufferModified); set_message(std::string("Rectangle yanked (") + std::to_string(rectangle_kill_ring_.size()) + " lines)"); } void EditorCore::string_rectangle(const std::string& text) { auto& buf = buffer(); Position cursor = active_window_->cursor(); auto region = buf.get_region(cursor); if (!region) { set_message("No active region"); return; } Position start = region->start; Position end = region->end; if (start.line > end.line || (start.line == end.line && start.column > end.column)) { std::swap(start, end); } // Fill rectangle with the given text for (size_t line = start.line; line <= end.line; ++line) { if (line >= buf.line_count()) break; std::string line_text = buf.line(line); size_t start_col = std::min(start.column, end.column); size_t end_col = std::max(start.column, end.column); // Pad line if necessary if (line_text.size() < end_col) { std::string padding(end_col - line_text.size(), ' '); buf.insert(Position{line, line_text.size()}, padding); line_text = buf.line(line); // Refresh } // Replace rectangle content with text if (start_col < line_text.size()) { end_col = std::min(end_col, line_text.size()); if (start_col < end_col) { Range replace_range{Position(line, start_col), Position(line, end_col)}; std::string fill_text = text; if (fill_text.size() > end_col - start_col) { fill_text = fill_text.substr(0, end_col - start_col); } else if (fill_text.size() < end_col - start_col) { fill_text += std::string(end_col - start_col - fill_text.size(), ' '); } buf.replace(replace_range, fill_text); } } } buf.deactivate_mark(); emit_event(EditorEvent::BufferModified); set_message("Rectangle filled"); } // === Private === void EditorCore::emit_event(EditorEvent event) { for (const auto& callback : event_callbacks_) { callback(event); } } } // namespace lumacs