1

Update generated neovim config

This commit is contained in:
2024-08-15 14:28:54 +02:00
parent 07409c223d
commit 25cfcf2941
3809 changed files with 351157 additions and 0 deletions

View File

@ -0,0 +1,7 @@
root = true
[*]
insert_final_newline = true
indent_style = space
indent_size = 2
charset = utf-8

View File

@ -0,0 +1,3 @@
{
".": "3.13.2"
}

View File

@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Ask a question
url: https://github.com/folke/which-key.nvim/discussions
about: Use Github discussions instead

View File

@ -0,0 +1,16 @@
## Description
<!-- Describe the big picture of your changes to communicate to the maintainers
why we should accept this pull request. -->
## Related Issue(s)
<!--
If this PR fixes any issues, please link to the issue here.
- Fixes #<issue_number>
-->
## Screenshots
<!-- Add screenshots of the changes if applicable. -->

View File

@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

View File

@ -0,0 +1,9 @@
{
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
"packages": {
".": {
"release-type": "simple",
"extra-files": ["lua/which-key/config.lua"]
}
}
}

View File

@ -0,0 +1,8 @@
name: "PR Labeler"
on:
- pull_request_target
jobs:
labeler:
uses: folke/github/.github/workflows/labeler.yml@main
secrets: inherit

View File

@ -0,0 +1,18 @@
name: PR Title
on:
pull_request_target:
types:
- opened
- edited
- synchronize
- reopened
- ready_for_review
permissions:
pull-requests: read
jobs:
pr-title:
uses: folke/github/.github/workflows/pr.yml@main
secrets: inherit

View File

@ -0,0 +1,11 @@
name: Stale Issues & PRs
on:
schedule:
- cron: "30 1 * * *"
jobs:
stale:
if: contains(fromJSON('["folke", "LazyVim"]'), github.repository_owner)
uses: folke/github/.github/workflows/stale.yml@main
secrets: inherit

View File

@ -0,0 +1,13 @@
name: Update Repo
on:
workflow_dispatch:
schedule:
# Run every hour
- cron: "0 * * * *"
jobs:
update:
if: contains(fromJSON('["folke", "LazyVim"]'), github.repository_owner)
uses: folke/github/.github/workflows/update.yml@main
secrets: inherit

View File

