-- smartparens.lua -- ============================================================================ -- Automatic pairing of brackets, quotes, and other delimiters. -- Similar to Emacs smartparens or electric-pair-mode. -- ============================================================================ local smartparens = {} -- Configuration smartparens.config = { enabled = true, pairs = { ["("] = ")", ["["] = "]", ["{"] = "}", ['"'] = '"', ["'"] = "'", ["`"] = "`", }, -- Skip closing when next char is the same closer skip_when_next = true, -- Delete pair together when backspacing empty pair delete_pair = true, -- Wrap region with pair when mark is active wrap_region = true, } -- State smartparens.active = true -- Check if char is an opener function smartparens.is_opener(char) return smartparens.config.pairs[char] ~= nil end -- Check if char is a closer function smartparens.is_closer(char) for opener, closer in pairs(smartparens.config.pairs) do if closer == char and opener ~= closer then return true, opener end end return false, nil end -- Get closer for an opener function smartparens.get_closer(opener) return smartparens.config.pairs[opener] end -- Check if we're inside a string (simple heuristic) function smartparens.in_string() local buf = editor.buffer local cursor = editor.cursor local line = buf:line(cursor.line) local quote_count = 0 local dquote_count = 0 for i = 1, cursor.column do local c = line:sub(i, i) if c == "'" then quote_count = quote_count + 1 end if c == '"' then dquote_count = dquote_count + 1 end end return (quote_count % 2 == 1) or (dquote_count % 2 == 1) end -- Get char at cursor function smartparens.char_at_cursor() local buf = editor.buffer local cursor = editor.cursor local line = buf:line(cursor.line) if cursor.column < #line then return line:sub(cursor.column + 1, cursor.column + 1) end return nil end -- Get char before cursor function smartparens.char_before_cursor() local buf = editor.buffer local cursor = editor.cursor local line = buf:line(cursor.line) if cursor.column > 0 then return line:sub(cursor.column, cursor.column) end return nil end -- Insert pair and position cursor between them function smartparens.insert_pair(opener) if not smartparens.active or not smartparens.config.enabled then return false end local closer = smartparens.get_closer(opener) if not closer then return false end local buf = editor.buffer local cursor = editor.cursor -- Check if we have a region selected (for wrapping) if smartparens.config.wrap_region and buf:has_mark() then local region = buf:get_region(cursor) if region then local text = buf:get_text_in_range(region) buf:erase(region) buf:insert(region.start, opener .. text .. closer) -- Position cursor after the wrapped text editor.cursor = lumacs.Position(region.start.line, region.start.column + #text + 1) buf:deactivate_mark() return true end end -- For symmetric pairs (quotes), check if we should skip if opener == closer then local next_char = smartparens.char_at_cursor() if next_char == opener then -- Skip instead of inserting editor.cursor = lumacs.Position(cursor.line, cursor.column + 1) return true end end -- Insert the pair buf:insert(cursor, opener .. closer) -- Position cursor between the pair editor.cursor = lumacs.Position(cursor.line, cursor.column + 1) return true end -- Handle closing delimiter - skip if already there function smartparens.handle_closer(closer) if not smartparens.active or not smartparens.config.skip_when_next then return false end local next_char = smartparens.char_at_cursor() if next_char == closer then local cursor = editor.cursor editor.cursor = lumacs.Position(cursor.line, cursor.column + 1) return true end return false end -- Handle backspace - delete pair together if empty function smartparens.handle_backspace() if not smartparens.active or not smartparens.config.delete_pair then return false end local prev = smartparens.char_before_cursor() local next = smartparens.char_at_cursor() if prev and next then local expected_closer = smartparens.get_closer(prev) if expected_closer == next then -- Delete both chars local buf = editor.buffer local cursor = editor.cursor local range = lumacs.Range( lumacs.Position(cursor.line, cursor.column - 1), lumacs.Position(cursor.line, cursor.column + 1) ) buf:erase(range) editor.cursor = lumacs.Position(cursor.line, cursor.column - 1) return true end end return false end -- Toggle smartparens function smartparens.toggle() smartparens.active = not smartparens.active if smartparens.active then editor:message("Smartparens enabled") else editor:message("Smartparens disabled") end end -- Register commands editor:register_command("smartparens-mode", "Toggle smartparens auto-pairing", function(args) smartparens.toggle() return {success = true} end, {"sp-mode"}) editor:register_command("smartparens-strict-mode", "Toggle strict pairing rules", function(args) smartparens.config.skip_when_next = not smartparens.config.skip_when_next smartparens.config.delete_pair = not smartparens.config.delete_pair local state = smartparens.config.skip_when_next and "enabled" or "disabled" editor:message("Strict mode " .. state) return {success = true} end) -- Define minor mode lumacs.define_minor_mode("smartparens-mode", { lighter = "()", global = false, setup = function() smartparens.active = true editor:message("Smartparens enabled") end, cleanup = function() smartparens.active = false editor:message("Smartparens disabled") end }) -- Store in lumacs namespace lumacs.smartparens = smartparens print("[smartparens] Package loaded") return smartparens