init.lua 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. -- smartparens.lua
  2. -- ============================================================================
  3. -- Automatic pairing of brackets, quotes, and other delimiters.
  4. -- Similar to Emacs smartparens or electric-pair-mode.
  5. -- ============================================================================
  6. local smartparens = {}
  7. -- Configuration
  8. smartparens.config = {
  9. enabled = true,
  10. pairs = {
  11. ["("] = ")",
  12. ["["] = "]",
  13. ["{"] = "}",
  14. ['"'] = '"',
  15. ["'"] = "'",
  16. ["`"] = "`",
  17. },
  18. -- Skip closing when next char is the same closer
  19. skip_when_next = true,
  20. -- Delete pair together when backspacing empty pair
  21. delete_pair = true,
  22. -- Wrap region with pair when mark is active
  23. wrap_region = true,
  24. }
  25. -- State
  26. smartparens.active = true
  27. -- Check if char is an opener
  28. function smartparens.is_opener(char)
  29. return smartparens.config.pairs[char] ~= nil
  30. end
  31. -- Check if char is a closer
  32. function smartparens.is_closer(char)
  33. for opener, closer in pairs(smartparens.config.pairs) do
  34. if closer == char and opener ~= closer then
  35. return true, opener
  36. end
  37. end
  38. return false, nil
  39. end
  40. -- Get closer for an opener
  41. function smartparens.get_closer(opener)
  42. return smartparens.config.pairs[opener]
  43. end
  44. -- Check if we're inside a string (simple heuristic)
  45. function smartparens.in_string()
  46. local buf = editor.buffer
  47. local cursor = editor.cursor
  48. local line = buf:line(cursor.line)
  49. local quote_count = 0
  50. local dquote_count = 0
  51. for i = 1, cursor.column do
  52. local c = line:sub(i, i)
  53. if c == "'" then quote_count = quote_count + 1 end
  54. if c == '"' then dquote_count = dquote_count + 1 end
  55. end
  56. return (quote_count % 2 == 1) or (dquote_count % 2 == 1)
  57. end
  58. -- Get char at cursor
  59. function smartparens.char_at_cursor()
  60. local buf = editor.buffer
  61. local cursor = editor.cursor
  62. local line = buf:line(cursor.line)
  63. if cursor.column < #line then
  64. return line:sub(cursor.column + 1, cursor.column + 1)
  65. end
  66. return nil
  67. end
  68. -- Get char before cursor
  69. function smartparens.char_before_cursor()
  70. local buf = editor.buffer
  71. local cursor = editor.cursor
  72. local line = buf:line(cursor.line)
  73. if cursor.column > 0 then
  74. return line:sub(cursor.column, cursor.column)
  75. end
  76. return nil
  77. end
  78. -- Insert pair and position cursor between them
  79. function smartparens.insert_pair(opener)
  80. if not smartparens.active or not smartparens.config.enabled then
  81. return false
  82. end
  83. local closer = smartparens.get_closer(opener)
  84. if not closer then
  85. return false
  86. end
  87. local buf = editor.buffer
  88. local cursor = editor.cursor
  89. -- Check if we have a region selected (for wrapping)
  90. if smartparens.config.wrap_region and buf:has_mark() then
  91. local region = buf:get_region(cursor)
  92. if region then
  93. local text = buf:get_text_in_range(region)
  94. buf:erase(region)
  95. buf:insert(region.start, opener .. text .. closer)
  96. -- Position cursor after the wrapped text
  97. editor.cursor = lumacs.Position(region.start.line, region.start.column + #text + 1)
  98. buf:deactivate_mark()
  99. return true
  100. end
  101. end
  102. -- For symmetric pairs (quotes), check if we should skip
  103. if opener == closer then
  104. local next_char = smartparens.char_at_cursor()
  105. if next_char == opener then
  106. -- Skip instead of inserting
  107. editor.cursor = lumacs.Position(cursor.line, cursor.column + 1)
  108. return true
  109. end
  110. end
  111. -- Insert the pair
  112. buf:insert(cursor, opener .. closer)
  113. -- Position cursor between the pair
  114. editor.cursor = lumacs.Position(cursor.line, cursor.column + 1)
  115. return true
  116. end
  117. -- Handle closing delimiter - skip if already there
  118. function smartparens.handle_closer(closer)
  119. if not smartparens.active or not smartparens.config.skip_when_next then
  120. return false
  121. end
  122. local next_char = smartparens.char_at_cursor()
  123. if next_char == closer then
  124. local cursor = editor.cursor
  125. editor.cursor = lumacs.Position(cursor.line, cursor.column + 1)
  126. return true
  127. end
  128. return false
  129. end
  130. -- Handle backspace - delete pair together if empty
  131. function smartparens.handle_backspace()
  132. if not smartparens.active or not smartparens.config.delete_pair then
  133. return false
  134. end
  135. local prev = smartparens.char_before_cursor()
  136. local next = smartparens.char_at_cursor()
  137. if prev and next then
  138. local expected_closer = smartparens.get_closer(prev)
  139. if expected_closer == next then
  140. -- Delete both chars
  141. local buf = editor.buffer
  142. local cursor = editor.cursor
  143. local range = lumacs.Range(
  144. lumacs.Position(cursor.line, cursor.column - 1),
  145. lumacs.Position(cursor.line, cursor.column + 1)
  146. )
  147. buf:erase(range)
  148. editor.cursor = lumacs.Position(cursor.line, cursor.column - 1)
  149. return true
  150. end
  151. end
  152. return false
  153. end
  154. -- Toggle smartparens
  155. function smartparens.toggle()
  156. smartparens.active = not smartparens.active
  157. if smartparens.active then
  158. editor:message("Smartparens enabled")
  159. else
  160. editor:message("Smartparens disabled")
  161. end
  162. end
  163. -- Register commands
  164. editor:register_command("smartparens-mode", "Toggle smartparens auto-pairing", function(args)
  165. smartparens.toggle()
  166. return {success = true}
  167. end, {"sp-mode"})
  168. editor:register_command("smartparens-strict-mode", "Toggle strict pairing rules", function(args)
  169. smartparens.config.skip_when_next = not smartparens.config.skip_when_next
  170. smartparens.config.delete_pair = not smartparens.config.delete_pair
  171. local state = smartparens.config.skip_when_next and "enabled" or "disabled"
  172. editor:message("Strict mode " .. state)
  173. return {success = true}
  174. end)
  175. -- Define minor mode
  176. lumacs.define_minor_mode("smartparens-mode", {
  177. lighter = "()",
  178. global = false,
  179. setup = function()
  180. smartparens.active = true
  181. editor:message("Smartparens enabled")
  182. end,
  183. cleanup = function()
  184. smartparens.active = false
  185. editor:message("Smartparens disabled")
  186. end
  187. })
  188. -- Store in lumacs namespace
  189. lumacs.smartparens = smartparens
  190. print("[smartparens] Package loaded")
  191. return smartparens