-- Lumacs Configuration File -- This file is executed on startup and allows you to customize keybindings, -- create commands, and extend the editor with Lua. -- Lumacs Configuration File -- This file is executed on startup and allows you to customize keybindings, -- create commands, and extend the editor with Lua. -- editor:message("Loading init.lua...") -- ============================================================================ -- MODE SYSTEM (Emacs-style Major and Minor Modes) -- ============================================================================ -- Mode registries local major_modes = {} local minor_modes = {} -- Active modes per buffer (keyed by buffer name) local buffer_major_modes = {} local buffer_minor_modes = {} -- Define a major mode function define_major_mode(name, config) major_modes[name] = { name = name, file_patterns = config.file_patterns or {}, setup = config.setup or function() end, cleanup = config.cleanup or function() end, highlight = config.highlight or nil, keybindings = config.keybindings or {}, comment_syntax = config.comment_syntax or "--", } editor:message(string.format("[Mode] Registered major mode: %s", name)) end -- Define a minor mode function define_minor_mode(name, config) minor_modes[name] = { name = name, setup = config.setup or function() end, cleanup = config.cleanup or function() end, keybindings = config.keybindings or {}, global = config.global or false, -- If true, applies to all buffers } editor:message(string.format("[Mode] Registered minor mode: %s", name)) end -- Activate a major mode for the current buffer function activate_major_mode(mode_name) local buf = editor.buffer local buf_name = buf:name() local mode = major_modes[mode_name] if not mode then editor:message("Unknown major mode: " .. mode_name) return false end -- Deactivate current major mode if any if buffer_major_modes[buf_name] then deactivate_major_mode() end editor:message(string.format("[Mode] Activating major mode '%s' for buffer '%s'", mode_name, buf_name)) -- Store active mode buffer_major_modes[buf_name] = mode_name -- Set up event handler for auto-highlighting if mode.highlight then buf:on_buffer_event(function(event_data) local current_buf = editor.buffer if event_data.event == lumacs.BufferEvent.Loaded or event_data.event == lumacs.BufferEvent.LanguageChanged then mode.highlight() editor:message(string.format("[Mode] Auto-highlighted buffer with %s", mode_name)) end end) -- Highlight immediately mode.highlight() end -- Apply mode-specific keybindings (these are temporary for this buffer) for key, func in pairs(mode.keybindings) do editor:bind_key(key, func) end -- Run setup function mode.setup() editor:message(string.format("Major mode: %s", mode_name)) return true end -- Deactivate current major mode function deactivate_major_mode() local buf = editor.buffer local buf_name = buf:name() local mode_name = buffer_major_modes[buf_name] if not mode_name then return end local mode = major_modes[mode_name] if mode and mode.cleanup then mode.cleanup() end buffer_major_modes[buf_name] = nil editor:message(string.format("[Mode] Deactivated major mode '%s'", mode_name)) end -- Toggle a minor mode function toggle_minor_mode(mode_name) local buf = editor.buffer local buf_name = buf:name() local mode = minor_modes[mode_name] if not mode then editor:message("Unknown minor mode: " .. mode_name) return end -- Initialize minor modes table for this buffer if not buffer_minor_modes[buf_name] then buffer_minor_modes[buf_name] = {} end local is_active = buffer_minor_modes[buf_name][mode_name] if is_active then -- Deactivate if mode.cleanup then mode.cleanup() end buffer_minor_modes[buf_name][mode_name] = nil editor:message(string.format("Minor mode disabled: %s", mode_name)) else -- Activate buffer_minor_modes[buf_name][mode_name] = true if mode.setup then mode.setup() end editor:message(string.format("Minor mode enabled: %s", mode_name)) end end -- Auto-detect and activate major mode based on file extension function auto_activate_major_mode() local buf = editor.buffer local buf_name = buf:name() -- Try to match file pattern for mode_name, mode in pairs(major_modes) do for _, pattern in ipairs(mode.file_patterns) do if string.match(buf_name, pattern) then activate_major_mode(mode_name) return end end end -- No match, use fundamental mode (default) editor:message(string.format("[Mode] No major mode matched for '%s', using fundamental-mode", buf_name)) end -- Get current major mode name function current_major_mode() local buf = editor.buffer local buf_name = buf:name() return buffer_major_modes[buf_name] or "fundamental-mode" end -- ============================================================================ -- MAJOR MODES -- ============================================================================ -- Lua Mode define_major_mode("lua-mode", { file_patterns = {"%.lua$"}, comment_syntax = "--", highlight = function() local buf = editor.buffer buf:clear_styles() -- Keywords to highlight local keywords = { "function", "local", "end", "if", "then", "else", "elseif", "for", "while", "do", "return", "break", "and", "or", "not", "true", "false", "nil", "in", "repeat", "until" } -- 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 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 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, setup = function() editor:message("[lua-mode] Lua mode activated") end, cleanup = function() editor:message("[lua-mode] Lua mode deactivated") end, keybindings = { -- Lua-specific keybindings can go here } }) -- Fundamental Mode (default/fallback) define_major_mode("fundamental-mode", { file_patterns = {}, setup = function() editor:message("[fundamental-mode] Fundamental mode activated") end }) -- ============================================================================ -- MINOR MODES -- ============================================================================ -- Auto-save minor mode define_minor_mode("auto-save-mode", { global = false, setup = function() -- TODO: Set up auto-save timer editor:message("[auto-save-mode] Auto-save enabled") end, cleanup = function() editor:message("[auto-save-mode] Auto-save disabled") end }) -- Line numbers minor mode (conceptual - already always shown) define_minor_mode("line-numbers-mode", { global = true, setup = function() editor:message("[line-numbers-mode] Line numbers enabled") end, cleanup = function() editor:message("[line-numbers-mode] Line numbers disabled") end }) -- ============================================================================ -- GLOBAL KEYBINDINGS -- ============================================================================ -- Example: Custom keybindings -- Syntax: editor:bind_key("key", function() ... end) -- Basic Editing Commands (moved from C++ fallback) function lumacs_insert_newline() local cursor = editor.cursor editor.buffer:insert_newline(cursor) editor:set_cursor(lumacs.Position(cursor.line + 1, 0)) end editor:bind_key("Return", lumacs_insert_newline) editor:register_command("insert-newline", "Insert a new line at cursor position.", lumacs_insert_newline, true) function lumacs_backward_delete_char() local cursor_original = editor.cursor -- If at (0,0), nothing to delete before it. if cursor_original.column == 0 and cursor_original.line == 0 then return end -- Call erase_char with the current cursor position. -- erase_char will internally delete the char at (pos.column - 1). editor.buffer:erase_char(cursor_original) -- The cursor position should be adjusted *after* the erase. -- This is the same logic as before, calculating the new cursor position. local new_cursor_pos = cursor_original if new_cursor_pos.column > 0 then new_cursor_pos = lumacs.Position(new_cursor_pos.line, new_cursor_pos.column - 1) elseif new_cursor_pos.line > 0 then -- If line was joined, new cursor is at end of previous line. local prev_line_len = #editor.buffer:line(new_cursor_pos.line - 1) new_cursor_pos = lumacs.Position(new_cursor_pos.line - 1, prev_line_len) end editor.cursor = new_cursor_pos end editor:bind_key("Backspace", lumacs_backward_delete_char) editor:register_command("backward-delete-char", "Delete the character before cursor.", lumacs_backward_delete_char, true) editor:bind_key("C-m", lumacs_backward_delete_char) editor:register_command("backward-delete-char", "Delete the character before cursor.", lumacs_backward_delete_char, true) function lumacs_delete_char() local cursor = editor.cursor editor.buffer:erase_char(lumacs.Position(cursor.line, cursor.column + 1)) end editor:bind_key("Delete", lumacs_delete_char) editor:register_command("delete-char", "Delete the character at cursor position.", lumacs_delete_char, true) -- Navigation Commands (explicitly bound arrow keys) editor:bind_key("ArrowUp", function() editor:move_up() end) editor:bind_key("ArrowDown", function() editor:move_down() end) editor:bind_key("ArrowLeft", function() editor:move_left() end) editor:bind_key("ArrowRight", function() editor:move_right() end) editor:bind_key("Home", function() editor:move_to_line_start() end) editor:bind_key("End", function() editor:move_to_line_end() end) -- Generic self-insert command for printable characters -- This command is special; it's called by the C++ core if no other binding matches a printable char. function self_insert_command(args) local char_to_insert = args[1] if not char_to_insert then return end local cursor = editor.cursor editor.buffer:insert(cursor, char_to_insert) editor:move_right() end editor:register_command("self-insert-command", "Insert the character pressed.", self_insert_command, true) -- Emacs-style navigation (Ctrl+N/P for next/previous line) editor:bind_key("C-n", function() editor:move_down() editor:message("Moved down") end) editor:bind_key("C-p", function() editor:move_up() editor:message("Moved up") end) editor:bind_key("C-f", function() editor:move_right() end) editor:bind_key("C-b", function() editor:move_left() end) -- Emacs-style line navigation editor:bind_key("C-a", function() editor:move_to_line_start() end) editor:bind_key("C-e", function() editor:move_to_line_end() end) -- M-f (forward-word) - Move forward one word editor:bind_key("M-f", function() editor:move_forward_word() end) -- M-b (backward-word) - Move backward one word editor:bind_key("M-b", function() editor:move_backward_word() end) -- C-v (scroll-up) - Page down editor:bind_key("C-v", function() editor:page_down() end) -- M-v (scroll-down) - Page up editor:bind_key("M-v", function() editor:page_up() end) -- M-< (beginning-of-buffer) - Go to start editor:bind_key("M-<", function() editor:goto_beginning() editor:message("Beginning of buffer") end) -- M-> (end-of-buffer) - Go to end editor:bind_key("M->", function() editor:goto_end() editor:message("End of buffer") end) -- M-g M-g (goto-line) - Jump to line number editor:bind_key("M-g g", function() editor:command_mode() -- TODO: Implement line number input in command mode end) -- Note: C-s binding moved to avoid conflicts with isearch -- Custom command: Insert timestamp editor:bind_key("C-t", function() local cursor_pos = editor.cursor local timestamp = os.date("%Y-%m-%d %H:%M:%S") editor.buffer:insert(cursor_pos, timestamp) editor:message("Inserted timestamp") end) -- Example: Helper functions you can define function goto_line(line_num) local pos = lumacs.Position(line_num - 1, 0) editor:set_cursor(pos) editor:message("Jumped to line " .. line_num) end -- Example: Buffer inspection function buffer_info() local buf = editor.buffer local cursor = editor.cursor editor:message(string.format( "Buffer: %s | Lines: %d | Cursor: %d,%d | Modified: %s", buf:name(), buf:line_count(), cursor.line + 1, cursor.column + 1, buf:is_modified() and "yes" or "no" )) end -- Bind to show buffer info -- Note: C-i and Tab are indistinguishable in most terminals (both send ASCII 9) -- Using C-u instead for buffer info editor:bind_key("C-u", 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 editor:message("Found '" .. query .. "' at " .. res.start.line .. ":" .. res.start.column) -- Optional: Highlight the found range? else editor:message("'" .. query .. "' not found") end end -- Example binding: Find "TODO" editor: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 editor:message("Already at first line") return end editor: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) editor: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 editor: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) editor:message("Swapped line down") end -- Bind to M-ArrowUp and M-ArrowDown (Meta/Alt + arrows) editor:bind_key("M-ArrowUp", swap_line_up) editor:bind_key("M-ArrowDown", swap_line_down) -- ============================================================================ -- WINDOW MANAGEMENT -- ============================================================================ -- Split horizontal (like Emacs C-x 2, simplified to M-2) editor:bind_key("M-2", function() editor:split_horizontally() editor:message("Split horizontally") end) -- Split vertical (like Emacs C-x 3, simplified to M-3) editor:bind_key("M-3", function() editor:split_vertically() editor:message("Split vertically") end) -- Close window (like Emacs C-x 0, simplified to M-0) editor:bind_key("M-0", function() editor:close_window() editor:message("Closed window") end) -- Command Mode (Minibuffer) editor:bind_key("M-x", function() editor:command_mode() end) -- ============================================================================ -- MARK AND REGION (Emacs-style selection) -- ============================================================================ -- C-@ or C-SPC (set-mark-command) - Set the mark at cursor editor:bind_key("C-@", function() local buf = editor.buffer local cursor = editor.cursor buf:set_mark(cursor) editor:message("Mark set") end) -- For terminals that don't support C-@, also bind to C-SPC (but C-SPC is hard to detect) -- Most terminals send C-@ for C-SPC, so the above should work -- C-x C-x (exchange-point-and-mark) - Swap cursor and mark editor:bind_key("C-x C-x", function() local buf = editor.buffer local mark = buf.mark if not mark then editor:message("No mark set") return end local cursor = editor.cursor buf:set_mark(cursor) -- Set mark at old cursor position editor.cursor = mark -- Move cursor to old mark position editor:message("Mark and point exchanged") end) -- C-x h (mark-whole-buffer) - Select entire buffer editor:bind_key("C-x h", function() local buf = editor.buffer -- Set mark at beginning buf:set_mark(lumacs.Position(0, 0)) -- Move cursor to end local last_line = buf:line_count() - 1 local last_col = #buf:line(last_line) editor.cursor = lumacs.Position(last_line, last_col) editor:message("Buffer marked") end) -- ============================================================================ -- KILL RING (Emacs cut/copy/paste) -- ============================================================================ -- C-w (kill-region) - Cut selection editor:bind_key("C-w", function() editor:kill_region() end) -- M-w (kill-ring-save) - Copy selection editor:bind_key("M-w", function() editor:copy_region_as_kill() end) -- C-k (kill-line) - Cut from cursor to end of line editor:bind_key("C-k", function() editor:kill_line() editor:message("Killed line") end) -- C-y (yank) - Paste editor:bind_key("C-y", function() editor:yank() end) -- M-y (yank-pop) - Cycle through kill ring after yanking editor:bind_key("M-y", function() editor:yank_pop() end) -- M-d (kill-word) - Kill word forward editor:bind_key("M-d", function() editor:kill_word() end) -- M-Backspace (backward-kill-word) - Kill word backward editor:bind_key("M-Backspace", function() editor:backward_kill_word() end) -- ============================================================================ -- UNDO/REDO -- ============================================================================ -- C-/ or C-_ for undo (traditional Emacs binding) editor:bind_key("C-/", function() if editor:undo() then editor:message("Undid change") else editor:message("Nothing to undo") end end) -- Also keep C-z for undo (common in other editors) editor:bind_key("C-z", function() if editor:undo() then editor:message("Undid change") else editor:message("Nothing to undo") end end) -- C-x u for redo (less common but sometimes used) editor:bind_key("C-x u", function() if editor:redo() then editor:message("Redid change") else editor:message("Nothing to redo") end end) -- Manual re-highlight key (re-applies current major mode's highlighting) editor:bind_key("C-l", function() local mode_name = current_major_mode() local mode = major_modes[mode_name] if mode and mode.highlight then mode.highlight() -- 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 editor:message(string.format("Re-highlighted with %s (%d styles)", mode_name, styles_count)) else editor:message("Current mode has no highlighting: " .. mode_name) end end) -- Mode information and control editor:bind_key("C-h m", function() local mode_name = current_major_mode() local buf = editor.buffer local buf_name = buf:name() -- Get active minor modes local minor_list = {} if buffer_minor_modes[buf_name] then for mode, _ in pairs(buffer_minor_modes[buf_name]) do table.insert(minor_list, mode) end end local minor_str = #minor_list > 0 and table.concat(minor_list, ", ") or "none" editor:message(string.format("Major: %s | Minor: %s", mode_name, minor_str)) end) -- Test Escape key binding editor:bind_key("Escape", function() editor:message("Escape pressed! (Direct binding works)") end) -- C-x sequence bindings (Emacs style) editor:bind_key("C-x o", function() editor:next_window() editor:message("Switched window with C-x o") end) editor:bind_key("C-x 2", function() editor:split_horizontally() editor:message("Split horizontally with C-x 2") end) editor:bind_key("C-x 3", function() editor:split_vertically() editor:message("Split vertically with C-x 3") end) editor:bind_key("C-x 0", function() editor:close_window() editor:message("Closed window with C-x 0") end) -- ============================================================================ -- BUFFER MANAGEMENT (C-x b, C-x C-b, C-x k) -- ============================================================================ -- C-x b: Switch to buffer (with tab completion) editor:bind_key("C-x b", function() editor:buffer_switch_mode() end) -- C-x k: Kill buffer (with tab completion) editor:bind_key("C-x k", function() editor:kill_buffer_mode() end) -- C-x C-b: List all buffers editor:bind_key("C-x C-b", function() local buffer_info = editor:get_all_buffer_info() if #buffer_info == 0 then editor:message("No buffers open") return end -- Format buffer list local lines = {} table.insert(lines, "Buffer List:") table.insert(lines, "------------") table.insert(lines, "") table.insert(lines, string.format("% -3s % -20s % -10s %s", "Mod", "Name", "Size", "File")) table.insert(lines, string.format("% -3s % -20s % -10s %s", "---", "----")) for i, info in ipairs(buffer_info) do local modified = info.modified and " * " or " " local filepath = "" if info.filepath then filepath = tostring(info.filepath) end local line = string.format("%s % -20s % -10d %s", modified, info.name, info.size, filepath ) table.insert(lines, line) end local list_text = table.concat(lines, "\n") local list_buf_name = "*Buffer List*" -- Switch to or create buffer local list_buf = editor:get_buffer_by_name(list_buf_name) if list_buf then editor:switch_buffer_in_window(list_buf_name) else editor:new_buffer(list_buf_name) end local buf = editor.buffer buf:clear() buf:insert(lumacs.Position(0,0), list_text) editor:goto_beginning() editor:message(string.format("Buffer list (%d buffers)", #buffer_info)) end) -- ============================================================================ -- CASE CONVERSION (M-u, M-l, M-c) -- ============================================================================ -- M-u (upcase-word) editor:bind_key("M-u", function() local start_pos = editor.cursor editor:move_forward_word() local end_pos = editor.cursor local range = lumacs.Range(start_pos, end_pos) local text = editor.buffer:get_text_in_range(range) if #text > 0 then editor.buffer:replace(range, string.upper(text)) end end) -- M-l (downcase-word) editor:bind_key("M-l", function() local start_pos = editor.cursor editor:move_forward_word() local end_pos = editor.cursor local range = lumacs.Range(start_pos, end_pos) local text = editor.buffer:get_text_in_range(range) if #text > 0 then editor.buffer:replace(range, string.lower(text)) end end) -- M-c (capitalize-word) editor:bind_key("M-c", function() local start_pos = editor.cursor editor:move_forward_word() local end_pos = editor.cursor local range = lumacs.Range(start_pos, end_pos) local text = editor.buffer:get_text_in_range(range) if #text > 0 then local cap_text = text:sub(1,1):upper() .. text:sub(2):lower() editor.buffer:replace(range, cap_text) end end) -- C-x C-u (upcase-region) editor:bind_key("C-x C-u", function() local range = editor.buffer:get_region(editor.cursor) if not range then editor:message("No active region") return end local text = editor.buffer:get_text_in_range(range) if #text > 0 then editor.buffer:replace(range, string.upper(text)) editor.buffer:deactivate_mark() end end) -- C-x C-l (downcase-region) editor:bind_key("C-x C-l", function() local range = editor.buffer:get_region(editor.cursor) if not range then editor:message("No active region") return end local text = editor.buffer:get_text_in_range(range) if #text > 0 then editor.buffer:replace(range, string.lower(text)) editor.buffer:deactivate_mark() end end) -- ============================================================================ -- COMMENTING (M- ;) -- ============================================================================ function escape_pattern(text) return text:gsub("([^%w])", "%%%1") end function comment_dwim() local mode_name = current_major_mode() local mode = major_modes[mode_name] local prefix = mode and mode.comment_syntax or "--" local escaped_prefix = escape_pattern(prefix) local range = editor.buffer:get_region(editor.cursor) if range then -- Region handling local start_line = range.start.line local end_line = range["end"].line -- Check if all lines are commented local all_commented = true for i = start_line, end_line do local line = editor.buffer:line(i) -- Ignore empty lines for decision if #line > 0 and not string.match(line, "^%s*" .. escaped_prefix) then all_commented = false break end end -- Apply change for i = start_line, end_line do local line = editor.buffer:line(i) local line_range = lumacs.Range(lumacs.Position(i, 0), lumacs.Position(i, #line)) if #line > 0 then if all_commented then -- Uncomment local s, e = string.find(line, escaped_prefix) if s then local suffix = line:sub(e+1) if suffix:sub(1,1) == " " then suffix = suffix:sub(2) end local new_line = line:sub(1, s-1) .. suffix editor.buffer:replace(line_range, new_line) end else -- Comment (if not already commented) if not string.match(line, "^%s*" .. escaped_prefix) then local indent = string.match(line, "^%s*") local content = line:sub(#indent + 1) local new_line = indent .. prefix .. " " .. content editor.buffer:replace(line_range, new_line) end end end end else -- Single line local line_num = editor.cursor.line local line = editor.buffer:line(line_num) local line_range = lumacs.Range(lumacs.Position(line_num, 0), lumacs.Position(line_num, #line)) if string.match(line, "^%s*" .. escaped_prefix) then -- Uncomment local s, e = string.find(line, escaped_prefix) if s then local suffix = line:sub(e+1) if suffix:sub(1,1) == " " then suffix = suffix:sub(2) end local new_line = line:sub(1, s-1) .. suffix editor.buffer:replace(line_range, new_line) end else -- Comment local indent = string.match(line, "^%s*") local content = line:sub(#indent + 1) local new_line = indent .. prefix .. " " .. content editor.buffer:replace(line_range, new_line) end end end editor:bind_key("M-;", comment_dwim) -- ============================================================================ -- REGISTERS (C-x r s, C-x r i) -- ============================================================================ -- Helper function to prompt for register character function get_register_char(prompt_msg) editor:message(prompt_msg) -- For now we'll use a simple approach - this would need UI integration -- In a full implementation, this would wait for a single character input -- For demo purposes, we'll use 'a' as default return 'a' end -- C-x r s (copy-to-register) - Save region to register editor:bind_key("C-x r s", function() local register_char = get_register_char("Save to register:") editor:copy_region_to_register(register_char) end) -- C-x r i (insert-register) - Insert from register editor:bind_key("C-x r i", function() local register_char = get_register_char("Insert register:") editor:yank_from_register(register_char) end) -- ============================================================================ -- RECTANGLES (C-x r k/y/t) -- ============================================================================ -- C-x r k (kill-rectangle) - Cut rectangular region editor:bind_key("C-x r k", function() editor:kill_rectangle() end) -- C-x r y (yank-rectangle) - Paste rectangular region editor:bind_key("C-x r y", function() editor:yank_rectangle() end) -- C-x r t (string-rectangle) - Fill rectangle with text editor:bind_key("C-x r t", function() -- For demo purposes, we'll fill with asterisks -- In a full implementation, this would prompt for text input editor:string_rectangle("*") editor:message("Rectangle filled with '*' - C-x r t demo") end) -- ============================================================================ -- KEYBOARD MACROS (F3, F4) -- ============================================================================ -- F3 - start-kbd-macro editor:bind_key("F3", function() editor:start_kbd_macro() end) -- F4 - end-kbd-macro or call-last-kbd-macro editor:bind_key("F4", function() editor:end_kbd_macro_or_call() end) -- ============================================================================ -- INCREMENTAL SEARCH (C-s, C-r) -- ============================================================================ -- C-s (save-buffer) - changed from isearch to save for now editor:bind_key("C-s", function() local buf = editor.buffer if buf:save() then editor:message("Buffer saved: " .. buf:name()) else editor:message("Failed to save buffer") end end) -- C-r (isearch-backward) editor:bind_key("C-r", function() editor:isearch_backward_mode() end) -- Note: C-k and C-s already defined above, removed duplicates -- ============================================================================ -- CONFIGURATION FUNCTIONS -- ============================================================================ -- Toggle line numbers on/off function toggle_line_numbers() local config = editor.config local current = config:get_bool("show_line_numbers", true) config:set("show_line_numbers", not current) if not current then editor:message("Line numbers enabled") else editor:message("Line numbers disabled") end end -- Set line number width function set_line_number_width(width) editor.config:set("line_number_width", width) editor:message("Line number width set to " .. width) end -- Show current configuration function show_config() local config = editor.config local show_nums = config:get_bool("show_line_numbers", true) local width = config:get_int("line_number_width", 6) editor:message(string.format("Line numbers: %s, Width: %d", show_nums and "on" or "off", width)) end -- Toggle modeline (status bar for each window) function function toggle_modeline() local current = editor.config:get_bool("show_modeline", true) editor.config:set_bool("show_modeline", not current) if current then editor:message("Modeline disabled") else editor:message("Modeline enabled") end end -- Bind configuration functions editor:bind_key("C-x l", toggle_line_numbers) -- C-x l to toggle line numbers editor:bind_key("C-x m", toggle_modeline) -- C-x m to toggle modeline editor:bind_key("C-x C-c", function() editor:quit() end) -- C-x C-c to quit editor:bind_key("C-x C-s", show_config) -- C-x C-s to show config -- Load theme configuration dofile("themes.lua") -- Welcome message editor:message("Lumacs ready! C-k=kill, C-y=yank, C-@=mark, C-w=cut, M-w=copy, M-f/b=word, C-v/M-v=page") -- Auto-activate mode for initial buffer auto_activate_major_mode() -- File and Buffer Commands editor:register_command("save-buffer", "Save current buffer", function(args) if editor.buffer:save() then editor:message("Saved " .. editor.buffer:name()) else editor:message("Save failed") end end, true, "b") -- "b" means prompt for existing buffer name editor:register_command("find-file", "Find file", function(args) editor:find_file_mode() end, true, "f") -- "f" means prompt for file name editor:register_command("kill-buffer", "Kill buffer", function(args) editor:kill_buffer_mode() end, true, "b") editor:register_command("switch-buffer", "Switch buffer", function(args) editor:buffer_switch_mode() end, true, "b") -- Alias for switch-buffer to match Emacs expectations editor:register_command("switch-to-buffer", "Switch buffer (Alias)", function(args) editor:buffer_switch_mode() end, true, "b") editor:register_command("list-buffers", "List all buffers", function(args) local buffer_info = editor:get_all_buffer_info() if #buffer_info == 0 then editor:message("No buffers open") return {success = true, message = "No buffers open"} end local lines = {} table.insert(lines, "Buffer List:") table.insert(lines, "------------") table.insert(lines, "") table.insert(lines, string.format("% -3s % -20s % -10s %s", "Mod", "Name", "Size", "File")) table.insert(lines, string.format("% -3s % -20s % -10s %s", "---", "----")) for i, info in ipairs(buffer_info) do local modified = info.modified and " * " or " " local filepath = "" if info.filepath then filepath = tostring(info.filepath) end table.insert(lines, string.format("%s % -20s % -10d %s", modified, info.name, info.size, filepath)) end local list_text = table.concat(lines, "\n") local list_buf_name = "*Buffer List*" local list_buf = editor:get_buffer_by_name(list_buf_name) if list_buf then editor:switch_buffer_in_window(list_buf_name) else editor:new_buffer(list_buf_name) end editor.buffer:clear() editor.buffer:insert(lumacs.Position(0,0), list_text) editor:goto_beginning() return {success = true, message = string.format("Buffer list (%d buffers)", #buffer_info)} end, false) -- Not typically interactive through prompt -- Navigation editor:register_command("next-line", "Move cursor down", function() editor:move_down() end, true) editor:register_command("previous-line", "Move cursor up", function() editor:move_up() end, true) editor:register_command("forward-char", "Move cursor right", function() editor:move_right() end, true) editor:register_command("backward-char", "Move cursor left", function() editor:move_left() end, true) editor:register_command("forward-word", "Move forward one word", function() editor:move_forward_word() end, true) editor:register_command("backward-word", "Move backward one word", function() editor:move_backward_word() end, true) editor:register_command("beginning-of-buffer", "Go to beginning of buffer", function() editor:goto_beginning() end, true) editor:register_command("end-of-buffer", "Go to end of buffer", function() editor:goto_end() end, true) editor:register_command("scroll-up-command", "Page down", function() editor:page_down() end, true) editor:register_command("scroll-down-command", "Page up", function() editor:page_up() end, true) -- Window Management editor:register_command("split-window-below", "Split window horizontally", function() editor:split_horizontally() end, true) editor:register_command("split-window-right", "Split window vertically", function() editor:split_vertically() end, true) editor:register_command("delete-window", "Close current window", function() editor:close_window() end, true) editor:register_command("other-window", "Select other window", function() editor:next_window() end, true) editor:register_command("delete-other-windows", "Delete all other windows", function() -- Simplified: keep closing others until only 1? -- Or just implement properly in C++ later. -- For now, assume users use C-x 1 if implemented? -- C-x 1 isn't implemented in init.lua yet. editor:message("delete-other-windows not implemented yet") return {success = false, message = "delete-other-windows not implemented yet"} end, false) -- Editing editor:register_command("kill-line", "Kill rest of line", function() editor:kill_line() end, true) editor:register_command("kill-region", "Kill selected region", function() editor:kill_region() end, true) editor:register_command("copy-region-as-kill", "Copy region", function() editor:copy_region_as_kill() end, true) editor:register_command("yank", "Paste from kill ring", function() editor:yank() end, true) editor:register_command("undo", "Undo last change", function() if editor:undo() then editor:message("Undid") else editor:message("No undo") end end, true) editor:register_command("redo", "Redo last undo", function() if editor:redo() then editor:message("Redid") else editor:message("No redo") end end, true) -- Modes editor:register_command("lua-mode", "Switch to Lua mode", function() activate_major_mode("lua-mode") end, true) editor:register_command("fundamental-mode", "Switch to Fundamental mode", function() activate_major_mode("fundamental-mode") end, true) editor:register_command("auto-save-mode", "Toggle auto-save", function() toggle_minor_mode("auto-save-mode") end, true) editor:message("Commands loaded. Try M-x list-buffers") -- ============================================================================ -- NEW COMMAND SYSTEM INTEGRATION -- ============================================================================ -- Register additional commands with the new command system editor:register_command("describe-mode", "Show current major and minor modes", function(args) local mode_name = current_major_mode() local buf = editor.buffer local buf_name = buf:name() local minor_list = {} if buffer_minor_modes[buf_name] then for mode, _ in pairs(buffer_minor_modes[buf_name]) do table.insert(minor_list, mode) end end local minor_str = #minor_list > 0 and table.concat(minor_list, ", ") or "none" return {success = true, message = string.format("Major: %s | Minor: %s", mode_name, minor_str)} end, false) editor:register_command("count-lines", "Count lines in buffer or region", function(args) local buf = editor.buffer local region = buf:get_region(editor.cursor) if region then local lines = region["end"].line - region.start.line + 1 return {success = true, message = string.format("Region has %d lines", lines)} else local lines = buf:line_count() return {success = true, message = string.format("Buffer has %d lines", lines)} end end, false) editor:register_command("word-count", "Count words in buffer or region", function(args) local buf = editor.buffer local region = buf:get_region(editor.cursor) local text if region then text = buf:get_text_in_range(region) else text = buf:get_all_text() end local words = 0 for word in text:gmatch("%S+") do words = words + 1 end local target = region and "region" or "buffer" return {success = true, message = string.format("%s has %d words", target, words)} end, false) editor:register_command("goto-char", "Go to character position", function(args) if #args == 0 then return {success = false, message = "Character position required"} end local pos = tonumber(args[1]) if not pos then return {success = false, message = "Invalid character position: " .. args[1]} end local buf = editor.buffer local text = buf:get_all_text() if pos < 1 or pos > #text then return {success = false, message = "Position out of range"} end -- Convert character position to line/column local line = 0 local col = 0 for i = 1, pos - 1 do if text:sub(i, i) == '\n' then line = line + 1 col = 0 else col = col + 1 end end editor.cursor = lumacs.Position(line, col) return {success = true, message = string.format("Moved to character %d (line %d, column %d)", pos, line + 1, col + 1)} end, true, "n") editor:register_command("insert-date", "Insert current date", function(args) local cursor_pos = editor.cursor local timestamp = os.date("%Y-%m-%d") editor.buffer:insert(cursor_pos, timestamp) return {success = true, message = "Inserted current date"} end, false) editor:register_command("insert-datetime", "Insert current date and time", function(args) local cursor_pos = editor.cursor local timestamp = os.date("%Y-%m-%d %H:%M:%S") editor.buffer:insert(cursor_pos, timestamp) return {success = true, message = "Inserted current date and time"} end, false) editor:register_command("revert-buffer", "Reload buffer from file", function(args) local buf = editor.buffer local filepath = buf:filepath() if not filepath then return {success = false, message = "Buffer is not visiting a file"} end if buf:is_modified() then return {success = false, message = "Buffer has unsaved changes"} end if editor:load_file(filepath) then return {success = true, message = "Reverted " .. buf:name()} else return {success = false, message = "Failed to revert buffer"} end end, false) editor:register_command("rename-buffer", "Rename current buffer", function(args) if #args == 0 then return {success = false, message = "New buffer name required"} end local new_name = args[1] local buf = editor.buffer local old_name = buf:name() -- Check if name is already taken if editor:get_buffer_by_name(new_name) then return {success = false, message = "Buffer name already exists: " .. new_name} end buf:set_name(new_name) return {success = true, message = string.format("Renamed buffer '%s' to '%s'", old_name, new_name)} end, true, "s") -- "s" means prompt for string -- Development commands editor:register_command("eval-expression", "Evaluate Lua expression", function(args) if #args == 0 then return {success = false, message = "Lua expression required"} end local expr = table.concat(args, " ") local func, err = load("return " .. expr) if not func then return {success = false, message = "Parse error: " .. err} end local success, result = pcall(func) if success then return {success = true, message = tostring(result)} else return {success = false, message = "Error: " .. tostring(result)} end end, true, "s") -- Example of how to define a custom command that changes theme based on time of day editor:register_command("auto-theme", "Automatically set theme based on time of day", function(args) local hour = tonumber(os.date("%H")) local theme_name if hour >= 6 and hour < 18 then -- Daytime: use light theme theme_name = "gruvbox-light" elseif hour >= 18 and hour < 22 then -- Evening: use warm theme theme_name = "everforest-dark" else -- Night: use dark theme theme_name = "nord" end local success, message = editor:execute_command("set-theme", {theme_name}) if success then return {success = true, message = string.format("Auto-selected %s theme for %d:00", theme_name, hour)} else return {success = false, message = "Failed to auto-select theme: " .. message} end end, false) editor:register_command("theme-demo", "Demonstrate theme switching", function(args) local themes = {"solarized-dark", "nord", "gruvbox-light", "dracula"} local demo_msg = "Theme demo - switching through: " .. table.concat(themes, ", ") -- This would ideally be enhanced with a timer to show themes in sequence -- For now, just switch to a demo theme local success, message = editor:execute_command("set-theme", {themes[1]}) if success then return {success = true, message = demo_msg .. " (switched to " .. themes[1] .. ")"} else return {success = false, message = "Demo failed: " .. message} end end, false) -- ============================================================================ -- COMPLETION SYSTEM (Minibuffer Auto-Complete) -- ============================================================================ -- Returns a list of completion candidates based on the current mode and input function get_completion_candidates(mode_name, input) local candidates = {} if mode_name == "Command" then -- Command completion (M-x) for name, _ in pairs(lumacs.command_registry) do if name:find(input, 1, true) == 1 then -- Prefix match table.insert(candidates, name) end end table.sort(candidates) elseif mode_name == "BufferSwitch" or mode_name == "KillBuffer" then -- Buffer name completion local buffers = editor:get_buffer_names() for _, name in ipairs(buffers) do if name:find(input, 1, true) == 1 then -- Prefix match table.insert(candidates, name) end end table.sort(candidates) elseif mode_name == "FindFile" then -- File path completion (simple version) -- Note: Full file system completion is complex to do in pure Lua without bindings -- This is a placeholder or relies on a bound helper if available. -- For now, we'll return empty list or maybe just current directory files if exposed. -- Since we don't have 'ls' exposed, we can't do much here yet without C++ help. end return candidates end