init.lua 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. -- ido.lua
  2. -- ============================================================================
  3. -- Interactive Do - Enhanced completion with fuzzy matching and inline display.
  4. -- Similar to Emacs ido-mode.
  5. -- ============================================================================
  6. local ido = {}
  7. -- Configuration
  8. ido.config = {
  9. enabled = true,
  10. separator = " | ",
  11. max_candidates = 10,
  12. show_count = true,
  13. fuzzy = true,
  14. case_sensitive = false,
  15. -- Display format: "inline" shows in minibuffer, "vertical" would need popup
  16. display = "inline",
  17. }
  18. -- State
  19. ido.active = false
  20. ido.candidates = {}
  21. ido.filtered = {}
  22. ido.selected_index = 1
  23. ido.input = ""
  24. -- Fuzzy match score function
  25. -- Returns score (higher is better) or nil for no match
  26. function ido.fuzzy_match(pattern, candidate)
  27. if pattern == "" then
  28. return 1000 -- Empty pattern matches everything
  29. end
  30. local p = ido.config.case_sensitive and pattern or pattern:lower()
  31. local c = ido.config.case_sensitive and candidate or candidate:lower()
  32. -- Exact match is best
  33. if c == p then
  34. return 10000
  35. end
  36. -- Prefix match is very good
  37. if c:sub(1, #p) == p then
  38. return 5000 + (1000 / #c)
  39. end
  40. -- Substring match
  41. local substr_pos = c:find(p, 1, true)
  42. if substr_pos then
  43. return 3000 - substr_pos + (1000 / #c)
  44. end
  45. -- Fuzzy match: all chars in pattern appear in order in candidate
  46. if ido.config.fuzzy then
  47. local score = 0
  48. local j = 1
  49. local consecutive = 0
  50. local last_match = -1
  51. for i = 1, #p do
  52. local char = p:sub(i, i)
  53. local found = false
  54. while j <= #c do
  55. if c:sub(j, j) == char then
  56. found = true
  57. -- Bonus for consecutive matches
  58. if j == last_match + 1 then
  59. consecutive = consecutive + 1
  60. score = score + consecutive * 10
  61. else
  62. consecutive = 0
  63. end
  64. -- Bonus for matching at word boundaries
  65. if j == 1 or c:sub(j-1, j-1):match("[%s_%-/\\.]") then
  66. score = score + 50
  67. end
  68. last_match = j
  69. j = j + 1
  70. break
  71. end
  72. j = j + 1
  73. end
  74. if not found then
  75. return nil -- No match
  76. end
  77. end
  78. -- Penalty for length difference
  79. score = score + 1000 - (#c - #p) * 5
  80. return math.max(1, score)
  81. end
  82. return nil
  83. end
  84. -- Filter and sort candidates based on input
  85. function ido.filter_candidates(input)
  86. local results = {}
  87. for _, candidate in ipairs(ido.candidates) do
  88. local text = type(candidate) == "table" and candidate.text or candidate
  89. local score = ido.fuzzy_match(input, text)
  90. if score then
  91. table.insert(results, {
  92. candidate = candidate,
  93. text = text,
  94. score = score
  95. })
  96. end
  97. end
  98. -- Sort by score (descending)
  99. table.sort(results, function(a, b)
  100. return a.score > b.score
  101. end)
  102. -- Extract candidates
  103. ido.filtered = {}
  104. for i, r in ipairs(results) do
  105. if i > ido.config.max_candidates * 2 then break end
  106. table.insert(ido.filtered, r.candidate)
  107. end
  108. return ido.filtered
  109. end
  110. -- Format candidates for display in minibuffer
  111. function ido.format_display(input)
  112. local parts = {}
  113. -- Show input
  114. if input ~= "" then
  115. table.insert(parts, input)
  116. end
  117. -- Show candidates
  118. local shown = 0
  119. for i, candidate in ipairs(ido.filtered) do
  120. if shown >= ido.config.max_candidates then
  121. if #ido.filtered > ido.config.max_candidates then
  122. table.insert(parts, "...")
  123. end
  124. break
  125. end
  126. local text = type(candidate) == "table" and candidate.text or candidate
  127. if i == ido.selected_index then
  128. text = "[" .. text .. "]"
  129. end
  130. table.insert(parts, text)
  131. shown = shown + 1
  132. end
  133. -- Add count if enabled
  134. if ido.config.show_count and #ido.filtered > ido.config.max_candidates then
  135. table.insert(parts, "(" .. #ido.filtered .. " total)")
  136. end
  137. return table.concat(parts, ido.config.separator)
  138. end
  139. -- Select next candidate
  140. function ido.next()
  141. if #ido.filtered > 0 then
  142. ido.selected_index = (ido.selected_index % #ido.filtered) + 1
  143. end
  144. end
  145. -- Select previous candidate
  146. function ido.prev()
  147. if #ido.filtered > 0 then
  148. ido.selected_index = ((ido.selected_index - 2) % #ido.filtered) + 1
  149. end
  150. end
  151. -- Get selected candidate
  152. function ido.get_selected()
  153. if ido.selected_index <= #ido.filtered then
  154. return ido.filtered[ido.selected_index]
  155. end
  156. return nil
  157. end
  158. -- Complete buffer names with ido
  159. function ido.complete_buffer()
  160. local buf_names = editor:get_buffer_names()
  161. ido.candidates = buf_names
  162. ido.filtered = ido.filter_candidates("")
  163. ido.selected_index = 1
  164. local display = ido.format_display("")
  165. editor:message("Switch to buffer: " .. display)
  166. end
  167. -- Complete commands with ido
  168. function ido.complete_command()
  169. local cmd_names = get_command_names()
  170. ido.candidates = cmd_names
  171. ido.filtered = ido.filter_candidates("")
  172. ido.selected_index = 1
  173. local display = ido.format_display("")
  174. editor:message("M-x " .. display)
  175. end
  176. -- Register commands
  177. editor:register_command("ido-switch-buffer", "Switch buffer with ido completion", function(args)
  178. ido.complete_buffer()
  179. return {success = true}
  180. end)
  181. editor:register_command("ido-execute-command", "Execute command with ido completion", function(args)
  182. ido.complete_command()
  183. return {success = true}
  184. end)
  185. editor:register_command("ido-mode", "Toggle ido-mode for enhanced completion", function(args)
  186. ido.config.enabled = not ido.config.enabled
  187. if ido.config.enabled then
  188. editor:message("ido-mode enabled")
  189. else
  190. editor:message("ido-mode disabled")
  191. end
  192. return {success = true}
  193. end)
  194. editor:register_command("ido-toggle-fuzzy", "Toggle fuzzy matching in ido", function(args)
  195. ido.config.fuzzy = not ido.config.fuzzy
  196. editor:message("Fuzzy matching: " .. (ido.config.fuzzy and "on" or "off"))
  197. return {success = true}
  198. end)
  199. -- Utility: simple buffer selection demo
  200. editor:register_command("ido-demo", "Demonstrate ido buffer selection", function(args)
  201. local buf_names = editor:get_buffer_names()
  202. ido.candidates = buf_names
  203. local results = ido.filter_candidates(args[1] or "")
  204. ido.selected_index = 1
  205. if #results > 0 then
  206. local display = ido.format_display(args[1] or "")
  207. return {success = true, message = display}
  208. else
  209. return {success = false, message = "No matching buffers"}
  210. end
  211. end, {}, true, "s")
  212. -- Store in lumacs namespace
  213. lumacs.ido = ido
  214. print("[ido] Package loaded")
  215. return ido