|
@@ -1,19 +1,28 @@
|
|
|
-- which-key.lua
|
|
-- which-key.lua
|
|
|
-- ============================================================================
|
|
-- ============================================================================
|
|
|
--- Displays available keybindings when a prefix key is pressed.
|
|
|
|
|
--- Similar to Emacs which-key package.
|
|
|
|
|
|
|
+-- Displays available keybindings when a prefix key is pressed and the user
|
|
|
|
|
+-- waits for a configurable delay. Similar to Emacs which-key package.
|
|
|
|
|
+--
|
|
|
|
|
+-- This package uses the editor's idle time tracking and extended echo area
|
|
|
|
|
+-- to show hints without requiring any C++ code changes.
|
|
|
-- ============================================================================
|
|
-- ============================================================================
|
|
|
|
|
|
|
|
local which_key = {}
|
|
local which_key = {}
|
|
|
|
|
|
|
|
-- Configuration
|
|
-- Configuration
|
|
|
which_key.config = {
|
|
which_key.config = {
|
|
|
- delay = 0.5, -- Delay before showing hints (not yet implemented - needs timer)
|
|
|
|
|
- max_display = 10, -- Max number of hints to show
|
|
|
|
|
- separator = " | ", -- Separator between hints
|
|
|
|
|
|
|
+ delay_ms = 1000, -- Delay before showing hints (milliseconds)
|
|
|
|
|
+ max_hints = 20, -- Max number of hints to show
|
|
|
|
|
+ max_lines = 5, -- Max lines in echo area
|
|
|
|
|
+ separator = " ", -- Separator between hints on same line
|
|
|
show_description = true, -- Show command descriptions
|
|
show_description = true, -- Show command descriptions
|
|
|
|
|
+ enabled = true, -- Enable which-key
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+-- State
|
|
|
|
|
+which_key.last_prefix = nil
|
|
|
|
|
+which_key.showing_hints = false
|
|
|
|
|
+
|
|
|
-- Parse a key sequence like "C-x C-f" into parts
|
|
-- Parse a key sequence like "C-x C-f" into parts
|
|
|
local function parse_sequence(seq)
|
|
local function parse_sequence(seq)
|
|
|
local parts = {}
|
|
local parts = {}
|
|
@@ -105,76 +114,123 @@ function which_key.get_bindings_for_prefix(prefix)
|
|
|
return matching
|
|
return matching
|
|
|
end
|
|
end
|
|
|
|
|
|
|
|
--- Format hints for display
|
|
|
|
|
|
|
+-- Format hints into lines for display
|
|
|
function which_key.format_hints(bindings, max_width)
|
|
function which_key.format_hints(bindings, max_width)
|
|
|
max_width = max_width or 80
|
|
max_width = max_width or 80
|
|
|
- local hints = {}
|
|
|
|
|
|
|
+ local lines = {}
|
|
|
|
|
+ local current_line = ""
|
|
|
|
|
|
|
|
for i, binding in ipairs(bindings) do
|
|
for i, binding in ipairs(bindings) do
|
|
|
- if i > which_key.config.max_display then
|
|
|
|
|
- table.insert(hints, "...")
|
|
|
|
|
|
|
+ if i > which_key.config.max_hints then
|
|
|
|
|
+ if current_line ~= "" then
|
|
|
|
|
+ table.insert(lines, current_line)
|
|
|
|
|
+ end
|
|
|
|
|
+ table.insert(lines, "... and more")
|
|
|
break
|
|
break
|
|
|
end
|
|
end
|
|
|
|
|
|
|
|
local hint
|
|
local hint
|
|
|
if binding.is_prefix then
|
|
if binding.is_prefix then
|
|
|
- hint = binding.key .. ":+prefix"
|
|
|
|
|
|
|
+ hint = binding.key .. ":+"
|
|
|
elseif which_key.config.show_description and binding.description ~= "" then
|
|
elseif which_key.config.show_description and binding.description ~= "" then
|
|
|
- hint = binding.key .. ":" .. binding.description
|
|
|
|
|
|
|
+ -- Truncate long descriptions
|
|
|
|
|
+ local desc = binding.description
|
|
|
|
|
+ if #desc > 15 then
|
|
|
|
|
+ desc = desc:sub(1, 12) .. "..."
|
|
|
|
|
+ end
|
|
|
|
|
+ hint = binding.key .. ":" .. desc
|
|
|
else
|
|
else
|
|
|
- hint = binding.key .. ":" .. binding.command
|
|
|
|
|
|
|
+ -- Truncate long command names
|
|
|
|
|
+ local cmd = binding.command
|
|
|
|
|
+ if #cmd > 15 then
|
|
|
|
|
+ cmd = cmd:sub(1, 12) .. "..."
|
|
|
|
|
+ end
|
|
|
|
|
+ hint = binding.key .. ":" .. cmd
|
|
|
end
|
|
end
|
|
|
|
|
|
|
|
- table.insert(hints, hint)
|
|
|
|
|
- end
|
|
|
|
|
|
|
+ if current_line == "" then
|
|
|
|
|
+ current_line = hint
|
|
|
|
|
+ elseif #current_line + #which_key.config.separator + #hint <= max_width then
|
|
|
|
|
+ current_line = current_line .. which_key.config.separator .. hint
|
|
|
|
|
+ else
|
|
|
|
|
+ table.insert(lines, current_line)
|
|
|
|
|
+ current_line = hint
|
|
|
|
|
|
|
|
- -- Join with separator
|
|
|
|
|
- local result = table.concat(hints, which_key.config.separator)
|
|
|
|
|
|
|
+ if #lines >= which_key.config.max_lines then
|
|
|
|
|
+ table.insert(lines, "...")
|
|
|
|
|
+ return lines
|
|
|
|
|
+ end
|
|
|
|
|
+ end
|
|
|
|
|
+ end
|
|
|
|
|
|
|
|
- -- Truncate if too long
|
|
|
|
|
- if #result > max_width then
|
|
|
|
|
- result = result:sub(1, max_width - 3) .. "..."
|
|
|
|
|
|
|
+ if current_line ~= "" then
|
|
|
|
|
+ table.insert(lines, current_line)
|
|
|
end
|
|
end
|
|
|
|
|
|
|
|
- return result
|
|
|
|
|
|
|
+ return lines
|
|
|
end
|
|
end
|
|
|
|
|
|
|
|
--- Show which-key hints for current prefix
|
|
|
|
|
-function which_key.show_hints()
|
|
|
|
|
- if not editor:is_building_sequence() then
|
|
|
|
|
|
|
+-- Show which-key hints in the extended echo area
|
|
|
|
|
+function which_key.show_hints(prefix)
|
|
|
|
|
+ if not which_key.config.enabled then
|
|
|
return
|
|
return
|
|
|
end
|
|
end
|
|
|
|
|
|
|
|
- local current = editor:current_sequence()
|
|
|
|
|
- -- Remove trailing dash
|
|
|
|
|
- current = current:gsub("%-$", "")
|
|
|
|
|
-
|
|
|
|
|
- local bindings = which_key.get_bindings_for_prefix(current)
|
|
|
|
|
|
|
+ local bindings = which_key.get_bindings_for_prefix(prefix)
|
|
|
|
|
|
|
|
if #bindings > 0 then
|
|
if #bindings > 0 then
|
|
|
- local hints = which_key.format_hints(bindings)
|
|
|
|
|
- editor:message(current .. "- " .. hints)
|
|
|
|
|
|
|
+ local lines = which_key.format_hints(bindings)
|
|
|
|
|
+ -- Prepend the current prefix
|
|
|
|
|
+ table.insert(lines, 1, prefix .. "-")
|
|
|
|
|
+ editor:set_echo_area(lines)
|
|
|
|
|
+ which_key.showing_hints = true
|
|
|
end
|
|
end
|
|
|
end
|
|
end
|
|
|
|
|
|
|
|
--- Manually show which-key for a prefix
|
|
|
|
|
-function which_key.show_for_prefix(prefix)
|
|
|
|
|
- local bindings = which_key.get_bindings_for_prefix(prefix)
|
|
|
|
|
|
|
+-- Hide which-key hints
|
|
|
|
|
+function which_key.hide_hints()
|
|
|
|
|
+ if which_key.showing_hints then
|
|
|
|
|
+ editor:clear_echo_area()
|
|
|
|
|
+ which_key.showing_hints = false
|
|
|
|
|
+ end
|
|
|
|
|
+end
|
|
|
|
|
|
|
|
- if #bindings > 0 then
|
|
|
|
|
- local hints = which_key.format_hints(bindings)
|
|
|
|
|
- editor:message(prefix .. " " .. hints)
|
|
|
|
|
|
|
+-- Check if we should show hints (called during idle)
|
|
|
|
|
+function which_key.check_idle()
|
|
|
|
|
+ if not which_key.config.enabled then
|
|
|
|
|
+ return
|
|
|
|
|
+ end
|
|
|
|
|
+
|
|
|
|
|
+ -- Check if we're building a key sequence
|
|
|
|
|
+ if editor:is_building_sequence() then
|
|
|
|
|
+ local current = editor:current_sequence()
|
|
|
|
|
+ -- Remove trailing dash for comparison
|
|
|
|
|
+ current = current:gsub("%-$", "")
|
|
|
|
|
+
|
|
|
|
|
+ -- Check if we've been idle long enough
|
|
|
|
|
+ local idle_ms = editor:idle_time_ms()
|
|
|
|
|
+
|
|
|
|
|
+ if idle_ms >= which_key.config.delay_ms then
|
|
|
|
|
+ -- Show hints if we haven't already or prefix changed
|
|
|
|
|
+ if not which_key.showing_hints or which_key.last_prefix ~= current then
|
|
|
|
|
+ which_key.show_hints(current)
|
|
|
|
|
+ which_key.last_prefix = current
|
|
|
|
|
+ end
|
|
|
|
|
+ end
|
|
|
else
|
|
else
|
|
|
- editor:message("No bindings for prefix: " .. prefix)
|
|
|
|
|
|
|
+ -- Not building a sequence, hide hints if showing
|
|
|
|
|
+ which_key.hide_hints()
|
|
|
|
|
+ which_key.last_prefix = nil
|
|
|
end
|
|
end
|
|
|
end
|
|
end
|
|
|
|
|
|
|
|
-- Register commands
|
|
-- Register commands
|
|
|
editor:register_command("which-key", "Show available keybindings for prefix", function(args)
|
|
editor:register_command("which-key", "Show available keybindings for prefix", function(args)
|
|
|
if #args > 0 then
|
|
if #args > 0 then
|
|
|
- which_key.show_for_prefix(args[1])
|
|
|
|
|
|
|
+ which_key.show_hints(args[1])
|
|
|
elseif editor:is_building_sequence() then
|
|
elseif editor:is_building_sequence() then
|
|
|
- which_key.show_hints()
|
|
|
|
|
|
|
+ local current = editor:current_sequence():gsub("%-$", "")
|
|
|
|
|
+ which_key.show_hints(current)
|
|
|
else
|
|
else
|
|
|
-- Show all prefixes
|
|
-- Show all prefixes
|
|
|
local all_bindings = editor:get_all_bindings()
|
|
local all_bindings = editor:get_all_bindings()
|
|
@@ -223,16 +279,47 @@ editor:register_command("describe-key", "Describe a key binding", function(args)
|
|
|
|
|
|
|
|
-- Check if it's a prefix
|
|
-- Check if it's a prefix
|
|
|
if editor:has_prefix_bindings(key) then
|
|
if editor:has_prefix_bindings(key) then
|
|
|
- which_key.show_for_prefix(key)
|
|
|
|
|
|
|
+ which_key.show_hints(key)
|
|
|
return {success = true}
|
|
return {success = true}
|
|
|
end
|
|
end
|
|
|
|
|
|
|
|
return {success = false, message = key .. " is not bound"}
|
|
return {success = false, message = key .. " is not bound"}
|
|
|
end, {"dk"}, true, "s")
|
|
end, {"dk"}, true, "s")
|
|
|
|
|
|
|
|
|
|
+-- Toggle which-key
|
|
|
|
|
+editor:register_command("which-key-mode", "Toggle which-key hints", function(args)
|
|
|
|
|
+ which_key.config.enabled = not which_key.config.enabled
|
|
|
|
|
+ if which_key.config.enabled then
|
|
|
|
|
+ editor:message("which-key enabled")
|
|
|
|
|
+ else
|
|
|
|
|
+ which_key.hide_hints()
|
|
|
|
|
+ editor:message("which-key disabled")
|
|
|
|
|
+ end
|
|
|
|
|
+ return {success = true}
|
|
|
|
|
+end)
|
|
|
|
|
+
|
|
|
|
|
+-- Set delay
|
|
|
|
|
+editor:register_command("which-key-delay", "Set which-key delay in milliseconds", function(args)
|
|
|
|
|
+ if #args == 0 then
|
|
|
|
|
+ return {success = true, message = "which-key delay: " .. which_key.config.delay_ms .. "ms"}
|
|
|
|
|
+ end
|
|
|
|
|
+
|
|
|
|
|
+ local delay = tonumber(args[1])
|
|
|
|
|
+ if delay and delay >= 0 then
|
|
|
|
|
+ which_key.config.delay_ms = delay
|
|
|
|
|
+ return {success = true, message = "which-key delay set to " .. delay .. "ms"}
|
|
|
|
|
+ else
|
|
|
|
|
+ return {success = false, message = "Invalid delay value"}
|
|
|
|
|
+ end
|
|
|
|
|
+end, {}, true, "s")
|
|
|
|
|
+
|
|
|
-- Bind C-h k to describe-key (Emacs standard)
|
|
-- Bind C-h k to describe-key (Emacs standard)
|
|
|
editor:bind_key("C-h k", "describe-key", "Describe a key binding")
|
|
editor:bind_key("C-h k", "describe-key", "Describe a key binding")
|
|
|
|
|
|
|
|
|
|
+-- Register the idle check function globally so the editor can call it
|
|
|
|
|
+-- This gets called during the UI's idle loop
|
|
|
|
|
+lumacs.which_key_check_idle = which_key.check_idle
|
|
|
|
|
+
|
|
|
-- Store module
|
|
-- Store module
|
|
|
lumacs.which_key = which_key
|
|
lumacs.which_key = which_key
|
|
|
|
|
|