which-key.lua 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. -- which-key.lua
  2. -- ============================================================================
  3. -- Displays available keybindings when a prefix key is pressed and the user
  4. -- waits for a configurable delay. Similar to Emacs which-key package.
  5. --
  6. -- This package uses the editor's idle time tracking and extended echo area
  7. -- to show hints without requiring any C++ code changes.
  8. -- ============================================================================
  9. local which_key = {}
  10. -- Configuration
  11. which_key.config = {
  12. delay_ms = 1000, -- Delay before showing hints (milliseconds)
  13. max_hints = 20, -- Max number of hints to show
  14. max_lines = 5, -- Max lines in echo area
  15. separator = " ", -- Separator between hints on same line
  16. show_description = true, -- Show command descriptions
  17. enabled = true, -- Enable which-key
  18. }
  19. -- State
  20. which_key.last_prefix = nil
  21. which_key.showing_hints = false
  22. -- Parse a key sequence like "C-x C-f" into parts
  23. local function parse_sequence(seq)
  24. local parts = {}
  25. for part in seq:gmatch("%S+") do
  26. table.insert(parts, part)
  27. end
  28. return parts
  29. end
  30. -- Check if sequence starts with prefix
  31. local function starts_with(sequence, prefix)
  32. local seq_parts = parse_sequence(sequence)
  33. local prefix_parts = parse_sequence(prefix)
  34. if #seq_parts < #prefix_parts then
  35. return false
  36. end
  37. for i, part in ipairs(prefix_parts) do
  38. if seq_parts[i] ~= part then
  39. return false
  40. end
  41. end
  42. return true
  43. end
  44. -- Get the next key after the prefix
  45. local function get_next_key(sequence, prefix)
  46. local seq_parts = parse_sequence(sequence)
  47. local prefix_parts = parse_sequence(prefix)
  48. if #seq_parts > #prefix_parts then
  49. return seq_parts[#prefix_parts + 1]
  50. end
  51. return nil
  52. end
  53. -- Get all bindings that start with the given prefix
  54. function which_key.get_bindings_for_prefix(prefix)
  55. local all_bindings = editor:get_all_bindings()
  56. local matching = {}
  57. -- Normalize prefix (remove trailing dash if present)
  58. prefix = prefix:gsub("%-$", "")
  59. for _, binding in ipairs(all_bindings) do
  60. if starts_with(binding.sequence, prefix) and binding.sequence ~= prefix then
  61. local next_key = get_next_key(binding.sequence, prefix)
  62. if next_key then
  63. -- Check if we already have this next_key
  64. local found = false
  65. for _, m in ipairs(matching) do
  66. if m.key == next_key then
  67. found = true
  68. break
  69. end
  70. end
  71. if not found then
  72. -- Determine if this is a prefix or a command
  73. local is_prefix = false
  74. local prefix_with_key = prefix .. " " .. next_key
  75. for _, b in ipairs(all_bindings) do
  76. if starts_with(b.sequence, prefix_with_key) and b.sequence ~= prefix_with_key then
  77. is_prefix = true
  78. break
  79. end
  80. end
  81. table.insert(matching, {
  82. key = next_key,
  83. command = binding.command,
  84. description = binding.description,
  85. full_sequence = binding.sequence,
  86. is_prefix = is_prefix,
  87. })
  88. end
  89. end
  90. end
  91. end
  92. -- Sort by key
  93. table.sort(matching, function(a, b)
  94. return a.key < b.key
  95. end)
  96. return matching
  97. end
  98. -- Format hints into lines for display
  99. function which_key.format_hints(bindings, max_width)
  100. max_width = max_width or 80
  101. local lines = {}
  102. local current_line = ""
  103. for i, binding in ipairs(bindings) do
  104. if i > which_key.config.max_hints then
  105. if current_line ~= "" then
  106. table.insert(lines, current_line)
  107. end
  108. table.insert(lines, "... and more")
  109. break
  110. end
  111. local hint
  112. if binding.is_prefix then
  113. hint = binding.key .. ":+"
  114. elseif which_key.config.show_description and binding.description ~= "" then
  115. -- Truncate long descriptions
  116. local desc = binding.description
  117. if #desc > 15 then
  118. desc = desc:sub(1, 12) .. "..."
  119. end
  120. hint = binding.key .. ":" .. desc
  121. else
  122. -- Truncate long command names
  123. local cmd = binding.command or "?"
  124. if #cmd > 15 then
  125. cmd = cmd:sub(1, 12) .. "..."
  126. end
  127. hint = binding.key .. ":" .. cmd
  128. end
  129. if current_line == "" then
  130. current_line = hint
  131. elseif #current_line + #which_key.config.separator + #hint <= max_width then
  132. current_line = current_line .. which_key.config.separator .. hint
  133. else
  134. table.insert(lines, current_line)
  135. current_line = hint
  136. if #lines >= which_key.config.max_lines then
  137. table.insert(lines, "...")
  138. return lines
  139. end
  140. end
  141. end
  142. if current_line ~= "" then
  143. table.insert(lines, current_line)
  144. end
  145. return lines
  146. end
  147. -- Show which-key hints in the extended echo area
  148. function which_key.show_hints(prefix)
  149. if not which_key.config.enabled then
  150. return
  151. end
  152. local bindings = which_key.get_bindings_for_prefix(prefix)
  153. if #bindings > 0 then
  154. local lines = which_key.format_hints(bindings)
  155. -- Prepend the current prefix
  156. table.insert(lines, 1, prefix .. "-")
  157. editor:set_echo_area(lines)
  158. which_key.showing_hints = true
  159. end
  160. end
  161. -- Hide which-key hints
  162. function which_key.hide_hints()
  163. if which_key.showing_hints then
  164. editor:clear_echo_area()
  165. which_key.showing_hints = false
  166. end
  167. end
  168. -- Check if we should show hints (called during idle)
  169. function which_key.check_idle()
  170. if not which_key.config.enabled then
  171. return
  172. end
  173. -- Check if we're building a key sequence
  174. if editor:is_building_sequence() then
  175. local current = editor:current_sequence()
  176. -- Remove trailing dash for comparison
  177. current = current:gsub("%-$", "")
  178. -- Check if we've been idle long enough
  179. local idle_ms = editor:idle_time_ms()
  180. if idle_ms >= which_key.config.delay_ms then
  181. -- Show hints if we haven't already or prefix changed
  182. if not which_key.showing_hints or which_key.last_prefix ~= current then
  183. which_key.show_hints(current)
  184. which_key.last_prefix = current
  185. end
  186. end
  187. else
  188. -- Not building a sequence, hide hints if showing
  189. which_key.hide_hints()
  190. which_key.last_prefix = nil
  191. end
  192. end
  193. -- Register commands
  194. editor:register_command("which-key", "Show available keybindings for prefix", function(args)
  195. if #args > 0 then
  196. which_key.show_hints(args[1])
  197. elseif editor:is_building_sequence() then
  198. local current = editor:current_sequence():gsub("%-$", "")
  199. which_key.show_hints(current)
  200. else
  201. -- Show all prefixes
  202. local all_bindings = editor:get_all_bindings()
  203. local prefixes = {}
  204. for _, binding in ipairs(all_bindings) do
  205. local parts = parse_sequence(binding.sequence)
  206. if #parts > 1 then
  207. local prefix = parts[1]
  208. if not prefixes[prefix] then
  209. prefixes[prefix] = 0
  210. end
  211. prefixes[prefix] = prefixes[prefix] + 1
  212. end
  213. end
  214. local hints = {}
  215. for prefix, count in pairs(prefixes) do
  216. table.insert(hints, prefix .. ":+" .. count)
  217. end
  218. table.sort(hints)
  219. editor:message("Prefix keys: " .. table.concat(hints, " | "))
  220. end
  221. return {success = true}
  222. end, {"wk"}, true, "s")
  223. -- Describe key binding
  224. editor:register_command("describe-key", "Describe a key binding", function(args)
  225. if #args == 0 then
  226. return {success = false, message = "Usage: describe-key <key-sequence>"}
  227. end
  228. local key = args[1]
  229. local all_bindings = editor:get_all_bindings()
  230. for _, binding in ipairs(all_bindings) do
  231. if binding.sequence == key then
  232. local msg = key .. " runs " .. binding.command
  233. if binding.description ~= "" then
  234. msg = msg .. " (" .. binding.description .. ")"
  235. end
  236. return {success = true, message = msg}
  237. end
  238. end
  239. -- Check if it's a prefix
  240. if editor:has_prefix_bindings(key) then
  241. which_key.show_hints(key)
  242. return {success = true}
  243. end
  244. return {success = false, message = key .. " is not bound"}
  245. end, {"dk"}, true, "s")
  246. -- Toggle which-key
  247. editor:register_command("which-key-mode", "Toggle which-key hints", function(args)
  248. which_key.config.enabled = not which_key.config.enabled
  249. if which_key.config.enabled then
  250. editor:message("which-key enabled")
  251. else
  252. which_key.hide_hints()
  253. editor:message("which-key disabled")
  254. end
  255. return {success = true}
  256. end)
  257. -- Set delay
  258. editor:register_command("which-key-delay", "Set which-key delay in milliseconds", function(args)
  259. if #args == 0 then
  260. return {success = true, message = "which-key delay: " .. which_key.config.delay_ms .. "ms"}
  261. end
  262. local delay = tonumber(args[1])
  263. if delay and delay >= 0 then
  264. which_key.config.delay_ms = delay
  265. return {success = true, message = "which-key delay set to " .. delay .. "ms"}
  266. else
  267. return {success = false, message = "Invalid delay value"}
  268. end
  269. end, {}, true, "s")
  270. -- Bind C-h k to describe-key (Emacs standard)
  271. editor:bind_key("C-h k", "describe-key", "Describe a key binding")
  272. -- Register the idle check function globally so the editor can call it
  273. -- This gets called during the UI's idle loop
  274. lumacs.which_key_check_idle = which_key.check_idle
  275. -- Store module
  276. lumacs.which_key = which_key
  277. return which_key