Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ in a popup as you type.
- 🌐 **Plugins**: built-in plugins for marks, registers, presets, and spelling suggestions
- 🚀 **Operators, Motions, Text Objects**: help for operators, motions and text objects
- 🐙 **Hydra Mode**: keep the popup open until you hit `<esc>`
- 🔭 **Search**: fuzzy-find a keybinding by its description and execute it

## ⚡️ Requirements

Expand Down Expand Up @@ -395,6 +396,34 @@ When the **WhichKey** popup is open, you can use the following key bindings (the
- `<c-d>` scroll down
- `<c-u>` scroll up

## 🔭 Search

Forgot a keybinding? Search for it by description instead of hunting through the
popup. The search collects every keybinding available for the current mode and
buffer and lets you fuzzy-find one by its description (including the parent group
names). Picking an entry executes its keybinding.

```vim
" search keymaps for the current mode (defaults to normal mode)
:WhichKeySearch
" or for a specific mode
:WhichKeySearch v
```

```lua
-- or from Lua, e.g. to bind it
vim.keymap.set("n", "<leader>sk", function()
require("which-key").search()
end, { desc = "Search Keymaps" })
```

The picker uses [`vim.ui.select`](https://neovim.io/doc/user/lua.html#vim.ui.select()),
so it integrates with whatever selection UI you already use (the builtin,
[telescope](https://github.com/nvim-telescope/telescope.nvim),
[snacks](https://github.com/folke/snacks.nvim),
[fzf-lua](https://github.com/ibhagwan/fzf-lua),
[dressing](https://github.com/stevearc/dressing.nvim), …).

## 🐙 Hydra Mode

Hydra mode is a special mode that keeps the popup open until you hit `<esc>`.
Expand Down
8 changes: 8 additions & 0 deletions lua/which-key/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,14 @@ function M.setup(opts)
end, {
nargs = "*",
})

vim.api.nvim_create_user_command("WhichKeySearch", function(cmd)
load()
local mode = cmd.args ~= "" and cmd.args or nil
require("which-key").search({ mode = mode })
end, {
nargs = "?",
})
end

---@param opts? wk.Parse
Expand Down
7 changes: 7 additions & 0 deletions lua/which-key/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ function M.show(opts)
end
end

--- Fuzzy search keymaps by description (using `vim.ui.select`).
--- Picking an entry executes its keybind.
---@param opts? wk.Filter
function M.search(opts)
require("which-key.search").open(opts)
end

---@param opts? wk.Opts
function M.setup(opts)
M.did_setup = true
Expand Down
113 changes: 113 additions & 0 deletions lua/which-key/search.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
local Buf = require("which-key.buf")

local M = {}

---@class wk.Search.item
---@field keys string the raw lhs key sequence used to execute the mapping
---@field display string formatted key sequence (with icons/symbols)
---@field desc string searchable description, prefixed with parent groups
---@field mode string

--- Returns the description for a node, falling back to its rhs.
---@param node wk.Node
---@return string?
local function desc_of(node)
local desc = node.desc
if not desc and node.keymap and type(node.keymap.rhs) == "string" and node.keymap.rhs ~= "" then
desc = node.keymap.rhs --[[@as string]]
end
return desc
end

--- Builds the breadcrumb of parent group descriptions, e.g. "git → commit".
---@param node wk.Node
local function breadcrumb(node)
local groups = {} ---@type string[]
local parent = node.parent
while parent and parent.parent do
local d = parent.desc
if d then
table.insert(groups, 1, d)
end
parent = parent.parent
end
return groups
end

--- Collects all executable keymaps for the given mode as searchable items.
---@param mode wk.Mode
---@return wk.Search.item[]
local function collect(mode)
local View = require("which-key.view")
---@type wk.Search.item[]
local items = {}
mode.tree:walk(function(node)
-- only real, executable keymaps are searchable keybinds
if not node.keymap then
return
end
local desc = desc_of(node)
local crumbs = breadcrumb(node)
crumbs[#crumbs + 1] = desc or ""
items[#items + 1] = {
keys = node.keys,
display = View.format(node.keys),
desc = table.concat(crumbs, " → "),
mode = mode.mode,
}
end)
table.sort(items, function(a, b)
if a.desc ~= b.desc then
return a.desc < b.desc
end
return a.keys < b.keys
end)
return items
end

--- Executes the selected keymap by feeding its keys.
---@param item wk.Search.item
local function execute(item)
local feed = vim.api.nvim_replace_termcodes(item.keys, true, true, true)
vim.api.nvim_feedkeys(feed, "mt", false)
end

--- Fuzzy search keymaps by their description using `vim.ui.select`.
--- Picking an entry executes its keybind.
---@param opts? wk.Filter
function M.open(opts)
opts = opts or {}
local mode = Buf.get(opts)
if not mode then
require("which-key.util").warn("No keymaps found for mode `" .. (opts.mode or "n") .. "`")
return
end

local items = collect(mode)
if #items == 0 then
require("which-key.util").warn("No keymaps found to search")
return
end

-- align the key column for nicer formatting
local width = 0
for _, item in ipairs(items) do
width = math.max(width, vim.fn.strdisplaywidth(item.display))
end

vim.ui.select(items, {
prompt = "Search keymaps",
kind = "which-key",
---@param item wk.Search.item
format_item = function(item)
local pad = string.rep(" ", width - vim.fn.strdisplaywidth(item.display))
return item.display .. pad .. " " .. item.desc
end,
}, function(item)
if item then
execute(item)
end
end)
end

return M