@ -0,0 +1,35 @@
# 💥 What's New in 3.0?
Major update for [which-key.nvim](https://github.com/folke/which-key.nvim)! This release includes a complete rewrite and several new features.
**which-key** was my very first plugin, so it was time for a fresh start. 🎉
-**Full Rewrite**: Improved performance and functionality.
- 👀 **Visual & Operator Pending Mode Integration**: Now uses `ModeChanged`, eliminating the need for operator remappings.
- 🔧 **Simplified Mappings**: Removed obscure secret mappings.
- 🔒 **Safer Auto Triggers**: Auto triggers are now never created for single keys apart from `g` and `z`. All other letters are unsafe.
- ⏱️ **Delay**: Set delay independently of `timeoutlen`.
- 🛠️ **Layout**:
- Presets: `classic`, `modern`, and `helix`.
- Enable/disable which-key for specific modes.
- Configurable sorting with options like `local`, `order`, `group`, `alphanum`, `mod`, `lower`, `icase`, `desc`, and `manual`.
- Expand groups with fewer keymaps.
- Customizable string replacements for `key` and `desc`.
- 🎨 **Icon Support**:
- Auto-detect icons for keymaps using `lazy.nvim`.
- Custom icon rules and specifications for mapping levels.
- 🚫 **Never Get in the Way**: Avoids overlapping with the cursor.
- 🗂️ **New Mapping Spec**: New and better mappings spec, more in line with `vim.keymap.set` and how you define keymaps with [lazy.nvim](https://github.com/folke/lazy.nvim)
- 🐛 New Bugs: Lots of new and exciting bugs to discover! 🐞
## Screenshots
**Classic Mode**
![image](https://github.com/folke/which-key.nvim/assets/292349/14195bd3-1015-4c44-81c6-4ef8f2410c1b)
**Modern Mode**
![image](https://github.com/folke/which-key.nvim/assets/292349/842e9311-ded9-458a-bed4-2b12f075c85f)
**Helix Mode**
![image](https://github.com/folke/which-key.nvim/assets/292349/ca553e0c-e92d-4968-9dce-de91601c5c5c)
For detailed configuration and usage instructions, refer to the updated README.

View File

@ -0,0 +1,247 @@
local Config = require("which-key.config")
local Tree = require("which-key.tree")
local Triggers = require("which-key.triggers")
local Util = require("which-key.util")
---@class wk.Mode
---@field buf wk.Buffer
---@field mode string
---@field tree wk.Tree
---@field triggers wk.Node[]
local Mode = {}
Mode.__index = Mode
---@param node wk.Node
local function is_special(node)
return (node:is_plugin() or node:is_proxy()) and not node.keymap
end
--- Checks if it's safe to add a trigger for the given node
---@param node wk.Node
---@param no_single? boolean
local function is_safe(node, no_single)
if node.keymap or is_special(node) or node:count() == 0 then
return false
end
if no_single and #node.path == 1 then
local key = node.path[1]
-- only z or g are safe
if key:match("^[a-z]$") and not key:match("^[gz]$") then
return false
end
-- only Z is safe
if key:match("^[A-Z]$") and not key:match("^[Z]$") then
return false
end
end
return true
end
function Mode:__tostring()
return string.format("Mode(%s:%d)", self.mode, self.buf.buf)
end
---@param buf wk.Buffer
---@param mode string
function Mode.new(buf, mode)
local self = setmetatable({}, Mode)
self.buf = buf
self.mode = mode
self.tree = Tree.new()
self.triggers = {}
self:update()
return self
end
function Mode:attach()
self.triggers = {}
-- NOTE: order is important for nowait to work!
-- * first add plugin mappings
-- * then add triggers
self.tree:walk(function(node)
if is_special(node) then
table.insert(self.triggers, node)
return false
end
end)
if Config.triggers.modes[self.mode] then
-- Auto triggers
self.tree:walk(function(node)
if is_safe(node, true) then
table.insert(self.triggers, node)
return false
end
end)
end
-- Manual triggers
for _, t in ipairs(Config.triggers.mappings) do
if self:has(t) then
local node = self.tree:find(t.lhs)
if node and is_safe(node) then
table.insert(self.triggers, node)
end
end
end
Triggers.schedule(self)
end
function Mode:xo()
return self.mode:find("[xo]") ~= nil
end
function Mode:clear()
Triggers.detach(self)
self.tree:clear()
end
---@param mode string
function Mode:is(mode)
if mode == "v" then
return self.mode == "x" or self.mode == "s"
end
return self.mode == mode
end
---@param mapping wk.Keymap
function Mode:has(mapping)
return self:is(mapping.mode) and (not mapping.buffer or mapping.buffer == self.buf.buf)
end
function Mode:update()
self.tree:clear()
local mappings = vim.api.nvim_get_keymap(self.mode)
vim.list_extend(mappings, vim.api.nvim_buf_get_keymap(self.buf.buf, self.mode))
---@cast mappings wk.Keymap[]
for _, mapping in ipairs(mappings) do
if mapping.desc and mapping.desc:find("which-key-trigger", 1, true) then
-- ignore which-key triggers
elseif Util.is_nop(mapping.rhs) then
self.tree:add(mapping, true)
elseif mapping.lhs:sub(1, 6) ~= "<Plug>" and mapping.lhs:sub(1, 5) ~= "<SNR>" then
self.tree:add(mapping)
end
end
for _, m in ipairs(Config.mappings) do
if self:has(m) then
self.tree:add(m, true)
end
end
self.tree:fix()
self:attach()
vim.schedule(function()
require("which-key.state").update()
end)
end
---@class wk.Buffer
---@field buf number
---@field modes table<string, wk.Mode>
local Buf = {}
Buf.__index = Buf
---@param buf? number
function Buf.new(buf)
local self = setmetatable({}, Buf)
buf = buf or 0
self.buf = buf == 0 and vim.api.nvim_get_current_buf() or buf
self.modes = {}
return self
end
---@param opts? wk.Filter
function Buf:clear(opts)
opts = opts or {}
assert(not opts.buf or opts.buf == self.buf, "buffer mismatch")
---@type string[]
local modes = opts.mode and { opts.mode } or vim.tbl_keys(self.modes)
for _, m in ipairs(modes) do
local mode = self.modes[m]
if mode then
mode:clear()
self.modes[m] = nil
end
end
end
function Buf:valid()
return vim.api.nvim_buf_is_valid(self.buf)
end
---@param opts? wk.Filter
---@return wk.Mode?
function Buf:get(opts)
if not self:valid() then
return
end
opts = opts or {}
local mode = opts.mode or Util.mapmode()
local ret = self.modes[mode]
if not ret then
self.modes[mode] = Mode.new(self, mode)
Util.debug("new " .. tostring(self.modes[mode]))
return self.modes[mode]
elseif opts.update then
Util.debug("update " .. tostring(ret))
ret:update()
end
return ret
end
local M = {}
M.Buf = Buf
M.bufs = {} ---@type table<number,wk.Buffer>
---@param opts? wk.Filter
function M.get(opts)
M.cleanup()
opts = opts or {}
local buf = opts.buf or vim.api.nvim_get_current_buf()
if not vim.api.nvim_buf_is_valid(buf) then
return
end
local ft = vim.bo[buf].filetype
local bt = vim.bo[buf].buftype
if vim.tbl_contains(Config.disable.ft, ft) then
return
end
if vim.tbl_contains(Config.disable.bt, bt) then
return
end
M.bufs[buf] = M.bufs[buf] or Buf.new(buf)
return M.bufs[buf]:get(opts)
end
function M.cleanup()
for buf, _ in pairs(M.bufs) do
if not vim.api.nvim_buf_is_valid(buf) then
M.bufs[buf] = nil
end
end
end
---@param opts? wk.Filter
function M.clear(opts)
M.cleanup()
opts = opts or {}
---@type number[]
local bufs = opts.buf and { opts.buf } or vim.tbl_keys(M.bufs)
for _, b in ipairs(bufs) do
if M.bufs[b] then
M.bufs[b]:clear(opts)
end
end
end
return M

View File

@ -0,0 +1,21 @@
local Docs = require("lazy.docs")
local M = {}
function M.update()
local config = Docs.extract("lua/which-key/config.lua", "\n(--@class wk%.Opts.-\n})")
config = config:gsub("%s*debug = false.\n", "\n")
Docs.save({
config = config,
colors = Docs.colors({
modname = "which-key.colors",
path = "lua/which-key/colors.lua",
name = "WhichKey",
}),
})
end
M.update()
print("Updated docs")
return M

View File

@ -0,0 +1,70 @@
local M = {}
---@type table<string, fun():wk.Spec>
M.expand = {}
---@return number[]
function M.bufs()
local current = vim.api.nvim_get_current_buf()
return vim.tbl_filter(function(buf)
return buf ~= current and vim.bo[buf].buflisted
end, vim.api.nvim_list_bufs())
end
function M.bufname(buf)
local name = vim.api.nvim_buf_get_name(buf)
return name == "" and "[No Name]" or vim.fn.fnamemodify(name, ":~:.")
end
---@param spec wk.Spec[]
function M.add_keys(spec)
table.sort(spec, function(a, b)
return a.desc < b.desc
end)
spec = vim.list_slice(spec, 1, 10)
for i, v in ipairs(spec) do
v[1] = tostring(i - 1)
end
return spec
end
function M.expand.buf()
local ret = {} ---@type wk.Spec[]
for _, buf in ipairs(M.bufs()) do
local name = M.bufname(buf)
ret[#ret + 1] = {
"",
function()
vim.api.nvim_set_current_buf(buf)
end,
desc = name,
icon = { cat = "file", name = name },
}
end
return M.add_keys(ret)
end
function M.expand.win()
---@type wk.Spec[]
local ret = {}
local current = vim.api.nvim_get_current_win()
for _, win in ipairs(vim.api.nvim_list_wins()) do
local is_float = vim.api.nvim_win_get_config(win).relative ~= ""
if win ~= current and not is_float then
local buf = vim.api.nvim_win_get_buf(win)
local name = M.bufname(buf)
ret[#ret + 1] = {
"",
function()
vim.api.nvim_set_current_win(win)
end,
desc = name,
icon = { cat = "file", name = name },
}
end
end
return M.add_keys(ret)
end
return M

View File

@ -0,0 +1,209 @@
local Config = require("which-key.config")
local M = {}
--- * `WhichKeyColorAzure` - azure.
--- * `WhichKeyColorBlue` - blue.
--- * `WhichKeyColorCyan` - cyan.
--- * `WhichKeyColorGreen` - green.
--- * `WhichKeyColorGrey` - grey.
--- * `WhichKeyColorOrange` - orange.
--- * `WhichKeyColorPurple` - purple.
--- * `WhichKeyColorRed` - red.
--- * `WhichKeyColorYellow` - yellow.
---@type wk.IconRule[]
M.rules = {
{ plugin = "fzf-lua", cat = "filetype", name = "fzf" },
{ plugin = "neo-tree.nvim", cat = "filetype", name = "neo-tree" },
{ plugin = "octo.nvim", cat = "filetype", name = "git" },
{ plugin = "yanky.nvim", icon = "󰅇", color = "yellow" },
{ plugin = "zen-mode.nvim", icon = "󱅻 ", color = "cyan" },
{ plugin = "telescope.nvim", pattern = "telescope", icon = "", color = "blue" },
{ plugin = "trouble.nvim", cat = "filetype", name = "trouble" },
{ plugin = "todo-comments.nvim", cat = "file", name = "TODO" },
{ plugin = "nvim-spectre", icon = "󰛔 ", color = "blue" },
{ plugin = "grug-far.nvim", pattern = "grug", icon = "󰛔 ", color = "blue" },
{ plugin = "noice.nvim", pattern = "noice", icon = "󰈸", color = "orange" },
{ plugin = "persistence.nvim", icon = "", color = "azure" },
{ plugin = "neotest", cat = "filetype", name = "neotest-summary" },
{ plugin = "lazy.nvim", cat = "filetype", name = "lazy" },
{ plugin = "CopilotChat.nvim", icon = "", color = "orange" },
{ pattern = "%f[%a]git", cat = "filetype", name = "git" },
{ pattern = "terminal", icon = "", color = "red" },
{ pattern = "find", icon = "", color = "green" },
{ pattern = "search", icon = "", color = "green" },
{ pattern = "test", cat = "filetype", name = "neotest-summary" },
{ pattern = "lazy", cat = "filetype", name = "lazy" },
{ pattern = "buffer", icon = "󰈔", color = "cyan" },
{ pattern = "file", icon = "󰈔", color = "cyan" },
{ pattern = "window", icon = " ", color = "blue" },
{ pattern = "diagnostic", icon = "󱖫 ", color = "green" },
{ pattern = "format", icon = "", color = "cyan" },
{ pattern = "debug", icon = "󰃤 ", color = "red" },
{ pattern = "code", icon = "", color = "orange" },
{ pattern = "notif", icon = "󰵅 ", color = "blue" },
{ pattern = "toggle", icon = "", color = "yellow" },
{ pattern = "session", icon = "", color = "azure" },
{ pattern = "exit", icon = "󰈆 ", color = "red" },
{ pattern = "quit", icon = "󰈆 ", color = "red" },
{ pattern = "tab", icon = "󰓩 ", color = "purple" },
{ pattern = "%f[%a]ai", icon = "", color = "green" },
{ pattern = "ui", icon = "󰙵 ", color = "cyan" },
}
---@type wk.IconProvider[]
M.providers = {
{
name = "mini.icons",
get = function(icon)
local Icons = require("mini.icons")
local ico, ico_hl, ico_def = Icons.get(icon.cat, icon.name) --[[@as string, string, boolean]]
if not ico_def then
return ico, ico_hl
end
end,
},
{
name = "nvim-web-devicons",
get = function(icon)
local Icons = require("nvim-web-devicons")
if icon.cat == "filetype" then
return Icons.get_icon_by_filetype(icon.name, { default = false })
elseif icon.cat == "file" then
return Icons.get_icon(icon.name, nil, { default = false }) --[[@as string, string]]
elseif icon.cat == "extension" then
return Icons.get_icon(nil, icon.name, { default = false }) --[[@as string, string]]
end
end,
},
}
---@return wk.IconProvider?
function M.get_provider()
for _, provider in ipairs(M.providers) do
if provider.available == nil then
provider.available = pcall(require, provider.name)
end
if provider.available then
return provider
end
end
end
function M.have()
return M.get_provider() ~= nil
end
---@param icon wk.Icon|string
---@return string?, string?
function M.get_icon(icon)
icon = type(icon) == "string" and { cat = "filetype", name = icon } or icon --[[@as wk.Icon]]
---@type string?, string?
local ret, hl
if icon.icon then
ret, hl = icon.icon, icon.hl
elseif icon.cat and icon.name then
local provider = M.get_provider()
if provider then
ret, hl = provider.get(icon)
end
end
if ret then
if icon.color then
hl = "WhichKeyIcon" .. icon.color:sub(1, 1):upper() .. icon.color:sub(2)
end
if not hl or Config.icons.colors == false or icon.color == false then
hl = "WhichKeyIcon"
end
return ret, hl
end
end
---@param rules wk.IconRule[]
---@param opts? {keymap?: wk.Keymap, desc?: string, ft?:string|string[]}|wk.Icon
---@param check_ft? boolean
---@return string?, string?
function M._get(rules, opts, check_ft)
opts = opts or {}
opts.ft = type(opts.ft) == "string" and { opts.ft } or opts.ft
---@type string?
local plugin
local fts = opts.ft or {} --[[@as string[] ]]
if opts.keymap and package.loaded.lazy then
local LazyUtil = require("lazy.core.util")
local Keys = require("lazy.core.handler").handlers.keys --[[@as LazyKeysHandler]]
local keys = Keys.parse(opts.keymap.lhs, opts.keymap.mode)
plugin = Keys.managed[keys.id]
if plugin then
fts[#fts + 1] = LazyUtil.normname(plugin)
end
end
-- plugin icons
if plugin then
for _, icon in ipairs(rules) do
if icon.plugin == plugin then
local ico, hl = M.get_icon(icon)
if ico then
return ico, hl
end
end
end
end
-- filetype icons
if check_ft then
if opts.keymap and opts.keymap.buffer and opts.keymap.buffer ~= 0 then
pcall(function()
fts[#fts + 1] = vim.bo[opts.keymap.buffer].filetype
end)
end
for _, ft in ipairs(fts) do
local icon, hl = M.get_icon({ cat = "filetype", name = ft })
if icon then
return icon, hl
end
end
end
-- pattern icons
if opts.desc then
for _, icon in ipairs(rules) do
if icon.pattern and opts.desc:lower():find(icon.pattern) then
local ico, hl = M.get_icon(icon)
if ico then
return ico, hl
end
end
end
end
end
---@param opts {keymap?: wk.Keymap, desc?: string, ft?:string|string[]}|wk.Icon|string
function M.get(opts)
if not Config.icons.mappings then
return
end
if type(opts) == "string" then
opts = { icon = opts }
end
if opts.icon or opts.cat then
return M.get_icon(opts)
end
if Config.icons.rules == false then
return
end
local icon, hl = M._get(Config.icons.rules, opts)
if icon then
return icon, hl
end
return M._get(M.rules, opts, true)
end
return M

View File

@ -0,0 +1,76 @@
local Mappings = require("which-key.mappings")
local M = {}
---@param spec wk.Spec
function M.migrate(spec)
spec = vim.deepcopy(spec)
local mappings = Mappings.parse(spec, { version = 1, notify = false })
---@type table<string, {m:wk.Mapping, mode:string[]}>
local mapping_modes = {}
for _, m in ipairs(mappings) do
m.preset = nil
m[1] = m.lhs:gsub("<lt>", "<")
m[2] = m.rhs
m.lhs = nil
m.rhs = nil
local mode = m.mode
m.mode = nil
if m.silent then
m.silent = nil
end
if m.group then
m.group = m.desc
m.desc = nil
end
local id = vim.inspect(m)
mapping_modes[id] = mapping_modes[id] or { m = m, mode = {} }
table.insert(mapping_modes[id].mode, mode)
end
mappings = vim.tbl_map(function(v)
local m = v.m
if not vim.deep_equal(v.mode, { "n" }) then
m.mode = v.mode
end
return m
end, vim.tbl_values(mapping_modes))
table.sort(mappings, function(a, b)
return a[1] < b[1]
end)
-- Group by modes
---@type table<string, wk.Mapping[]>
local modes = {}
for _, m in pairs(mappings) do
local mode = m.mode or {}
table.sort(mode)
local id = table.concat(mode)
modes[id] = modes[id] or {}
table.insert(modes[id], m)
end
local lines = {}
for mode, maps in pairs(modes) do
if #maps > 2 and mode ~= "" then
lines[#lines + 1] = " {"
lines[#lines + 1] = " mode = " .. vim.inspect(maps[1].mode) .. ","
for _, m in ipairs(maps) do
m.mode = nil
lines[#lines + 1] = " " .. vim.inspect(m):gsub("%s+", " ") .. ","
end
lines[#lines + 1] = " },"
else
for _, m in ipairs(maps) do
if m.mode and #m.mode == 1 then
m.mode = m.mode[1]
end
lines[#lines + 1] = " " .. vim.inspect(m):gsub("%s+", " ") .. ","
end
end
end
return "{\n" .. table.concat(lines, "\n") .. "\n}"
end
return M

View File

@ -0,0 +1,205 @@
local Util = require("which-key.util")
---@class wk.Node
---@field _children table<string, wk.Node>
local M = {}
---@param parent? wk.Node
---@param key? string
---@return wk.Node
function M.new(parent, key)
local self = setmetatable({}, M)
self.parent = parent
self.key = key or ""
self.path = {}
self.global = true
self._children = {}
self.keys = (parent and parent.keys or "") .. self.key
for _, p in ipairs(parent and parent.path or {}) do
table.insert(self.path, p)
end
if key then
table.insert(self.path, key)
end
return self
end
function M:is_local()
if self.path[1] == Util.norm("<localleader>") then
return true
end
if self.buffer and self.buffer > 0 then
return true
end
for _, child in pairs(self._children) do
if child:is_local() then
return true
end
end
return false
end
function M:__index(k)
if k == "mapping" or k == "keymap" then
return
end
local v = rawget(M, k)
if v ~= nil then
return v
end
for _, m in ipairs({ "mapping", "keymap" }) do
local mm = rawget(self, m)
if k == m then
return mm
end
if mm and mm[k] ~= nil then
return mm[k]
end
end
end
function M:__tostring()
local info = { "Node(" .. self.keys .. ")" }
if self:is_plugin() then
info[#info + 1] = "Plugin(" .. self.plugin .. ")"
end
if self:is_proxy() then
info[#info + 1] = "Proxy(" .. self.mapping.proxy .. ")"
end
return table.concat(info, " ")
end
---@param depth? number
function M:inspect(depth)
local indent = (" "):rep(depth or 0)
local ret = { indent .. tostring(self) }
for _, child in ipairs(self:children()) do
table.insert(ret, child:inspect((depth or 0) + 1))
end
return table.concat(ret, "\n")
end
function M:count()
return #self:children()
end
function M:is_group()
return self:can_expand() or self:count() > 0
end
function M:is_proxy()
return self.mapping and self.mapping.proxy
end
function M:is_plugin()
return self.plugin ~= nil
end
function M:can_expand()
return self.plugin or self:is_proxy() or (self.mapping and self.mapping.expand)
end
---@return wk.Node[]
function M:children()
return vim.tbl_values(self:expand())
end
---@return table<string, wk.Node>
function M:expand()
if not self:can_expand() then
return self._children
end
---@type table<string, wk.Node>
local ret = {}
-- plugin mappings
if self.plugin then
local plugin = require("which-key.plugins").plugins[self.plugin or ""]
assert(plugin, "plugin not found")
Util.debug(("Plugin(%q).expand"):format(self.plugin))
for i, item in ipairs(plugin.expand()) do
item.order = i
local child = M.new(self, item.key) --[[@as wk.Node.plugin.item]]
setmetatable(child, { __index = setmetatable(item, M) })
ret[item.key] = child
end
end
-- proxy mappings
local proxy = self.mapping.proxy
if proxy then
local keys = Util.keys(proxy)
local root = self:root()
local node = root:find(keys, { expand = true })
if node then
for k, v in pairs(node:expand()) do
ret[k] = v
end
end
end
-- expand mappings
local expand = self.mapping and self.mapping.expand
if expand then
local Tree = require("which-key.tree")
local tmp = Tree.new()
local mappings = require("which-key.mappings").parse(expand())
for _, mapping in ipairs(mappings) do
tmp:add(mapping, true)
end
for _, child in ipairs(tmp.root:children()) do
local action = child.mapping and child.mapping.rhs
if type(action) == "function" then
child.action = action
elseif type(action) == "string" then
Util.error("expand mappings only support functions as rhs:\n" .. vim.inspect(child.mapping))
end
ret[child.key] = child
end
end
-- custom mappings
for k, v in pairs(self._children) do
ret[k] = v
end
return ret
end
function M:root()
local node = self
while node.parent do
node = node.parent
end
return node
end
---@param path string[]|string
---@param opts? { create?: boolean, expand?: boolean }
---@return wk.Node?
function M:find(path, opts)
path = (type(path) == "string" and { path } or path) --[[@as string[] ]]
opts = opts or {}
local node = self
for _, key in ipairs(path) do
local child ---@type wk.Node?
if opts.expand then
child = node:expand()[key]
else
child = node._children[key]
end
if not child then
if not opts.create then
return
end
child = M.new(node, key)
node._children[key] = child
end
node = child
end
return node
end
return M

View File

@ -0,0 +1,215 @@
local M = {}
M.name = "presets"
M.operators = {
preset = true,
mode = { "n", "x" },
{ "!", desc = "Run program" },
{ "<", desc = "Indent left" },
{ ">", desc = "Indent right" },
{ "V", desc = "Visual Line" },
{ "c", desc = "Change" },
{ "d", desc = "Delete" },
{ "gU", desc = "Uppercase" },
{ "gu", desc = "Lowercase" },
{ "g~", desc = "Toggle case" },
{ "gw", desc = "Format" },
{ "r", desc = "Replace" },
{ "v", desc = "Visual" },
{ "y", desc = "Yank" },
{ "zf", desc = "Create fold" },
{ "~", desc = "Toggle case" },
}
M.motions = {
mode = { "o", "x", "n" },
preset = true,
{ "$", desc = "End of line" },
{ "%", desc = "Matching (){}[]" },
{ "0", desc = "Start of line" },
{ "F", desc = "Move to prev char" },
{ "G", desc = "Last line" },
{ "T", desc = "Move before prev char" },
{ "^", desc = "Start of line (non ws)" },
{ "b", desc = "Prev word" },
{ "e", desc = "Next end of word" },
{ "f", desc = "Move to next char" },
{ "ge", desc = "Prev end of word" },
{ "gg", desc = "First line" },
{ "h", desc = "Left" },
{ "j", desc = "Down" },
{ "k", desc = "Up" },
{ "l", desc = "Right" },
{ "t", desc = "Move before next char" },
{ "w", desc = "Next word" },
{ "{", desc = "Prev empty line" },
{ "}", desc = "Next empty line" },
{ ";", desc = "Next ftFT" },
{ ",", desc = "Prev ftFT" },
{ "/", desc = "Search forward" },
{ "?", desc = "Search backward" },
{ "B", desc = "Prev WORD" },
{ "E", desc = "Next end of WORD" },
{ "W", desc = "Next WORD" },
}
M.text_objects = {
mode = { "o", "x" },
preset = true,
{ "a", group = "around" },
{ 'a"', desc = '" string' },
{ "a'", desc = "' string" },
{ "a(", desc = "[(]) block" },
{ "a)", desc = "[(]) block" },
{ "a<", desc = "<> block" },
{ "a>", desc = "<> block" },
{ "aB", desc = "[{]} block" },
{ "aW", desc = "WORD with ws" },
{ "a[", desc = "[] block" },
{ "a]", desc = "[] block" },
{ "a`", desc = "` string" },
{ "ab", desc = "[(]) block" },
{ "ap", desc = "paragraph" },
{ "as", desc = "sentence" },
{ "at", desc = "tag block" },
{ "aw", desc = "word with ws" },
{ "a{", desc = "[{]} block" },
{ "a}", desc = "[{]} block" },
{ "i", group = "inside" },
{ 'i"', desc = 'inner " string' },
{ "i'", desc = "inner ' string" },
{ "i(", desc = "inner [(])" },
{ "i)", desc = "inner [(])" },
{ "i<", desc = "inner <>" },
{ "i>", desc = "inner <>" },
{ "iB", desc = "inner [{]}" },
{ "iW", desc = "inner WORD" },
{ "i[", desc = "inner []" },
{ "i]", desc = "inner []" },
{ "i`", desc = "inner ` string" },
{ "ib", desc = "inner [(])" },
{ "ip", desc = "inner paragraph" },
{ "is", desc = "inner sentence" },
{ "it", desc = "inner tag block" },
{ "iw", desc = "inner word" },
{ "i{", desc = "inner [{]}" },
{ "i}", desc = "inner [{]}" },
}
M.windows = {
preset = true,
mode = { "n", "x" },
{ "<c-w>", group = "window" },
{ "<c-w>+", desc = "Increase height" },
{ "<c-w>-", desc = "Decrease height" },
{ "<c-w><", desc = "Decrease width" },
{ "<c-w>=", desc = "Equally high and wide" },
{ "<c-w>>", desc = "Increase width" },
{ "<c-w>T", desc = "Break out into a new tab" },
{ "<c-w>_", desc = "Max out the height" },
{ "<c-w>h", desc = "Go to the left window" },
{ "<c-w>j", desc = "Go to the down window" },
{ "<c-w>k", desc = "Go to the up window" },
{ "<c-w>l", desc = "Go to the right window" },
{ "<c-w>o", desc = "Close all other windows" },
{ "<c-w>q", desc = "Quit a window" },
{ "<c-w>s", desc = "Split window" },
{ "<c-w>v", desc = "Split window vertically" },
{ "<c-w>w", desc = "Switch windows" },
{ "<c-w>x", desc = "Swap current with next" },
{ "<c-w>|", desc = "Max out the width" },
}
M.z = {
preset = true,
{ "z<CR>", desc = "Top this line" },
{ "z=", desc = "Spelling suggestions" },
{ "zA", desc = "Toggle all folds under cursor" },
{ "zC", desc = "Close all folds under cursor" },
{ "zD", desc = "Delete all folds under cursor" },
{ "zE", desc = "Delete all folds in file" },
{ "zH", desc = "Half screen to the left" },
{ "zL", desc = "Half screen to the right" },
{ "zM", desc = "Close all folds" },
{ "zO", desc = "Open all folds under cursor" },
{ "zR", desc = "Open all folds" },
{ "za", desc = "Toggle fold under cursor" },
{ "zb", desc = "Bottom this line" },
{ "zc", desc = "Close fold under cursor" },
{ "zd", desc = "Delete fold under cursor" },
{ "ze", desc = "Right this line" },
{ "zg", desc = "Add word to spell list" },
{ "zi", desc = "Toggle folding" },
{ "zm", desc = "Fold more" },
{ "zo", desc = "Open fold under cursor" },
{ "zr", desc = "Fold less" },
{ "zs", desc = "Left this line" },
{ "zt", desc = "Top this line" },
{ "zv", desc = "Show cursor line" },
{ "zw", desc = "Mark word as bad/misspelling" },
{ "zx", desc = "Update folds" },
{ "zz", desc = "Center this line" },
}
M.nav = {
preset = true,
{ "H", desc = "Home line of window (top)" },
{ "L", desc = "Last line of window" },
{ "M", desc = "Middle line of window" },
{ "[%", desc = "Previous unmatched group" },
{ "[(", desc = "Previous (" },
{ "[<", desc = "Previous <" },
{ "[M", desc = "Previous method end" },
{ "[m", desc = "Previous method start" },
{ "[s", desc = "Previous misspelled word" },
{ "[{", desc = "Previous {" },
{ "]%", desc = "Next unmatched group" },
{ "](", desc = "Next (" },
{ "]<", desc = "Next <" },
{ "]M", desc = "Next method end" },
{ "]m", desc = "Next method start" },
{ "]s", desc = "Next misspelled word" },
{ "]{", desc = "Next {" },
}
M.g = {
preset = true,
{ "g%", desc = "Cycle backwards through results" },
{ "gN", desc = "Search backwards and select" },
{ "gT", desc = "Go to previous tab page" },
{ "gf", desc = "Go to file under cursor" },
{ "gi", desc = "Go to last insert" },
{ "gn", desc = "Search forwards and select" },
{ "gt", desc = "Go to next tab page" },
{ "gv", desc = "Last visual selection" },
{ "gx", desc = "Open file with system app" },
}
function M.setup(opts)
local wk = require("which-key")
-- Operators
if opts.operators then
wk.add(M.operators)
end
-- Motions
if opts.motions then
wk.add(M.motions)
end
-- Text objects
if opts.text_objects then
wk.add(M.text_objects)
end
-- Misc
for _, preset in pairs({ "windows", "nav", "z", "g" }) do
if opts[preset] ~= false then
wk.add(M[preset])
end
end
end
return M

View File

@ -0,0 +1,38 @@
---@type table<string, wk.Opts>
return {
helix = {
win = {
width = { min = 30, max = 60 },
height = { min = 4, max = 0.75 },
padding = { 0, 1 },
col = -1,
row = -1,
border = "rounded",
title = true,
title_pos = "left",
},
layout = {
width = { min = 30 },
},
},
modern = {
win = {
width = 0.9,
height = { min = 4, max = 25 },
col = 0.5,
row = -1,
border = "rounded",
title = true,
title_pos = "center",
},
},
classic = {
win = {
width = math.huge,
height = { min = 4, max = 25 },
col = 0,
row = -1,
border = "none",
},
},
}

View File

@ -0,0 +1,386 @@
local Buf = require("which-key.buf")
local Config = require("which-key.config")
local Triggers = require("which-key.triggers")
local Util = require("which-key.util")
local uv = vim.uv or vim.loop
local M = {}
---@class wk.State
---@field mode wk.Mode
---@field node wk.Node
---@field filter wk.Filter
---@field started number
---@field show boolean
---@type wk.State?
M.state = nil
M.recursion = 0
M.recursion_timer = uv.new_timer()
---@return boolean safe, string? reason
function M.safe(mode_change)
local old, _new = unpack(vim.split(mode_change, ":", { plain = true }))
if old == "c" then
return false, "command-mode"
elseif vim.fn.reg_recording() ~= "" then
return false, "recording"
elseif vim.fn.reg_executing() ~= "" then
return false, "executing"
elseif mode_change:lower() == "v:v" then
return false, "visual-block"
end
local pending = vim.fn.getcharstr(1)
if pending ~= "" then
return false, "pending " .. ("%q"):format(vim.fn.strtrans(pending))
end
return true
end
function M.setup()
local group = vim.api.nvim_create_augroup("wk", { clear = true })
if Config.debug then
vim.on_key(function(_raw, key)
if key and #key > 0 then
key = vim.fn.keytrans(key)
if not key:find("ScrollWheel") and not key:find("Mouse") then
Util.debug("on_key", key)
end
end
end)
end
vim.api.nvim_create_autocmd({ "RecordingEnter", "RecordingLeave" }, {
group = group,
callback = function(ev)
Util.debug(ev.event)
if ev.event == "RecordingEnter" then
Buf.clear({ buf = ev.buf })
M.stop()
end
end,
})
local hide = uv.new_timer()
vim.api.nvim_create_autocmd({ "FocusLost", "FocusGained" }, {
group = group,
callback = function(ev)
if ev.event == "FocusGained" then
hide:stop()
elseif M.state then
hide:start(5000, 0, function()
vim.api.nvim_input("<esc>")
end)
end
end,
})
local function defer()
local mode = vim.api.nvim_get_mode().mode
local mode_keys = Util.keys(mode)
local ctx = {
operator = mode:find("o") and vim.v.operator or nil,
mode = mode_keys[1],
}
return Config.defer(ctx)
end
local cooldown = Util.cooldown()
-- this prevents restarting which-key in the same tick
vim.api.nvim_create_autocmd("ModeChanged", {
group = group,
callback = function(ev)
Util.trace("ModeChanged(" .. ev.match .. ")")
local mode = Buf.get()
if cooldown() then
Util.debug("cooldown")
Util.trace()
return
end
local safe, reason = M.safe(ev.match)
Util.debug(safe and "Safe(true)" or ("Unsafe(" .. reason .. ")"))
if not safe then
if mode then
Triggers.suspend(mode)
end
-- dont start when recording or when chars are pending
cooldown(true) -- cooldown till next tick
M.stop()
-- make sure the buffer mode exists
elseif mode and Util.xo() then
if Config.triggers.modes[mode.mode] and not M.state then
M.start({ defer = defer() })
end
elseif not ev.match:find("c") then
M.stop()
end
Util.trace()
end,
})
vim.api.nvim_create_autocmd({ "LspAttach", "LspDetach" }, {
group = group,
callback = function(ev)
Util.trace(ev.event .. "(" .. ev.buf .. ")")
Buf.clear({ buf = ev.buf })
Util.trace()
end,
})
vim.api.nvim_create_autocmd({ "BufReadPost", "BufNew" }, {
group = group,
callback = function(ev)
Util.trace(ev.event .. "(" .. ev.buf .. ")")
Buf.clear({ buf = ev.buf })
Util.trace()
end,
})
local current_buf = vim.api.nvim_get_current_buf()
vim.api.nvim_create_autocmd({ "BufEnter" }, {
group = group,
callback = function(ev)
current_buf = ev.buf ---@type number
Util.trace(ev.event .. "(" .. ev.buf .. ")")
Buf.get()
Util.trace()
end,
})
-- HACK: ModeChanged does not always trigger, so we need to manually
-- check for mode changes. This seems to be due to the usage of `:norm` in autocmds.
-- See https://github.com/folke/which-key.nvim/issues/787
local timer = uv.new_timer()
timer:start(0, 50, function()
local mode = Util.mapmode()
-- check if the mode exists for the current buffer
if Buf.bufs[current_buf] and Buf.bufs[current_buf].modes[mode] then
return
end
vim.schedule(Buf.get)
end)
end
function M.stop()
if M.state == nil then
return
end
Util.debug("state:stop")
M.state = nil
vim.schedule(function()
if not M.state then
require("which-key.view").hide()
end
end)
end
---@param state wk.State
---@param key? string
---@return wk.Node? node
function M.check(state, key)
local View = require("which-key.view")
local node = key == nil and state.node or (key and state.node:find(key, { expand = true }))
local delta = uv.hrtime() / 1e6 - state.started
local timedout = vim.o.timeout and delta > vim.o.timeoutlen
if node then
-- NOTE: a node can be both a keymap and a group
-- when it's both, we honor timeoutlen and nowait to decide what to do
local has_children = node:count() > 0
local is_nowait = node.keymap and (node.keymap.nowait == 1 or not timedout)
local is_action = node.action ~= nil
if has_children and not is_nowait and not is_action then
Util.debug("continue", node.keys, tostring(state.mode), node.plugin)
return node
end
elseif key == "<Esc>" then
if state.mode:xo() then
Util.exit() -- cancel and exit if in xo mode
end
return
elseif key == "<BS>" then
return state.node.parent or state.mode.tree.root
elseif View.valid() and key == Config.keys.scroll_down then
View.scroll(false)
return state.node
elseif View.valid() and key == Config.keys.scroll_up then
View.scroll(true)
return state.node
end
M.execute(state, key, node)
end
---@param state wk.State
---@param key? string
---@param node? wk.Node
---@return false|wk.Node?
function M.execute(state, key, node)
Triggers.suspend(state.mode)
if node and node.action then
return node.action()
end
local keystr = node and node.keys or (state.node.keys .. (key or ""))
if not state.mode:xo() then
if vim.v.count > 0 and state.mode.mode ~= "i" and state.mode.mode ~= "c" then
keystr = vim.v.count .. keystr
end
if vim.v.register ~= Util.reg() and state.mode.mode ~= "i" and state.mode.mode ~= "c" then
keystr = '"' .. vim.v.register .. keystr
end
end
Util.debug("feedkeys", tostring(state.mode), keystr)
local feed = vim.api.nvim_replace_termcodes(keystr, true, true, true)
vim.api.nvim_feedkeys(feed, "mit", false)
end
---@param state wk.State
---@return wk.Node? node, boolean? exit
function M.step(state)
vim.schedule(function()
vim.cmd.redraw()
if vim.api.nvim__redraw then
vim.api.nvim__redraw({ cursor = true })
end
end)
Util.debug("getchar")
local ok, char = pcall(vim.fn.getcharstr)
if not ok then
Util.debug("nok", char)
return nil, true
end
local key = vim.fn.keytrans(char)
Util.debug("got", key)
local node = M.check(state, key)
if node == state.node then
return M.step(state) -- same node, so try again (scrolling)
end
return node, key == "<Esc>"
end
---@param opts? wk.Filter
function M.start(opts)
Util.trace("State(start)", function()
local mode = opts and opts.mode or Util.mapmode()
local buf = opts and opts.buf or 0
local keys = opts and opts.keys or ""
return { "Mode(" .. mode .. ":" .. buf .. ") Node(" .. keys .. ")", opts }
end)
opts = opts or {}
opts.update = true
local mode = Buf.get(opts)
opts.update = nil
if not mode then
Util.debug("no mode")
Util.trace()
return false
end
local node = mode.tree:find(opts.keys or {}, { expand = true })
if not node then
Util.debug("no node")
Util.trace()
return false
end
local mapmode = mode.mode
M.recursion = M.recursion + 1
M.recursion_timer:start(500, 0, function()
M.recursion = 0
end)
if M.recursion > 50 then
Util.error({
"Recursion detected.",
"Are you manually loading which-key in a keymap?",
"Use `opts.triggers` instad.",
"Please check the docs.",
})
Util.debug("recursion detected. Aborting")
Util.trace()
return false
end
local View = require("which-key.view")
M.state = {
mode = mode,
node = node,
filter = opts,
started = uv.hrtime() / 1e6 - (opts.waited or 0),
show = opts.defer ~= true,
}
if not M.check(M.state) then
Util.debug("executed")
Util.trace()
return true
end
local exit = false
while M.state do
mode = Buf.get(opts)
if not mode or mode.mode ~= mapmode then
break
end
if M.state.show then
View.update(opts)
end
local child, _exit = M.step(M.state)
if child and M.state then
M.state.node = child
M.state.show = true
else
exit = _exit or false
break
end
end
if opts.loop and not exit then
-- NOTE: flush pending keys to prevent a trigger loop
vim.api.nvim_feedkeys("", "x", false)
vim.schedule(function()
M.start(opts)
end)
else
M.state = nil
View.hide()
end
Util.trace()
return true
end
function M.update()
if not M.state then
return
end
local mode = Buf.get()
if not mode or mode.mode ~= M.state.mode.mode then
return
end
local node = mode.tree:find(M.state.node.path)
if not node then
return
end
M.state.node = node
require("which-key.view").update({ schedule = false })
end
---@param opts {delay?:number, mode:string, keys:string, plugin?:string, waited?: number}
function M.delay(opts)
local delay = opts.delay or type(Config.delay) == "function" and Config.delay(opts) or Config.delay --[[@as number]]
if opts.waited then
delay = delay - opts.waited
end
return math.max(0, delay)
end
return M

View File

@ -0,0 +1,171 @@
local Util = require("which-key.util")
---@class wk.Trigger
---@field buf number
---@field mode string
---@field keys string
---@field plugin? string
local M = {}
M._triggers = {} ---@type table<string, wk.Trigger>
M.suspended = {} ---@type table<wk.Mode, boolean>
M.timer = (vim.uv or vim.loop).new_timer()
--- Checks if a mapping already exists that is not a which-key trigger.
---@param trigger wk.Trigger
function M.is_mapped(trigger)
---@type wk.Keymap?
local km
pcall(vim.api.nvim_buf_call, trigger.buf, function()
km = vim.fn.maparg(trigger.keys, trigger.mode, false, true) --[[@as wk.Keymap]]
end)
-- not mapped
if not km or vim.tbl_isempty(km) then
return false
end
-- ignore <Nop> mappings
if Util.is_nop(km.rhs) then
return false
end
-- ignore which-key triggers
if km.desc and km.desc:find("which-key-trigger", 1, true) then
return false
end
return true
end
---@param trigger wk.Trigger
function M.add(trigger)
if M.is_mapped(trigger) then
return
end
vim.keymap.set(trigger.mode, trigger.keys, function()
require("which-key.state").start({
keys = trigger.keys,
})
end, {
buffer = trigger.buf,
nowait = true,
desc = "which-key-trigger" .. (trigger.plugin and " " .. trigger.plugin or ""),
})
M._triggers[M.id(trigger)] = trigger
end
function M.is_active()
return vim.tbl_isempty(M._triggers)
end
---@param trigger wk.Trigger
function M.del(trigger)
M._triggers[M.id(trigger)] = nil
if not vim.api.nvim_buf_is_valid(trigger.buf) then
return
end
if M.is_mapped(trigger) then
return
end
pcall(vim.keymap.del, trigger.mode, trigger.keys, { buffer = trigger.buf })
end
---@param trigger wk.Trigger
function M.id(trigger)
return trigger.buf .. ":" .. trigger.mode .. ":" .. trigger.keys
end
---@param trigger wk.Trigger
function M.has(trigger)
return M._triggers[M.id(trigger)] ~= nil
end
---@param mode wk.Mode
---@param triggers? wk.Trigger[]
function M.update(mode, triggers)
M.cleanup()
if not mode.buf:valid() then
for _, trigger in pairs(M._triggers) do
if trigger.buf == mode.buf.buf then
M.del(trigger)
end
end
return
end
local adds = {} ---@type string[]
local dels = {} ---@type string[]
local keep = {} ---@type table<string, boolean>
for _, node in ipairs(triggers or mode.triggers) do
---@type wk.Trigger
local trigger = {
buf = mode.buf.buf,
mode = mode.mode,
keys = node.keys,
plugin = node.plugin,
}
local id = M.id(trigger)
keep[id] = true
if not M.has(trigger) then
adds[#adds + 1] = trigger.keys
M.add(trigger)
end
end
for id, trigger in pairs(M._triggers) do
if trigger.buf == mode.buf.buf and trigger.mode == mode.mode and not keep[id] then
M.del(trigger)
dels[#dels + 1] = trigger.keys
end
end
if #adds > 0 then
Util.debug("Trigger(add) " .. tostring(mode) .. " " .. table.concat(adds, " "))
end
if #dels > 0 then
Util.debug("Trigger(del) " .. tostring(mode) .. " " .. table.concat(dels, " "))
end
end
---@param mode wk.Mode
function M.attach(mode)
if M.suspended[mode] then
return
end
M.update(mode)
end
---@param mode wk.Mode
function M.detach(mode)
M.update(mode, {})
end
---@param mode? wk.Mode
function M.schedule(mode)
if mode then
M.suspended[mode] = true
end
M.timer:start(
0,
0,
vim.schedule_wrap(function()
for m, _ in pairs(M.suspended) do
M.suspended[m] = nil
M.attach(m)
end
end)
)
end
function M.cleanup()
for _, trigger in pairs(M._triggers) do
if not vim.api.nvim_buf_is_valid(trigger.buf) then
M.del(trigger)
end
end
end
---@param mode wk.Mode
function M.suspend(mode)
Util.debug("suspend", tostring(mode))
M.detach(mode)
M.suspended[mode] = true
M.schedule()
end
return M

View File

@ -0,0 +1,120 @@
local Util = require("which-key.util")
---@class wk.Win
---@field win? number
---@field buf? number
---@field opts wk.Win.opts
local M = {}
M.__index = M
---@class wk.Win.opts
local override = {
relative = "editor",
style = "minimal",
focusable = false,
noautocmd = true,
wo = {
scrolloff = 0,
foldenable = false,
winhighlight = "Normal:WhichKeyNormal,FloatBorder:WhichKeyBorder,FloatTitle:WhichKeyTitle",
winbar = "",
statusline = "",
wrap = false,
},
bo = {
buftype = "nofile",
bufhidden = "wipe",
filetype = "wk",
},
}
---@type wk.Win.opts
local defaults = { col = 0, row = math.huge, zindex = 1000 }
---@param opts? wk.Win.opts
function M.defaults(opts)
return vim.tbl_deep_extend("force", {}, defaults, opts or {}, override)
end
---@param opts? wk.Win.opts
function M.new(opts)
local self = setmetatable({}, M)
self.opts = M.defaults(opts)
return self
end
function M:valid()
return self.buf and vim.api.nvim_buf_is_valid(self.buf) and self.win and vim.api.nvim_win_is_valid(self.win) or false
end
function M:hide()
if not (self.buf or self.win) then
return
end
---@type number?, number?
local buf, win = self.buf, self.win
self.buf, self.win = nil, nil
local function try_close()
pcall(vim.api.nvim_win_close, win, true)
pcall(vim.api.nvim_buf_delete, buf, { force = true })
win = win and vim.api.nvim_win_is_valid(win) and win or nil
buf = buf and vim.api.nvim_buf_is_valid(buf) and buf or nil
if win or buf then
vim.schedule(try_close)
end
end
try_close()
end
---@param opts? wk.Win.opts
function M:show(opts)
if opts then
self.opts = vim.tbl_deep_extend("force", self.opts, opts)
end
local win_opts = vim.deepcopy(self.opts)
win_opts.wo = nil
win_opts.bo = nil
win_opts.padding = nil
win_opts.no_overlap = nil
if vim.fn.has("nvim-0.10") == 0 then
win_opts.footer = nil
end
if self:valid() then
win_opts.noautocmd = nil
return vim.api.nvim_win_set_config(self.win, win_opts)
end
local ei = vim.go.eventignore
vim.go.eventignore = "all"
self.buf = vim.api.nvim_create_buf(false, true)
Util.bo(self.buf, self.opts.bo or {})
self.win = vim.api.nvim_open_win(self.buf, false, win_opts)
Util.wo(self.win, self.opts.wo or {})
vim.go.eventignore = ei
end
---@param up boolean
function M:scroll(up)
if not self:valid() then
return
end
local height = vim.api.nvim_win_get_height(self.win)
local delta = math.ceil((up and -1 or 1) * height / 2)
local view = vim.api.nvim_win_call(self.win, vim.fn.winsaveview)
local top = view.topline ---@type number
top = top + delta
top = math.max(top, 1)
top = math.min(top, vim.api.nvim_buf_line_count(self.buf) - height + 1)
vim.api.nvim_win_call(self.win, function()
vim.fn.winrestview({ topline = top, lnum = top })
end)
end
return M

View File

@ -0,0 +1,11 @@
local timer = (vim.uv or vim.loop).new_timer()
timer:start(
500,
0,
vim.schedule_wrap(function()
local wk = require("which-key")
if not wk.did_setup then
wk.setup()
end
end)
)

View File

@ -0,0 +1,3 @@
#!/nix/store/4bj2kxdm1462fzcc2i2s4dn33g2angcc-bash-5.2p32/bin/bash
nvim -u tests/minit.lua -l lua/which-key/docs.lua

View File

@ -0,0 +1,3 @@
#!/nix/store/4bj2kxdm1462fzcc2i2s4dn33g2angcc-bash-5.2p32/bin/bash
nvim -l tests/minit.lua --minitest

View File

@ -0,0 +1,14 @@
local Buf = require("which-key.buf")
before_each(function()
require("helpers").reset()
end)
describe("triggers", function()
it("does not create hooks for default mappings", function()
vim.keymap.set("n", "aa", "<nop>")
Buf.get({ mode = "n" })
local m = vim.fn.maparg("a", "n", false, true)
assert.same(vim.empty_dict(), m)
end)
end)

View File

@ -0,0 +1,12 @@
local M = {}
---@param lines? string[]
function M.reset(lines)
vim.o.showmode = false
vim.api.nvim_feedkeys(vim.keycode("<Ignore><C-\\><C-n><esc>"), "nx", false)
vim.cmd("enew")
vim.cmd("normal! <c-w>o")
vim.api.nvim_buf_set_lines(0, 0, -1, false, lines or {})
end
return M

View File

@ -0,0 +1,38 @@
local layout = require("which-key.layout")
describe("dim", function()
local tests = {
{ 100, { 200 }, 100 },
{ 0.2, { 100 }, 20 },
{ -0.2, { 100 }, 80 },
{ -20, { 100 }, 80 },
{ 1, { 100 }, 1 },
{ 100, { 200, { min = 50 } }, 100 },
{ 100, { 200, { max = 150 } }, 100 },
{ 100, { 200, { max = 150, min = 50 } }, 100 },
{ 100, { 200, { max = 150, min = 150 } }, 150 },
{ 0.2, { 100, { max = 150, min = 20 } }, 20 },
{ 0.2, { 100, { max = 50, min = 20 } }, 20 },
{ math.huge, { 200 }, 200 },
{ -0.5, { 200 }, 100 },
{ 0.5, { 200 }, 100 },
{ 0.5, { 200, { min = 150 } }, 150 },
{ -0.5, { 200, { max = 50 } }, 50 },
{ 300, { 200, { max = 250 } }, 200 },
{ 300, { 200, { min = 250 } }, 200 },
{ -100, { 100, { max = 90, min = 20 } }, 20 },
{ -200, { 100, { max = -50, min = -50 } }, 0 },
{ 0.2, { 100, { min = 0.5 } }, 50 },
{ -200, { 100 }, 0 },
{ -1, { 100 }, 99 },
{ -0.1, { 100 }, 90 },
{ 0.1, { 100 }, 10 },
{ 14, { 212, 0.9 }, 191 },
}
for _, test in ipairs(tests) do
it("size=" .. test[1] .. ", parent=" .. test[2][1] .. ", result = " .. test[3], function()
assert.are.equal(test[3], layout.dim(test[1], unpack(test[2])))
end)
end
end)

View File

@ -0,0 +1,101 @@
local Mappings = require("which-key.mappings")
before_each(function()
Mappings.notifs = {}
end)
describe("specs v1", function()
local tests = {
{
spec = {
["<leader>"] = {
name = "leader",
["a"] = { "a" },
["b"] = { "b" },
["c"] = { "c" },
},
},
mappings = {
{ lhs = "<leader>", group = true, desc = "leader", mode = "n" },
{ lhs = "<leader>a", desc = "a", mode = "n" },
{ lhs = "<leader>b", desc = "b", mode = "n" },
{ lhs = "<leader>c", desc = "c", mode = "n" },
},
},
{
spec = {
mode = "v",
["<leader>"] = {
name = "leader",
["a"] = { "a" },
["b"] = { "b" },
["c"] = { "c" },
},
},
mappings = {
{ lhs = "<leader>", group = true, desc = "leader", mode = "v" },
{ lhs = "<leader>a", desc = "a", mode = "v" },
{ lhs = "<leader>b", desc = "b", mode = "v" },
{ lhs = "<leader>c", desc = "c", mode = "v" },
},
},
{
spec = { desc = "foo", noremap = true },
mappings = {},
},
{
spec = { a = { desc = "which_key_ignore" } },
mappings = {
{ lhs = "a", hidden = true, mode = "n" },
},
},
{
spec = { a = { "foo", cond = false } },
mappings = {},
},
{
spec = { a = { "foo", cond = true } },
mappings = {
{ desc = "foo", lhs = "a", mode = "n" },
},
},
{
spec = {
a = { "a", cmd = "aa" },
b = { "b", callback = "bb" },
c = { "cc", "c" },
d = { "dd", desc = "d" },
},
mappings = {
{ lhs = "a", desc = "a", rhs = "aa", mode = "n", silent = true },
{ lhs = "b", desc = "b", rhs = "bb", mode = "n", silent = true },
{ lhs = "c", desc = "c", rhs = "cc", mode = "n", silent = true },
{ lhs = "d", desc = "dd", mode = "n" },
},
},
{
spec = {
a = { "a1" },
b = { "b1", "b2" },
c = { "c1", desc = "c2" },
},
mappings = {
{ lhs = "a", desc = "a1", mode = "n" },
{ lhs = "b", desc = "b2", rhs = "b1", mode = "n", silent = true },
{ lhs = "c", desc = "c1", mode = "n" },
},
},
}
-- Function to run the tests
for t, test in ipairs(tests) do
it(tostring(t), function()
local result = Mappings.parse(test.spec, { version = 1 })
assert.same(test.mappings, result)
local errors = vim.tbl_filter(function(n)
return n.level >= vim.log.levels.ERROR
end, Mappings.notifs)
assert.same({}, errors)
end)
end
end)

View File

@ -0,0 +1,17 @@
#!/usr/bin/env -S nvim -l
vim.env.LAZY_STDPATH = ".tests"
vim.env.LAZY_PATH = vim.fs.normalize("~/projects/lazy.nvim")
load(vim.fn.system("curl -s https://raw.githubusercontent.com/folke/lazy.nvim/main/bootstrap.lua"))()
-- Setup lazy.nvim
require("lazy.minit").setup({
spec = {
{
dir = vim.uv.cwd(),
opts = {
notify = false,
},
},
},
})

View File

@ -0,0 +1,125 @@
---@module 'luassert'
local Util = require("which-key.util")
describe("parse keys", function()
local tests = {
[" <c-a><esc>Ä<lt>🔥foo"] = {
"<Space>",
"<C-A>",
"<Esc>",
"Ä",
"<",
"🔥",
"f",
"o",
"o",
},
["\1<esc>Ä<lt>🔥foo"] = {
"<C-A>",
"<Esc>",
"Ä",
"<",
"🔥",
"f",
"o",
"o",
},
["<esc>"] = { "<Esc>" },
["foo<baz>"] = { "f", "o", "o", "<", "b", "a", "z", ">" },
["foo<bar>"] = { "f", "o", "o", "|" },
["foo<a-2>"] = { "f", "o", "o", "<M-2>" },
["foo<A-2>"] = { "f", "o", "o", "<M-2>" },
["foo<m-2>"] = { "f", "o", "o", "<M-2>" },
["foo<M-2>"] = { "f", "o", "o", "<M-2>" },
["foo<"] = { "f", "o", "o", "<" },
["foo<bar"] = { "f", "o", "o", "<", "b", "a", "r" },
["foo>"] = { "f", "o", "o", ">" },
-- test with japanese chars
["fooあ"] = { "f", "o", "o", "" },
["fooあ<lt>"] = { "f", "o", "o", "", "<" },
["fooあ<lt>bar"] = { "f", "o", "o", "", "<", "b", "a", "r" },
["fooあ<lt>bar<lt>"] = { "f", "o", "o", "", "<", "b", "a", "r", "<" },
["fooあ<lt>bar<lt>baz"] = { "f", "o", "o", "", "<", "b", "a", "r", "<", "b", "a", "z" },
["fooあ<lt>bar<lt>baz<lt>"] = { "f", "o", "o", "", "<", "b", "a", "r", "<", "b", "a", "z", "<" },
["fooあ<lt>bar<lt>baz<lt>qux"] = {
"f",
"o",
"o",
"",
"<",
"b",
"a",
"r",
"<",
"b",
"a",
"z",
"<",
"q",
"u",
"x",
},
["fooあ<lt>bar<lt>baz<lt>qux<lt>"] = {
"f",
"o",
"o",
"",
"<",
"b",
"a",
"r",
"<",
"b",
"a",
"z",
"<",
"q",
"u",
"x",
"<",
},
}
for input, output in pairs(tests) do
it(("should parse %q"):format(input), function()
local keys = Util.keys(input)
assert.same(output, keys)
end)
end
end)
describe("modes", function()
before_each(function()
require("helpers").reset()
end)
local tests = {
["gg"] = "n",
["vl"] = "x",
["<c-v>j"] = "x",
["gh"] = "s",
["aa"] = "i",
["ciw"] = "o",
["c"] = "n",
["<cmd>terminal exit<cr>"] = "n",
}
local inputs = vim.tbl_keys(tests)
table.sort(inputs)
for _, input in ipairs(inputs) do
local output = tests[input]
it(("should return %q for %q"):format(output, input), function()
local mode = "n"
assert.same(mode, Util.mapmode())
vim.api.nvim_create_autocmd("ModeChanged", {
once = true,
callback = function()
mode = Util.mapmode()
end,
})
vim.api.nvim_feedkeys(vim.keycode(input), "nitx", false)
assert.same(output, mode)
end)
end
end)