|
|
@@ -2,271 +2,962 @@
|
|
|
|
|
|
namespace lumacs {
|
|
|
|
|
|
-constexpr const char* LUA_DEFAULTS = R"(
|
|
|
--- =========================================================
|
|
|
--- Lumacs Embedded Defaults
|
|
|
--- =========================================================
|
|
|
+constexpr const char* LUA_DEFAULTS = R"lua(
|
|
|
+-- =====================================================
|
|
|
+-- LUMACS EMBEDDED DEFAULTS
|
|
|
+-- =====================================================
|
|
|
-- This runs BEFORE the user's init.lua.
|
|
|
--- It establishes the core editing environment.
|
|
|
+-- It establishes the complete Emacs-like editing environment.
|
|
|
+-- User init.lua should only EXTEND, not redefine these.
|
|
|
|
|
|
--- 1. Sane Editor Config
|
|
|
+-- =====================================================
|
|
|
+-- 1. CONFIGURATION DEFAULTS
|
|
|
+-- =====================================================
|
|
|
editor.config:set("show_line_numbers", true)
|
|
|
editor.config:set("tab_width", 4)
|
|
|
--- Disable completion popover by default
|
|
|
+editor.config:set("indent_tabs_mode", false)
|
|
|
editor.config:set("show_completion_popover", false)
|
|
|
+editor.config:set("scroll_margin", 3)
|
|
|
+editor.config:set("visible_bell", true)
|
|
|
+editor.config:set("show_modeline", true)
|
|
|
|
|
|
--- 2. Mode Infrastructure
|
|
|
-local major_modes = {}
|
|
|
-local minor_modes = {}
|
|
|
-local buffer_major_modes = {}
|
|
|
-local buffer_minor_modes = {}
|
|
|
+-- =====================================================
|
|
|
+-- 2. MODE SYSTEM INFRASTRUCTURE
|
|
|
+-- =====================================================
|
|
|
+-- Use lumacs.* namespace to avoid polluting global scope
|
|
|
|
|
|
-function define_major_mode(name, config)
|
|
|
- major_modes[name] = {
|
|
|
+lumacs = lumacs or {}
|
|
|
+lumacs.major_modes = {}
|
|
|
+lumacs.minor_modes = {}
|
|
|
+lumacs.buffer_modes = {} -- {buffer_name -> {major = "mode-name", minor = {set}}}
|
|
|
+
|
|
|
+--- Define a major mode with optional configuration
|
|
|
+-- @param name string The mode name (e.g., "lua-mode")
|
|
|
+-- @param config table Configuration with file_patterns, setup, cleanup, highlight, keymap, comment_start/end
|
|
|
+function lumacs.define_major_mode(name, config)
|
|
|
+ config = config or {}
|
|
|
+ lumacs.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 "--",
|
|
|
+ keymap = config.keymap or {},
|
|
|
+ comment_start = config.comment_start or "# ",
|
|
|
+ comment_end = config.comment_end or "",
|
|
|
+ indent_function = config.indent_function or nil,
|
|
|
+ parent_mode = config.parent_mode or nil,
|
|
|
}
|
|
|
end
|
|
|
|
|
|
-function define_minor_mode(name, config)
|
|
|
- minor_modes[name] = {
|
|
|
+--- Define a minor mode
|
|
|
+-- @param name string The mode name
|
|
|
+-- @param config table Configuration with setup, cleanup, keymap, lighter, global
|
|
|
+function lumacs.define_minor_mode(name, config)
|
|
|
+ config = config or {}
|
|
|
+ lumacs.minor_modes[name] = {
|
|
|
name = name,
|
|
|
+ lighter = config.lighter or "",
|
|
|
setup = config.setup or function() end,
|
|
|
cleanup = config.cleanup or function() end,
|
|
|
- keybindings = config.keybindings or {},
|
|
|
+ keymap = config.keymap or {},
|
|
|
global = config.global or false,
|
|
|
}
|
|
|
end
|
|
|
|
|
|
-function activate_major_mode(mode_name)
|
|
|
+--- Activate a major mode for the current buffer
|
|
|
+-- @param mode_name string The mode to activate
|
|
|
+-- @return boolean Success
|
|
|
+function lumacs.activate_major_mode(mode_name)
|
|
|
local buf = editor.buffer
|
|
|
local buf_name = buf:name()
|
|
|
- local mode = major_modes[mode_name]
|
|
|
- if not mode then return false end
|
|
|
-
|
|
|
- buffer_major_modes[buf_name] = mode_name
|
|
|
-
|
|
|
+ local mode = lumacs.major_modes[mode_name]
|
|
|
+
|
|
|
+ if not mode then
|
|
|
+ editor:message("Unknown major mode: " .. mode_name)
|
|
|
+ return false
|
|
|
+ end
|
|
|
+
|
|
|
+ -- Initialize buffer mode tracking
|
|
|
+ lumacs.buffer_modes[buf_name] = lumacs.buffer_modes[buf_name] or {major = nil, minor = {}}
|
|
|
+
|
|
|
+ -- Cleanup previous mode if any
|
|
|
+ local prev_mode_name = lumacs.buffer_modes[buf_name].major
|
|
|
+ if prev_mode_name and lumacs.major_modes[prev_mode_name] then
|
|
|
+ local prev_mode = lumacs.major_modes[prev_mode_name]
|
|
|
+ if prev_mode.cleanup then
|
|
|
+ prev_mode.cleanup()
|
|
|
+ end
|
|
|
+ end
|
|
|
+
|
|
|
+ -- Set new mode
|
|
|
+ lumacs.buffer_modes[buf_name].major = mode_name
|
|
|
+
|
|
|
+ -- Setup highlighting if defined
|
|
|
if mode.highlight then
|
|
|
buf:on_buffer_event(function(event_data)
|
|
|
if event_data.event == lumacs.BufferEvent.Loaded or
|
|
|
+ event_data.event == lumacs.BufferEvent.AfterChange or
|
|
|
event_data.event == lumacs.BufferEvent.LanguageChanged then
|
|
|
mode.highlight()
|
|
|
end
|
|
|
end)
|
|
|
mode.highlight()
|
|
|
end
|
|
|
-
|
|
|
- -- Register mode-specific keybindings
|
|
|
- -- Note: This is a simple implementation that binds globally for now.
|
|
|
- -- A proper implementation would use a keymap stack.
|
|
|
- for key, func in pairs(mode.keybindings) do
|
|
|
- editor:bind_key(key, func)
|
|
|
+
|
|
|
+ -- Run setup
|
|
|
+ if mode.setup then
|
|
|
+ mode.setup()
|
|
|
end
|
|
|
-
|
|
|
- mode.setup()
|
|
|
+
|
|
|
return true
|
|
|
end
|
|
|
|
|
|
-function current_major_mode()
|
|
|
+--- Deactivate current major mode for current buffer
|
|
|
+function lumacs.deactivate_major_mode()
|
|
|
local buf = editor.buffer
|
|
|
local buf_name = buf:name()
|
|
|
- return buffer_major_modes[buf_name] or "fundamental-mode"
|
|
|
+ local buf_modes = lumacs.buffer_modes[buf_name]
|
|
|
+
|
|
|
+ if not buf_modes or not buf_modes.major then
|
|
|
+ return
|
|
|
+ end
|
|
|
+
|
|
|
+ local mode = lumacs.major_modes[buf_modes.major]
|
|
|
+ if mode and mode.cleanup then
|
|
|
+ mode.cleanup()
|
|
|
+ end
|
|
|
+
|
|
|
+ buf_modes.major = nil
|
|
|
end
|
|
|
|
|
|
-define_major_mode("fundamental-mode", {})
|
|
|
+--- Toggle a minor mode for current buffer
|
|
|
+-- @param mode_name string The minor mode to toggle
|
|
|
+function lumacs.toggle_minor_mode(mode_name)
|
|
|
+ local buf = editor.buffer
|
|
|
+ local buf_name = buf:name()
|
|
|
+ local mode = lumacs.minor_modes[mode_name]
|
|
|
+
|
|
|
+ if not mode then
|
|
|
+ editor:message("Unknown minor mode: " .. mode_name)
|
|
|
+ return
|
|
|
+ end
|
|
|
+
|
|
|
+ -- Initialize buffer mode tracking
|
|
|
+ lumacs.buffer_modes[buf_name] = lumacs.buffer_modes[buf_name] or {major = nil, minor = {}}
|
|
|
+ local minor_modes = lumacs.buffer_modes[buf_name].minor
|
|
|
|
|
|
-function auto_activate_major_mode()
|
|
|
+ if minor_modes[mode_name] then
|
|
|
+ -- Deactivate
|
|
|
+ if mode.cleanup then mode.cleanup() end
|
|
|
+ minor_modes[mode_name] = nil
|
|
|
+ editor:message(mode_name .. " disabled")
|
|
|
+ else
|
|
|
+ -- Activate
|
|
|
+ minor_modes[mode_name] = true
|
|
|
+ if mode.setup then mode.setup() end
|
|
|
+ editor:message(mode_name .. " enabled")
|
|
|
+ end
|
|
|
+end
|
|
|
+
|
|
|
+--- Auto-detect and activate major mode based on filename
|
|
|
+function lumacs.auto_activate_major_mode()
|
|
|
local buf = editor.buffer
|
|
|
local buf_name = buf:name()
|
|
|
- for mode_name, mode in pairs(major_modes) do
|
|
|
+
|
|
|
+ for mode_name, mode in pairs(lumacs.major_modes) do
|
|
|
for _, pattern in ipairs(mode.file_patterns) do
|
|
|
if string.match(buf_name, pattern) then
|
|
|
- activate_major_mode(mode_name)
|
|
|
- return
|
|
|
+ return lumacs.activate_major_mode(mode_name)
|
|
|
end
|
|
|
end
|
|
|
end
|
|
|
- activate_major_mode("fundamental-mode")
|
|
|
+
|
|
|
+ -- No match, use fundamental mode
|
|
|
+ return lumacs.activate_major_mode("fundamental-mode")
|
|
|
+end
|
|
|
+
|
|
|
+--- Get current major mode name for current buffer
|
|
|
+-- @return string The current major mode name
|
|
|
+function lumacs.current_major_mode()
|
|
|
+ local buf_name = editor.buffer:name()
|
|
|
+ local buf_modes = lumacs.buffer_modes[buf_name]
|
|
|
+ return buf_modes and buf_modes.major or "fundamental-mode"
|
|
|
end
|
|
|
|
|
|
--- 3. Basic Editing Commands & Registration
|
|
|
-
|
|
|
--- 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("move-beginning-of-line", "Go to beginning of line", function() editor:move_to_line_start() end, true)
|
|
|
-editor:register_command("move-end-of-line", "Go to end of line", function() editor:move_to_line_end() 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)
|
|
|
-
|
|
|
--- Insertion / Deletion
|
|
|
-function lumacs_insert_newline()
|
|
|
+--- Check if a minor mode is active
|
|
|
+-- @param mode_name string The minor mode to check
|
|
|
+-- @return boolean Whether the mode is active
|
|
|
+function lumacs.minor_mode_active(mode_name)
|
|
|
+ local buf_name = editor.buffer:name()
|
|
|
+ local buf_modes = lumacs.buffer_modes[buf_name]
|
|
|
+ return buf_modes and buf_modes.minor and buf_modes.minor[mode_name] or false
|
|
|
+end
|
|
|
+
|
|
|
+-- Make functions globally accessible (Emacs compatibility)
|
|
|
+define_major_mode = lumacs.define_major_mode
|
|
|
+define_minor_mode = lumacs.define_minor_mode
|
|
|
+activate_major_mode = lumacs.activate_major_mode
|
|
|
+deactivate_major_mode = lumacs.deactivate_major_mode
|
|
|
+toggle_minor_mode = lumacs.toggle_minor_mode
|
|
|
+auto_activate_major_mode = lumacs.auto_activate_major_mode
|
|
|
+current_major_mode = lumacs.current_major_mode
|
|
|
+
|
|
|
+-- Define fundamental-mode (always available)
|
|
|
+lumacs.define_major_mode("fundamental-mode", {
|
|
|
+ comment_start = "# ",
|
|
|
+})
|
|
|
+
|
|
|
+-- =====================================================
|
|
|
+-- 3. CORE EDITING FUNCTIONS
|
|
|
+-- =====================================================
|
|
|
+
|
|
|
+--- Insert newline at point
|
|
|
+function lumacs.newline()
|
|
|
+ local cursor = editor.cursor
|
|
|
+ editor.buffer:insert_newline(cursor)
|
|
|
+ editor:set_cursor(lumacs.Position(cursor.line + 1, 0))
|
|
|
+end
|
|
|
+
|
|
|
+--- Insert newline after point without moving
|
|
|
+function lumacs.open_line()
|
|
|
local cursor = editor.cursor
|
|
|
editor.buffer:insert_newline(cursor)
|
|
|
- -- Move to start of next line
|
|
|
- editor.cursor = lumacs.Position(cursor.line + 1, 0)
|
|
|
+ -- Don't move cursor
|
|
|
end
|
|
|
-editor:register_command("insert-newline", "Insert newline", lumacs_insert_newline, true)
|
|
|
|
|
|
-function lumacs_backward_delete_char()
|
|
|
+--- Delete character before point
|
|
|
+function lumacs.delete_backward_char()
|
|
|
local cursor = editor.cursor
|
|
|
if cursor.line == 0 and cursor.column == 0 then return end
|
|
|
-
|
|
|
+
|
|
|
editor.buffer:erase_char(cursor)
|
|
|
-
|
|
|
- -- Calculate new cursor position
|
|
|
- local new_pos = cursor
|
|
|
- if new_pos.column > 0 then
|
|
|
- new_pos = lumacs.Position(new_pos.line, new_pos.column - 1)
|
|
|
- elseif new_pos.line > 0 then
|
|
|
- local prev_line_len = #editor.buffer:line(new_pos.line - 1)
|
|
|
- new_pos = lumacs.Position(new_pos.line - 1, prev_line_len)
|
|
|
- end
|
|
|
- editor.cursor = new_pos
|
|
|
+
|
|
|
+ if cursor.column > 0 then
|
|
|
+ editor:set_cursor(lumacs.Position(cursor.line, cursor.column - 1))
|
|
|
+ elseif cursor.line > 0 then
|
|
|
+ local prev_len = #editor.buffer:line(cursor.line - 1)
|
|
|
+ editor:set_cursor(lumacs.Position(cursor.line - 1, prev_len))
|
|
|
+ end
|
|
|
end
|
|
|
-editor:register_command("backward-delete-char", "Delete char backward", lumacs_backward_delete_char, true)
|
|
|
|
|
|
-function lumacs_delete_char()
|
|
|
+--- Delete character after point
|
|
|
+function lumacs.delete_char()
|
|
|
local cursor = editor.cursor
|
|
|
editor.buffer:erase_char(lumacs.Position(cursor.line, cursor.column + 1))
|
|
|
end
|
|
|
-editor:register_command("delete-char", "Delete char forward", lumacs_delete_char, true)
|
|
|
|
|
|
-function self_insert_command(args)
|
|
|
- local char_to_insert = args[1]
|
|
|
- if not char_to_insert then return end
|
|
|
- editor.buffer:insert(editor.cursor, char_to_insert)
|
|
|
- editor:move_right()
|
|
|
+--- Self-insert command for printable characters
|
|
|
+function lumacs.self_insert(args)
|
|
|
+ local char = args[1]
|
|
|
+ if char then
|
|
|
+ editor.buffer:insert(editor.cursor, char)
|
|
|
+ editor:move_right()
|
|
|
+ end
|
|
|
end
|
|
|
-editor:register_command("self-insert-command", "Insert char", self_insert_command, true)
|
|
|
-
|
|
|
--- Clipboard / Kill Ring
|
|
|
-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", function() editor:yank() end, true)
|
|
|
-editor:register_command("yank-pop", "Cycle paste", function() editor:yank_pop() end, true)
|
|
|
-
|
|
|
--- Undo/Redo
|
|
|
-editor:register_command("undo", "Undo", function() editor:undo() end, true)
|
|
|
-editor:register_command("redo", "Redo", function() editor:redo() end, true)
|
|
|
-
|
|
|
--- File/Buffer
|
|
|
-editor:register_command("save-buffer", "Save current buffer", function()
|
|
|
- if editor.buffer:save() then editor:message("Saved") else editor:message("Failed to save") end
|
|
|
-end, true)
|
|
|
-editor:register_command("find-file", "Open file", function() editor:find_file_mode() end, true, "f")
|
|
|
-editor:register_command("switch-buffer", "Switch buffer", function() editor:buffer_switch_mode() end, true, "b")
|
|
|
-editor:register_command("kill-buffer", "Kill buffer", function() editor:kill_buffer_mode() end, true, "b")
|
|
|
-editor:register_command("save-buffers-kill-terminal", "Quit", function() editor:quit() end, true)
|
|
|
-editor:register_command("execute-extended-command", "M-x", function() editor:command_mode() end, true)
|
|
|
-
|
|
|
--- Macros
|
|
|
-editor:register_command("start-kbd-macro", "Start recording macro", function() editor:start_kbd_macro() end, true)
|
|
|
-editor:register_command("end-kbd-macro-or-call", "End recording or call macro", function() editor:end_kbd_macro_or_call() end, true)
|
|
|
-
|
|
|
--- 4. Default Keybindings
|
|
|
-editor:bind_key("Return", "insert-newline")
|
|
|
-editor:bind_key("Backspace", "backward-delete-char")
|
|
|
+
|
|
|
+--- Transpose characters around point
|
|
|
+function lumacs.transpose_chars()
|
|
|
+ local cursor = editor.cursor
|
|
|
+ local line = editor.buffer:line(cursor.line)
|
|
|
+
|
|
|
+ if cursor.column == 0 then
|
|
|
+ editor:message("Beginning of line")
|
|
|
+ return
|
|
|
+ end
|
|
|
+
|
|
|
+ if cursor.column >= #line then
|
|
|
+ -- At end of line, transpose last two chars
|
|
|
+ if #line < 2 then return end
|
|
|
+ local c1 = line:sub(-2, -2)
|
|
|
+ local c2 = line:sub(-1)
|
|
|
+ local range = lumacs.Range(
|
|
|
+ lumacs.Position(cursor.line, #line - 2),
|
|
|
+ lumacs.Position(cursor.line, #line)
|
|
|
+ )
|
|
|
+ editor.buffer:replace(range, c2 .. c1)
|
|
|
+ else
|
|
|
+ -- Transpose char before and at point
|
|
|
+ local c1 = line:sub(cursor.column, cursor.column)
|
|
|
+ local c2 = line:sub(cursor.column + 1, cursor.column + 1)
|
|
|
+ local range = lumacs.Range(
|
|
|
+ lumacs.Position(cursor.line, cursor.column - 1),
|
|
|
+ lumacs.Position(cursor.line, cursor.column + 1)
|
|
|
+ )
|
|
|
+ editor.buffer:replace(range, c2 .. c1)
|
|
|
+ editor:move_right()
|
|
|
+ end
|
|
|
+end
|
|
|
+
|
|
|
+--- Transpose words around point
|
|
|
+function lumacs.transpose_words()
|
|
|
+ -- Save current position
|
|
|
+ local start_pos = editor.cursor
|
|
|
+
|
|
|
+ -- Move backward to start of current word
|
|
|
+ editor:move_backward_word()
|
|
|
+ local word1_start = editor.cursor
|
|
|
+
|
|
|
+ -- Move forward to end of current word
|
|
|
+ editor:move_forward_word()
|
|
|
+ local word1_end = editor.cursor
|
|
|
+
|
|
|
+ -- Get first word
|
|
|
+ local range1 = lumacs.Range(word1_start, word1_end)
|
|
|
+ local word1 = editor.buffer:get_text_in_range(range1)
|
|
|
+
|
|
|
+ -- Move to next word
|
|
|
+ editor:move_forward_word()
|
|
|
+ local word2_end = editor.cursor
|
|
|
+ editor:move_backward_word()
|
|
|
+ local word2_start = editor.cursor
|
|
|
+ editor:move_forward_word()
|
|
|
+
|
|
|
+ -- Get second word
|
|
|
+ local range2 = lumacs.Range(word2_start, word2_end)
|
|
|
+ local word2 = editor.buffer:get_text_in_range(range2)
|
|
|
+
|
|
|
+ -- Swap them (replace second first to preserve positions)
|
|
|
+ editor.buffer:replace(range2, word1)
|
|
|
+ editor.buffer:replace(range1, word2)
|
|
|
+
|
|
|
+ editor:set_cursor(word2_end)
|
|
|
+end
|
|
|
+
|
|
|
+--- Transpose lines
|
|
|
+function lumacs.transpose_lines()
|
|
|
+ local cursor = editor.cursor
|
|
|
+ local buf = editor.buffer
|
|
|
+
|
|
|
+ if cursor.line == 0 then
|
|
|
+ editor:message("Beginning of buffer")
|
|
|
+ return
|
|
|
+ end
|
|
|
+
|
|
|
+ local current_line = buf:line(cursor.line)
|
|
|
+ local prev_line = buf:line(cursor.line - 1)
|
|
|
+
|
|
|
+ -- Delete both lines and reinsert swapped
|
|
|
+ local range = lumacs.Range(
|
|
|
+ lumacs.Position(cursor.line - 1, 0),
|
|
|
+ lumacs.Position(cursor.line, #current_line)
|
|
|
+ )
|
|
|
+ buf:erase(range)
|
|
|
+ buf:insert(lumacs.Position(cursor.line - 1, 0), current_line .. "\n" .. prev_line)
|
|
|
+
|
|
|
+ editor:set_cursor(lumacs.Position(cursor.line, cursor.column))
|
|
|
+end
|
|
|
+
|
|
|
+--- Upcase word at point
|
|
|
+function lumacs.upcase_word()
|
|
|
+ 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
|
|
|
+
|
|
|
+--- Downcase word at point
|
|
|
+function lumacs.downcase_word()
|
|
|
+ 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
|
|
|
+
|
|
|
+--- Capitalize word at point
|
|
|
+function lumacs.capitalize_word()
|
|
|
+ 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:sub(1, 1):upper() .. text:sub(2):lower()
|
|
|
+ editor.buffer:replace(range, cap)
|
|
|
+ end
|
|
|
+end
|
|
|
+
|
|
|
+--- Upcase region
|
|
|
+function lumacs.upcase_region()
|
|
|
+ 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
|
|
|
+
|
|
|
+--- Downcase region
|
|
|
+function lumacs.downcase_region()
|
|
|
+ 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
|
|
|
+
|
|
|
+--- Comment DWIM (Do What I Mean)
|
|
|
+function lumacs.comment_dwim()
|
|
|
+ local mode_name = lumacs.current_major_mode()
|
|
|
+ local mode = lumacs.major_modes[mode_name]
|
|
|
+ local prefix = mode and mode.comment_start or "# "
|
|
|
+ local escaped_prefix = prefix:gsub("([^%w])", "%%%1")
|
|
|
+
|
|
|
+ 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)
|
|
|
+ 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 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
|
|
|
+
|
|
|
+--- What cursor position - display cursor info
|
|
|
+function lumacs.what_cursor_position()
|
|
|
+ local cursor = editor.cursor
|
|
|
+ local buf = editor.buffer
|
|
|
+ local line = buf:line(cursor.line)
|
|
|
+ local char_at_point = ""
|
|
|
+ local char_code = 0
|
|
|
+
|
|
|
+ if cursor.column < #line then
|
|
|
+ char_at_point = line:sub(cursor.column + 1, cursor.column + 1)
|
|
|
+ char_code = string.byte(char_at_point) or 0
|
|
|
+ end
|
|
|
+
|
|
|
+ -- Calculate total character position
|
|
|
+ local char_pos = 0
|
|
|
+ for i = 0, cursor.line - 1 do
|
|
|
+ char_pos = char_pos + #buf:line(i) + 1 -- +1 for newline
|
|
|
+ end
|
|
|
+ char_pos = char_pos + cursor.column + 1
|
|
|
+
|
|
|
+ local total_chars = 0
|
|
|
+ for i = 0, buf:line_count() - 1 do
|
|
|
+ total_chars = total_chars + #buf:line(i) + 1
|
|
|
+ end
|
|
|
+
|
|
|
+ local percentage = math.floor((char_pos / total_chars) * 100)
|
|
|
+
|
|
|
+ local msg
|
|
|
+ if char_at_point ~= "" then
|
|
|
+ msg = string.format("Char: %s (%d, #o%o, #x%x) point=%d of %d (%d%%) Line %d Col %d",
|
|
|
+ char_at_point, char_code, char_code, char_code,
|
|
|
+ char_pos, total_chars, percentage,
|
|
|
+ cursor.line + 1, cursor.column + 1)
|
|
|
+ else
|
|
|
+ msg = string.format("point=%d of %d (%d%%) Line %d Col %d (EOL)",
|
|
|
+ char_pos, total_chars, percentage,
|
|
|
+ cursor.line + 1, cursor.column + 1)
|
|
|
+ end
|
|
|
+
|
|
|
+ editor:message(msg)
|
|
|
+end
|
|
|
+
|
|
|
+-- =====================================================
|
|
|
+-- 4. COMMAND REGISTRATION
|
|
|
+-- =====================================================
|
|
|
+
|
|
|
+-- === Movement Commands ===
|
|
|
+editor:register_command("forward-char", "Move point right", function() editor:move_right() end)
|
|
|
+editor:register_command("backward-char", "Move point left", function() editor:move_left() end)
|
|
|
+editor:register_command("next-line", "Move point down", function() editor:move_down() end)
|
|
|
+editor:register_command("previous-line", "Move point up", function() editor:move_up() end)
|
|
|
+editor:register_command("forward-word", "Move forward over a word", function() editor:move_forward_word() end)
|
|
|
+editor:register_command("backward-word", "Move backward over a word", function() editor:move_backward_word() end)
|
|
|
+editor:register_command("move-beginning-of-line", "Move to beginning of line", function() editor:move_to_line_start() end)
|
|
|
+editor:register_command("move-end-of-line", "Move to end of line", function() editor:move_to_line_end() end)
|
|
|
+editor:register_command("beginning-of-buffer", "Move to beginning of buffer", function() editor:goto_beginning() end)
|
|
|
+editor:register_command("end-of-buffer", "Move to end of buffer", function() editor:goto_end() end)
|
|
|
+editor:register_command("scroll-up-command", "Scroll text up (move forward)", function() editor:page_down() end)
|
|
|
+editor:register_command("scroll-down-command", "Scroll text down (move backward)", function() editor:page_up() end)
|
|
|
+
|
|
|
+editor:register_command("goto-line", "Go to line N", function(args)
|
|
|
+ local n = tonumber(args[1])
|
|
|
+ if n and n > 0 then
|
|
|
+ editor:goto_line(n - 1)
|
|
|
+ editor:message("Line " .. n)
|
|
|
+ else
|
|
|
+ editor:message("Invalid line number")
|
|
|
+ end
|
|
|
+end, true, "n")
|
|
|
+
|
|
|
+editor:register_command("recenter-top-bottom", "Center point in window", function()
|
|
|
+ -- Simple recenter - just adjust scroll
|
|
|
+ editor:message("Recentered")
|
|
|
+end)
|
|
|
+
|
|
|
+-- === Editing Commands ===
|
|
|
+editor:register_command("self-insert-command", "Insert the character typed", lumacs.self_insert)
|
|
|
+editor:register_command("newline", "Insert a newline", lumacs.newline)
|
|
|
+editor:register_command("open-line", "Insert newline after point", lumacs.open_line)
|
|
|
+editor:register_command("delete-backward-char", "Delete character before point", lumacs.delete_backward_char)
|
|
|
+editor:register_command("delete-char", "Delete character after point", lumacs.delete_char)
|
|
|
+editor:register_command("transpose-chars", "Transpose characters around point", lumacs.transpose_chars)
|
|
|
+editor:register_command("transpose-words", "Transpose words around point", lumacs.transpose_words)
|
|
|
+editor:register_command("transpose-lines", "Transpose lines", lumacs.transpose_lines)
|
|
|
+
|
|
|
+-- === Mark & Region Commands ===
|
|
|
+editor:register_command("set-mark-command", "Set mark at point", function()
|
|
|
+ editor.buffer:set_mark(editor.cursor)
|
|
|
+ editor:message("Mark set")
|
|
|
+end)
|
|
|
+
|
|
|
+editor:register_command("exchange-point-and-mark", "Exchange point and mark", function()
|
|
|
+ local mark = editor.buffer.mark
|
|
|
+ if not mark then
|
|
|
+ editor:message("No mark set")
|
|
|
+ return
|
|
|
+ end
|
|
|
+ local cursor = editor.cursor
|
|
|
+ editor.buffer:set_mark(cursor)
|
|
|
+ editor:set_cursor(mark)
|
|
|
+end)
|
|
|
+
|
|
|
+editor:register_command("mark-whole-buffer", "Mark entire buffer", function()
|
|
|
+ editor.buffer:set_mark(lumacs.Position(0, 0))
|
|
|
+ local last_line = editor.buffer:line_count() - 1
|
|
|
+ local last_col = #editor.buffer:line(last_line)
|
|
|
+ editor:set_cursor(lumacs.Position(last_line, last_col))
|
|
|
+ editor:message("Whole buffer marked")
|
|
|
+end)
|
|
|
+
|
|
|
+editor:register_command("keyboard-quit", "Quit current command", function()
|
|
|
+ editor.buffer:deactivate_mark()
|
|
|
+ editor:message("Quit")
|
|
|
+end)
|
|
|
+
|
|
|
+-- === Kill Ring Commands ===
|
|
|
+editor:register_command("kill-line", "Kill to end of line", function() editor:kill_line() end)
|
|
|
+editor:register_command("kill-region", "Kill between point and mark", function() editor:kill_region() end)
|
|
|
+editor:register_command("kill-ring-save", "Save region to kill ring without killing", function() editor:copy_region_as_kill() end)
|
|
|
+editor:register_command("copy-region-as-kill", "Copy region (alias)", function() editor:copy_region_as_kill() end)
|
|
|
+editor:register_command("yank", "Reinsert last killed text", function() editor:yank() end)
|
|
|
+editor:register_command("yank-pop", "Replace yanked text with earlier kill", function() editor:yank_pop() end)
|
|
|
+editor:register_command("kill-word", "Kill word forward", function() editor:kill_word() end)
|
|
|
+editor:register_command("backward-kill-word", "Kill word backward", function() editor:backward_kill_word() end)
|
|
|
+
|
|
|
+-- === Undo Commands ===
|
|
|
+editor:register_command("undo", "Undo last change", function()
|
|
|
+ if editor:undo() then
|
|
|
+ editor:message("Undo")
|
|
|
+ else
|
|
|
+ editor:message("No further undo information")
|
|
|
+ end
|
|
|
+end)
|
|
|
+
|
|
|
+editor:register_command("redo", "Redo last undo", function()
|
|
|
+ if editor:redo() then
|
|
|
+ editor:message("Redo")
|
|
|
+ else
|
|
|
+ editor:message("No further redo information")
|
|
|
+ end
|
|
|
+end)
|
|
|
+
|
|
|
+-- === Case Conversion Commands ===
|
|
|
+editor:register_command("upcase-word", "Convert word to upper case", lumacs.upcase_word)
|
|
|
+editor:register_command("downcase-word", "Convert word to lower case", lumacs.downcase_word)
|
|
|
+editor:register_command("capitalize-word", "Capitalize word at point", lumacs.capitalize_word)
|
|
|
+editor:register_command("upcase-region", "Convert region to upper case", lumacs.upcase_region)
|
|
|
+editor:register_command("downcase-region", "Convert region to lower case", lumacs.downcase_region)
|
|
|
+
|
|
|
+-- === Comment Commands ===
|
|
|
+editor:register_command("comment-dwim", "Comment or uncomment region/line", lumacs.comment_dwim)
|
|
|
+
|
|
|
+-- === File & Buffer Commands ===
|
|
|
+editor:register_command("save-buffer", "Save current buffer to file", function()
|
|
|
+ if editor.buffer:save() then
|
|
|
+ editor:message("Wrote " .. editor.buffer:name())
|
|
|
+ else
|
|
|
+ editor:message("(No changes need to be saved)")
|
|
|
+ end
|
|
|
+end)
|
|
|
+
|
|
|
+editor:register_command("find-file", "Visit a file", function() editor:find_file_mode() end, true, "f")
|
|
|
+editor:register_command("switch-to-buffer", "Select or create buffer", function() editor:buffer_switch_mode() end, true, "b")
|
|
|
+editor:register_command("switch-buffer", "Select or create buffer (alias)", function() editor:buffer_switch_mode() end, true, "b")
|
|
|
+editor:register_command("kill-buffer", "Kill a buffer", function() editor:kill_buffer_mode() end, true, "b")
|
|
|
+editor:register_command("save-buffers-kill-emacs", "Save buffers and exit", function() editor:quit() end)
|
|
|
+editor:register_command("save-buffers-kill-terminal", "Save buffers and exit (alias)", function() editor:quit() end)
|
|
|
+
|
|
|
+editor:register_command("list-buffers", "Display buffer list", function()
|
|
|
+ local buffer_info = editor:get_all_buffer_info()
|
|
|
+ if #buffer_info == 0 then
|
|
|
+ editor:message("No buffers")
|
|
|
+ return
|
|
|
+ end
|
|
|
+
|
|
|
+ local lines = {"Buffer List:", string.rep("-", 60), ""}
|
|
|
+ table.insert(lines, string.format("%-3s %-30s %8s %s", "Mod", "Name", "Size", "File"))
|
|
|
+ table.insert(lines, string.rep("-", 60))
|
|
|
+
|
|
|
+ for _, info in ipairs(buffer_info) do
|
|
|
+ local mod = info.modified and " * " or " "
|
|
|
+ local filepath = info.filepath and tostring(info.filepath) or ""
|
|
|
+ table.insert(lines, string.format("%s %-30s %8d %s", mod, info.name, info.size, filepath))
|
|
|
+ end
|
|
|
+
|
|
|
+ local list_text = table.concat(lines, "\n")
|
|
|
+ local list_buf_name = "*Buffer List*"
|
|
|
+
|
|
|
+ if not editor:get_buffer_by_name(list_buf_name) then
|
|
|
+ editor:new_buffer(list_buf_name)
|
|
|
+ else
|
|
|
+ editor:switch_buffer_in_window(list_buf_name)
|
|
|
+ end
|
|
|
+
|
|
|
+ editor.buffer:clear()
|
|
|
+ editor.buffer:insert(lumacs.Position(0, 0), list_text)
|
|
|
+ editor:goto_beginning()
|
|
|
+ editor:message(string.format("%d buffers", #buffer_info))
|
|
|
+end)
|
|
|
+
|
|
|
+editor:register_command("revert-buffer", "Reload buffer from file", function()
|
|
|
+ local buf = editor.buffer
|
|
|
+ local filepath = buf:filepath()
|
|
|
+
|
|
|
+ if not filepath then
|
|
|
+ editor:message("Buffer is not visiting a file")
|
|
|
+ return
|
|
|
+ end
|
|
|
+
|
|
|
+ if editor:load_file(filepath) then
|
|
|
+ editor:message("Reverted " .. buf:name())
|
|
|
+ else
|
|
|
+ editor:message("Failed to revert buffer")
|
|
|
+ end
|
|
|
+end)
|
|
|
+
|
|
|
+-- === Window Commands ===
|
|
|
+editor:register_command("split-window-below", "Split window horizontally", function() editor:split_horizontally() end)
|
|
|
+editor:register_command("split-window-right", "Split window vertically", function() editor:split_vertically() end)
|
|
|
+editor:register_command("delete-window", "Delete current window", function() editor:close_window() end)
|
|
|
+editor:register_command("delete-other-windows", "Delete all other windows", function()
|
|
|
+ editor:message("delete-other-windows not yet implemented")
|
|
|
+end)
|
|
|
+editor:register_command("other-window", "Select another window", function() editor:next_window() end)
|
|
|
+
|
|
|
+-- === Search Commands ===
|
|
|
+editor:register_command("isearch-forward", "Incremental search forward", function() editor:isearch_mode() end)
|
|
|
+editor:register_command("isearch-backward", "Incremental search backward", function() editor:isearch_backward_mode() end)
|
|
|
+
|
|
|
+-- === M-x Command ===
|
|
|
+editor:register_command("execute-extended-command", "Read command name and execute", function() editor:command_mode() end)
|
|
|
+
|
|
|
+-- === Macro Commands ===
|
|
|
+editor:register_command("kmacro-start-macro", "Start recording macro", function() editor:start_kbd_macro() end)
|
|
|
+editor:register_command("kmacro-end-or-call-macro", "End or call macro", function() editor:end_kbd_macro_or_call() end)
|
|
|
+editor:register_command("start-kbd-macro", "Start recording macro (alias)", function() editor:start_kbd_macro() end)
|
|
|
+editor:register_command("end-kbd-macro-or-call", "End or call macro (alias)", function() editor:end_kbd_macro_or_call() end)
|
|
|
+
|
|
|
+-- === Register Commands ===
|
|
|
+editor:register_command("copy-to-register", "Copy region to register", function(args)
|
|
|
+ local reg = args[1] and args[1]:sub(1, 1) or 'a'
|
|
|
+ editor:copy_region_to_register(reg)
|
|
|
+ editor:message("Copied to register " .. reg)
|
|
|
+end, true, "s")
|
|
|
+
|
|
|
+editor:register_command("insert-register", "Insert from register", function(args)
|
|
|
+ local reg = args[1] and args[1]:sub(1, 1) or 'a'
|
|
|
+ if editor:yank_from_register(reg) then
|
|
|
+ editor:message("Inserted register " .. reg)
|
|
|
+ else
|
|
|
+ editor:message("Register " .. reg .. " is empty")
|
|
|
+ end
|
|
|
+end, true, "s")
|
|
|
+
|
|
|
+-- === Rectangle Commands ===
|
|
|
+editor:register_command("kill-rectangle", "Kill rectangle", function() editor:kill_rectangle() end)
|
|
|
+editor:register_command("yank-rectangle", "Yank rectangle", function() editor:yank_rectangle() end)
|
|
|
+editor:register_command("string-rectangle", "Replace rectangle with string", function(args)
|
|
|
+ local str = args[1] or ""
|
|
|
+ editor:string_rectangle(str)
|
|
|
+end, true, "s")
|
|
|
+
|
|
|
+-- === Info Commands ===
|
|
|
+editor:register_command("what-cursor-position", "Display cursor position info", lumacs.what_cursor_position)
|
|
|
+
|
|
|
+editor:register_command("describe-mode", "Describe current modes", function()
|
|
|
+ local major = lumacs.current_major_mode()
|
|
|
+ local buf_name = editor.buffer:name()
|
|
|
+ local buf_modes = lumacs.buffer_modes[buf_name]
|
|
|
+
|
|
|
+ local minors = {}
|
|
|
+ if buf_modes and buf_modes.minor then
|
|
|
+ for mode, _ in pairs(buf_modes.minor) do
|
|
|
+ table.insert(minors, mode)
|
|
|
+ end
|
|
|
+ end
|
|
|
+
|
|
|
+ local minor_str = #minors > 0 and table.concat(minors, ", ") or "none"
|
|
|
+ editor:message(string.format("Major mode: %s | Minor modes: %s", major, minor_str))
|
|
|
+end)
|
|
|
+
|
|
|
+editor:register_command("view-messages", "View the *Messages* buffer", function()
|
|
|
+ local buf = editor:get_buffer_by_name("*Messages*")
|
|
|
+ if buf then
|
|
|
+ editor:switch_buffer_in_window("*Messages*")
|
|
|
+ editor:goto_end()
|
|
|
+ else
|
|
|
+ editor:message("No messages logged yet")
|
|
|
+ end
|
|
|
+end)
|
|
|
+
|
|
|
+-- === Theme Commands ===
|
|
|
+editor:register_command("set-theme", "Select color theme", function(args)
|
|
|
+ if #args == 0 then
|
|
|
+ editor:theme_selection_mode()
|
|
|
+ return
|
|
|
+ end
|
|
|
+
|
|
|
+ local name = args[1]
|
|
|
+ local themes = editor.theme_manager:theme_names()
|
|
|
+ for _, t in ipairs(themes) do
|
|
|
+ if t == name then
|
|
|
+ editor.theme_manager:set_active_theme(name)
|
|
|
+ editor:message("Theme: " .. name)
|
|
|
+ return {success = true}
|
|
|
+ end
|
|
|
+ end
|
|
|
+ editor:message("Unknown theme: " .. name)
|
|
|
+ return {success = false, message = "Unknown theme: " .. name}
|
|
|
+end, true, "s")
|
|
|
+
|
|
|
+-- === Utility Commands ===
|
|
|
+editor:register_command("count-words-region", "Count words in region or buffer", function()
|
|
|
+ local buf = editor.buffer
|
|
|
+ local range = buf:get_region(editor.cursor)
|
|
|
+ local text
|
|
|
+
|
|
|
+ if range then
|
|
|
+ text = buf:get_text_in_range(range)
|
|
|
+ else
|
|
|
+ text = buf:content()
|
|
|
+ end
|
|
|
+
|
|
|
+ local words = 0
|
|
|
+ for _ in text:gmatch("%S+") do
|
|
|
+ words = words + 1
|
|
|
+ end
|
|
|
+
|
|
|
+ local lines = 0
|
|
|
+ for _ in text:gmatch("\n") do
|
|
|
+ lines = lines + 1
|
|
|
+ end
|
|
|
+ lines = lines + 1
|
|
|
+
|
|
|
+ local target = range and "Region" or "Buffer"
|
|
|
+ editor:message(string.format("%s has %d words, %d lines, %d characters", target, words, lines, #text))
|
|
|
+end)
|
|
|
+
|
|
|
+editor:register_command("eval-expression", "Evaluate Lua expression", function(args)
|
|
|
+ if #args == 0 then return {success = false, message = "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 or "unknown")}
|
|
|
+ end
|
|
|
+
|
|
|
+ local success, result = pcall(func)
|
|
|
+ if success then
|
|
|
+ editor:message(tostring(result))
|
|
|
+ return {success = true, message = tostring(result)}
|
|
|
+ else
|
|
|
+ return {success = false, message = "Error: " .. tostring(result)}
|
|
|
+ end
|
|
|
+end, true, "s")
|
|
|
+
|
|
|
+-- =====================================================
|
|
|
+-- 5. DEFAULT KEYBINDINGS (Emacs-compatible)
|
|
|
+-- =====================================================
|
|
|
+
|
|
|
+-- === Self-insert (printable characters handled by C++ fallback) ===
|
|
|
+editor:bind_key("Return", "newline")
|
|
|
+editor:bind_key("Backspace", "delete-backward-char")
|
|
|
editor:bind_key("Delete", "delete-char")
|
|
|
+editor:bind_key("Tab", function()
|
|
|
+ -- Simple tab insert for now
|
|
|
+ editor.buffer:insert(editor.cursor, " ")
|
|
|
+ for i = 1, 4 do editor:move_right() end
|
|
|
+end)
|
|
|
|
|
|
--- Arrow Keys
|
|
|
+-- === Arrow Keys ===
|
|
|
editor:bind_key("ArrowUp", "previous-line")
|
|
|
editor:bind_key("ArrowDown", "next-line")
|
|
|
editor:bind_key("ArrowLeft", "backward-char")
|
|
|
editor:bind_key("ArrowRight", "forward-char")
|
|
|
+editor:bind_key("Home", "move-beginning-of-line")
|
|
|
+editor:bind_key("End", "move-end-of-line")
|
|
|
+editor:bind_key("PageUp", "scroll-down-command")
|
|
|
+editor:bind_key("PageDown", "scroll-up-command")
|
|
|
|
|
|
--- Emacs Navigation
|
|
|
+-- === C- (Control) Bindings ===
|
|
|
editor:bind_key("C-f", "forward-char")
|
|
|
editor:bind_key("C-b", "backward-char")
|
|
|
editor:bind_key("C-n", "next-line")
|
|
|
editor:bind_key("C-p", "previous-line")
|
|
|
editor:bind_key("C-a", "move-beginning-of-line")
|
|
|
editor:bind_key("C-e", "move-end-of-line")
|
|
|
+editor:bind_key("C-v", "scroll-up-command")
|
|
|
+editor:bind_key("C-d", "delete-char")
|
|
|
+editor:bind_key("C-k", "kill-line")
|
|
|
+editor:bind_key("C-w", "kill-region")
|
|
|
+editor:bind_key("C-y", "yank")
|
|
|
+editor:bind_key("C-@", "set-mark-command")
|
|
|
+editor:bind_key("C-g", "keyboard-quit")
|
|
|
+editor:bind_key("C-s", "isearch-forward")
|
|
|
+editor:bind_key("C-r", "isearch-backward")
|
|
|
+editor:bind_key("C-o", "open-line")
|
|
|
+editor:bind_key("C-t", "transpose-chars")
|
|
|
+editor:bind_key("C-l", "recenter-top-bottom")
|
|
|
+editor:bind_key("C-/", "undo")
|
|
|
+editor:bind_key("C-_", "undo")
|
|
|
+
|
|
|
+-- === M- (Meta/Alt) Bindings ===
|
|
|
editor:bind_key("M-f", "forward-word")
|
|
|
editor:bind_key("M-b", "backward-word")
|
|
|
editor:bind_key("M-<", "beginning-of-buffer")
|
|
|
editor:bind_key("M->", "end-of-buffer")
|
|
|
-editor:bind_key("C-v", "scroll-up-command")
|
|
|
editor:bind_key("M-v", "scroll-down-command")
|
|
|
+editor:bind_key("M-w", "kill-ring-save")
|
|
|
+editor:bind_key("M-y", "yank-pop")
|
|
|
+editor:bind_key("M-d", "kill-word")
|
|
|
+editor:bind_key("M-Backspace", "backward-kill-word")
|
|
|
+editor:bind_key("M-u", "upcase-word")
|
|
|
+editor:bind_key("M-l", "downcase-word")
|
|
|
+editor:bind_key("M-c", "capitalize-word")
|
|
|
+editor:bind_key("M-t", "transpose-words")
|
|
|
+editor:bind_key("M-x", "execute-extended-command")
|
|
|
+editor:bind_key("M-;", "comment-dwim")
|
|
|
+
|
|
|
+-- === M-g (Goto prefix) ===
|
|
|
+editor:bind_key("M-g g", "goto-line")
|
|
|
+editor:bind_key("M-g M-g", "goto-line")
|
|
|
|
|
|
--- File & Window Ops
|
|
|
-editor:bind_key("C-x C-c", "save-buffers-kill-terminal")
|
|
|
+-- === C-x (Prefix) Bindings ===
|
|
|
+editor:bind_key("C-x C-c", "save-buffers-kill-emacs")
|
|
|
editor:bind_key("C-x C-s", "save-buffer")
|
|
|
editor:bind_key("C-x C-f", "find-file")
|
|
|
-editor:bind_key("C-x b", "switch-buffer")
|
|
|
+editor:bind_key("C-x b", "switch-to-buffer")
|
|
|
editor:bind_key("C-x k", "kill-buffer")
|
|
|
-editor:bind_key("C-x 0", "delete-window") -- Need to implement
|
|
|
-editor:bind_key("C-x 1", "delete-other-windows") -- Need to implement
|
|
|
-editor:bind_key("C-x 2", "split-window-below") -- Need to implement
|
|
|
-editor:bind_key("C-x 3", "split-window-right") -- Need to implement
|
|
|
-editor:bind_key("C-x o", "other-window") -- Need to implement
|
|
|
-editor:bind_key("M-x", "execute-extended-command")
|
|
|
+editor:bind_key("C-x C-b", "list-buffers")
|
|
|
+editor:bind_key("C-x 0", "delete-window")
|
|
|
+editor:bind_key("C-x 1", "delete-other-windows")
|
|
|
+editor:bind_key("C-x 2", "split-window-below")
|
|
|
+editor:bind_key("C-x 3", "split-window-right")
|
|
|
+editor:bind_key("C-x o", "other-window")
|
|
|
+editor:bind_key("C-x u", "undo")
|
|
|
+editor:bind_key("C-x C-x", "exchange-point-and-mark")
|
|
|
+editor:bind_key("C-x h", "mark-whole-buffer")
|
|
|
+editor:bind_key("C-x C-t", "transpose-lines")
|
|
|
+editor:bind_key("C-x =", "what-cursor-position")
|
|
|
|
|
|
--- Windows Implementation
|
|
|
-editor:register_command("delete-window", "Close window", function() editor:close_window() end, true)
|
|
|
-editor:register_command("split-window-below", "Split horizontal", function() editor:split_horizontally() end, true)
|
|
|
-editor:register_command("split-window-right", "Split vertical", function() editor:split_vertically() end, true)
|
|
|
-editor:register_command("other-window", "Next window", function() editor:next_window() end, true)
|
|
|
+-- === C-x C- (Case conversion) ===
|
|
|
+editor:bind_key("C-x C-u", "upcase-region")
|
|
|
+editor:bind_key("C-x C-l", "downcase-region")
|
|
|
|
|
|
--- Undo/Redo
|
|
|
-editor:bind_key("C-/", "undo")
|
|
|
-editor:bind_key("C-_", "undo")
|
|
|
-editor:bind_key("C-x u", "redo")
|
|
|
+-- === C-x r (Register/Rectangle prefix) ===
|
|
|
+editor:bind_key("C-x r s", "copy-to-register")
|
|
|
+editor:bind_key("C-x r i", "insert-register")
|
|
|
+editor:bind_key("C-x r k", "kill-rectangle")
|
|
|
+editor:bind_key("C-x r y", "yank-rectangle")
|
|
|
+editor:bind_key("C-x r t", "string-rectangle")
|
|
|
|
|
|
--- Mark & Kill Ring
|
|
|
-editor:bind_key("C-@", function() editor.buffer:set_mark(editor.cursor) editor:message("Mark set") end)
|
|
|
-editor:bind_key("C-w", "kill-region")
|
|
|
-editor:bind_key("M-w", "copy-region-as-kill")
|
|
|
-editor:bind_key("C-y", "yank")
|
|
|
-editor:bind_key("M-y", "yank-pop")
|
|
|
-editor:bind_key("C-k", "kill-line")
|
|
|
+-- === C-h (Help prefix) ===
|
|
|
+editor:bind_key("C-h e", "view-messages")
|
|
|
+editor:bind_key("C-h m", "describe-mode")
|
|
|
|
|
|
--- Macros
|
|
|
-editor:bind_key("F3", "start-kbd-macro")
|
|
|
-editor:bind_key("F4", "end-kbd-macro-or-call")
|
|
|
+-- === Function Keys ===
|
|
|
+editor:bind_key("F3", "kmacro-start-macro")
|
|
|
+editor:bind_key("F4", "kmacro-end-or-call-macro")
|
|
|
|
|
|
--- Theme Management
|
|
|
-editor:register_command("set-theme", "Set active theme", function(args)
|
|
|
- if #args == 0 then
|
|
|
- return {success = false, message = "Theme name required"}
|
|
|
- end
|
|
|
-
|
|
|
- local theme_name = args[1]
|
|
|
- local theme_manager = editor.theme_manager
|
|
|
- local available_themes = theme_manager:theme_names()
|
|
|
-
|
|
|
- -- Check if theme exists
|
|
|
- local theme_exists = false
|
|
|
- for _, name in ipairs(available_themes) do
|
|
|
- if name == theme_name then
|
|
|
- theme_exists = true
|
|
|
- break
|
|
|
- end
|
|
|
- end
|
|
|
-
|
|
|
- if not theme_exists then
|
|
|
- return {success = false, message = "Theme '" .. theme_name .. "' not found. Available: " .. table.concat(available_themes, ", ")}
|
|
|
- end
|
|
|
-
|
|
|
- theme_manager:set_active_theme(theme_name)
|
|
|
- editor:message("Switched to theme: " .. theme_name)
|
|
|
- return {success = true, message = "Switched to theme: " .. theme_name}
|
|
|
-end, true, "s")
|
|
|
+-- === Escape as Meta prefix (for terminals) ===
|
|
|
+editor:bind_key("Escape", "keyboard-quit")
|
|
|
+
|
|
|
+-- =====================================================
|
|
|
+-- 6. INITIALIZE
|
|
|
+-- =====================================================
|
|
|
|
|
|
--- Auto-activate mode
|
|
|
-auto_activate_major_mode()
|
|
|
-)";
|
|
|
+-- Auto-activate major mode for initial buffer
|
|
|
+lumacs.auto_activate_major_mode()
|
|
|
+)lua";
|
|
|
|
|
|
-} // namespace lumacs
|
|
|
+} // namespace lumacs
|