Refresh generated neovim config
This commit is contained in:
@ -1,19 +1,56 @@
|
||||
local M = {}
|
||||
|
||||
local links = {
|
||||
[""] = "Function",
|
||||
Separator = "Comment",
|
||||
Group = "Keyword",
|
||||
Desc = "Identifier",
|
||||
Float = "NormalFloat",
|
||||
Border = "FloatBorder",
|
||||
Value = "Comment",
|
||||
M.colors = {
|
||||
[""] = "Function", -- the key
|
||||
Separator = "Comment", -- the separator between the key and its description
|
||||
Group = "Keyword", -- group name
|
||||
Desc = "Identifier", -- description
|
||||
Normal = "NormalFloat", -- Normal in th which-key window
|
||||
Title = "FloatTitle", -- Title of the which-key window
|
||||
Border = "FloatBorder", -- Border of the which-key window
|
||||
Value = "Comment", -- values by plugins (like marks, registers, etc)
|
||||
Icon = "@markup.link", -- icons
|
||||
IconAzure = "Function",
|
||||
IconBlue = "DiagnosticInfo",
|
||||
IconCyan = "DiagnosticHint",
|
||||
IconGreen = "DiagnosticOk",
|
||||
IconGrey = "Normal",
|
||||
IconOrange = "DiagnosticWarn",
|
||||
IconPurple = "Constant",
|
||||
IconRed = "DiagnosticError",
|
||||
IconYellow = "DiagnosticWarn",
|
||||
}
|
||||
|
||||
function M.setup()
|
||||
for k, v in pairs(links) do
|
||||
for k, v in pairs(M.colors) do
|
||||
vim.api.nvim_set_hl(0, "WhichKey" .. k, { link = v, default = true })
|
||||
end
|
||||
M.fix_colors()
|
||||
vim.api.nvim_create_autocmd("ColorScheme", {
|
||||
group = vim.api.nvim_create_augroup("wk-colors", { clear = true }),
|
||||
callback = M.fix_colors,
|
||||
})
|
||||
end
|
||||
|
||||
function M.fix_colors()
|
||||
for k in pairs(M.colors) do
|
||||
if k:find("^Icon") then
|
||||
local color = k:gsub("^Icon", "")
|
||||
local wk_hl_group = "WhichKeyIcon" .. color
|
||||
local mini_hl_group = "MiniIcons" .. color
|
||||
local wk_hl = vim.api.nvim_get_hl(0, {
|
||||
name = wk_hl_group,
|
||||
link = true,
|
||||
})
|
||||
local mini_hl = vim.api.nvim_get_hl(0, {
|
||||
name = mini_hl_group,
|
||||
link = true,
|
||||
})
|
||||
if wk_hl.default and not vim.tbl_isempty(mini_hl) then
|
||||
vim.api.nvim_set_hl(0, wk_hl_group, { link = mini_hl_group })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@ -1,9 +1,42 @@
|
||||
---@class wk.Config: wk.Opts
|
||||
---@field triggers {mappings: wk.Mapping[], modes: table<string,boolean>}
|
||||
local M = {}
|
||||
|
||||
M.namespace = vim.api.nvim_create_namespace("WhichKey")
|
||||
M.version = "3.13.2" -- x-release-please-version
|
||||
|
||||
---@class Options
|
||||
---@class wk.Opts
|
||||
local defaults = {
|
||||
---@type false | "classic" | "modern" | "helix"
|
||||
preset = "classic",
|
||||
-- Delay before showing the popup. Can be a number or a function that returns a number.
|
||||
---@type number | fun(ctx: { keys: string, mode: string, plugin?: string }):number
|
||||
delay = function(ctx)
|
||||
return ctx.plugin and 0 or 200
|
||||
end,
|
||||
---@param mapping wk.Mapping
|
||||
filter = function(mapping)
|
||||
-- example to exclude mappings without a description
|
||||
-- return mapping.desc and mapping.desc ~= ""
|
||||
return true
|
||||
end,
|
||||
--- You can add any mappings here, or use `require('which-key').add()` later
|
||||
---@type wk.Spec
|
||||
spec = {},
|
||||
-- show a warning when issues were detected with your mappings
|
||||
notify = true,
|
||||
-- Which-key automatically sets up triggers for your mappings.
|
||||
-- But you can disable this and setup the triggers manually.
|
||||
-- Check the docs for more info.
|
||||
---@type wk.Spec
|
||||
triggers = {
|
||||
{ "<auto>", mode = "nxsot" },
|
||||
},
|
||||
-- Start hidden and wait for a key to be pressed before showing the popup
|
||||
-- Only used by enabled xo mapping modes.
|
||||
---@param ctx { mode: string, operator: string }
|
||||
defer = function(ctx)
|
||||
return ctx.mode == "V" or ctx.mode == "<C-V>"
|
||||
end,
|
||||
plugins = {
|
||||
marks = true, -- shows a list of your marks on ' and `
|
||||
registers = true, -- shows your registers on " in NORMAL or <C-r> in INSERT mode
|
||||
@ -23,86 +56,288 @@ local defaults = {
|
||||
g = true, -- bindings for prefixed with g
|
||||
},
|
||||
},
|
||||
-- add operators that will trigger motion and text object completion
|
||||
-- to enable all native operators, set the preset / operators plugin above
|
||||
operators = { gc = "Comments" },
|
||||
key_labels = {
|
||||
-- override the label used to display some keys. It doesn't effect WK in any other way.
|
||||
-- For example:
|
||||
-- ["<space>"] = "SPC",
|
||||
-- ["<cr>"] = "RET",
|
||||
-- ["<tab>"] = "TAB",
|
||||
---@type wk.Win.opts
|
||||
win = {
|
||||
-- don't allow the popup to overlap with the cursor
|
||||
no_overlap = true,
|
||||
-- width = 1,
|
||||
-- height = { min = 4, max = 25 },
|
||||
-- col = 0,
|
||||
-- row = math.huge,
|
||||
-- border = "none",
|
||||
padding = { 1, 2 }, -- extra window padding [top/bottom, right/left]
|
||||
title = true,
|
||||
title_pos = "center",
|
||||
zindex = 1000,
|
||||
-- Additional vim.wo and vim.bo options
|
||||
bo = {},
|
||||
wo = {
|
||||
-- winblend = 10, -- value between 0-100 0 for fully opaque and 100 for fully transparent
|
||||
},
|
||||
},
|
||||
motions = {
|
||||
count = true,
|
||||
layout = {
|
||||
width = { min = 20 }, -- min and max width of the columns
|
||||
spacing = 3, -- spacing between columns
|
||||
},
|
||||
keys = {
|
||||
scroll_down = "<c-d>", -- binding to scroll down inside the popup
|
||||
scroll_up = "<c-u>", -- binding to scroll up inside the popup
|
||||
},
|
||||
---@type (string|wk.Sorter)[]
|
||||
--- Mappings are sorted using configured sorters and natural sort of the keys
|
||||
--- Available sorters:
|
||||
--- * local: buffer-local mappings first
|
||||
--- * order: order of the items (Used by plugins like marks / registers)
|
||||
--- * group: groups last
|
||||
--- * alphanum: alpha-numerical first
|
||||
--- * mod: special modifier keys last
|
||||
--- * manual: the order the mappings were added
|
||||
--- * case: lower-case first
|
||||
sort = { "local", "order", "group", "alphanum", "mod" },
|
||||
---@type number|fun(node: wk.Node):boolean?
|
||||
expand = 0, -- expand groups when <= n mappings
|
||||
-- expand = function(node)
|
||||
-- return not node.desc -- expand all nodes without a description
|
||||
-- end,
|
||||
-- Functions/Lua Patterns for formatting the labels
|
||||
---@type table<string, ({[1]:string, [2]:string}|fun(str:string):string)[]>
|
||||
replace = {
|
||||
key = {
|
||||
function(key)
|
||||
return require("which-key.view").format(key)
|
||||
end,
|
||||
-- { "<Space>", "SPC" },
|
||||
},
|
||||
desc = {
|
||||
{ "<Plug>%(?(.*)%)?", "%1" },
|
||||
{ "^%+", "" },
|
||||
{ "<[cC]md>", "" },
|
||||
{ "<[cC][rR]>", "" },
|
||||
{ "<[sS]ilent>", "" },
|
||||
{ "^lua%s+", "" },
|
||||
{ "^call%s+", "" },
|
||||
{ "^:%s*", "" },
|
||||
},
|
||||
},
|
||||
icons = {
|
||||
breadcrumb = "»", -- symbol used in the command line area that shows your active key combo
|
||||
separator = "➜", -- symbol used between a key and it's label
|
||||
group = "+", -- symbol prepended to a group
|
||||
ellipsis = "…",
|
||||
-- set to false to disable all mapping icons,
|
||||
-- both those explicitely added in a mapping
|
||||
-- and those from rules
|
||||
mappings = true,
|
||||
--- See `lua/which-key/icons.lua` for more details
|
||||
--- Set to `false` to disable keymap icons from rules
|
||||
---@type wk.IconRule[]|false
|
||||
rules = {},
|
||||
-- use the highlights from mini.icons
|
||||
-- When `false`, it will use `WhichKeyIcon` instead
|
||||
colors = true,
|
||||
-- used by key format
|
||||
keys = {
|
||||
Up = " ",
|
||||
Down = " ",
|
||||
Left = " ",
|
||||
Right = " ",
|
||||
C = " ",
|
||||
M = " ",
|
||||
D = " ",
|
||||
S = " ",
|
||||
CR = " ",
|
||||
Esc = " ",
|
||||
ScrollWheelDown = " ",
|
||||
ScrollWheelUp = " ",
|
||||
NL = " ",
|
||||
BS = "",
|
||||
Space = " ",
|
||||
Tab = " ",
|
||||
F1 = "",
|
||||
F2 = "",
|
||||
F3 = "",
|
||||
F4 = "",
|
||||
F5 = "",
|
||||
F6 = "",
|
||||
F7 = "",
|
||||
F8 = "",
|
||||
F9 = "",
|
||||
F10 = "",
|
||||
F11 = "",
|
||||
F12 = "",
|
||||
},
|
||||
},
|
||||
popup_mappings = {
|
||||
scroll_down = "<c-d>", -- binding to scroll down inside the popup
|
||||
scroll_up = "<c-u>", -- binding to scroll up inside the popup
|
||||
},
|
||||
window = {
|
||||
border = "none", -- none, single, double, shadow
|
||||
position = "bottom", -- bottom, top
|
||||
margin = { 1, 0, 1, 0 }, -- extra window margin [top, right, bottom, left]. When between 0 and 1, will be treated as a percentage of the screen size.
|
||||
padding = { 1, 2, 1, 2 }, -- extra window padding [top, right, bottom, left]
|
||||
winblend = 0, -- value between 0-100 0 for fully opaque and 100 for fully transparent
|
||||
zindex = 1000, -- positive value to position WhichKey above other floating windows.
|
||||
},
|
||||
layout = {
|
||||
height = { min = 4, max = 25 }, -- min and max height of the columns
|
||||
width = { min = 20, max = 50 }, -- min and max width of the columns
|
||||
spacing = 3, -- spacing between columns
|
||||
align = "left", -- align columns left, center or right
|
||||
},
|
||||
ignore_missing = false, -- enable this to hide mappings for which you didn't specify a label
|
||||
hidden = { "<silent>", "<cmd>", "<Cmd>", "<CR>", "^:", "^ ", "^call ", "^lua " }, -- hide mapping boilerplate
|
||||
show_help = true, -- show a help message in the command line for using WhichKey
|
||||
show_keys = true, -- show the currently pressed key and its label as a message in the command line
|
||||
triggers = "auto", -- automatically setup triggers
|
||||
-- triggers = {"<leader>"} -- or specify a list manually
|
||||
-- list of triggers, where WhichKey should not wait for timeoutlen and show immediately
|
||||
triggers_nowait = {
|
||||
-- marks
|
||||
"`",
|
||||
"'",
|
||||
"g`",
|
||||
"g'",
|
||||
-- registers
|
||||
'"',
|
||||
"<c-r>",
|
||||
-- spelling
|
||||
"z=",
|
||||
},
|
||||
triggers_blacklist = {
|
||||
-- list of mode / prefixes that should never be hooked by WhichKey
|
||||
-- this is mostly relevant for keymaps that start with a native binding
|
||||
i = { "j", "k" },
|
||||
v = { "j", "k" },
|
||||
},
|
||||
-- disable the WhichKey popup for certain buf types and file types.
|
||||
-- Disabled by default for Telescope
|
||||
-- disable WhichKey for certain buf types and file types.
|
||||
disable = {
|
||||
buftypes = {},
|
||||
filetypes = {},
|
||||
ft = {},
|
||||
bt = {},
|
||||
},
|
||||
debug = false, -- enable wk.log in the current directory
|
||||
}
|
||||
|
||||
---@type Options
|
||||
M.options = {}
|
||||
M.loaded = false
|
||||
|
||||
---@param options? Options
|
||||
function M.setup(options)
|
||||
if vim.fn.has("nvim-0.9") == 0 then
|
||||
return vim.notify("WhichKey.nvim requires Neovim 0.9 or higher", vim.log.levels.ERROR)
|
||||
---@type wk.Keymap[]
|
||||
M.mappings = {}
|
||||
|
||||
---@type wk.Opts
|
||||
M.options = nil
|
||||
|
||||
---@type {opt:string, msg:string}[]
|
||||
M.issues = {}
|
||||
|
||||
function M.validate()
|
||||
local deprecated = {
|
||||
["operators"] = "see `opts.defer`",
|
||||
["key_labels"] = "see `opts.replace`",
|
||||
"motions",
|
||||
["popup_mappings"] = "see `opts.keys`",
|
||||
["window"] = "see `opts.win`",
|
||||
["ignore_missing"] = "see `opts.filter`",
|
||||
"hidden",
|
||||
["triggers_nowait"] = "see `opts.delay`",
|
||||
["triggers_blacklist"] = "see `opts.triggers`",
|
||||
["disable.trigger"] = "see `opts.triggers`",
|
||||
["modes"] = "see `opts.triggers`",
|
||||
}
|
||||
for k, v in pairs(deprecated) do
|
||||
local opt = type(k) == "number" and v or k
|
||||
local msg = "option is deprecated." .. (type(k) == "number" and "" or " " .. v)
|
||||
local parts = vim.split(opt, ".", { plain = true })
|
||||
if vim.tbl_get(M.options, unpack(parts)) ~= nil then
|
||||
table.insert(M.issues, { opt = opt, msg = msg })
|
||||
end
|
||||
end
|
||||
if type(M.options.triggers) ~= "table" then
|
||||
table.insert(M.issues, { opt = "triggers", msg = "triggers must be a table" })
|
||||
end
|
||||
M.options = vim.tbl_deep_extend("force", {}, defaults, options or {})
|
||||
end
|
||||
|
||||
M.setup()
|
||||
---@param opts? wk.Opts
|
||||
function M.setup(opts)
|
||||
if vim.fn.has("nvim-0.9.4") == 0 then
|
||||
return vim.notify("which-key.nvim requires Neovim >= 0.9.4", vim.log.levels.ERROR)
|
||||
end
|
||||
M.options = vim.tbl_deep_extend("force", {}, defaults, opts or {})
|
||||
|
||||
return M
|
||||
local function load()
|
||||
if M.loaded then
|
||||
return
|
||||
end
|
||||
local Util = require("which-key.util")
|
||||
|
||||
if M.options.preset then
|
||||
local Presets = require("which-key.presets")
|
||||
M.options = vim.tbl_deep_extend("force", {}, defaults, Presets[M.options.preset] or {}, opts or {})
|
||||
end
|
||||
|
||||
M.validate()
|
||||
if #M.issues > 0 then
|
||||
Util.warn({
|
||||
"There are issues with your config.",
|
||||
"Use `:checkhealth which-key` to find out more.",
|
||||
}, { once = true })
|
||||
end
|
||||
|
||||
for k, v in pairs(M.options.keys) do
|
||||
M.options.keys[k] = Util.norm(v)
|
||||
end
|
||||
|
||||
if M.options.debug then
|
||||
Util.debug("\n\nDebug Started for v" .. M.version)
|
||||
if package.loaded.lazy then
|
||||
local Git = require("lazy.manage.git")
|
||||
local plugin = require("lazy.core.config").plugins["which-key.nvim"]
|
||||
Util.debug(vim.inspect(Git.info(plugin.dir)))
|
||||
end
|
||||
end
|
||||
|
||||
local wk = require("which-key")
|
||||
|
||||
-- replace by the real add function
|
||||
wk.add = M.add
|
||||
|
||||
if type(M.options.triggers) ~= "table" then
|
||||
---@diagnostic disable-next-line: inject-field
|
||||
M.options.triggers = defaults.triggers
|
||||
end
|
||||
|
||||
M.triggers = {
|
||||
mappings = require("which-key.mappings").parse(M.options.triggers),
|
||||
modes = {},
|
||||
}
|
||||
---@param m wk.Mapping
|
||||
M.triggers.mappings = vim.tbl_filter(function(m)
|
||||
if m.lhs == "<auto>" then
|
||||
M.triggers.modes[m.mode] = true
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end, M.triggers.mappings)
|
||||
|
||||
-- load presets first so that they can be overriden by the user
|
||||
require("which-key.plugins").setup()
|
||||
|
||||
-- process mappings queue
|
||||
for _, todo in ipairs(wk._queue) do
|
||||
M.add(todo.spec, todo.opts)
|
||||
end
|
||||
wk._queue = {}
|
||||
|
||||
-- finally, add the mapppings from the config
|
||||
M.add(M.options.spec)
|
||||
|
||||
-- setup colors and start which-key
|
||||
require("which-key.colors").setup()
|
||||
require("which-key.state").setup()
|
||||
|
||||
M.loaded = true
|
||||
end
|
||||
load = vim.schedule_wrap(load)
|
||||
|
||||
if vim.v.vim_did_enter == 1 then
|
||||
load()
|
||||
else
|
||||
vim.api.nvim_create_autocmd("VimEnter", { once = true, callback = load })
|
||||
end
|
||||
|
||||
vim.api.nvim_create_user_command("WhichKey", function(cmd)
|
||||
local mode, keys = cmd.args:match("^([nixsotc]?)%s*(.*)$")
|
||||
if not mode then
|
||||
return require("which-key.util").error("Usage: WhichKey [mode] [keys]")
|
||||
end
|
||||
if mode == "" then
|
||||
mode = "n"
|
||||
end
|
||||
require("which-key").show({ mode = mode, keys = keys })
|
||||
end, {
|
||||
nargs = "*",
|
||||
})
|
||||
end
|
||||
|
||||
---@param opts? wk.Parse
|
||||
---@param mappings wk.Spec
|
||||
function M.add(mappings, opts)
|
||||
opts = opts or {}
|
||||
opts.create = opts.create ~= false
|
||||
local Mappings = require("which-key.mappings")
|
||||
for _, km in ipairs(Mappings.parse(mappings, opts)) do
|
||||
table.insert(M.mappings, km)
|
||||
km.idx = #M.mappings
|
||||
end
|
||||
if M.loaded then
|
||||
require("which-key.buf").clear()
|
||||
end
|
||||
end
|
||||
|
||||
return setmetatable(M, {
|
||||
__index = function(_, k)
|
||||
if rawget(M, "options") == nil then
|
||||
M.setup()
|
||||
end
|
||||
local opts = rawget(M, "options")
|
||||
return k == "options" and opts or opts[k]
|
||||
end,
|
||||
})
|
||||
|
||||
@ -1,4 +1,10 @@
|
||||
local Keys = require("which-key.keys")
|
||||
local Buf = require("which-key.buf")
|
||||
local Config = require("which-key.config")
|
||||
local Icons = require("which-key.icons")
|
||||
local Mappings = require("which-key.mappings")
|
||||
local Migrate = require("which-key.migrate")
|
||||
local Tree = require("which-key.tree")
|
||||
local Util = require("which-key.util")
|
||||
|
||||
local M = {}
|
||||
|
||||
@ -8,49 +14,169 @@ local warn = vim.health.warn or vim.health.report_warn
|
||||
local error = vim.health.error or vim.health.report_error
|
||||
local info = vim.health.info or vim.health.report_info
|
||||
|
||||
function M.check()
|
||||
start("WhichKey: checking conflicting keymaps")
|
||||
local conflicts = 0
|
||||
for _, tree in pairs(Keys.mappings) do
|
||||
Keys.update_keymaps(tree.mode, tree.buf)
|
||||
tree.tree:walk(
|
||||
---@param node Node
|
||||
function(node)
|
||||
local count = 0
|
||||
for _ in pairs(node.children) do
|
||||
count = count + 1
|
||||
end
|
||||
-- TODO: Add more checks
|
||||
-- * duplicate desc
|
||||
-- * mapping.desc ~= keymap.desc
|
||||
-- * check for old-style mappings
|
||||
|
||||
local auto_prefix = not node.mapping or (node.mapping.group == true and not node.mapping.cmd)
|
||||
if node.prefix_i ~= "" and count > 0 and not auto_prefix then
|
||||
conflicts = conflicts + 1
|
||||
local msg = ("conflicting keymap exists for mode **%q**, lhs: **%q**"):format(tree.mode, node.mapping.prefix)
|
||||
warn(msg)
|
||||
local cmd = node.mapping.cmd or " "
|
||||
info(("rhs: `%s`"):format(cmd))
|
||||
end
|
||||
end
|
||||
function M.check()
|
||||
ok(
|
||||
"Most of these checks are for informational purposes only.\n"
|
||||
.. "WARNINGS should be treated as a warning, and don't necessarily indicate a problem with your config.\n"
|
||||
.. "Please |DON't| report these warnings as an issue."
|
||||
)
|
||||
|
||||
start("Checking your config")
|
||||
|
||||
if #Config.issues > 0 then
|
||||
local msg = {
|
||||
"There are issues with your config:",
|
||||
}
|
||||
vim.list_extend(
|
||||
msg,
|
||||
vim.tbl_map(function(issue)
|
||||
return "- `opts." .. issue.opt .. "`: " .. issue.msg
|
||||
end, Config.issues)
|
||||
)
|
||||
msg[#msg + 1] = "Please refer to the docs for more info."
|
||||
warn(table.concat(msg, "\n"))
|
||||
end
|
||||
if conflicts == 0 then
|
||||
ok("No conflicting keymaps found")
|
||||
return
|
||||
|
||||
local have_icons = false
|
||||
for _, provider in ipairs(Icons.providers) do
|
||||
if provider.available == nil then
|
||||
provider.available = pcall(require, provider.name)
|
||||
end
|
||||
if provider.available then
|
||||
ok("|" .. provider.name .. "| is installed")
|
||||
have_icons = true
|
||||
else
|
||||
warn("|" .. provider.name .. "| is not installed")
|
||||
end
|
||||
end
|
||||
for _, dup in ipairs(Keys.duplicates) do
|
||||
local msg = ""
|
||||
if dup.buf == dup.other.buffer then
|
||||
msg = "duplicate keymap"
|
||||
else
|
||||
msg = "buffer-local keymap overriding global"
|
||||
if not have_icons then
|
||||
warn("Keymap icon support will be limited.")
|
||||
end
|
||||
|
||||
start("Checking for issues with your mappings")
|
||||
if #Mappings.notifs == 0 then
|
||||
ok("No issues reported")
|
||||
end
|
||||
for _, notif in ipairs(Mappings.notifs) do
|
||||
local msg = notif.msg
|
||||
if notif.spec then
|
||||
msg = msg .. ": >\n" .. vim.inspect(notif.spec)
|
||||
if msg:find("old version") then
|
||||
local fixed = Migrate.migrate(notif.spec)
|
||||
msg = msg .. "\n\n-- Suggested Spec:\n" .. fixed
|
||||
end
|
||||
end
|
||||
msg = (msg .. " for mode **%q**, buf: %d, lhs: **%q**"):format(dup.mode, dup.buf or 0, dup.prefix)
|
||||
if dup.buf == dup.other.buffer then
|
||||
error(msg)
|
||||
else
|
||||
warn(msg)
|
||||
(notif.level >= vim.log.levels.ERROR and error or warn)(msg)
|
||||
end
|
||||
|
||||
start("checking for overlapping keymaps")
|
||||
local found = false
|
||||
|
||||
Buf.cleanup()
|
||||
|
||||
---@type table<string, boolean>
|
||||
local reported = {}
|
||||
|
||||
local mapmodes = vim.split("nixsotc", "")
|
||||
|
||||
for _, buf in pairs(Buf.bufs) do
|
||||
for _, mapmode in ipairs(mapmodes) do
|
||||
local mode = buf:get({ mode = mapmode })
|
||||
if mode then
|
||||
mode.tree:walk(function(node)
|
||||
local km = node.keymap
|
||||
if not km or Util.is_nop(km.rhs) or node.keys:sub(1, 6) == "<Plug>" then
|
||||
return
|
||||
end
|
||||
if node.keymap and node:count() > 0 then
|
||||
local id = mode.mode .. ":" .. node.keys
|
||||
if reported[id] then
|
||||
return
|
||||
end
|
||||
reported[id] = true
|
||||
local overlaps = {}
|
||||
local descs = {}
|
||||
if node.desc and node.desc ~= "" then
|
||||
descs[#descs + 1] = "- <" .. node.keys .. ">: " .. node.desc
|
||||
end
|
||||
local queue = node:children()
|
||||
while #queue > 0 do
|
||||
local child = table.remove(queue)
|
||||
if child.keymap then
|
||||
table.insert(overlaps, "<" .. child.keys .. ">")
|
||||
if child.desc and child.desc ~= "" then
|
||||
descs[#descs + 1] = "- <" .. child.keys .. ">: " .. child.desc
|
||||
end
|
||||
end
|
||||
vim.list_extend(queue, child:children())
|
||||
end
|
||||
if #overlaps > 0 then
|
||||
found = true
|
||||
warn(
|
||||
"In mode `"
|
||||
.. mode.mode
|
||||
.. "`, <"
|
||||
.. node.keys
|
||||
.. "> overlaps with "
|
||||
.. table.concat(overlaps, ", ")
|
||||
.. ":\n"
|
||||
.. table.concat(descs, "\n")
|
||||
)
|
||||
end
|
||||
return false
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
info(("old rhs: `%s`"):format(dup.other.rhs or ""))
|
||||
info(("new rhs: `%s`"):format(dup.cmd or ""))
|
||||
end
|
||||
|
||||
if found then
|
||||
ok(
|
||||
"Overlapping keymaps are only reported for informational purposes.\n"
|
||||
.. "This doesn't necessarily mean there is a problem with your config."
|
||||
)
|
||||
else
|
||||
ok("No overlapping keymaps found")
|
||||
end
|
||||
|
||||
start("Checking for duplicate mappings")
|
||||
|
||||
if vim.tbl_isempty(Tree.dups) then
|
||||
ok("No duplicate mappings found")
|
||||
else
|
||||
for _, mappings in pairs(Tree.dups) do
|
||||
---@type wk.Mapping[]
|
||||
mappings = vim.tbl_keys(mappings)
|
||||
local first = mappings[1]
|
||||
warn(
|
||||
"Duplicates for <"
|
||||
.. first.lhs
|
||||
.. "> in mode `"
|
||||
.. first.mode
|
||||
.. "`:\n"
|
||||
.. table.concat(
|
||||
vim.tbl_map(function(m)
|
||||
m = vim.deepcopy(m)
|
||||
local desc = (m.desc and (m.desc .. ": ") or "")
|
||||
m.desc = nil
|
||||
m.idx = nil
|
||||
m.mode = nil
|
||||
m.lhs = nil
|
||||
return "* " .. desc .. "`" .. vim.inspect(m):gsub("%s+", " ") .. "`"
|
||||
end, mappings),
|
||||
"\n"
|
||||
)
|
||||
)
|
||||
end
|
||||
ok(
|
||||
"Duplicate mappings are only reported for informational purposes.\n"
|
||||
.. "This doesn't necessarily mean there is a problem with your config."
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@ -1,96 +1,52 @@
|
||||
local Keys = require("which-key.keys")
|
||||
local Util = require("which-key.util")
|
||||
|
||||
---@class WhichKey
|
||||
---@class wk
|
||||
---@field private _queue {spec: wk.Spec, opts?: wk.Parse}[]
|
||||
local M = {}
|
||||
|
||||
local loaded = false -- once we loaded everything
|
||||
local scheduled = false
|
||||
M._queue = {}
|
||||
M.did_setup = false
|
||||
|
||||
local function schedule_load()
|
||||
if scheduled then
|
||||
return
|
||||
end
|
||||
scheduled = true
|
||||
if vim.v.vim_did_enter == 0 then
|
||||
vim.cmd([[au VimEnter * ++once lua require("which-key").load()]])
|
||||
else
|
||||
M.load()
|
||||
end
|
||||
end
|
||||
|
||||
---@param options? Options
|
||||
function M.setup(options)
|
||||
require("which-key.config").setup(options)
|
||||
schedule_load()
|
||||
end
|
||||
|
||||
function M.show(keys, opts)
|
||||
--- Open which-key
|
||||
---@param opts? wk.Filter|string
|
||||
function M.show(opts)
|
||||
opts = opts or {}
|
||||
if type(opts) == "string" then
|
||||
opts = { mode = opts }
|
||||
opts = type(opts) == "string" and { keys = opts } or opts
|
||||
if opts.delay == nil then
|
||||
opts.delay = 0
|
||||
end
|
||||
|
||||
keys = keys or ""
|
||||
|
||||
opts.mode = opts.mode or Util.get_mode()
|
||||
local buf = vim.api.nvim_get_current_buf()
|
||||
-- make sure the trees exist for update
|
||||
Keys.get_tree(opts.mode)
|
||||
Keys.get_tree(opts.mode, buf)
|
||||
-- update only trees related to buf
|
||||
Keys.update(buf)
|
||||
-- trigger which key
|
||||
require("which-key.view").open(keys, opts)
|
||||
end
|
||||
|
||||
function M.show_command(keys, mode)
|
||||
keys = keys or ""
|
||||
keys = (keys == '""' or keys == "''") and "" or keys
|
||||
mode = (mode == '""' or mode == "''") and "" or mode
|
||||
mode = mode or "n"
|
||||
keys = Util.t(keys)
|
||||
if not Util.check_mode(mode) then
|
||||
Util.error(
|
||||
"Invalid mode passed to :WhichKey (Don't create any keymappings to trigger WhichKey. WhichKey does this automatically)"
|
||||
opts.waited = vim.o.timeoutlen
|
||||
---@diagnostic disable-next-line: param-type-mismatch
|
||||
if not require("which-key.state").start(opts) then
|
||||
require("which-key.util").warn(
|
||||
"No mappings found for mode `" .. (opts.mode or "n") .. "` and keys `" .. (opts.keys or "") .. "`"
|
||||
)
|
||||
else
|
||||
M.show(keys, { mode = mode })
|
||||
end
|
||||
end
|
||||
|
||||
local queue = {}
|
||||
---@param opts? wk.Opts
|
||||
function M.setup(opts)
|
||||
M.did_setup = true
|
||||
require("which-key.config").setup(opts)
|
||||
end
|
||||
|
||||
-- Defer registering keymaps until VimEnter
|
||||
-- Use `require("which-key").add()` instead.
|
||||
-- The spec is different though, so check the docs!
|
||||
---@deprecated
|
||||
---@param mappings wk.Spec
|
||||
---@param opts? wk.Mapping
|
||||
function M.register(mappings, opts)
|
||||
schedule_load()
|
||||
if loaded then
|
||||
Keys.register(mappings, opts)
|
||||
Keys.update()
|
||||
else
|
||||
table.insert(queue, { mappings, opts })
|
||||
if opts then
|
||||
for k, v in pairs(opts) do
|
||||
mappings[k] = v
|
||||
end
|
||||
end
|
||||
M.add(mappings, { version = 1 })
|
||||
end
|
||||
|
||||
-- Load mappings and update only once
|
||||
function M.load()
|
||||
if loaded then
|
||||
return
|
||||
end
|
||||
require("which-key.plugins").setup()
|
||||
require("which-key.colors").setup()
|
||||
Keys.register({}, { prefix = "<leader>", mode = "n" })
|
||||
Keys.register({}, { prefix = "<leader>", mode = "v" })
|
||||
Keys.setup()
|
||||
|
||||
for _, reg in pairs(queue) do
|
||||
local opts = reg[2] or {}
|
||||
opts.update = false
|
||||
Keys.register(reg[1], opts)
|
||||
end
|
||||
Keys.update()
|
||||
queue = {}
|
||||
loaded = true
|
||||
--- Add mappings to which-key
|
||||
---@param mappings wk.Spec
|
||||
---@param opts? wk.Parse
|
||||
function M.add(mappings, opts)
|
||||
table.insert(M._queue, { spec = mappings, opts = opts })
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@ -1,449 +0,0 @@
|
||||
local Tree = require("which-key.tree")
|
||||
local Util = require("which-key.util")
|
||||
local Config = require("which-key.config")
|
||||
|
||||
-- secret character that will be used to create <nop> mappings
|
||||
local secret = "Þ"
|
||||
|
||||
---@class Keys
|
||||
local M = {}
|
||||
|
||||
M.operators = {} ---@type table<string, boolean>
|
||||
M.nowait = {} ---@type table<string, boolean>
|
||||
M.blacklist = {} ---@type table<string, table<string, boolean>>
|
||||
|
||||
function M.setup()
|
||||
local builtin_ops = require("which-key.plugins.presets").operators
|
||||
for op, _ in pairs(builtin_ops) do
|
||||
M.operators[op] = true
|
||||
end
|
||||
local mappings = {}
|
||||
for op, label in pairs(Config.options.operators) do
|
||||
M.operators[op] = true
|
||||
if builtin_ops[op] then
|
||||
mappings[op] = { name = label, i = { name = "inside" }, a = { name = "around" } }
|
||||
end
|
||||
end
|
||||
for _, t in pairs(Config.options.triggers_nowait) do
|
||||
M.nowait[t] = true
|
||||
end
|
||||
M.register(mappings, { mode = "n", preset = true })
|
||||
M.register({ i = { name = "inside" }, a = { name = "around" } }, { mode = "v", preset = true })
|
||||
for mode, blacklist in pairs(Config.options.triggers_blacklist) do
|
||||
for _, prefix_n in ipairs(blacklist) do
|
||||
M.blacklist[mode] = M.blacklist[mode] or {}
|
||||
M.blacklist[mode][prefix_n] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M.get_operator(prefix_i)
|
||||
local ret = { i = nil, n = nil, len = nil }
|
||||
for op_n, _ in pairs(Config.options.operators) do
|
||||
local op_i = Util.t(op_n)
|
||||
if prefix_i:sub(1, #op_i) == op_i and (ret.len == nil or #op_i > ret.len) then
|
||||
ret = { i = op_i, n = op_n, len = #op_i }
|
||||
end
|
||||
end
|
||||
return ret.i, ret.n
|
||||
end
|
||||
|
||||
function M.process_motions(ret, mode, prefix_i, buf)
|
||||
local op_i, op_n = "", ""
|
||||
if mode ~= "v" then
|
||||
op_i, op_n = M.get_operator(prefix_i)
|
||||
end
|
||||
if (mode == "n" or mode == "v") and op_i then
|
||||
local op_prefix_i = prefix_i:sub(#op_i + 1)
|
||||
local op_count = op_prefix_i:match("^(%d+)")
|
||||
if op_count == "0" then
|
||||
op_count = nil
|
||||
end
|
||||
if Config.options.motions.count == false then
|
||||
op_count = nil
|
||||
end
|
||||
if op_count then
|
||||
op_prefix_i = op_prefix_i:sub(#op_count + 1)
|
||||
end
|
||||
local op_results = M.get_mappings("o", op_prefix_i, buf)
|
||||
|
||||
if not ret.mapping and op_results.mapping then
|
||||
ret.mapping = op_results.mapping
|
||||
ret.mapping.prefix = op_n .. (op_count or "") .. ret.mapping.prefix
|
||||
ret.mapping.keys = Util.parse_keys(ret.mapping.prefix)
|
||||
end
|
||||
|
||||
for _, mapping in pairs(op_results.mappings) do
|
||||
mapping.prefix = op_n .. (op_count or "") .. mapping.prefix
|
||||
mapping.keys = Util.parse_keys(mapping.prefix)
|
||||
table.insert(ret.mappings, mapping)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@return MappingGroup
|
||||
function M.get_mappings(mode, prefix_i, buf)
|
||||
---@class MappingGroup
|
||||
---@field mode string
|
||||
---@field prefix_i string
|
||||
---@field buf number
|
||||
---@field mapping? Mapping
|
||||
---@field mappings VisualMapping[]
|
||||
local ret
|
||||
ret = { mapping = nil, mappings = {}, mode = mode, buf = buf, prefix_i = prefix_i }
|
||||
|
||||
local prefix_len = #Util.parse_internal(prefix_i)
|
||||
|
||||
---@param node? Node
|
||||
local function add(node)
|
||||
if node then
|
||||
if node.mapping then
|
||||
ret.mapping = vim.tbl_deep_extend("force", {}, ret.mapping or {}, node.mapping)
|
||||
end
|
||||
for k, child in pairs(node.children) do
|
||||
if
|
||||
child.mapping
|
||||
and child.mapping.desc ~= "which_key_ignore"
|
||||
and not (child.mapping.group and vim.tbl_isempty(child.children))
|
||||
then
|
||||
local child_mapping = vim.deepcopy(child.mapping)
|
||||
ret.mappings[k] = vim.tbl_deep_extend("force", ret.mappings[k] or {}, child_mapping)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local plugin_context = { buf = buf, mode = mode }
|
||||
add(M.get_tree(mode).tree:get(prefix_i, nil, plugin_context))
|
||||
add(M.get_tree(mode, buf).tree:get(prefix_i, nil, plugin_context))
|
||||
|
||||
-- Handle motions
|
||||
M.process_motions(ret, mode, prefix_i, buf)
|
||||
|
||||
-- Fix labels
|
||||
local tmp = {}
|
||||
for _, value in pairs(ret.mappings) do
|
||||
value.key = value.keys.notation[prefix_len + 1]
|
||||
if Config.options.key_labels[value.key] then
|
||||
value.key = Config.options.key_labels[value.key]
|
||||
end
|
||||
local skip = not value.desc and Config.options.ignore_missing == true
|
||||
if Util.t(value.key) == Util.t("<esc>") then
|
||||
skip = true
|
||||
end
|
||||
if not skip then
|
||||
if value.group then
|
||||
value.desc = value.desc or "+prefix"
|
||||
value.desc = value.desc:gsub("^%+", "")
|
||||
value.desc = Config.options.icons.group .. value.desc
|
||||
elseif not value.desc then
|
||||
value.desc = value.cmd or ""
|
||||
for _, v in ipairs(Config.options.hidden) do
|
||||
value.desc = value.desc:gsub(v, "")
|
||||
end
|
||||
end
|
||||
if value.value then
|
||||
value.value = vim.fn.strtrans(value.value)
|
||||
end
|
||||
-- remove duplicated keymap
|
||||
local exists = false
|
||||
for k, v in pairs(tmp) do
|
||||
if type(v) == "table" and v.key == value.key then
|
||||
tmp[k] = value
|
||||
exists = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not exists then
|
||||
table.insert(tmp, value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Sort items, but not for plugins
|
||||
table.sort(tmp, function(a, b)
|
||||
if a.order and b.order then
|
||||
return a.order < b.order
|
||||
end
|
||||
if a.group == b.group then
|
||||
local ak = (a.key or ""):lower()
|
||||
local bk = (b.key or ""):lower()
|
||||
local aw = ak:match("[a-z]") and 1 or 0
|
||||
local bw = bk:match("[a-z]") and 1 or 0
|
||||
if aw == bw then
|
||||
return ak < bk
|
||||
end
|
||||
return aw < bw
|
||||
else
|
||||
return (a.group and 1 or 0) < (b.group and 1 or 0)
|
||||
end
|
||||
end)
|
||||
ret.mappings = tmp
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
---@type table<string, MappingTree>
|
||||
M.mappings = {}
|
||||
M.duplicates = {}
|
||||
|
||||
function M.map(mode, prefix_n, cmd, buf, opts)
|
||||
local other = vim.api.nvim_buf_call(buf or 0, function()
|
||||
local ret = vim.fn.maparg(prefix_n, mode, false, true)
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
return (ret and ret.lhs and ret.rhs and ret.rhs ~= cmd) and ret or nil
|
||||
end)
|
||||
if other and other.buffer == buf then
|
||||
table.insert(M.duplicates, { mode = mode, prefix = prefix_n, cmd = cmd, buf = buf, other = other })
|
||||
end
|
||||
if buf ~= nil then
|
||||
pcall(vim.api.nvim_buf_set_keymap, buf, mode, prefix_n, cmd, opts)
|
||||
else
|
||||
pcall(vim.api.nvim_set_keymap, mode, prefix_n, cmd, opts)
|
||||
end
|
||||
end
|
||||
|
||||
function M.register(mappings, opts)
|
||||
opts = opts or {}
|
||||
|
||||
mappings = require("which-key.mappings").parse(mappings, opts)
|
||||
|
||||
-- always create the root node for the mode, even if there's no mappings,
|
||||
-- to ensure we have at least a trigger hooked for non documented keymaps
|
||||
local modes = {}
|
||||
|
||||
for _, mapping in pairs(mappings) do
|
||||
if not modes[mapping.mode] then
|
||||
modes[mapping.mode] = true
|
||||
M.get_tree(mapping.mode)
|
||||
end
|
||||
if mapping.cmd ~= nil then
|
||||
M.map(mapping.mode, mapping.prefix, mapping.cmd, mapping.buf, mapping.opts)
|
||||
end
|
||||
M.get_tree(mapping.mode, mapping.buf).tree:add(mapping)
|
||||
end
|
||||
end
|
||||
|
||||
M.hooked = {}
|
||||
|
||||
function M.hook_id(prefix_n, mode, buf)
|
||||
return mode .. (buf or "") .. Util.t(prefix_n)
|
||||
end
|
||||
|
||||
function M.is_hooked(prefix_n, mode, buf)
|
||||
return M.hooked[M.hook_id(prefix_n, mode, buf)]
|
||||
end
|
||||
|
||||
function M.hook_del(prefix_n, mode, buf)
|
||||
local id = M.hook_id(prefix_n, mode, buf)
|
||||
M.hooked[id] = nil
|
||||
if buf then
|
||||
pcall(vim.api.nvim_buf_del_keymap, buf, mode, prefix_n)
|
||||
pcall(vim.api.nvim_buf_del_keymap, buf, mode, prefix_n .. secret)
|
||||
else
|
||||
pcall(vim.api.nvim_del_keymap, mode, prefix_n)
|
||||
pcall(vim.api.nvim_del_keymap, mode, prefix_n .. secret)
|
||||
end
|
||||
end
|
||||
|
||||
function M.hook_add(prefix_n, mode, buf, secret_only)
|
||||
-- check if this trigger is blacklisted
|
||||
if M.blacklist[mode] and M.blacklist[mode][prefix_n] then
|
||||
return
|
||||
end
|
||||
-- don't hook numbers. See #118
|
||||
if tonumber(prefix_n) then
|
||||
return
|
||||
end
|
||||
-- don't hook to j or k in INSERT mode
|
||||
if mode == "i" and (prefix_n == "j" or prefix_n == "k") then
|
||||
return
|
||||
end
|
||||
-- never hook q
|
||||
if mode == "n" and prefix_n == "q" then
|
||||
return
|
||||
end
|
||||
-- never hook into select mode
|
||||
if mode == "s" then
|
||||
return
|
||||
end
|
||||
-- never hook into operator pending mode
|
||||
-- this is handled differently
|
||||
if mode == "o" then
|
||||
return
|
||||
end
|
||||
if Util.t(prefix_n) == Util.t("<esc>") then
|
||||
return
|
||||
end
|
||||
-- never hook into operators in visual mode
|
||||
if (mode == "v" or mode == "x") and (prefix_n == "a" or prefix_n == "i" or M.operators[prefix_n]) then
|
||||
return
|
||||
end
|
||||
|
||||
-- Check if we need to create the hook
|
||||
if type(Config.options.triggers) == "string" and Config.options.triggers ~= "auto" then
|
||||
if Util.t(prefix_n) ~= Util.t(Config.options.triggers) then
|
||||
return
|
||||
end
|
||||
end
|
||||
if type(Config.options.triggers) == "table" then
|
||||
local ok = false
|
||||
for _, trigger in pairs(Config.options.triggers) do
|
||||
if Util.t(trigger) == Util.t(prefix_n) then
|
||||
ok = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not ok then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local opts = { noremap = true, silent = true }
|
||||
local id = M.hook_id(prefix_n, mode, buf)
|
||||
local id_global = M.hook_id(prefix_n, mode)
|
||||
-- hook up if needed
|
||||
if not M.hooked[id] and not M.hooked[id_global] then
|
||||
local cmd = [[<cmd>lua require("which-key").show(%q, {mode = %q, auto = true})<cr>]]
|
||||
cmd = string.format(cmd, Util.t(prefix_n), mode)
|
||||
-- map group triggers and nops
|
||||
-- nops are needed, so that WhichKey always respects timeoutlen
|
||||
|
||||
local mapmode = mode == "v" and "x" or mode
|
||||
if secret_only ~= true then
|
||||
M.map(mapmode, prefix_n, cmd, buf, opts)
|
||||
end
|
||||
if not M.nowait[prefix_n] then
|
||||
M.map(mapmode, prefix_n .. secret, "<nop>", buf, opts)
|
||||
end
|
||||
|
||||
M.hooked[id] = true
|
||||
end
|
||||
end
|
||||
|
||||
function M.update(buf)
|
||||
for k, tree in pairs(M.mappings) do
|
||||
if tree.buf and not vim.api.nvim_buf_is_valid(tree.buf) then
|
||||
-- remove group for invalid buffers
|
||||
M.mappings[k] = nil
|
||||
elseif not buf or not tree.buf or buf == tree.buf then
|
||||
-- only update buffer maps, if:
|
||||
-- 1. we dont pass a buffer
|
||||
-- 2. this is a global node
|
||||
-- 3. this is a local buffer node for the passed buffer
|
||||
M.update_keymaps(tree.mode, tree.buf)
|
||||
M.add_hooks(tree.mode, tree.buf, tree.tree.root)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param node Node
|
||||
function M.add_hooks(mode, buf, node, secret_only)
|
||||
if not node.mapping then
|
||||
node.mapping = { prefix = node.prefix_n, group = true, keys = Util.parse_keys(node.prefix_n) }
|
||||
end
|
||||
if node.prefix_n ~= "" and node.mapping.group == true and not (node.mapping.cmd or node.mapping.callback) then
|
||||
-- first non-cmd level, so create hook and make all descendents secret only
|
||||
M.hook_add(node.prefix_n, mode, buf, secret_only)
|
||||
secret_only = true
|
||||
end
|
||||
for _, child in pairs(node.children) do
|
||||
M.add_hooks(mode, buf, child, secret_only)
|
||||
end
|
||||
end
|
||||
|
||||
function M.dump()
|
||||
local ok = {}
|
||||
local todo = {}
|
||||
for _, tree in pairs(M.mappings) do
|
||||
M.update_keymaps(tree.mode, tree.buf)
|
||||
tree.tree:walk(
|
||||
---@param node Node
|
||||
function(node)
|
||||
if node.mapping then
|
||||
if node.mapping.desc then
|
||||
ok[node.mapping.prefix] = true
|
||||
todo[node.mapping.prefix] = nil
|
||||
elseif not ok[node.mapping.prefix] then
|
||||
todo[node.mapping.prefix] = { node.mapping.cmd or "" }
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
return todo
|
||||
end
|
||||
|
||||
---@param mode string
|
||||
---@param buf? number
|
||||
function M.get_tree(mode, buf)
|
||||
if mode == "s" or mode == "x" then
|
||||
mode = "v"
|
||||
end
|
||||
Util.check_mode(mode, buf)
|
||||
local idx = mode .. (buf or "")
|
||||
if not M.mappings[idx] then
|
||||
M.mappings[idx] = { mode = mode, buf = buf, tree = Tree:new() }
|
||||
end
|
||||
return M.mappings[idx]
|
||||
end
|
||||
|
||||
---@param prefix string
|
||||
---@param cmd string?
|
||||
function M.is_hook(prefix, cmd)
|
||||
-- skip mappings with our secret nop command
|
||||
if prefix:find(secret, 1, true) then
|
||||
return true
|
||||
end
|
||||
-- skip auto which-key mappings
|
||||
return cmd and cmd:find("which-key", 1, true) and cmd:find("auto", 1, true)
|
||||
end
|
||||
|
||||
---@param mode string
|
||||
---@param buf? number
|
||||
function M.update_keymaps(mode, buf)
|
||||
---@type Keymap[]
|
||||
local keymaps = buf and vim.api.nvim_buf_get_keymap(buf, mode) or vim.api.nvim_get_keymap(mode)
|
||||
local tree = M.get_tree(mode, buf).tree
|
||||
|
||||
local function is_nop(keymap)
|
||||
return not keymap.callback and (keymap.rhs == "" or keymap.rhs:lower() == "<nop>")
|
||||
end
|
||||
|
||||
for _, keymap in pairs(keymaps) do
|
||||
local skip = M.is_hook(keymap.lhs, keymap.rhs)
|
||||
local is_group = false
|
||||
|
||||
if not skip and is_nop(keymap) then
|
||||
if keymap.desc then
|
||||
pcall(vim.keymap.del, { mode }, keymap.lhs, { buffer = buf })
|
||||
is_group = true
|
||||
else
|
||||
skip = true
|
||||
end
|
||||
end
|
||||
|
||||
if not skip then
|
||||
---@type Mapping
|
||||
local mapping = {
|
||||
prefix = keymap.lhs,
|
||||
cmd = not is_group and keymap.rhs or nil,
|
||||
desc = keymap.desc,
|
||||
group = is_group,
|
||||
callback = keymap.callback,
|
||||
keys = Util.parse_keys(keymap.lhs),
|
||||
buf = buf,
|
||||
mode = mode,
|
||||
}
|
||||
-- don't include Plug keymaps
|
||||
if mapping.keys.notation[1]:lower() ~= "<plug>" then
|
||||
local node = tree:add(mapping)
|
||||
if node.mapping and node.mapping.preset and mapping.desc then
|
||||
node.mapping.desc = mapping.desc
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -1,216 +1,147 @@
|
||||
local Config = require("which-key.config")
|
||||
local Text = require("which-key.text")
|
||||
local Keys = require("which-key.keys")
|
||||
local Util = require("which-key.util")
|
||||
local M = {}
|
||||
|
||||
---@class Layout
|
||||
---@field mapping Mapping
|
||||
---@field items VisualMapping[]
|
||||
---@field options Options
|
||||
---@field text Text
|
||||
---@field results MappingGroup
|
||||
local Layout = {}
|
||||
Layout.__index = Layout
|
||||
local dw = vim.fn.strdisplaywidth
|
||||
|
||||
---@param mappings MappingGroup
|
||||
---@param options? Options
|
||||
function Layout:new(mappings, options)
|
||||
options = options or Config.options
|
||||
local this = {
|
||||
results = mappings,
|
||||
mapping = mappings.mapping,
|
||||
items = mappings.mappings,
|
||||
options = options,
|
||||
text = Text:new(),
|
||||
}
|
||||
setmetatable(this, self)
|
||||
return this
|
||||
end
|
||||
--- When `size` is a number, it is returned as is (fixed dize).
|
||||
--- Otherwise, it is a percentage of `parent` (relative size).
|
||||
--- If `size` is negative, it is subtracted from `parent`.
|
||||
--- If `size` is a table, it is a range of values.
|
||||
---@alias wk.Dim number|{min:number, max:number}
|
||||
|
||||
function Layout:max_width(key)
|
||||
local max = 0
|
||||
for _, item in pairs(self.items) do
|
||||
if item[key] and Text.len(item[key]) > max then
|
||||
max = Text.len(item[key])
|
||||
---@param size number
|
||||
---@param parent number
|
||||
---@param ... wk.Dim
|
||||
---@return number
|
||||
function M.dim(size, parent, ...)
|
||||
size = math.abs(size) < 1 and parent * size or size
|
||||
size = size < 0 and parent + size or size
|
||||
for _, dim in ipairs({ ... } --[[ @as wk.Dim[] ]]) do
|
||||
if type(dim) == "number" then
|
||||
size = M.dim(dim, parent)
|
||||
else
|
||||
local min = dim.min and M.dim(dim.min, parent) or 0
|
||||
local max = dim.max and M.dim(dim.max, parent) or parent
|
||||
size = math.max(min, math.min(max, size))
|
||||
end
|
||||
end
|
||||
return max
|
||||
return math.floor(math.max(0, math.min(parent, size)) + 0.5)
|
||||
end
|
||||
|
||||
function Layout:trail()
|
||||
local prefix_i = self.results.prefix_i
|
||||
local buf_path = Keys.get_tree(self.results.mode, self.results.buf).tree:path(prefix_i)
|
||||
local path = Keys.get_tree(self.results.mode).tree:path(prefix_i)
|
||||
local len = #self.results.mapping.keys.notation
|
||||
local cmd_line = { { " " } }
|
||||
for i = 1, len, 1 do
|
||||
local node = buf_path[i]
|
||||
if not (node and node.mapping and node.mapping.desc) then
|
||||
node = path[i]
|
||||
---@class wk.Table: wk.Table.opts
|
||||
local Table = {}
|
||||
Table.__index = Table
|
||||
|
||||
---@param opts wk.Table.opts
|
||||
function Table.new(opts)
|
||||
local self = setmetatable({}, Table)
|
||||
self.cols = opts.cols
|
||||
self.rows = opts.rows
|
||||
return self
|
||||
end
|
||||
|
||||
---@param opts? {spacing?: number}
|
||||
---@return string[][], number[], number
|
||||
function Table:cells(opts)
|
||||
opts = opts or {}
|
||||
opts.spacing = opts.spacing or 1
|
||||
|
||||
local widths = {} ---@type number[] actual column widths
|
||||
|
||||
local cells = {} ---@type string[][]
|
||||
|
||||
local total = 0
|
||||
for c, col in ipairs(self.cols) do
|
||||
widths[c] = 0
|
||||
local all_ws = true
|
||||
for r, row in ipairs(self.rows) do
|
||||
cells[r] = cells[r] or {}
|
||||
local value = row[col.key] or col.default or ""
|
||||
value = tostring(value)
|
||||
value = value:gsub("%s*$", "")
|
||||
value = value:gsub("\n", Config.icons.keys.NL)
|
||||
value = vim.fn.strtrans(value)
|
||||
if value:find("%S") then
|
||||
all_ws = false
|
||||
end
|
||||
if col.padding then
|
||||
value = (" "):rep(col.padding[1] or 0) .. value .. (" "):rep(col.padding[2] or 0)
|
||||
end
|
||||
if c ~= #self.cols then
|
||||
value = value .. (" "):rep(opts.spacing)
|
||||
end
|
||||
cells[r][c] = value
|
||||
widths[c] = math.max(widths[c], dw(value))
|
||||
end
|
||||
local step = self.mapping.keys.notation[i]
|
||||
if node and node.mapping and node.mapping.desc then
|
||||
step = self.options.icons.group .. node.mapping.desc
|
||||
end
|
||||
if Config.options.key_labels[step] then
|
||||
step = Config.options.key_labels[step]
|
||||
end
|
||||
if Config.options.show_keys then
|
||||
table.insert(cmd_line, { step, "WhichKeyGroup" })
|
||||
if i ~= #self.mapping.keys.notation then
|
||||
table.insert(cmd_line, { " " .. self.options.icons.breadcrumb .. " ", "WhichKeySeparator" })
|
||||
if all_ws then
|
||||
widths[c] = 0
|
||||
for _, cell in pairs(cells) do
|
||||
cell[c] = ""
|
||||
end
|
||||
end
|
||||
end
|
||||
local width = 0
|
||||
if Config.options.show_keys then
|
||||
for _, line in pairs(cmd_line) do
|
||||
width = width + Text.len(line[1])
|
||||
end
|
||||
end
|
||||
local help = { --
|
||||
["<bs>"] = "go up one level",
|
||||
["<esc>"] = "close",
|
||||
}
|
||||
if #self.text.lines > self.options.layout.height.max then
|
||||
help[Config.options.popup_mappings.scroll_down] = "scroll down"
|
||||
help[Config.options.popup_mappings.scroll_up] = "scroll up"
|
||||
end
|
||||
local help_line = {}
|
||||
local help_width = 0
|
||||
for key, label in pairs(help) do
|
||||
help_width = help_width + Text.len(key) + Text.len(label) + 2
|
||||
table.insert(help_line, { key .. " ", "WhichKey" })
|
||||
table.insert(help_line, { label .. " ", "WhichKeySeparator" })
|
||||
end
|
||||
if Config.options.show_keys then
|
||||
table.insert(cmd_line, { string.rep(" ", math.floor(vim.o.columns / 2 - help_width / 2) - width) })
|
||||
total = total + widths[c]
|
||||
end
|
||||
|
||||
if self.options.show_help then
|
||||
for _, l in pairs(help_line) do
|
||||
table.insert(cmd_line, l)
|
||||
end
|
||||
end
|
||||
if vim.o.cmdheight > 0 then
|
||||
vim.api.nvim_echo(cmd_line, false, {})
|
||||
vim.cmd([[redraw]])
|
||||
else
|
||||
local col = 1
|
||||
self.text:nl()
|
||||
local row = #self.text.lines
|
||||
for _, text in ipairs(cmd_line) do
|
||||
self.text:set(row, col, text[1], text[2] and text[2]:gsub("WhichKey", "") or nil)
|
||||
col = col + vim.fn.strwidth(text[1])
|
||||
end
|
||||
end
|
||||
return cells, widths, total
|
||||
end
|
||||
|
||||
function Layout:layout(win)
|
||||
local pad_top, pad_right, pad_bot, pad_left = unpack(self.options.window.padding)
|
||||
local window_width = vim.api.nvim_win_get_width(win)
|
||||
local width = window_width
|
||||
width = width - pad_right - pad_left
|
||||
---@param opts {width: number, spacing?: number}
|
||||
function Table:layout(opts)
|
||||
local cells, widths = self:cells(opts)
|
||||
|
||||
local max_key_width = self:max_width("key")
|
||||
local max_label_width = self:max_width("desc")
|
||||
local max_value_width = self:max_width("value")
|
||||
local free = opts.width
|
||||
|
||||
local intro_width = max_key_width + 2 + Text.len(self.options.icons.separator) + self.options.layout.spacing
|
||||
local max_width = max_label_width + intro_width + max_value_width
|
||||
if max_width > width then
|
||||
max_width = width
|
||||
end
|
||||
|
||||
local column_width = max_width
|
||||
|
||||
if max_value_width == 0 then
|
||||
if column_width > self.options.layout.width.max then
|
||||
column_width = self.options.layout.width.max
|
||||
for c, col in ipairs(self.cols) do
|
||||
if not col.width then
|
||||
free = free - widths[c]
|
||||
end
|
||||
if column_width < self.options.layout.width.min then
|
||||
column_width = self.options.layout.width.min
|
||||
end
|
||||
else
|
||||
max_value_width = math.min(max_value_width, math.floor((column_width - intro_width) / 2))
|
||||
end
|
||||
free = math.max(free, 0)
|
||||
|
||||
max_label_width = column_width - (intro_width + max_value_width)
|
||||
|
||||
local columns = math.floor(width / column_width)
|
||||
|
||||
local height = math.ceil(#self.items / columns)
|
||||
if height < self.options.layout.height.min then
|
||||
height = self.options.layout.height.min
|
||||
end
|
||||
-- if height > self.options.layout.height.max then height = self.options.layout.height.max end
|
||||
|
||||
local col = 1
|
||||
local row = 1
|
||||
|
||||
local columns_used = math.min(columns, math.ceil(#self.items / height))
|
||||
local offset_x = 0
|
||||
if columns_used < columns then
|
||||
if self.options.layout.align == "right" then
|
||||
offset_x = (columns - columns_used) * column_width
|
||||
elseif self.options.layout.align == "center" then
|
||||
offset_x = math.floor((columns - columns_used) * column_width / 2)
|
||||
for c, col in ipairs(self.cols) do
|
||||
if col.width then
|
||||
widths[c] = M.dim(widths[c], free, { max = col.width })
|
||||
free = free - widths[c]
|
||||
end
|
||||
end
|
||||
|
||||
for _, item in pairs(self.items) do
|
||||
local start = (col - 1) * column_width + self.options.layout.spacing + offset_x + pad_left
|
||||
local key = item.key or ""
|
||||
if key == "<lt>" then
|
||||
key = "<"
|
||||
end
|
||||
if key == Util.t("<esc>") then
|
||||
key = "<esc>"
|
||||
end
|
||||
if Text.len(key) < max_key_width then
|
||||
key = string.rep(" ", max_key_width - Text.len(key)) .. key
|
||||
end
|
||||
---@type {value: string, hl?:string}[][]
|
||||
local ret = {}
|
||||
|
||||
self.text:set(row + pad_top, start, key, "")
|
||||
start = start + Text.len(key) + 1
|
||||
|
||||
self.text:set(row + pad_top, start, self.options.icons.separator, "Separator")
|
||||
start = start + Text.len(self.options.icons.separator) + 1
|
||||
|
||||
if item.value then
|
||||
local value = item.value
|
||||
start = start + 1
|
||||
if Text.len(value) > max_value_width then
|
||||
value = vim.fn.strcharpart(value, 0, max_value_width - 2) .. " …"
|
||||
end
|
||||
self.text:set(row + pad_top, start, value, "Value")
|
||||
if item.highlights then
|
||||
for _, hl in pairs(item.highlights) do
|
||||
self.text:highlight(row + pad_top, start + hl[1] - 1, start + hl[2] - 1, hl[3])
|
||||
for _, row in ipairs(cells) do
|
||||
---@type {value: string, hl?:string}[]
|
||||
local line = {}
|
||||
for c, col in ipairs(self.cols) do
|
||||
local value = row[c]
|
||||
local width = dw(value)
|
||||
if width > widths[c] then
|
||||
local old = value
|
||||
value = ""
|
||||
for i = 0, vim.fn.strchars(old) do
|
||||
value = value .. vim.fn.strcharpart(old, i, 1)
|
||||
if dw(value) >= widths[c] - 1 - (opts.spacing or 1) then
|
||||
break
|
||||
end
|
||||
end
|
||||
value = value .. Config.icons.ellipsis .. string.rep(" ", opts.spacing or 1)
|
||||
else
|
||||
local align = col.align or "left"
|
||||
if align == "left" then
|
||||
value = value .. (" "):rep(widths[c] - width)
|
||||
elseif align == "right" then
|
||||
value = (" "):rep(widths[c] - width) .. value
|
||||
elseif align == "center" then
|
||||
local pad = (widths[c] - width) / 2
|
||||
value = (" "):rep(math.floor(pad)) .. value .. (" "):rep(math.ceil(pad))
|
||||
end
|
||||
end
|
||||
start = start + max_value_width + 2
|
||||
end
|
||||
|
||||
local label = item.desc
|
||||
if Text.len(label) > max_label_width then
|
||||
label = vim.fn.strcharpart(label, 0, max_label_width - 2) .. " …"
|
||||
end
|
||||
self.text:set(row + pad_top, start, label, item.group and "Group" or "Desc")
|
||||
|
||||
if row % height == 0 then
|
||||
col = col + 1
|
||||
row = 1
|
||||
else
|
||||
row = row + 1
|
||||
line[#line + 1] = { value = value, hl = col.hl }
|
||||
end
|
||||
ret[#ret + 1] = line
|
||||
end
|
||||
|
||||
for _ = 1, pad_bot, 1 do
|
||||
self.text:nl()
|
||||
end
|
||||
self:trail()
|
||||
return self.text
|
||||
return ret
|
||||
end
|
||||
|
||||
return Layout
|
||||
M.new = Table.new
|
||||
|
||||
return M
|
||||
|
||||
@ -1,215 +1,320 @@
|
||||
local Config = require("which-key.config")
|
||||
local Util = require("which-key.util")
|
||||
|
||||
local M = {}
|
||||
|
||||
local function lookup(...)
|
||||
local ret = {}
|
||||
for _, t in ipairs({ ... }) do
|
||||
for _, v in ipairs(t) do
|
||||
ret[v] = v
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
M.VERSION = 2
|
||||
M.notifs = {} ---@type {msg:string, level:number, spec?:wk.Spec}[]
|
||||
|
||||
local mapargs = {
|
||||
"noremap",
|
||||
"desc",
|
||||
"expr",
|
||||
"silent",
|
||||
"nowait",
|
||||
"script",
|
||||
"unique",
|
||||
"callback",
|
||||
"replace_keycodes", -- TODO: add config setting for default value
|
||||
---@class wk.Field
|
||||
---@field transform? string|(fun(value: any, parent:table): (value:any, key:string?))
|
||||
---@field inherit? boolean
|
||||
---@field deprecated? boolean
|
||||
|
||||
---@class wk.Parse
|
||||
---@field version? number
|
||||
---@field create? boolean
|
||||
---@field notify? boolean
|
||||
|
||||
M.notify = true
|
||||
|
||||
---@type table<string, wk.Field>
|
||||
M.fields = {
|
||||
-- map args
|
||||
rhs = {},
|
||||
lhs = {},
|
||||
buffer = { inherit = true },
|
||||
callback = { transform = "rhs" },
|
||||
desc = {},
|
||||
expr = { inherit = true },
|
||||
mode = { inherit = true },
|
||||
noremap = {
|
||||
transform = function(value)
|
||||
return not value, "remap"
|
||||
end,
|
||||
},
|
||||
nowait = { inherit = true },
|
||||
remap = { inherit = true },
|
||||
replace_keycodes = { inherit = true },
|
||||
script = {},
|
||||
silent = { inherit = true },
|
||||
unique = { inherit = true },
|
||||
-- wk args
|
||||
plugin = { inherit = true },
|
||||
group = {},
|
||||
hidden = { inherit = true },
|
||||
cond = { inherit = true },
|
||||
preset = { inherit = true },
|
||||
icon = { inherit = true },
|
||||
proxy = {},
|
||||
expand = {},
|
||||
-- deprecated
|
||||
name = { transform = "group", deprecated = true },
|
||||
prefix = { inherit = true, deprecated = true },
|
||||
cmd = { transform = "rhs", deprecated = true },
|
||||
}
|
||||
local wkargs = {
|
||||
"prefix",
|
||||
"mode",
|
||||
"plugin",
|
||||
"buffer",
|
||||
"remap",
|
||||
"cmd",
|
||||
"name",
|
||||
"group",
|
||||
"preset",
|
||||
"cond",
|
||||
}
|
||||
local transargs = lookup({
|
||||
"noremap",
|
||||
"expr",
|
||||
"silent",
|
||||
"nowait",
|
||||
"script",
|
||||
"unique",
|
||||
"prefix",
|
||||
"mode",
|
||||
"buffer",
|
||||
"preset",
|
||||
"replace_keycodes",
|
||||
})
|
||||
local args = lookup(mapargs, wkargs)
|
||||
|
||||
function M.child_opts(opts)
|
||||
local ret = {}
|
||||
for k, v in pairs(opts) do
|
||||
if transargs[k] then
|
||||
ret[k] = v
|
||||
end
|
||||
end
|
||||
return ret
|
||||
---@param msg string
|
||||
---@param spec? wk.Spec
|
||||
function M.error(msg, spec)
|
||||
M.log(msg, vim.log.levels.ERROR, spec)
|
||||
end
|
||||
|
||||
function M._process(value, opts)
|
||||
local list = {}
|
||||
local children = {}
|
||||
for k, v in pairs(value) do
|
||||
if type(k) == "number" then
|
||||
if type(v) == "table" then
|
||||
-- nested child, without key
|
||||
table.insert(children, v)
|
||||
else
|
||||
-- list value
|
||||
table.insert(list, v)
|
||||
end
|
||||
elseif args[k] then
|
||||
-- option
|
||||
opts[k] = v
|
||||
else
|
||||
-- nested child, with key
|
||||
children[k] = v
|
||||
end
|
||||
end
|
||||
return list, children
|
||||
---@param msg string
|
||||
---@param spec? wk.Spec
|
||||
function M.warn(msg, spec)
|
||||
M.log(msg, vim.log.levels.WARN, spec)
|
||||
end
|
||||
|
||||
function M._parse(value, mappings, opts)
|
||||
if type(value) ~= "table" then
|
||||
value = { value }
|
||||
---@param msg string
|
||||
---@param level number
|
||||
---@param spec? wk.Spec
|
||||
function M.log(msg, level, spec)
|
||||
if not M.notify then
|
||||
return
|
||||
end
|
||||
|
||||
local list, children = M._process(value, opts)
|
||||
|
||||
if opts.plugin then
|
||||
opts.group = true
|
||||
end
|
||||
if opts.name then
|
||||
-- remove + from group names
|
||||
opts.name = opts.name and opts.name:gsub("^%+", "")
|
||||
opts.group = true
|
||||
end
|
||||
|
||||
-- fix remap
|
||||
if opts.remap then
|
||||
opts.noremap = not opts.remap
|
||||
opts.remap = nil
|
||||
end
|
||||
|
||||
-- fix buffer
|
||||
if opts.buffer == 0 then
|
||||
opts.buffer = vim.api.nvim_get_current_buf()
|
||||
end
|
||||
|
||||
if opts.cond ~= nil then
|
||||
if type(opts.cond) == "function" then
|
||||
if not opts.cond() then
|
||||
return
|
||||
end
|
||||
elseif not opts.cond then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- process any array child mappings
|
||||
for k, v in pairs(children) do
|
||||
local o = M.child_opts(opts)
|
||||
if type(k) == "string" then
|
||||
o.prefix = (o.prefix or "") .. k
|
||||
end
|
||||
M._try_parse(v, mappings, o)
|
||||
end
|
||||
|
||||
-- { desc }
|
||||
if #list == 1 then
|
||||
if type(list[1]) ~= "string" then
|
||||
error("Invalid mapping for " .. vim.inspect({ value = value, opts = opts }))
|
||||
end
|
||||
opts.desc = opts.desc or list[1]
|
||||
-- { cmd, desc }
|
||||
elseif #list == 2 then
|
||||
-- desc
|
||||
assert(type(list[2]) == "string")
|
||||
opts.desc = list[2]
|
||||
|
||||
-- cmd
|
||||
if type(list[1]) == "string" then
|
||||
opts.cmd = list[1]
|
||||
elseif type(list[1]) == "function" then
|
||||
opts.cmd = ""
|
||||
opts.callback = list[1]
|
||||
else
|
||||
error("Incorrect mapping " .. vim.inspect(list))
|
||||
end
|
||||
elseif #list > 2 then
|
||||
error("Incorrect mapping " .. vim.inspect(list))
|
||||
end
|
||||
|
||||
if opts.desc or opts.group then
|
||||
if type(opts.mode) == "table" then
|
||||
for _, mode in pairs(opts.mode) do
|
||||
local mode_opts = vim.deepcopy(opts)
|
||||
mode_opts.mode = mode
|
||||
table.insert(mappings, mode_opts)
|
||||
end
|
||||
else
|
||||
table.insert(mappings, opts)
|
||||
end
|
||||
M.notifs[#M.notifs + 1] = { msg = msg, level = level, spec = spec }
|
||||
if Config.notify then
|
||||
Util.warn({
|
||||
"There were issues reported with your **which-key** mappings.",
|
||||
"Use `:checkhealth which-key` to find out more.",
|
||||
}, { once = true })
|
||||
end
|
||||
end
|
||||
|
||||
---@return Mapping
|
||||
function M.to_mapping(mapping)
|
||||
mapping.silent = mapping.silent ~= false
|
||||
mapping.noremap = mapping.noremap ~= false
|
||||
if mapping.cmd and mapping.cmd:lower():find("^<plug>") then
|
||||
mapping.noremap = false
|
||||
---@param spec wk.Spec
|
||||
---@param field string|number
|
||||
---@param types string|string[]
|
||||
function M.expect(spec, field, types)
|
||||
types = type(types) == "string" and { types } or types
|
||||
local ok = false
|
||||
for _, t in ipairs(types) do
|
||||
if type(spec[field]) == t then
|
||||
ok = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
mapping.buf = mapping.buffer
|
||||
mapping.buffer = nil
|
||||
|
||||
mapping.mode = mapping.mode or "n"
|
||||
mapping.desc = mapping.desc or mapping.name
|
||||
mapping.name = nil
|
||||
mapping.keys = Util.parse_keys(mapping.prefix or "")
|
||||
|
||||
local opts = {}
|
||||
for _, o in ipairs(mapargs) do
|
||||
opts[o] = mapping[o]
|
||||
mapping[o] = nil
|
||||
end
|
||||
-- restore desc
|
||||
mapping.desc = opts.desc
|
||||
|
||||
mapping.opts = opts
|
||||
return mapping
|
||||
end
|
||||
|
||||
function M._try_parse(value, mappings, opts)
|
||||
local ok, err = pcall(M._parse, value, mappings, opts)
|
||||
if not ok then
|
||||
Util.error(err)
|
||||
M.error("Expected `" .. field .. "` to be " .. table.concat(types, ", "), spec)
|
||||
end
|
||||
return ok
|
||||
end
|
||||
|
||||
---@param spec wk.Spec
|
||||
---@param ret? wk.Mapping[]
|
||||
---@param opts? wk.Parse
|
||||
function M._parse(spec, ret, opts)
|
||||
opts = opts or {}
|
||||
opts.version = opts.version or M.VERSION
|
||||
if spec.version then
|
||||
opts.version = spec.version
|
||||
spec.version = nil
|
||||
end
|
||||
|
||||
if ret == nil and opts.version ~= M.VERSION then
|
||||
M.warn(
|
||||
"You're using an old version of the which-key spec.\n"
|
||||
.. "Your mappings will work, but it's recommended to update them to the new version.\n"
|
||||
.. "Please check the docs and suggested spec below for more info.\nMappings",
|
||||
vim.deepcopy(spec)
|
||||
)
|
||||
end
|
||||
|
||||
ret = ret or {}
|
||||
|
||||
spec = type(spec) == "string" and { desc = spec } or spec
|
||||
|
||||
---@type wk.Mapping
|
||||
local mapping = {}
|
||||
|
||||
---@type wk.Spec[]
|
||||
local children = {}
|
||||
|
||||
local keys = vim.tbl_keys(spec)
|
||||
|
||||
table.sort(keys, function(a, b)
|
||||
local ta, tb = type(a), type(b)
|
||||
if type(a) == type(b) then
|
||||
return a < b
|
||||
end
|
||||
return ta < tb
|
||||
end)
|
||||
|
||||
-- process fields
|
||||
for _, k in ipairs(keys) do
|
||||
local v = spec[k]
|
||||
local field = M.fields[k] ---@type wk.Field?
|
||||
if field then
|
||||
if type(field.transform) == "string" then
|
||||
k = field.transform --[[@as string]]
|
||||
elseif type(field.transform) == "function" then
|
||||
local vv, kk = field.transform(v, spec)
|
||||
v, k = vv, (kk or k)
|
||||
end
|
||||
mapping[k] = v
|
||||
elseif type(k) == "string" then
|
||||
if opts.version == 1 then
|
||||
if M.expect(spec, k, { "string", "table" }) then
|
||||
if type(v) == "string" then
|
||||
table.insert(children, { prefix = (spec.prefix or "") .. k, desc = v })
|
||||
elseif type(v) == "table" then
|
||||
v.prefix = (spec.prefix or "") .. k
|
||||
table.insert(children, v)
|
||||
end
|
||||
end
|
||||
else
|
||||
M.error("Invalid field `" .. k .. "`", spec)
|
||||
end
|
||||
elseif type(k) == "number" and type(v) == "table" then
|
||||
if opts.version == 1 then
|
||||
v.prefix = spec.prefix or ""
|
||||
end
|
||||
table.insert(children, v)
|
||||
spec[k] = nil
|
||||
end
|
||||
end
|
||||
|
||||
local count = #spec
|
||||
|
||||
-- process mapping
|
||||
if opts.version == M.VERSION then
|
||||
if count == 1 then
|
||||
if M.expect(spec, 1, "string") then
|
||||
mapping.lhs = spec[1] --[[@as string]]
|
||||
end
|
||||
elseif count == 2 then
|
||||
if M.expect(spec, 1, "string") and M.expect(spec, 2, { "string", "function" }) then
|
||||
mapping.lhs = spec[1] --[[@as string]]
|
||||
mapping.rhs = spec[2] --[[@as string]]
|
||||
end
|
||||
elseif count > 2 then
|
||||
M.error("expected 1 or 2 elements, got " .. count, spec)
|
||||
end
|
||||
elseif opts.version == 1 then
|
||||
if mapping.expr and mapping.replace_keycodes == nil then
|
||||
mapping.replace_keycodes = false
|
||||
end
|
||||
if count == 1 then
|
||||
if M.expect(spec, 1, "string") then
|
||||
if mapping.desc then
|
||||
M.warn("overwriting desc", spec)
|
||||
end
|
||||
mapping.desc = spec[1] --[[@as string]]
|
||||
end
|
||||
elseif count == 2 then
|
||||
if M.expect(spec, 1, { "string", "function" }) and M.expect(spec, 2, "string") then
|
||||
if mapping.desc then
|
||||
M.warn("overwriting desc", spec)
|
||||
end
|
||||
mapping.rhs = spec[1] --[[@as string]]
|
||||
mapping.desc = spec[2] --[[@as string]]
|
||||
end
|
||||
elseif count > 2 then
|
||||
M.error("expected 1 or 2 elements, got " .. count, spec)
|
||||
end
|
||||
end
|
||||
|
||||
-- add mapping
|
||||
M.add(mapping, ret, opts)
|
||||
|
||||
-- process children
|
||||
for _, child in ipairs(children) do
|
||||
for k, v in pairs(mapping) do
|
||||
if M.fields[k] and M.fields[k].inherit and child[k] == nil then
|
||||
child[k] = v
|
||||
end
|
||||
end
|
||||
M._parse(child, ret, opts)
|
||||
end
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
---@param mapping wk.Spec
|
||||
---@param opts? wk.Parse
|
||||
---@param ret wk.Mapping[]
|
||||
function M.add(mapping, ret, opts)
|
||||
opts = opts or {}
|
||||
if mapping.cond == false or ((type(mapping.cond) == "function") and not mapping.cond()) then
|
||||
return
|
||||
end
|
||||
---@cast mapping wk.Mapping|wk.Spec
|
||||
mapping.cond = nil
|
||||
if mapping.desc == "which_key_ignore" then
|
||||
mapping.hidden = true
|
||||
mapping.desc = nil
|
||||
end
|
||||
if type(mapping.group) == "string" or type(mapping.group) == "function" then
|
||||
mapping.desc = mapping.group --[[@as string]]
|
||||
mapping.group = true
|
||||
end
|
||||
if mapping.plugin then
|
||||
mapping.group = true
|
||||
end
|
||||
if mapping.group and mapping.desc then
|
||||
mapping.desc = mapping.desc
|
||||
if type(mapping.desc) == "string" then
|
||||
mapping.desc = mapping.desc:gsub("^%+", "")
|
||||
end
|
||||
end
|
||||
if mapping.buffer == 0 or mapping.buffer == true then
|
||||
mapping.buffer = vim.api.nvim_get_current_buf()
|
||||
end
|
||||
if mapping.rhs then
|
||||
mapping.silent = mapping.silent ~= false
|
||||
end
|
||||
mapping.lhs = mapping.lhs or mapping.prefix
|
||||
if not mapping.lhs then
|
||||
return
|
||||
end
|
||||
mapping.prefix = nil
|
||||
|
||||
local has_desc = mapping.desc ~= nil
|
||||
Util.getters(mapping, { "desc", "icon" })
|
||||
|
||||
if has_desc or mapping.group or mapping.hidden or mapping.rhs or (opts.version == M.VERSION and mapping.lhs) then
|
||||
local modes = mapping.mode or { "n" } --[[@as string|string[] ]]
|
||||
modes = type(modes) == "string" and vim.split(modes, "") or modes --[[@as string[] ]]
|
||||
for _, mode in ipairs(modes) do
|
||||
if mode ~= "v" and mode ~= Util.mapmode(mode) then
|
||||
M.warn("Invalid mode `" .. mode .. "`", mapping)
|
||||
end
|
||||
local m = vim.deepcopy(mapping)
|
||||
m.mode = mode
|
||||
table.insert(ret, m)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@return Mapping[]
|
||||
function M.parse(mappings, opts)
|
||||
---@param mapping wk.Mapping
|
||||
function M.create(mapping)
|
||||
assert(mapping.lhs, "Missing lhs")
|
||||
assert(mapping.mode, "Missing mode")
|
||||
assert(mapping.rhs, "Missing rhs")
|
||||
local valid =
|
||||
{ "remap", "noremap", "buffer", "silent", "nowait", "expr", "unique", "script", "desc", "replace_keycodes" }
|
||||
local opts = {} ---@type vim.keymap.set.Opts
|
||||
for _, k in ipairs(valid) do
|
||||
if mapping[k] ~= nil then
|
||||
opts[k] = mapping[k]
|
||||
end
|
||||
end
|
||||
vim.keymap.set(mapping.mode, mapping.lhs, mapping.rhs, opts)
|
||||
end
|
||||
|
||||
---@param spec wk.Spec
|
||||
---@param opts? wk.Parse
|
||||
function M.parse(spec, opts)
|
||||
opts = opts or {}
|
||||
local ret = {}
|
||||
M._try_parse(mappings, ret, opts)
|
||||
return vim.tbl_map(function(m)
|
||||
return M.to_mapping(m)
|
||||
end, ret)
|
||||
M.notify = opts.notify ~= false
|
||||
local ret = M._parse(spec, nil, opts)
|
||||
M.notify = true
|
||||
for _, m in ipairs(ret) do
|
||||
if m.rhs and opts.create then
|
||||
M.create(m)
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
local Keys = require("which-key.keys")
|
||||
local Util = require("which-key.util")
|
||||
local Config = require("which-key.config")
|
||||
local Util = require("which-key.util")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@type table<string, wk.Plugin>
|
||||
M.plugins = {}
|
||||
|
||||
function M.setup()
|
||||
for name, opts in pairs(Config.options.plugins) do
|
||||
for name, opts in pairs(Config.plugins) do
|
||||
-- only setup plugin if we didnt load it before
|
||||
if not M.plugins[name] then
|
||||
if type(opts) == "boolean" then
|
||||
@ -22,38 +22,27 @@ function M.setup()
|
||||
end
|
||||
end
|
||||
|
||||
---@param plugin Plugin
|
||||
---@param plugin wk.Plugin
|
||||
function M._setup(plugin, opts)
|
||||
if plugin.actions then
|
||||
for _, trigger in pairs(plugin.actions) do
|
||||
local prefix = trigger.trigger
|
||||
local mode = trigger.mode or "n"
|
||||
local label = trigger.label or plugin.name
|
||||
Keys.register({ [prefix] = { label, plugin = plugin.name } }, { mode = mode })
|
||||
end
|
||||
if plugin.mappings then
|
||||
Config.add(plugin.mappings)
|
||||
end
|
||||
|
||||
if plugin.setup then
|
||||
plugin.setup(require("which-key"), opts, Config.options)
|
||||
plugin.setup(opts)
|
||||
end
|
||||
end
|
||||
|
||||
---@param mapping Mapping
|
||||
function M.invoke(mapping, context)
|
||||
local plugin = M.plugins[mapping.plugin]
|
||||
local prefix = mapping.prefix
|
||||
local items = plugin.run(prefix, context.mode, context.buf)
|
||||
|
||||
local ret = {}
|
||||
for i, item in
|
||||
ipairs(items --[[@as VisualMapping[] ]])
|
||||
do
|
||||
item.order = i
|
||||
item.keys = Util.parse_keys(prefix .. item.key)
|
||||
item.prefix = prefix .. item.key
|
||||
table.insert(ret, item)
|
||||
end
|
||||
---@param name string
|
||||
function M.cols(name)
|
||||
local plugin = M.plugins[name]
|
||||
assert(plugin, "plugin not found")
|
||||
local ret = {} ---@type wk.Col[]
|
||||
vim.list_extend(ret, plugin.cols or {})
|
||||
ret[#ret + 1] = { key = "value", hl = "WhichKeyValue", width = 0.5 }
|
||||
return ret
|
||||
end
|
||||
|
||||
---@class wk.Node.plugin.item: wk.Node,wk.Plugin.item
|
||||
|
||||
return M
|
||||
|
||||
@ -1,16 +1,18 @@
|
||||
---@diagnostic disable: missing-fields, inject-field
|
||||
---@type wk.Plugin
|
||||
local M = {}
|
||||
|
||||
M.name = "marks"
|
||||
|
||||
M.actions = {
|
||||
{ trigger = "`", mode = "n" },
|
||||
{ trigger = "'", mode = "n" },
|
||||
{ trigger = "g`", mode = "n" },
|
||||
{ trigger = "g'", mode = "n" },
|
||||
M.mappings = {
|
||||
icon = { icon = " ", color = "orange" },
|
||||
plugin = "marks",
|
||||
{ "`", desc = "marks" },
|
||||
{ "'", desc = "marks" },
|
||||
{ "g`", desc = "marks" },
|
||||
{ "g'", desc = "marks" },
|
||||
}
|
||||
|
||||
function M.setup(_wk, _config, options) end
|
||||
|
||||
local labels = {
|
||||
["^"] = "Last position of cursor in insert mode",
|
||||
["."] = "Last change in current buffer",
|
||||
@ -24,42 +26,38 @@ local labels = {
|
||||
[">"] = "To end of last visual selection",
|
||||
}
|
||||
|
||||
---@type Plugin
|
||||
---@return PluginItem[]
|
||||
function M.run(_trigger, _mode, buf)
|
||||
local items = {}
|
||||
M.cols = {
|
||||
{ key = "lnum", hl = "Number", align = "right" },
|
||||
}
|
||||
|
||||
local marks = {}
|
||||
function M.expand()
|
||||
local buf = vim.api.nvim_get_current_buf()
|
||||
local items = {} ---@type wk.Plugin.item[]
|
||||
|
||||
local marks = {} ---@type vim.fn.getmarklist.ret.item[]
|
||||
vim.list_extend(marks, vim.fn.getmarklist(buf))
|
||||
vim.list_extend(marks, vim.fn.getmarklist())
|
||||
|
||||
for _, mark in pairs(marks) do
|
||||
local key = mark.mark:sub(2, 2)
|
||||
if key == "<" then
|
||||
key = "<lt>"
|
||||
end
|
||||
local lnum = mark.pos[2]
|
||||
|
||||
local line
|
||||
local line ---@type string?
|
||||
if mark.pos[1] and mark.pos[1] ~= 0 then
|
||||
local lines = vim.fn.getbufline(mark.pos[1], lnum)
|
||||
if lines and lines[1] then
|
||||
line = lines[1]
|
||||
end
|
||||
line = vim.api.nvim_buf_get_lines(mark.pos[1], lnum - 1, lnum, false)[1]
|
||||
end
|
||||
|
||||
local file = mark.file and vim.fn.fnamemodify(mark.file, ":p:~:.")
|
||||
|
||||
local value = string.format("%4d ", lnum)
|
||||
value = value .. (line or file or "")
|
||||
|
||||
table.insert(items, {
|
||||
key = key,
|
||||
desc = labels[key] or file and ("file: " .. file) or "",
|
||||
value = value,
|
||||
value = vim.trim(line or file or ""),
|
||||
highlights = { { 1, 5, "Number" } },
|
||||
lnum = lnum,
|
||||
})
|
||||
end
|
||||
|
||||
return items
|
||||
end
|
||||
|
||||
|
||||
@ -1,107 +0,0 @@
|
||||
local M = {}
|
||||
|
||||
M.name = "presets"
|
||||
|
||||
M.operators = {
|
||||
d = "Delete",
|
||||
c = "Change",
|
||||
y = "Yank (copy)",
|
||||
["g~"] = "Toggle case",
|
||||
["gu"] = "Lowercase",
|
||||
["gU"] = "Uppercase",
|
||||
[">"] = "Indent right",
|
||||
["<lt>"] = "Indent left",
|
||||
["zf"] = "Create fold",
|
||||
["!"] = "Filter through external program",
|
||||
["v"] = "Visual Character Mode",
|
||||
-- ["V"] = "Visual Line Mode",
|
||||
}
|
||||
|
||||
M.motions = {
|
||||
["h"] = "Left",
|
||||
["j"] = "Down",
|
||||
["k"] = "Up",
|
||||
["l"] = "Right",
|
||||
["w"] = "Next word",
|
||||
["%"] = "Matching character: '()', '{}', '[]'",
|
||||
["b"] = "Previous word",
|
||||
["e"] = "Next end of word",
|
||||
["ge"] = "Previous end of word",
|
||||
["0"] = "Start of line",
|
||||
["^"] = "Start of line (non-blank)",
|
||||
["$"] = "End of line",
|
||||
["f"] = "Move to next char",
|
||||
["F"] = "Move to previous char",
|
||||
["t"] = "Move before next char",
|
||||
["T"] = "Move before previous char",
|
||||
["gg"] = "First line",
|
||||
["G"] = "Last line",
|
||||
["{"] = "Previous empty line",
|
||||
["}"] = "Next empty line",
|
||||
}
|
||||
|
||||
M.objects = {
|
||||
a = { name = "around" },
|
||||
i = { name = "inside" },
|
||||
['a"'] = [[double quoted string]],
|
||||
["a'"] = [[single quoted string]],
|
||||
["a("] = [[same as ab]],
|
||||
["a)"] = [[same as ab]],
|
||||
["a<lt>"] = [[a <> from '<' to the matching '>']],
|
||||
["a>"] = [[same as a<]],
|
||||
["aB"] = [[a Block from [{ to ]} (with brackets)]],
|
||||
["aW"] = [[a WORD (with white space)]],
|
||||
["a["] = [[a [] from '[' to the matching ']']],
|
||||
["a]"] = [[same as a[]],
|
||||
["a`"] = [[string in backticks]],
|
||||
["ab"] = [[a block from [( to ]) (with braces)]],
|
||||
["ap"] = [[a paragraph (with white space)]],
|
||||
["as"] = [[a sentence (with white space)]],
|
||||
["at"] = [[a tag block (with white space)]],
|
||||
["aw"] = [[a word (with white space)]],
|
||||
["a{"] = [[same as aB]],
|
||||
["a}"] = [[same as aB]],
|
||||
['i"'] = [[double quoted string without the quotes]],
|
||||
["i'"] = [[single quoted string without the quotes]],
|
||||
["i("] = [[same as ib]],
|
||||
["i)"] = [[same as ib]],
|
||||
["i<lt>"] = [[inner <> from '<' to the matching '>']],
|
||||
["i>"] = [[same as i<]],
|
||||
["iB"] = [[inner Block from [{ and ]}]],
|
||||
["iW"] = [[inner WORD]],
|
||||
["i["] = [[inner [] from '[' to the matching ']']],
|
||||
["i]"] = [[same as i[]],
|
||||
["i`"] = [[string in backticks without the backticks]],
|
||||
["ib"] = [[inner block from [( to ])]],
|
||||
["ip"] = [[inner paragraph]],
|
||||
["is"] = [[inner sentence]],
|
||||
["it"] = [[inner tag block]],
|
||||
["iw"] = [[inner word]],
|
||||
["i{"] = [[same as iB]],
|
||||
["i}"] = [[same as iB]],
|
||||
}
|
||||
|
||||
---@param config Options
|
||||
function M.setup(wk, opts, config)
|
||||
require("which-key.plugins.presets.misc").setup(wk, opts)
|
||||
|
||||
-- Operators
|
||||
if opts.operators then
|
||||
for op, label in pairs(M.operators) do
|
||||
config.operators[op] = label
|
||||
end
|
||||
end
|
||||
|
||||
-- Motions
|
||||
if opts.motions then
|
||||
wk.register(M.motions, { mode = "n", prefix = "", preset = true })
|
||||
wk.register(M.motions, { mode = "o", prefix = "", preset = true })
|
||||
end
|
||||
|
||||
-- Text objects
|
||||
if opts.text_objects then
|
||||
wk.register(M.objects, { mode = "o", prefix = "", preset = true })
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -1,101 +0,0 @@
|
||||
local M = {}
|
||||
|
||||
M.name = "misc"
|
||||
|
||||
local misc = {
|
||||
windows = {
|
||||
["<c-w>"] = {
|
||||
name = "window",
|
||||
s = "Split window",
|
||||
v = "Split window vertically",
|
||||
w = "Switch windows",
|
||||
q = "Quit a window",
|
||||
o = "Close all other windows",
|
||||
T = "Break out into a new tab",
|
||||
x = "Swap current with next",
|
||||
["-"] = "Decrease height",
|
||||
["+"] = "Increase height",
|
||||
["<lt>"] = "Decrease width",
|
||||
[">"] = "Increase width",
|
||||
["|"] = "Max out the width",
|
||||
["_"] = "Max out the height",
|
||||
["="] = "Equally high and wide",
|
||||
h = "Go to the left window",
|
||||
l = "Go to the right window",
|
||||
k = "Go to the up window",
|
||||
j = "Go to the down window",
|
||||
},
|
||||
},
|
||||
z = {
|
||||
["z"] = {
|
||||
o = "Open fold under cursor",
|
||||
O = "Open all folds under cursor",
|
||||
c = "Close fold under cursor",
|
||||
C = "Close all folds under cursor",
|
||||
d = "Delete fold under cursor",
|
||||
D = "Delete all folds under cursor",
|
||||
E = "Delete all folds in file",
|
||||
a = "Toggle fold under cursor",
|
||||
A = "Toggle all folds under cursor",
|
||||
v = "Show cursor line",
|
||||
M = "Close all folds",
|
||||
R = "Open all folds",
|
||||
m = "Fold more",
|
||||
r = "Fold less",
|
||||
x = "Update folds",
|
||||
z = "Center this line",
|
||||
t = "Top this line",
|
||||
["<CR>"] = "Top this line, 1st non-blank col",
|
||||
b = "Bottom this line",
|
||||
g = "Add word to spell list",
|
||||
w = "Mark word as bad/misspelling",
|
||||
e = "Right this line",
|
||||
s = "Left this line",
|
||||
H = "Half screen to the left",
|
||||
L = "Half screen to the right",
|
||||
i = "Toggle folding",
|
||||
["="] = "Spelling suggestions",
|
||||
},
|
||||
},
|
||||
nav = {
|
||||
["[{"] = "Previous {",
|
||||
["[("] = "Previous (",
|
||||
["[<lt>"] = "Previous <",
|
||||
["[m"] = "Previous method start",
|
||||
["[M"] = "Previous method end",
|
||||
["[%"] = "Previous unmatched group",
|
||||
["[s"] = "Previous misspelled word",
|
||||
["]{"] = "Next {",
|
||||
["]("] = "Next (",
|
||||
["]<lt>"] = "Next <",
|
||||
["]m"] = "Next method start",
|
||||
["]M"] = "Next method end",
|
||||
["]%"] = "Next unmatched group",
|
||||
["]s"] = "Next misspelled word",
|
||||
["H"] = "Home line of window (top)",
|
||||
["M"] = "Middle line of window",
|
||||
["L"] = "Last line of window",
|
||||
},
|
||||
g = {
|
||||
["gf"] = "Go to file under cursor",
|
||||
["gx"] = "Open the file under cursor with system app",
|
||||
["gi"] = "Move to the last insertion and INSERT",
|
||||
["gv"] = "Switch to VISUAL using last selection",
|
||||
["gn"] = "Search forwards and select",
|
||||
["gN"] = "Search backwards and select",
|
||||
["g%"] = "Cycle backwards through results",
|
||||
["gt"] = "Go to next tab page",
|
||||
["gT"] = "Go to previous tab page",
|
||||
},
|
||||
}
|
||||
|
||||
function M.setup(wk, config)
|
||||
for key, mappings in pairs(misc) do
|
||||
if config[key] ~= false then
|
||||
wk.register(mappings, { mode = "n", prefix = "", preset = true })
|
||||
end
|
||||
end
|
||||
wk.register({ ["zf"] = "Create fold from selection" }, { mode = "x", prefix = "", preset = true })
|
||||
end
|
||||
|
||||
return M
|
||||
@ -1,18 +1,18 @@
|
||||
---@type Plugin
|
||||
local Util = require("which-key.util")
|
||||
|
||||
---@diagnostic disable: missing-fields, inject-field
|
||||
---@type wk.Plugin
|
||||
local M = {}
|
||||
|
||||
M.name = "registers"
|
||||
|
||||
M.actions = {
|
||||
{ trigger = '"', mode = "n" },
|
||||
{ trigger = '"', mode = "v" },
|
||||
-- { trigger = "@", mode = "n" },
|
||||
{ trigger = "<c-r>", mode = "i" },
|
||||
{ trigger = "<c-r>", mode = "c" },
|
||||
M.mappings = {
|
||||
icon = { icon = " ", color = "blue" },
|
||||
plugin = "registers",
|
||||
{ '"', mode = { "n", "x" }, desc = "registers" },
|
||||
{ "<c-r>", mode = { "i", "c" }, desc = "registers" },
|
||||
}
|
||||
|
||||
function M.setup(_wk, _config, options) end
|
||||
|
||||
M.registers = '*+"-:.%/#=_abcdefghijklmnopqrstuvwxyz0123456789'
|
||||
|
||||
local labels = {
|
||||
@ -30,43 +30,33 @@ local labels = {
|
||||
["/"] = "last search pattern",
|
||||
}
|
||||
|
||||
-- This function makes the assumption that OSC 52 is set up per :help osc-52
|
||||
M.osc52_active = function()
|
||||
-- If no clipboard set, can't be OSC 52
|
||||
if not vim.g.clipboard then
|
||||
return false
|
||||
end
|
||||
M.replace = {
|
||||
["<Space>"] = " ",
|
||||
["<lt>"] = "<",
|
||||
["<NL>"] = "\n",
|
||||
["\r"] = "",
|
||||
}
|
||||
|
||||
-- Per the docs, OSC 52 should be set up with a name field in the table
|
||||
if vim.g.clipboard.name == "OSC 52" then
|
||||
return true
|
||||
end
|
||||
function M.expand()
|
||||
local items = {} ---@type wk.Plugin.item[]
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
---@type Plugin
|
||||
---@return PluginItem[]
|
||||
function M.run(_trigger, _mode, _buf)
|
||||
local items = {}
|
||||
|
||||
local osc52_skip_keys = { "+", "*" }
|
||||
local is_osc52 = vim.g.clipboard and vim.g.clipboard.name == "OSC 52"
|
||||
local has_clipboard = vim.g.loaded_clipboard_provider == 2
|
||||
|
||||
for i = 1, #M.registers, 1 do
|
||||
local key = M.registers:sub(i, i)
|
||||
|
||||
local value = ""
|
||||
|
||||
if M.osc52_active() and vim.tbl_contains(osc52_skip_keys, key) then
|
||||
if is_osc52 and key:match("[%+%*]") then
|
||||
value = "OSC 52 detected, register not checked to maintain compatibility"
|
||||
else
|
||||
elseif has_clipboard or not key:match("[%+%*]") then
|
||||
local ok, reg_value = pcall(vim.fn.getreg, key, 1)
|
||||
if ok then
|
||||
value = reg_value
|
||||
end
|
||||
value = (ok and reg_value or "") --[[@as string]]
|
||||
end
|
||||
|
||||
if value ~= "" then
|
||||
value = vim.fn.keytrans(value) --[[@as string]]
|
||||
for k, v in pairs(M.replace) do
|
||||
value = value:gsub(k, v) --[[@as string]]
|
||||
end
|
||||
table.insert(items, { key = key, desc = labels[key] or "", value = value })
|
||||
end
|
||||
end
|
||||
|
||||
@ -1,38 +1,42 @@
|
||||
---@diagnostic disable: missing-fields, inject-field
|
||||
---@type wk.Plugin
|
||||
local M = {}
|
||||
|
||||
M.name = "spelling"
|
||||
|
||||
M.actions = { { trigger = "z=", mode = "n" } }
|
||||
M.mappings = {
|
||||
{
|
||||
"z=",
|
||||
icon = { icon = " ", color = "red" },
|
||||
plugin = "spelling",
|
||||
desc = "Spelling Suggestions",
|
||||
},
|
||||
}
|
||||
|
||||
---@type table<string, any>
|
||||
M.opts = {}
|
||||
|
||||
function M.setup(_, config, options)
|
||||
M.opts = config
|
||||
function M.setup(opts)
|
||||
M.opts = opts
|
||||
end
|
||||
|
||||
---@type Plugin
|
||||
---@return PluginItem[]
|
||||
function M.run()
|
||||
function M.expand()
|
||||
-- if started with a count, let the default keybinding work
|
||||
local count = vim.api.nvim_get_vvar("count")
|
||||
local count = vim.v.count
|
||||
if count and count > 0 then
|
||||
return {}
|
||||
end
|
||||
|
||||
---@diagnostic disable-next-line: missing-parameter
|
||||
local cursor_word = vim.fn.expand("<cword>")
|
||||
-- get a misspelled word from under the cursor, if not found, then use the cursor_word instead
|
||||
---@diagnostic disable-next-line: redundant-parameter
|
||||
local bad = vim.fn.spellbadword(cursor_word)
|
||||
local word = bad[1]
|
||||
if word == "" then
|
||||
word = cursor_word
|
||||
end
|
||||
local word = bad[1] == "" and cursor_word or bad[1]
|
||||
|
||||
---@type string[]
|
||||
local suggestions = vim.fn.spellsuggest(word, M.opts.suggestions or 20, bad[2] == "caps" and 1 or 0)
|
||||
|
||||
local items = {}
|
||||
local keys = "1234567890abcdefghijklmnopqrstuvwxyz"
|
||||
local items = {} ---@type wk.Plugin.item[]
|
||||
local keys = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
for i, label in ipairs(suggestions) do
|
||||
local key = keys:sub(i, i)
|
||||
@ -40,8 +44,8 @@ function M.run()
|
||||
table.insert(items, {
|
||||
key = key,
|
||||
desc = label,
|
||||
fn = function()
|
||||
vim.cmd('norm! "_ciw' .. label)
|
||||
action = function()
|
||||
vim.cmd("norm! " .. i .. "z=")
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
@ -1,72 +1,164 @@
|
||||
---@class Highlight
|
||||
---@field group string
|
||||
---@field line number
|
||||
---@field from number
|
||||
---@field to number
|
||||
local Util = require("which-key.util")
|
||||
|
||||
---@class Text
|
||||
---@field lines string[]
|
||||
---@field hl Highlight[]
|
||||
---@field lineNr number
|
||||
---@field current string
|
||||
local Text = {}
|
||||
Text.__index = Text
|
||||
---@class wk.Segment
|
||||
---@field str string Text
|
||||
---@field hl? string Extmark hl group
|
||||
---@field line? number line number in a multiline segment
|
||||
---@field width? number
|
||||
|
||||
function Text.len(str)
|
||||
return vim.fn.strwidth(str)
|
||||
---@class wk.Text.opts
|
||||
---@field multiline? boolean
|
||||
---@field indent? boolean
|
||||
|
||||
---@class wk.Text
|
||||
---@field _lines wk.Segment[][]
|
||||
---@field _col number
|
||||
---@field _indents string[]
|
||||
---@field _opts wk.Text.opts
|
||||
local M = {}
|
||||
M.__index = M
|
||||
|
||||
M.ns = vim.api.nvim_create_namespace("wk.text")
|
||||
|
||||
---@param opts? wk.Text.opts
|
||||
function M.new(opts)
|
||||
local self = setmetatable({}, M)
|
||||
self._lines = {}
|
||||
self._col = 0
|
||||
self._opts = opts or {}
|
||||
self._indents = {}
|
||||
for i = 0, 100, 1 do
|
||||
self._indents[i] = (" "):rep(i)
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
function Text:new()
|
||||
local this = { lines = {}, hl = {}, lineNr = 0, current = "" }
|
||||
setmetatable(this, self)
|
||||
return this
|
||||
function M:height()
|
||||
return #self._lines
|
||||
end
|
||||
|
||||
function Text:fix_nl(line)
|
||||
return line:gsub("[\n]", "")
|
||||
---@return number
|
||||
function M:width()
|
||||
local width = 0
|
||||
for _, line in ipairs(self._lines) do
|
||||
local w = 0
|
||||
for _, segment in ipairs(line) do
|
||||
w = w + vim.fn.strdisplaywidth(segment.str)
|
||||
end
|
||||
width = math.max(width, w)
|
||||
end
|
||||
return width
|
||||
end
|
||||
|
||||
function Text:nl()
|
||||
local line = self:fix_nl(self.current)
|
||||
table.insert(self.lines, line)
|
||||
self.current = ""
|
||||
self.lineNr = self.lineNr + 1
|
||||
---@param text string|wk.Segment[]
|
||||
---@param opts? string|{hl?:string, line?:number}
|
||||
function M:append(text, opts)
|
||||
opts = opts or {}
|
||||
if #self._lines == 0 then
|
||||
self:nl()
|
||||
end
|
||||
|
||||
if type(text) == "table" then
|
||||
for _, s in ipairs(text) do
|
||||
s.width = s.width or #s.str
|
||||
self._col = self._col + s.width
|
||||
table.insert(self._lines[#self._lines], s)
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
opts = type(opts) == "string" and { hl = opts } or opts
|
||||
|
||||
for l, line in ipairs(vim.split(text, "\n", { plain = true })) do
|
||||
local width = #line
|
||||
self._col = self._col + width
|
||||
table.insert(self._lines[#self._lines], {
|
||||
str = line,
|
||||
width = width,
|
||||
hl = opts.hl,
|
||||
line = opts.line or l,
|
||||
})
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
function Text:set(row, col, str, group)
|
||||
str = self:fix_nl(str)
|
||||
function M:nl()
|
||||
table.insert(self._lines, {})
|
||||
self._col = 0
|
||||
return self
|
||||
end
|
||||
|
||||
-- extend lines if needed
|
||||
for i = 1, row, 1 do
|
||||
if not self.lines[i] then
|
||||
self.lines[i] = ""
|
||||
---@param opts? {sep?:string}
|
||||
function M:statusline(opts)
|
||||
local sep = opts and opts.sep or " "
|
||||
local lines = {} ---@type string[]
|
||||
for _, line in ipairs(self._lines) do
|
||||
local parts = {}
|
||||
for _, segment in ipairs(line) do
|
||||
local str = segment.str:gsub("%%", "%%%%")
|
||||
if segment.hl then
|
||||
str = ("%%#%s#%s%%*"):format(segment.hl, str)
|
||||
end
|
||||
parts[#parts + 1] = str
|
||||
end
|
||||
table.insert(lines, table.concat(parts, ""))
|
||||
end
|
||||
return table.concat(lines, sep)
|
||||
end
|
||||
|
||||
function M:render(buf)
|
||||
local lines = {}
|
||||
|
||||
for _, line in ipairs(self._lines) do
|
||||
local parts = {} ---@type string[]
|
||||
for _, segment in ipairs(line) do
|
||||
parts[#parts + 1] = segment.str
|
||||
end
|
||||
table.insert(lines, table.concat(parts, ""))
|
||||
end
|
||||
|
||||
vim.bo[buf].modifiable = true
|
||||
vim.api.nvim_buf_clear_namespace(buf, M.ns, 0, -1)
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
||||
|
||||
for l, line in ipairs(self._lines) do
|
||||
local col = 0
|
||||
local row = l - 1
|
||||
|
||||
for _, segment in ipairs(line) do
|
||||
local width = segment.width
|
||||
if segment.hl then
|
||||
Util.set_extmark(buf, M.ns, row, col, {
|
||||
hl_group = segment.hl,
|
||||
end_col = col + width,
|
||||
})
|
||||
end
|
||||
col = col + width
|
||||
end
|
||||
end
|
||||
|
||||
-- extend columns when needed
|
||||
local width = Text.len(self.lines[row])
|
||||
if width < col then
|
||||
self.lines[row] = self.lines[row] .. string.rep(" ", col - width)
|
||||
end
|
||||
|
||||
local before = vim.fn.strcharpart(self.lines[row], 0, col)
|
||||
local after = vim.fn.strcharpart(self.lines[row], col)
|
||||
self.lines[row] = before .. str .. after
|
||||
|
||||
if not group then
|
||||
return
|
||||
end
|
||||
-- set highlights
|
||||
self:highlight(row, col, col + Text.len(str), "WhichKey" .. group)
|
||||
vim.bo[buf].modifiable = false
|
||||
end
|
||||
|
||||
function Text:highlight(row, from, to, group)
|
||||
local line = self.lines[row]
|
||||
local before = vim.fn.strcharpart(line, 0, from)
|
||||
local str = vim.fn.strcharpart(line, 0, to)
|
||||
from = vim.fn.strlen(before)
|
||||
to = vim.fn.strlen(str)
|
||||
table.insert(self.hl, { line = row - 1, from = from, to = to, group = group })
|
||||
function M:trim()
|
||||
while #self._lines > 0 and #self._lines[#self._lines] == 0 do
|
||||
table.remove(self._lines)
|
||||
end
|
||||
end
|
||||
|
||||
return Text
|
||||
function M:row()
|
||||
return #self._lines == 0 and 1 or #self._lines
|
||||
end
|
||||
|
||||
---@param opts? {display:boolean}
|
||||
function M:col(opts)
|
||||
if opts and opts.display then
|
||||
local ret = 0
|
||||
for _, segment in ipairs(self._lines[#self._lines] or {}) do
|
||||
ret = ret + vim.fn.strdisplaywidth(segment.str)
|
||||
end
|
||||
return ret
|
||||
end
|
||||
return self._col
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@ -1,112 +1,103 @@
|
||||
local Config = require("which-key.config")
|
||||
local Node = require("which-key.node")
|
||||
local Util = require("which-key.util")
|
||||
|
||||
---@class Tree
|
||||
---@field root Node
|
||||
---@field nodes table<string, Node>
|
||||
local Tree = {}
|
||||
Tree.__index = Tree
|
||||
---@class wk.Tree
|
||||
---@field root wk.Node
|
||||
local M = {}
|
||||
M.__index = M
|
||||
|
||||
---@class Node
|
||||
---@field mapping Mapping
|
||||
---@field prefix_i string
|
||||
---@field prefix_n string
|
||||
---@field children table<string, Node>
|
||||
-- selene: allow(unused_variable)
|
||||
local Node
|
||||
---@type table<string, table<wk.Mapping|wk.Keymap, true>>
|
||||
M.dups = {}
|
||||
|
||||
---@return Tree
|
||||
function Tree:new()
|
||||
local this = { root = { children = {}, prefix_i = "", prefix_n = "" }, nodes = {} }
|
||||
setmetatable(this, self)
|
||||
return this
|
||||
function M.new()
|
||||
local self = setmetatable({}, M)
|
||||
self:clear()
|
||||
return self
|
||||
end
|
||||
|
||||
---@param prefix_i string
|
||||
---@param index? number defaults to last. If < 0, then offset from last
|
||||
---@param plugin_context? any
|
||||
---@return Node?
|
||||
function Tree:get(prefix_i, index, plugin_context)
|
||||
local prefix = Util.parse_internal(prefix_i)
|
||||
local node = self.root
|
||||
index = index or #prefix
|
||||
if index < 0 then
|
||||
index = #prefix + index
|
||||
end
|
||||
for i = 1, index, 1 do
|
||||
node = node.children[prefix[i]]
|
||||
if node and plugin_context and node.mapping and node.mapping.plugin then
|
||||
local children = require("which-key.plugins").invoke(node.mapping, plugin_context)
|
||||
node.children = {}
|
||||
for _, child in pairs(children) do
|
||||
self:add(child, { cache = false })
|
||||
end
|
||||
end
|
||||
if not node then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
return node
|
||||
function M:clear()
|
||||
self.root = Node.new()
|
||||
end
|
||||
|
||||
-- Returns the path (possibly incomplete) for the prefix
|
||||
---@param prefix_i string
|
||||
---@return Node[]
|
||||
function Tree:path(prefix_i)
|
||||
local prefix = Util.parse_internal(prefix_i)
|
||||
local node = self.root
|
||||
local path = {}
|
||||
for i = 1, #prefix, 1 do
|
||||
node = node.children[prefix[i]]
|
||||
table.insert(path, node)
|
||||
if not node then
|
||||
break
|
||||
end
|
||||
---@param keymap wk.Mapping|wk.Keymap
|
||||
---@param virtual? boolean
|
||||
function M:add(keymap, virtual)
|
||||
if not Config.filter(keymap) then
|
||||
return
|
||||
end
|
||||
local keys = Util.keys(keymap.lhs, { norm = true })
|
||||
local node = assert(self.root:find(keys, { create = true }))
|
||||
node.plugin = node.plugin or keymap.plugin
|
||||
if virtual then
|
||||
---@cast node wk.Node
|
||||
if node.mapping and not keymap.preset and not node.mapping.preset then
|
||||
local id = keymap.mode .. ":" .. node.keys
|
||||
M.dups[id] = M.dups[id] or {}
|
||||
M.dups[id][keymap] = true
|
||||
M.dups[id][node.mapping] = true
|
||||
end
|
||||
if not (keymap.preset and node.keymap and node.keymap.desc) then
|
||||
node.mapping = keymap --[[@as wk.Mapping]]
|
||||
end
|
||||
else
|
||||
node.keymap = keymap
|
||||
end
|
||||
return path
|
||||
end
|
||||
|
||||
---@param mapping Mapping
|
||||
---@param opts? {cache?: boolean}
|
||||
---@return Node
|
||||
function Tree:add(mapping, opts)
|
||||
---@param node wk.Node
|
||||
function M:del(node)
|
||||
if node == self.root then
|
||||
return self:clear()
|
||||
end
|
||||
local parent = node.parent
|
||||
assert(parent, "node has no parent")
|
||||
parent._children[node.key] = nil
|
||||
if not self:keep(parent) then
|
||||
self:del(parent)
|
||||
end
|
||||
end
|
||||
|
||||
---@param node wk.Node
|
||||
function M:keep(node)
|
||||
if node.hidden or (node.keymap and node.keymap.desc == "which_key_ignore") then
|
||||
return false
|
||||
end
|
||||
return node:can_expand() or node.keymap or node:is_group() or (node.mapping and not node.group)
|
||||
end
|
||||
|
||||
function M:fix()
|
||||
self:walk(function(node)
|
||||
if not self:keep(node) then
|
||||
self:del(node)
|
||||
return false
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---@param keys string|string[]
|
||||
---@param opts? { create?: boolean, expand?: boolean }
|
||||
---@return wk.Node?
|
||||
function M:find(keys, opts)
|
||||
keys = type(keys) == "string" and Util.keys(keys) or keys
|
||||
return self.root:find(keys, opts)
|
||||
end
|
||||
|
||||
---@param fn fun(node: wk.Node):boolean?
|
||||
---@param opts? {expand?: boolean}
|
||||
function M:walk(fn, opts)
|
||||
opts = opts or {}
|
||||
opts.cache = opts.cache ~= false
|
||||
local node_key = mapping.keys.keys
|
||||
local node = opts.cache and self.nodes[node_key]
|
||||
if not node then
|
||||
local prefix_i = mapping.keys.internal
|
||||
local prefix_n = mapping.keys.notation
|
||||
node = self.root
|
||||
local path_i = ""
|
||||
local path_n = ""
|
||||
for i = 1, #prefix_i, 1 do
|
||||
path_i = path_i .. prefix_i[i]
|
||||
path_n = path_n .. prefix_n[i]
|
||||
if not node.children[prefix_i[i]] then
|
||||
node.children[prefix_i[i]] = {
|
||||
children = {},
|
||||
prefix_i = path_i,
|
||||
prefix_n = path_n,
|
||||
}
|
||||
---@type wk.Node[]
|
||||
local queue = { self.root }
|
||||
while #queue > 0 do
|
||||
local node = table.remove(queue, 1) ---@type wk.Node
|
||||
if node == self.root or fn(node) ~= false then
|
||||
local children = opts.expand and node:children() or node._children
|
||||
for _, child in pairs(children) do
|
||||
queue[#queue + 1] = child
|
||||
end
|
||||
node = node.children[prefix_i[i]]
|
||||
end
|
||||
if opts.cache then
|
||||
self.nodes[node_key] = node
|
||||
end
|
||||
end
|
||||
node.mapping = vim.tbl_deep_extend("force", node.mapping or {}, mapping)
|
||||
return node
|
||||
end
|
||||
|
||||
---@param cb fun(node:Node)
|
||||
---@param node? Node
|
||||
function Tree:walk(cb, node)
|
||||
node = node or self.root
|
||||
cb(node)
|
||||
for _, child in pairs(node.children) do
|
||||
self:walk(cb, child)
|
||||
end
|
||||
end
|
||||
|
||||
return Tree
|
||||
return M
|
||||
|
||||
@ -2,72 +2,115 @@
|
||||
|
||||
--# selene: allow(unused_variable)
|
||||
|
||||
---@class Keymap
|
||||
---@field rhs string
|
||||
---@field lhs string
|
||||
---@field buffer number
|
||||
---@field expr number
|
||||
---@field lnum number
|
||||
---@field mode string
|
||||
---@field noremap number
|
||||
---@field nowait number
|
||||
---@field script number
|
||||
---@field sid number
|
||||
---@field silent number
|
||||
---@field callback fun()|nil
|
||||
---@field id string terminal keycodes for lhs
|
||||
---@field desc string
|
||||
|
||||
---@class KeyCodes
|
||||
---@field keys string
|
||||
---@field internal string[]
|
||||
---@field notation string[]
|
||||
|
||||
---@class MappingOptions
|
||||
---@field noremap boolean
|
||||
---@field silent boolean
|
||||
---@field nowait boolean
|
||||
---@field expr boolean
|
||||
|
||||
---@class Mapping
|
||||
---@field buf number
|
||||
---@field group boolean
|
||||
---@field desc string
|
||||
---@field prefix string
|
||||
---@field cmd string
|
||||
---@field opts MappingOptions
|
||||
---@field keys KeyCodes
|
||||
---@class wk.Filter
|
||||
---@field mode? string
|
||||
---@field callback fun()|nil
|
||||
---@field preset boolean
|
||||
---@field plugin string
|
||||
---@field fn fun()
|
||||
|
||||
---@class MappingTree
|
||||
---@field mode string
|
||||
---@field buf? number
|
||||
---@field tree Tree
|
||||
---@field keys? string
|
||||
---@field global? boolean
|
||||
---@field local? boolean
|
||||
---@field update? boolean
|
||||
---@field delay? number
|
||||
---@field loop? boolean
|
||||
---@field defer? boolean don't show the popup immediately. Wait for the first key to be pressed
|
||||
---@field waited? number
|
||||
---@field check? boolean
|
||||
---@field expand? boolean
|
||||
|
||||
---@class VisualMapping : Mapping
|
||||
---@field key string
|
||||
---@field highlights table
|
||||
---@field value string
|
||||
---@class wk.Icon
|
||||
---@field icon? string
|
||||
---@field hl? string
|
||||
---@field cat? "file" | "filetype" | "extension"
|
||||
---@field name? string
|
||||
---@field color? false | "azure" | "blue" | "cyan" | "green" | "grey" | "orange" | "purple" | "red" | "yellow"
|
||||
|
||||
---@class PluginItem
|
||||
---@field key string
|
||||
---@field label string
|
||||
---@field value string
|
||||
---@field cmd string
|
||||
---@field highlights table
|
||||
|
||||
---@class PluginAction
|
||||
---@field trigger string
|
||||
---@field mode string
|
||||
---@field label? string
|
||||
---@field delay? boolean
|
||||
|
||||
---@class Plugin
|
||||
---@class wk.IconProvider
|
||||
---@field name string
|
||||
---@field actions PluginAction[]
|
||||
---@field run fun(trigger:string, mode:string, buf:number):PluginItem[]
|
||||
---@field setup fun(wk, opts, Options)
|
||||
---@field available? boolean
|
||||
---@field get fun(icon: wk.Icon):(icon: string?, hl: string?)
|
||||
|
||||
---@class wk.IconRule: wk.Icon
|
||||
---@field pattern? string
|
||||
---@field plugin? string
|
||||
|
||||
---@class wk.Keymap: vim.api.keyset.keymap
|
||||
---@field lhs string
|
||||
---@field mode string
|
||||
---@field rhs? string|fun()
|
||||
---@field lhsraw? string
|
||||
---@field buffer? number
|
||||
|
||||
--- Represents a node in the which-key tree
|
||||
---@class wk.Node: wk.Mapping
|
||||
---@field key string single key of the node
|
||||
---@field path string[] path to the node (all keys leading to this node)
|
||||
---@field keys string full key sequence
|
||||
---@field parent? wk.Node parent node
|
||||
---@field keymap? wk.Keymap Real keymap
|
||||
---@field mapping? wk.Mapping Mapping info supplied by user
|
||||
---@field action? fun() action to execute when node is selected (used by plugins)
|
||||
|
||||
---@class wk.Mapping: wk.Keymap
|
||||
---@field idx? number
|
||||
---@field plugin? string
|
||||
---@field group? boolean
|
||||
---@field remap? boolean
|
||||
---@field hidden? boolean
|
||||
---@field preset? boolean
|
||||
---@field icon? wk.Icon|string
|
||||
---@field proxy? string
|
||||
---@field expand? fun():wk.Spec
|
||||
|
||||
---@class wk.Spec: {[number]: wk.Spec} , wk.Mapping
|
||||
---@field [1]? string
|
||||
---@field [2]? string|fun()
|
||||
---@field lhs? string
|
||||
---@field group? string|fun():string
|
||||
---@field desc? string|fun():string
|
||||
---@field icon? wk.Icon|string|fun():(wk.Icon|string)
|
||||
---@field buffer? number|boolean
|
||||
---@field mode? string|string[]
|
||||
---@field cond? boolean|fun():boolean?
|
||||
|
||||
---@class wk.Win.opts: vim.api.keyset.win_config
|
||||
---@field width? wk.Dim
|
||||
---@field height? wk.Dim
|
||||
---@field wo? vim.wo
|
||||
---@field bo? vim.bo
|
||||
---@field padding? {[1]: number, [2]:number}
|
||||
---@field no_overlap? boolean
|
||||
|
||||
---@class wk.Col
|
||||
---@field key string
|
||||
---@field hl? string
|
||||
---@field width? number
|
||||
---@field padding? number[]
|
||||
---@field default? string
|
||||
---@field align? "left"|"right"|"center"
|
||||
|
||||
---@class wk.Table.opts
|
||||
---@field cols wk.Col[]
|
||||
---@field rows table<string, string>[]
|
||||
|
||||
---@class wk.Plugin.item
|
||||
---@field key string
|
||||
---@field value string
|
||||
---@field desc string
|
||||
---@field order? number
|
||||
---@field action? fun()
|
||||
|
||||
---@class wk.Plugin
|
||||
---@field name string
|
||||
---@field cols? wk.Col[]
|
||||
---@field mappings? wk.Spec
|
||||
---@field expand fun():wk.Plugin.item[]
|
||||
---@field setup fun(opts: table<string, any>)
|
||||
|
||||
---@class wk.Item: wk.Node
|
||||
---@field node wk.Node
|
||||
---@field key string
|
||||
---@field raw_key string
|
||||
---@field desc string
|
||||
---@field group? boolean
|
||||
---@field order? number
|
||||
---@field icon? string
|
||||
---@field icon_hl? string
|
||||
|
||||
@ -1,196 +1,301 @@
|
||||
---@class Util
|
||||
local M = {}
|
||||
local strbyte = string.byte
|
||||
local strsub = string.sub
|
||||
---@type table<string, KeyCodes>
|
||||
local cache = {}
|
||||
---@type table<string,string>
|
||||
local tcache = {}
|
||||
local cache_leaders = ""
|
||||
|
||||
function M.check_cache()
|
||||
---@type string
|
||||
local leaders = (vim.g.mapleader or "") .. ":" .. (vim.g.maplocalleader or "")
|
||||
if leaders ~= cache_leaders then
|
||||
cache = {}
|
||||
tcache = {}
|
||||
cache_leaders = leaders
|
||||
end
|
||||
end
|
||||
|
||||
function M.count(tab)
|
||||
local ret = 0
|
||||
for _, _ in pairs(tab) do
|
||||
ret = ret + 1
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
function M.get_mode()
|
||||
local mode = vim.api.nvim_get_mode().mode
|
||||
mode = mode:gsub(M.t("<C-V>"), "v")
|
||||
mode = mode:gsub(M.t("<C-S>"), "s")
|
||||
return mode:lower()
|
||||
end
|
||||
|
||||
function M.is_empty(tab)
|
||||
return M.count(tab) == 0
|
||||
end
|
||||
M.cache = {
|
||||
keys = {}, ---@type table<string, string[]>
|
||||
norm = {}, ---@type table<string, string>
|
||||
termcodes = {}, ---@type table<string, string>
|
||||
}
|
||||
|
||||
function M.t(str)
|
||||
M.check_cache()
|
||||
if not tcache[str] then
|
||||
-- https://github.com/neovim/neovim/issues/17369
|
||||
tcache[str] = vim.api.nvim_replace_termcodes(str, false, true, true):gsub("\128\254X", "\128")
|
||||
end
|
||||
return tcache[str]
|
||||
M.cache.termcodes[str] = M.cache.termcodes[str] or vim.api.nvim_replace_termcodes(str, true, true, true)
|
||||
return M.cache.termcodes[str]
|
||||
end
|
||||
|
||||
-- stylua: ignore start
|
||||
local utf8len_tab = {
|
||||
-- ?1 ?2 ?3 ?4 ?5 ?6 ?7 ?8 ?9 ?A ?B ?C ?D ?E ?F
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 0?
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 1?
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 2?
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 3?
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 4?
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 5?
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 6?
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 7?
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 8?
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 9?
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- A?
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- B?
|
||||
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, -- C?
|
||||
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, -- D?
|
||||
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, -- E?
|
||||
4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 1, 1, -- F?
|
||||
}
|
||||
-- stylua: ignore end
|
||||
M.CR = M.t("<cr>")
|
||||
M.ESC = M.t("<esc>")
|
||||
M.BS = M.t("<bs>")
|
||||
M.EXIT = M.t("<C-\\><C-n>")
|
||||
M.LUA_CALLBACK = "\x80\253g"
|
||||
M.CMD = "\x80\253h"
|
||||
|
||||
local Tokens = {
|
||||
["<"] = strbyte("<"),
|
||||
[">"] = strbyte(">"),
|
||||
["-"] = strbyte("-"),
|
||||
}
|
||||
---@return KeyCodes
|
||||
function M.parse_keys(keystr)
|
||||
M.check_cache()
|
||||
if cache[keystr] then
|
||||
return cache[keystr]
|
||||
function M.exit()
|
||||
vim.api.nvim_feedkeys(M.EXIT, "n", false)
|
||||
vim.api.nvim_feedkeys(M.ESC, "n", false)
|
||||
end
|
||||
|
||||
---@param rhs string|fun()
|
||||
function M.is_nop(rhs)
|
||||
return type(rhs) == "string" and (rhs == "" or rhs:lower() == "<nop>")
|
||||
end
|
||||
|
||||
--- Normalizes (and fixes) the lhs of a keymap
|
||||
---@param lhs string
|
||||
function M.norm(lhs)
|
||||
M.cache.norm[lhs] = M.cache.norm[lhs] or vim.fn.keytrans(M.t(lhs))
|
||||
return M.cache.norm[lhs]
|
||||
end
|
||||
|
||||
-- Default register
|
||||
function M.reg()
|
||||
-- this will be set to 2 if there is a non-empty clipboard
|
||||
-- tool available
|
||||
if vim.g.loaded_clipboard_provider ~= 2 then
|
||||
return '"'
|
||||
end
|
||||
local cb = vim.o.clipboard
|
||||
return cb:find("unnamedplus") and "+" or cb:find("unnamed") and "*" or '"'
|
||||
end
|
||||
|
||||
local keys = M.t(keystr)
|
||||
local internal = M.parse_internal(keys)
|
||||
|
||||
if #internal == 0 then
|
||||
local ret = { keys = keys, internal = {}, notation = {} }
|
||||
cache[keystr] = ret
|
||||
return ret
|
||||
--- Returns the keys of a keymap, taking multibyte and special keys into account
|
||||
---@param lhs string
|
||||
---@param opts? {norm?: boolean}
|
||||
function M.keys(lhs, opts)
|
||||
lhs = opts and opts.norm == false and lhs or M.norm(lhs)
|
||||
if M.cache.keys[lhs] then
|
||||
return M.cache.keys[lhs]
|
||||
end
|
||||
|
||||
local keystr_orig = keystr
|
||||
keystr = keystr:gsub("<lt>", "<")
|
||||
local notation = {}
|
||||
---@alias ParseState
|
||||
--- | "Character"
|
||||
--- | "Special"
|
||||
--- | "SpecialNoClose"
|
||||
local start = 1
|
||||
local i = start
|
||||
---@type ParseState
|
||||
local state = "Character"
|
||||
while i <= #keystr do
|
||||
local c = strbyte(keystr, i, i)
|
||||
|
||||
if state == "Character" then
|
||||
start = i
|
||||
-- Only interpret special tokens if neovim also replaces it
|
||||
state = c == Tokens["<"] and internal[#notation + 1] ~= "<" and "Special" or state
|
||||
elseif state == "Special" then
|
||||
state = (c == Tokens["-"] and "SpecialNoClose") or (c == Tokens[">"] and "Character") or state
|
||||
local ret = {} ---@type string[]
|
||||
local bytes = vim.fn.str2list(lhs) ---@type number[]
|
||||
local special = nil ---@type string?
|
||||
for _, byte in ipairs(bytes) do
|
||||
local char = vim.fn.nr2char(byte) ---@type string
|
||||
if char == "<" then
|
||||
special = "<"
|
||||
elseif special then
|
||||
special = special .. char
|
||||
if char == ">" then
|
||||
ret[#ret + 1] = special == "<lt>" and "<" or special
|
||||
special = nil
|
||||
end
|
||||
else
|
||||
state = "Special"
|
||||
end
|
||||
|
||||
i = i + utf8len_tab[c + 1]
|
||||
if state == "Character" then
|
||||
local k = strsub(keystr, start, i - 1)
|
||||
notation[#notation + 1] = k == " " and "<space>" or k
|
||||
ret[#ret + 1] = char
|
||||
end
|
||||
end
|
||||
|
||||
local mapleader = vim.g.mapleader
|
||||
mapleader = mapleader and M.t(mapleader)
|
||||
notation[1] = internal[1] == mapleader and "<leader>" or notation[1]
|
||||
|
||||
if #notation ~= #internal then
|
||||
error(vim.inspect({ keystr = keystr, internal = internal, notation = notation }))
|
||||
end
|
||||
|
||||
local ret = {
|
||||
keys = keys,
|
||||
internal = internal,
|
||||
notation = notation,
|
||||
}
|
||||
|
||||
cache[keystr_orig] = ret
|
||||
|
||||
M.cache.keys[lhs] = ret
|
||||
return ret
|
||||
end
|
||||
|
||||
-- @return string[]
|
||||
function M.parse_internal(keystr)
|
||||
local keys = {}
|
||||
---@alias ParseInternalState
|
||||
--- | "Character"
|
||||
--- | "Special"
|
||||
---@type ParseInternalState
|
||||
local state = "Character"
|
||||
local start = 1
|
||||
local i = 1
|
||||
while i <= #keystr do
|
||||
local c = strbyte(keystr, i, i)
|
||||
|
||||
if state == "Character" then
|
||||
state = c == 128 and "Special" or state
|
||||
i = i + utf8len_tab[c + 1]
|
||||
|
||||
if state == "Character" then
|
||||
keys[#keys + 1] = strsub(keystr, start, i - 1)
|
||||
start = i
|
||||
end
|
||||
else
|
||||
-- This state is entered on the second byte of K_SPECIAL sequence.
|
||||
if c == 252 then
|
||||
-- K_SPECIAL KS_MODIFIER: skip this byte and the next
|
||||
i = i + 2
|
||||
else
|
||||
-- K_SPECIAL _: skip this byte
|
||||
i = i + 1
|
||||
end
|
||||
-- The last byte of this sequence should be between 0x02 and 0x7f,
|
||||
-- switch to Character state to collect.
|
||||
state = "Character"
|
||||
end
|
||||
---@param mode? string
|
||||
function M.mapmode(mode)
|
||||
mode = mode or vim.api.nvim_get_mode().mode
|
||||
mode = mode:gsub(M.t("<C-V>"), "v"):gsub(M.t("<C-S>"), "s"):lower()
|
||||
if mode:sub(1, 2) == "no" then
|
||||
return "o"
|
||||
end
|
||||
return keys
|
||||
if mode:sub(1, 1) == "v" then
|
||||
return "x" -- mapmode is actually "x" for visual only mappings
|
||||
end
|
||||
return mode:sub(1, 1):match("[ncitsxo]") or "n"
|
||||
end
|
||||
|
||||
function M.warn(msg)
|
||||
vim.notify(msg, vim.log.levels.WARN, { title = "WhichKey" })
|
||||
function M.xo()
|
||||
return M.mapmode():find("[xo]") ~= nil
|
||||
end
|
||||
|
||||
function M.error(msg)
|
||||
vim.notify(msg, vim.log.levels.ERROR, { title = "WhichKey" })
|
||||
---@alias NotifyOpts {level?: number, title?: string, once?: boolean, id?:string}
|
||||
|
||||
---@param msg string|string[]
|
||||
---@param opts? NotifyOpts
|
||||
function M.notify(msg, opts)
|
||||
opts = opts or {}
|
||||
msg = type(msg) == "table" and table.concat(msg, "\n") or msg
|
||||
---@cast msg string
|
||||
msg = vim.trim(msg)
|
||||
return vim[opts.once and "notify_once" or "notify"](msg, opts.level, {
|
||||
title = opts.title or "which-key.nvim",
|
||||
on_open = function(win)
|
||||
M.wo(win, { conceallevel = 3, spell = false, concealcursor = "n" })
|
||||
vim.treesitter.start(vim.api.nvim_win_get_buf(win), "markdown")
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
function M.check_mode(mode, buf)
|
||||
if not ("nvsxoiRct"):find(mode) then
|
||||
M.error(string.format("Invalid mode %q for buf %d", mode, buf or 0))
|
||||
---@param msg string|string[]
|
||||
---@param opts? NotifyOpts
|
||||
function M.warn(msg, opts)
|
||||
M.notify(msg, vim.tbl_extend("keep", { level = vim.log.levels.WARN }, opts or {}))
|
||||
end
|
||||
|
||||
---@param msg string|string[]
|
||||
---@param opts? NotifyOpts
|
||||
function M.info(msg, opts)
|
||||
M.notify(msg, vim.tbl_extend("keep", { level = vim.log.levels.INFO }, opts or {}))
|
||||
end
|
||||
|
||||
---@param msg string|string[]
|
||||
---@param opts? NotifyOpts
|
||||
function M.error(msg, opts)
|
||||
M.notify(msg, vim.tbl_extend("keep", { level = vim.log.levels.ERROR }, opts or {}))
|
||||
end
|
||||
|
||||
---@generic F: fun()
|
||||
---@param ms number
|
||||
---@param fn F
|
||||
---@return F
|
||||
function M.debounce(ms, fn)
|
||||
local timer = (vim.uv or vim.loop).new_timer()
|
||||
return function(...)
|
||||
local args = { ... }
|
||||
timer:start(
|
||||
ms,
|
||||
0,
|
||||
vim.schedule_wrap(function()
|
||||
fn(args)
|
||||
end)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
---@param opts? {msg?: string}
|
||||
function M.try(fn, opts)
|
||||
local ok, err = pcall(fn)
|
||||
if not ok then
|
||||
local msg = opts and opts.msg or "Something went wrong:"
|
||||
msg = msg .. "\n" .. err
|
||||
M.error(msg)
|
||||
end
|
||||
end
|
||||
|
||||
---@param buf number
|
||||
---@param row number
|
||||
---@param ns number
|
||||
---@param col number
|
||||
---@param opts vim.api.keyset.set_extmark
|
||||
---@param debug_info? any
|
||||
function M.set_extmark(buf, ns, row, col, opts, debug_info)
|
||||
local ok, err = pcall(vim.api.nvim_buf_set_extmark, buf, ns, row, col, opts)
|
||||
if not ok then
|
||||
M.error(
|
||||
"Failed to set extmark for preview:\n"
|
||||
.. vim.inspect({ info = debug_info, row = row, col = col, opts = opts, error = err })
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
---@param n number buffer or window number
|
||||
---@param type "win" | "buf"
|
||||
---@param opts vim.wo | vim.bo
|
||||
local function set_opts(n, type, opts)
|
||||
---@diagnostic disable-next-line: no-unknown
|
||||
for k, v in pairs(opts or {}) do
|
||||
---@diagnostic disable-next-line: no-unknown
|
||||
pcall(vim.api.nvim_set_option_value, k, v, type == "win" and {
|
||||
scope = "local",
|
||||
win = n,
|
||||
} or { buf = n })
|
||||
end
|
||||
end
|
||||
|
||||
---@param win number
|
||||
---@param opts vim.wo
|
||||
function M.wo(win, opts)
|
||||
set_opts(win, "win", opts)
|
||||
end
|
||||
|
||||
---@param buf number
|
||||
---@param opts vim.bo
|
||||
function M.bo(buf, opts)
|
||||
set_opts(buf, "buf", opts)
|
||||
end
|
||||
|
||||
local trace_level = 0
|
||||
---@param msg? string
|
||||
---@param ...? any
|
||||
function M.trace(msg, ...)
|
||||
if not msg then
|
||||
trace_level = trace_level - 1
|
||||
return
|
||||
end
|
||||
trace_level = math.max(trace_level, 0)
|
||||
M.debug(msg, ...)
|
||||
trace_level = trace_level + 1
|
||||
end
|
||||
|
||||
---@param msg? string
|
||||
---@param ...? any
|
||||
function M.debug(msg, ...)
|
||||
if not require("which-key.config").debug then
|
||||
return
|
||||
end
|
||||
local data = { ... }
|
||||
if #data == 0 then
|
||||
data = nil
|
||||
elseif #data == 1 then
|
||||
data = data[1]
|
||||
end
|
||||
if type(data) == "function" then
|
||||
data = data()
|
||||
end
|
||||
if type(data) == "table" then
|
||||
data = table.concat(
|
||||
vim.tbl_map(function(value)
|
||||
return type(value) == "string" and value or vim.inspect(value):gsub("%s+", " ")
|
||||
end, data),
|
||||
" "
|
||||
)
|
||||
end
|
||||
if data and type(data) ~= "string" then
|
||||
data = vim.inspect(data):gsub("%s+", " ")
|
||||
end
|
||||
msg = data and ("%s: %s"):format(msg, data) or msg
|
||||
msg = string.rep(" ", trace_level) .. msg
|
||||
M.log(msg .. "\n")
|
||||
end
|
||||
|
||||
function M.log(msg)
|
||||
local file = "./wk.log"
|
||||
local fd = io.open(file, "a+")
|
||||
if not fd then
|
||||
error(("Could not open file %s for writing"):format(file))
|
||||
end
|
||||
fd:write(msg)
|
||||
fd:close()
|
||||
end
|
||||
|
||||
--- Returns a function that returns true if the cooldown is active.
|
||||
--- The cooldown will be active for the given duration or 0 if no duration is given.
|
||||
--- Runs in the main loop.
|
||||
--- cooldown(true) will wait till the next tick.
|
||||
---@return fun(cooldown?: number|boolean): boolean
|
||||
function M.cooldown()
|
||||
local waiting = false
|
||||
---@param cooldown? number|boolean
|
||||
return function(cooldown)
|
||||
if waiting then
|
||||
return true
|
||||
elseif cooldown then
|
||||
waiting = true
|
||||
vim.defer_fn(function()
|
||||
waiting = false
|
||||
end, type(cooldown) == "number" and cooldown or 0)
|
||||
end
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
---@generic T: table
|
||||
---@param t T
|
||||
---@param fields string[]
|
||||
---@return T
|
||||
function M.getters(t, fields)
|
||||
local getters = {} ---@type table<string, fun():any>
|
||||
for _, prop in ipairs(fields) do
|
||||
if type(t[prop]) == "function" then
|
||||
getters[prop] = t[prop]
|
||||
rawset(t, prop, nil)
|
||||
end
|
||||
end
|
||||
|
||||
if not vim.tbl_isempty(getters) then
|
||||
setmetatable(t, {
|
||||
__index = function(_, key)
|
||||
if getters[key] then
|
||||
return getters[key](t)
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@ -1,349 +1,508 @@
|
||||
local Keys = require("which-key.keys")
|
||||
local config = require("which-key.config")
|
||||
local Buf = require("which-key.buf")
|
||||
local Config = require("which-key.config")
|
||||
local Icons = require("which-key.icons")
|
||||
local Layout = require("which-key.layout")
|
||||
local Plugins = require("which-key.plugins")
|
||||
local State = require("which-key.state")
|
||||
local Text = require("which-key.text")
|
||||
local Tree = require("which-key.tree")
|
||||
local Util = require("which-key.util")
|
||||
local Win = require("which-key.win")
|
||||
|
||||
local highlight = vim.api.nvim_buf_add_highlight
|
||||
|
||||
---@class View
|
||||
local M = {}
|
||||
M.view = nil ---@type wk.Win?
|
||||
M.footer = nil ---@type wk.Win?
|
||||
M.timer = (vim.uv or vim.loop).new_timer()
|
||||
|
||||
M.keys = ""
|
||||
M.mode = "n"
|
||||
M.reg = nil
|
||||
M.auto = false
|
||||
M.count = 0
|
||||
M.buf = nil
|
||||
M.win = nil
|
||||
---@alias wk.Sorter fun(node:wk.Item): (string|number)
|
||||
|
||||
function M.is_valid()
|
||||
return M.buf
|
||||
and M.win
|
||||
and vim.api.nvim_buf_is_valid(M.buf)
|
||||
and vim.api.nvim_buf_is_loaded(M.buf)
|
||||
and vim.api.nvim_win_is_valid(M.win)
|
||||
end
|
||||
---@type table<string, wk.Sorter>
|
||||
M.fields = {
|
||||
order = function(item)
|
||||
return item.order and item.order or 1000
|
||||
end,
|
||||
["local"] = function(item)
|
||||
return item.keymap and item.keymap.buffer ~= 0 and 0 or 1000
|
||||
end,
|
||||
manual = function(item)
|
||||
return item.mapping and item.mapping.idx or 10000
|
||||
end,
|
||||
desc = function(item)
|
||||
return item.desc or "~"
|
||||
end,
|
||||
group = function(item)
|
||||
return item.group and 1 or 0
|
||||
end,
|
||||
alphanum = function(item)
|
||||
return item.key:find("^%w+$") and 0 or 1
|
||||
end,
|
||||
mod = function(item)
|
||||
return item.key:find("^<.*>$") and 0 or 1
|
||||
end,
|
||||
case = function(item)
|
||||
return item.key:lower() == item.key and 0 or 1
|
||||
end,
|
||||
natural = function(item)
|
||||
local ret = item.key:gsub("%d+", function(d)
|
||||
return ("%09d"):format(tonumber(d))
|
||||
end)
|
||||
return ret:lower()
|
||||
end,
|
||||
}
|
||||
|
||||
function M.show()
|
||||
if vim.b.visual_multi then
|
||||
vim.b.VM_skip_reset_once_on_bufleave = true
|
||||
end
|
||||
if M.is_valid() then
|
||||
return
|
||||
end
|
||||
|
||||
-- non-floating windows
|
||||
local wins = vim.tbl_filter(function(w)
|
||||
return vim.api.nvim_win_is_valid(w) and vim.api.nvim_win_get_config(w).relative == ""
|
||||
end, vim.api.nvim_list_wins())
|
||||
|
||||
---@type number[]
|
||||
local margins = {}
|
||||
for i, m in ipairs(config.options.window.margin) do
|
||||
if m > 0 and m < 1 then
|
||||
if i % 2 == 0 then
|
||||
m = math.floor(vim.o.columns * m)
|
||||
else
|
||||
m = math.floor(vim.o.lines * m)
|
||||
---@param lhs string
|
||||
function M.format(lhs)
|
||||
local keys = Util.keys(lhs)
|
||||
local ret = vim.tbl_map(function(key)
|
||||
local inner = key:match("^<(.*)>$")
|
||||
if not inner then
|
||||
return key
|
||||
end
|
||||
if inner == "NL" then
|
||||
inner = "C-J"
|
||||
end
|
||||
local parts = vim.split(inner, "-", { plain = true })
|
||||
for i, part in ipairs(parts) do
|
||||
if i == 1 or i ~= #parts or not part:match("^%w$") then
|
||||
parts[i] = Config.icons.keys[part] or parts[i]
|
||||
end
|
||||
end
|
||||
margins[i] = m
|
||||
end
|
||||
|
||||
local opts = {
|
||||
relative = "editor",
|
||||
width = vim.o.columns - margins[2] - margins[4],
|
||||
height = config.options.layout.height.min,
|
||||
focusable = false,
|
||||
anchor = "SW",
|
||||
border = config.options.window.border,
|
||||
row = vim.o.lines
|
||||
- margins[3]
|
||||
+ ((vim.o.laststatus == 0 or vim.o.laststatus == 1 and #wins == 1) and 1 or 0)
|
||||
- vim.o.cmdheight,
|
||||
col = margins[4],
|
||||
style = "minimal",
|
||||
noautocmd = true,
|
||||
zindex = config.options.window.zindex,
|
||||
}
|
||||
if config.options.window.position == "top" then
|
||||
opts.anchor = "NW"
|
||||
opts.row = margins[1]
|
||||
end
|
||||
M.buf = vim.api.nvim_create_buf(false, true)
|
||||
M.win = vim.api.nvim_open_win(M.buf, false, opts)
|
||||
vim.bo[M.buf].filetype = "WhichKey"
|
||||
vim.bo[M.buf].buftype = "nofile"
|
||||
vim.bo[M.buf].bufhidden = "wipe"
|
||||
vim.bo[M.buf].modifiable = true
|
||||
vim.wo[M.win].winhighlight = "NormalFloat:WhichKeyFloat,FloatBorder:WhichKeyBorder"
|
||||
vim.wo[M.win].foldmethod = "manual"
|
||||
vim.wo[M.win].winblend = config.options.window.winblend
|
||||
return table.concat(parts, "")
|
||||
end, keys)
|
||||
return table.concat(ret, "")
|
||||
end
|
||||
|
||||
function M.read_pending()
|
||||
local esc = ""
|
||||
while true do
|
||||
local n = vim.fn.getchar(0)
|
||||
if n == 0 then
|
||||
break
|
||||
end
|
||||
local c = (type(n) == "number" and vim.fn.nr2char(n) or n)
|
||||
|
||||
-- HACK: for some reason, when executing a :norm command,
|
||||
-- vim keeps feeding <esc> at the end
|
||||
if c == Util.t("<esc>") then
|
||||
esc = esc .. c
|
||||
-- more than 10 <esc> in a row? most likely the norm bug
|
||||
if #esc > 10 then
|
||||
return
|
||||
end
|
||||
else
|
||||
-- we have <esc> characters, so add them to keys
|
||||
if esc ~= "" then
|
||||
M.keys = M.keys .. esc
|
||||
esc = ""
|
||||
end
|
||||
M.keys = M.keys .. c
|
||||
end
|
||||
end
|
||||
if esc ~= "" then
|
||||
M.keys = M.keys .. esc
|
||||
esc = ""
|
||||
end
|
||||
end
|
||||
|
||||
function M.getchar()
|
||||
local ok, n = pcall(vim.fn.getchar)
|
||||
|
||||
-- bail out on keyboard interrupt
|
||||
if not ok then
|
||||
return Util.t("<esc>")
|
||||
end
|
||||
|
||||
local c = (type(n) == "number" and vim.fn.nr2char(n) or n)
|
||||
return c
|
||||
end
|
||||
|
||||
function M.scroll(up)
|
||||
local height = vim.api.nvim_win_get_height(M.win)
|
||||
local cursor = vim.api.nvim_win_get_cursor(M.win)
|
||||
if up then
|
||||
cursor[1] = math.max(cursor[1] - height, 1)
|
||||
else
|
||||
cursor[1] = math.min(cursor[1] + height, vim.api.nvim_buf_line_count(M.buf))
|
||||
end
|
||||
vim.api.nvim_win_set_cursor(M.win, cursor)
|
||||
end
|
||||
|
||||
function M.on_close()
|
||||
M.hide()
|
||||
end
|
||||
|
||||
function M.hide()
|
||||
vim.api.nvim_echo({ { "" } }, false, {})
|
||||
M.hide_cursor()
|
||||
if M.buf and vim.api.nvim_buf_is_valid(M.buf) then
|
||||
vim.api.nvim_buf_delete(M.buf, { force = true })
|
||||
M.buf = nil
|
||||
end
|
||||
if M.win and vim.api.nvim_win_is_valid(M.win) then
|
||||
vim.api.nvim_win_close(M.win, true)
|
||||
M.win = nil
|
||||
end
|
||||
vim.cmd("redraw")
|
||||
end
|
||||
|
||||
function M.show_cursor()
|
||||
local buf = vim.api.nvim_get_current_buf()
|
||||
local cursor = vim.api.nvim_win_get_cursor(0)
|
||||
vim.api.nvim_buf_add_highlight(buf, config.namespace, "Cursor", cursor[1] - 1, cursor[2], cursor[2] + 1)
|
||||
end
|
||||
|
||||
function M.hide_cursor()
|
||||
local buf = vim.api.nvim_get_current_buf()
|
||||
vim.api.nvim_buf_clear_namespace(buf, config.namespace, 0, -1)
|
||||
end
|
||||
|
||||
function M.back()
|
||||
local node = Keys.get_tree(M.mode, M.buf).tree:get(M.keys, -1) or Keys.get_tree(M.mode).tree:get(M.keys, -1)
|
||||
if node then
|
||||
M.keys = node.prefix_i
|
||||
end
|
||||
end
|
||||
|
||||
function M.execute(prefix_i, mode, buf)
|
||||
local global_node = Keys.get_tree(mode).tree:get(prefix_i)
|
||||
local buf_node = buf and Keys.get_tree(mode, buf).tree:get(prefix_i) or nil
|
||||
|
||||
if global_node and global_node.mapping and Keys.is_hook(prefix_i, global_node.mapping.cmd) then
|
||||
return
|
||||
end
|
||||
if buf_node and buf_node.mapping and Keys.is_hook(prefix_i, buf_node.mapping.cmd) then
|
||||
return
|
||||
end
|
||||
|
||||
local hooks = {}
|
||||
|
||||
local function unhook(nodes, nodes_buf)
|
||||
for _, node in pairs(nodes) do
|
||||
if Keys.is_hooked(node.mapping.prefix, mode, nodes_buf) then
|
||||
table.insert(hooks, { node.mapping.prefix, nodes_buf })
|
||||
Keys.hook_del(node.mapping.prefix, mode, nodes_buf)
|
||||
---@param nodes wk.Item[]
|
||||
---@param fields? (string|wk.Sorter)[]
|
||||
function M.sort(nodes, fields)
|
||||
fields = vim.deepcopy(fields or Config.sort)
|
||||
vim.list_extend(fields, { "natural", "case" })
|
||||
table.sort(nodes, function(a, b)
|
||||
for _, f in ipairs(fields) do
|
||||
local field = type(f) == "function" and f or M.fields[f]
|
||||
if field then
|
||||
local aa = field(a)
|
||||
local bb = field(b)
|
||||
if aa ~= bb then
|
||||
return aa < bb
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- make sure we remove all WK hooks before executing the sequence
|
||||
-- this is to make existing keybindings work and prevent recursion
|
||||
unhook(Keys.get_tree(mode).tree:path(prefix_i))
|
||||
if buf then
|
||||
unhook(Keys.get_tree(mode, buf).tree:path(prefix_i), buf)
|
||||
end
|
||||
|
||||
-- feed CTRL-O again if called from CTRL-O
|
||||
local full_mode = Util.get_mode()
|
||||
if full_mode == "nii" or full_mode == "nir" or full_mode == "niv" or full_mode == "vs" then
|
||||
vim.api.nvim_feedkeys(Util.t("<C-O>"), "n", false)
|
||||
end
|
||||
|
||||
-- handle registers that were passed when opening the popup
|
||||
if M.reg ~= '"' and M.mode ~= "i" and M.mode ~= "c" then
|
||||
vim.api.nvim_feedkeys('"' .. M.reg, "n", false)
|
||||
end
|
||||
|
||||
if M.count and M.count ~= 0 then
|
||||
prefix_i = M.count .. prefix_i
|
||||
end
|
||||
|
||||
-- feed the keys with remap
|
||||
vim.api.nvim_feedkeys(prefix_i, "m", true)
|
||||
|
||||
-- defer hooking WK until after the keys were executed
|
||||
vim.defer_fn(function()
|
||||
for _, hook in pairs(hooks) do
|
||||
Keys.hook_add(hook[1], mode, hook[2])
|
||||
end
|
||||
end, 0)
|
||||
end
|
||||
|
||||
function M.open(keys, opts)
|
||||
opts = opts or {}
|
||||
M.keys = keys or ""
|
||||
M.mode = opts.mode or Util.get_mode()
|
||||
M.count = vim.api.nvim_get_vvar("count")
|
||||
M.reg = vim.api.nvim_get_vvar("register")
|
||||
|
||||
if string.find(vim.o.clipboard, "unnamedplus") and M.reg == "+" then
|
||||
M.reg = '"'
|
||||
end
|
||||
|
||||
if string.find(vim.o.clipboard, "unnamed") and M.reg == "*" then
|
||||
M.reg = '"'
|
||||
end
|
||||
|
||||
M.show_cursor()
|
||||
M.on_keys(opts)
|
||||
end
|
||||
|
||||
function M.is_enabled(buf)
|
||||
local buftype = vim.bo[buf].buftype
|
||||
for _, bt in ipairs(config.options.disable.buftypes) do
|
||||
if bt == buftype then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
local filetype = vim.bo[buf].filetype
|
||||
for _, bt in ipairs(config.options.disable.filetypes) do
|
||||
if bt == filetype then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
if vim.fn.getcmdwintype() ~= "" then
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function M.on_keys(opts)
|
||||
local buf = vim.api.nvim_get_current_buf()
|
||||
|
||||
while true do
|
||||
-- loop
|
||||
M.read_pending()
|
||||
|
||||
local results = Keys.get_mappings(M.mode, M.keys, buf)
|
||||
|
||||
--- Check for an exact match. Feedkeys with remap
|
||||
if results.mapping and not results.mapping.group and #results.mappings == 0 then
|
||||
M.hide()
|
||||
if results.mapping.fn then
|
||||
results.mapping.fn()
|
||||
else
|
||||
M.execute(M.keys, M.mode, buf)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- Check for no mappings found. Feedkeys without remap
|
||||
if #results.mappings == 0 then
|
||||
M.hide()
|
||||
-- only execute if an actual key was typed while WK was open
|
||||
if opts.auto then
|
||||
M.execute(M.keys, M.mode, buf)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local layout = Layout:new(results)
|
||||
|
||||
if M.is_enabled(buf) then
|
||||
if not M.is_valid() then
|
||||
M.show()
|
||||
end
|
||||
|
||||
M.render(layout:layout(M.win))
|
||||
end
|
||||
vim.cmd([[redraw]])
|
||||
|
||||
local c = M.getchar()
|
||||
|
||||
if c == Util.t("<esc>") then
|
||||
M.hide()
|
||||
break
|
||||
elseif c == Util.t(config.options.popup_mappings.scroll_down) then
|
||||
M.scroll(false)
|
||||
elseif c == Util.t(config.options.popup_mappings.scroll_up) then
|
||||
M.scroll(true)
|
||||
elseif c == Util.t("<bs>") then
|
||||
M.back()
|
||||
else
|
||||
M.keys = M.keys .. c
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param text Text
|
||||
function M.render(text)
|
||||
local view = vim.api.nvim_win_call(M.win, vim.fn.winsaveview)
|
||||
vim.api.nvim_buf_set_lines(M.buf, 0, -1, false, text.lines)
|
||||
local height = #text.lines
|
||||
if height > config.options.layout.height.max then
|
||||
height = config.options.layout.height.max
|
||||
end
|
||||
vim.api.nvim_win_set_height(M.win, height)
|
||||
if vim.api.nvim_buf_is_valid(M.buf) then
|
||||
vim.api.nvim_buf_clear_namespace(M.buf, config.namespace, 0, -1)
|
||||
end
|
||||
for _, data in ipairs(text.hl) do
|
||||
highlight(M.buf, config.namespace, data.group, data.line, data.from, data.to)
|
||||
end
|
||||
vim.api.nvim_win_call(M.win, function()
|
||||
vim.fn.winrestview(view)
|
||||
return a.raw_key < b.raw_key
|
||||
end)
|
||||
end
|
||||
|
||||
function M.valid()
|
||||
return M.view and M.view:valid()
|
||||
end
|
||||
|
||||
---@param opts? {delay?: number, schedule?: boolean, waited?: number}
|
||||
function M.update(opts)
|
||||
local state = State.state
|
||||
|
||||
if not state then
|
||||
M.hide()
|
||||
return
|
||||
end
|
||||
|
||||
opts = opts or {}
|
||||
if M.valid() then
|
||||
M.show()
|
||||
elseif opts.schedule ~= false then
|
||||
local delay = opts.delay
|
||||
or State.delay({
|
||||
mode = state.mode.mode,
|
||||
keys = state.node.keys,
|
||||
plugin = state.node.plugin,
|
||||
waited = opts.waited,
|
||||
})
|
||||
M.timer:start(
|
||||
delay,
|
||||
0,
|
||||
vim.schedule_wrap(function()
|
||||
Util.try(M.show)
|
||||
end)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
function M.hide()
|
||||
if M.view then
|
||||
M.view:hide()
|
||||
M.view = nil
|
||||
end
|
||||
if M.footer then
|
||||
M.footer:hide()
|
||||
M.footer = nil
|
||||
end
|
||||
end
|
||||
|
||||
---@param field string
|
||||
---@param value string
|
||||
---@return string
|
||||
function M.replace(field, value)
|
||||
for _, repl in pairs(Config.replace[field]) do
|
||||
value = type(repl) == "function" and (repl(value) or value) or value:gsub(repl[1], repl[2])
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
---@param node wk.Node
|
||||
function M.icon(node)
|
||||
-- plugin items should not get icons
|
||||
if node.parent and node.parent.plugin then
|
||||
return
|
||||
end
|
||||
if node.mapping and node.mapping.icon then
|
||||
return Icons.get(node.mapping.icon)
|
||||
end
|
||||
local icon, icon_hl = Icons.get({ keymap = node.keymap, desc = node.desc })
|
||||
if icon then
|
||||
return icon, icon_hl
|
||||
end
|
||||
if node.parent then
|
||||
return M.icon(node.parent)
|
||||
end
|
||||
end
|
||||
|
||||
---@param node wk.Node
|
||||
---@param opts? {default?: "count"|"path", parent?: wk.Node, group?: boolean}
|
||||
function M.item(node, opts)
|
||||
opts = opts or {}
|
||||
opts.default = opts.default or "count"
|
||||
local child_count = (node:can_expand() or opts.group == false) and 0 or node:count()
|
||||
local desc = node.desc
|
||||
if not desc and node.keymap and node.keymap.rhs ~= "" and type(node.keymap.rhs) == "string" then
|
||||
desc = node.keymap.rhs --[[@as string]]
|
||||
end
|
||||
if not desc and opts.default == "count" and child_count > 0 then
|
||||
desc = child_count .. " keymap" .. (child_count > 1 and "s" or "")
|
||||
end
|
||||
if not desc and opts.default == "path" then
|
||||
desc = node.keys
|
||||
end
|
||||
desc = M.replace("desc", desc or "")
|
||||
local icon, icon_hl = M.icon(node)
|
||||
|
||||
local raw_key = node.key
|
||||
if opts.parent and opts.parent ~= node and node.keys:find(opts.parent.keys, 1, true) == 1 then
|
||||
raw_key = node.keys:sub(opts.parent.keys:len() + 1)
|
||||
end
|
||||
|
||||
local group = node:is_group()
|
||||
---@type wk.Item
|
||||
return setmetatable({
|
||||
node = node,
|
||||
icon = icon or "",
|
||||
icon_hl = icon_hl,
|
||||
key = M.replace("key", raw_key),
|
||||
raw_key = raw_key,
|
||||
desc = group and Config.icons.group .. desc or desc,
|
||||
group = group,
|
||||
}, { __index = node })
|
||||
end
|
||||
|
||||
---@param node wk.Node
|
||||
---@param opts? {title?: boolean}
|
||||
function M.trail(node, opts)
|
||||
opts = opts or {}
|
||||
|
||||
---@param group? string
|
||||
local function hl(group)
|
||||
return opts.title and "WhichKeyTitle" or (group and ("WhichKey" .. group) or "WhichKeyGroup")
|
||||
end
|
||||
|
||||
local trail = {} ---@type string[][]
|
||||
local did_op = false
|
||||
while node do
|
||||
local desc = node.desc and (Config.icons.group .. M.replace("desc", node.desc))
|
||||
or node.key and M.replace("key", node.key)
|
||||
or ""
|
||||
node = node.parent
|
||||
if desc ~= "" then
|
||||
if node and #trail > 0 then
|
||||
table.insert(trail, 1, { " " .. Config.icons.breadcrumb .. " ", hl("Separator") })
|
||||
end
|
||||
table.insert(trail, 1, { desc, hl() })
|
||||
end
|
||||
local m = State.state.mode.mode
|
||||
if not did_op and not node and (m == "x" or m == "o") then
|
||||
did_op = true
|
||||
local mode = Buf.get({ buf = State.state.mode.buf.buf, mode = "n" })
|
||||
if mode then
|
||||
node = mode.tree:find(m == "x" and "v" or vim.v.operator)
|
||||
end
|
||||
end
|
||||
end
|
||||
if #trail > 0 then
|
||||
table.insert(trail, 1, { " ", hl() })
|
||||
table.insert(trail, { " ", hl() })
|
||||
return trail
|
||||
end
|
||||
end
|
||||
|
||||
---@param root wk.Node
|
||||
---@param node wk.Node
|
||||
---@param expand fun(node:wk.Node): boolean
|
||||
---@param filter fun(node:wk.Node): boolean
|
||||
---@param ret? wk.Item[]
|
||||
function M.expand(root, node, expand, filter, ret)
|
||||
ret = ret or {}
|
||||
if not filter(node) then
|
||||
return ret
|
||||
end
|
||||
if not node:is_plugin() and expand(node) then
|
||||
if node.keymap then
|
||||
ret[#ret + 1] = M.item(node, { group = false, parent = root })
|
||||
end
|
||||
for _, child in ipairs(node:children()) do
|
||||
M.expand(root, child, expand, filter, ret)
|
||||
end
|
||||
else
|
||||
ret[#ret + 1] = M.item(node, { parent = root })
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
function M.show()
|
||||
local state = State.state
|
||||
if not (state and state.show and state.node:is_group()) then
|
||||
M.hide()
|
||||
return
|
||||
end
|
||||
local text = Text.new()
|
||||
|
||||
---@type wk.Node[]
|
||||
local children = state.node:children()
|
||||
|
||||
if state.filter.global == false and state.filter.expand == nil then
|
||||
state.filter.expand = true
|
||||
end
|
||||
|
||||
---@param node wk.Node
|
||||
local function filter(node)
|
||||
local l = state.filter["local"] ~= false
|
||||
local g = state.filter.global ~= false
|
||||
if not g and not l then
|
||||
return false
|
||||
end
|
||||
if g and l then
|
||||
return true
|
||||
end
|
||||
local is_local = node:is_local()
|
||||
return l and is_local or g and not is_local
|
||||
end
|
||||
|
||||
---@param node wk.Node
|
||||
local function expand(node)
|
||||
if node:is_plugin() then
|
||||
return false
|
||||
end
|
||||
if state.filter.expand then
|
||||
return true
|
||||
end
|
||||
if node:can_expand() then
|
||||
return false
|
||||
end
|
||||
if type(Config.expand) == "function" then
|
||||
return Config.expand(node)
|
||||
end
|
||||
local child_count = node:count()
|
||||
return child_count > 0 and child_count <= Config.expand
|
||||
end
|
||||
|
||||
---@type wk.Item[]
|
||||
local items = {}
|
||||
for _, node in ipairs(children) do
|
||||
vim.list_extend(items, M.expand(state.node, node, expand, filter))
|
||||
end
|
||||
|
||||
M.sort(items)
|
||||
|
||||
---@type wk.Col[]
|
||||
local cols = {
|
||||
{ key = "key", hl = "WhichKey", align = "right" },
|
||||
{ key = "sep", hl = "WhichKeySeparator", default = Config.icons.separator },
|
||||
{ key = "icon", padding = { 0, 0 } },
|
||||
}
|
||||
if state.node.plugin then
|
||||
vim.list_extend(cols, Plugins.cols(state.node.plugin))
|
||||
end
|
||||
cols[#cols + 1] = { key = "desc", width = math.huge }
|
||||
|
||||
local t = Layout.new({ cols = cols, rows = items })
|
||||
|
||||
local opts = Win.defaults(Config.win)
|
||||
local container = {
|
||||
width = Layout.dim(vim.o.columns, vim.o.columns, opts.width),
|
||||
height = Layout.dim(vim.o.lines, vim.o.lines, opts.height),
|
||||
}
|
||||
local _, _, max_row_width = t:cells()
|
||||
local box_width = Layout.dim(max_row_width, container.width, Config.layout.width)
|
||||
local box_count = math.max(math.floor(container.width / (box_width + Config.layout.spacing)), 1)
|
||||
box_width = math.floor(container.width / box_count)
|
||||
local box_height = math.max(math.ceil(#items / box_count), 2)
|
||||
|
||||
local rows = t:layout({ width = box_width - Config.layout.spacing })
|
||||
|
||||
for _ = 1, Config.win.padding[1] + 1 do
|
||||
text:nl()
|
||||
end
|
||||
|
||||
for l = 1, box_height do
|
||||
text:append(string.rep(" ", Config.win.padding[2]))
|
||||
for b = 1, box_count do
|
||||
local i = (b - 1) * box_height + l
|
||||
local item = items[i]
|
||||
local row = rows[i]
|
||||
if b ~= 1 or box_count > 1 then
|
||||
text:append(string.rep(" ", Config.layout.spacing))
|
||||
end
|
||||
if item then
|
||||
for c, col in ipairs(row) do
|
||||
local hl = col.hl
|
||||
if cols[c].key == "desc" then
|
||||
hl = item.group and "WhichKeyGroup" or "WhichKeyDesc"
|
||||
end
|
||||
if cols[c].key == "icon" then
|
||||
hl = item.icon_hl
|
||||
end
|
||||
text:append(col.value, hl)
|
||||
end
|
||||
end
|
||||
end
|
||||
text:append(string.rep(" ", Config.win.padding[2]))
|
||||
text:nl()
|
||||
end
|
||||
text:trim()
|
||||
|
||||
for _ = 1, Config.win.padding[1] do
|
||||
text:nl()
|
||||
end
|
||||
|
||||
local show_keys = Config.show_keys
|
||||
|
||||
local has_border = opts.border and opts.border ~= "none"
|
||||
if has_border then
|
||||
if opts.title == true then
|
||||
opts.title = M.trail(state.node, { title = true })
|
||||
show_keys = false
|
||||
end
|
||||
if opts.footer == true then
|
||||
opts.footer = M.trail(state.node, { title = true })
|
||||
show_keys = false
|
||||
end
|
||||
if not opts.title then
|
||||
opts.title = ""
|
||||
opts.title_pos = nil
|
||||
end
|
||||
if not opts.footer then
|
||||
opts.footer = ""
|
||||
opts.footer_pos = nil
|
||||
end
|
||||
else
|
||||
opts.footer = nil
|
||||
opts.footer_pos = nil
|
||||
opts.title = nil
|
||||
opts.title_pos = nil
|
||||
end
|
||||
|
||||
local bw = has_border and 2 or 0
|
||||
|
||||
opts.width = Layout.dim(text:width() + bw, vim.o.columns, opts.width)
|
||||
opts.height = Layout.dim(text:height() + bw, vim.o.lines, opts.height)
|
||||
|
||||
if Config.show_help then
|
||||
opts.height = opts.height + 1
|
||||
end
|
||||
|
||||
-- top-left
|
||||
opts.col = Layout.dim(opts.col, vim.o.columns - opts.width)
|
||||
opts.row = Layout.dim(opts.row, vim.o.lines - opts.height - vim.o.cmdheight)
|
||||
|
||||
opts.width = opts.width - bw
|
||||
opts.height = opts.height - bw
|
||||
M.check_overlap(opts)
|
||||
|
||||
M.view = M.view or Win.new(opts)
|
||||
M.view:show(opts)
|
||||
|
||||
if Config.show_help or show_keys then
|
||||
text:nl()
|
||||
local footer = Text.new()
|
||||
if show_keys then
|
||||
footer:append(" ")
|
||||
for _, segment in ipairs(M.trail(state.node) or {}) do
|
||||
footer:append(segment[1], segment[2])
|
||||
end
|
||||
end
|
||||
if Config.show_help then
|
||||
---@type {key: string, desc: string}[]
|
||||
local keys = {
|
||||
{ key = "<esc>", desc = "close" },
|
||||
}
|
||||
if state.node.parent then
|
||||
keys[#keys + 1] = { key = "<bs>", desc = "back" }
|
||||
end
|
||||
if opts.height < text:height() then
|
||||
keys[#keys + 1] = { key = "<c-d>/<c-u>", desc = "scroll" }
|
||||
end
|
||||
local help = Text.new()
|
||||
for k, key in ipairs(keys) do
|
||||
help:append(M.replace("key", Util.norm(key.key)), "WhichKey"):append(" " .. key.desc, "WhichKeySeparator")
|
||||
if k < #keys then
|
||||
help:append(" ")
|
||||
end
|
||||
end
|
||||
local col = footer:col({ display = true })
|
||||
local ws = string.rep(" ", math.floor((opts.width - help:width()) / 2) - col)
|
||||
footer:append(ws)
|
||||
footer:append(help._lines[1])
|
||||
end
|
||||
footer:trim()
|
||||
M.footer = M.footer or Win.new()
|
||||
M.footer:show({
|
||||
relative = "win",
|
||||
win = M.view.win,
|
||||
col = 0,
|
||||
row = opts.height - 1,
|
||||
width = opts.width,
|
||||
height = 1,
|
||||
zindex = M.view.opts.zindex + 1,
|
||||
})
|
||||
footer:render(M.footer.buf)
|
||||
end
|
||||
|
||||
text:render(M.view.buf)
|
||||
vim.api.nvim_win_call(M.view.win, function()
|
||||
vim.fn.winrestview({ topline = 1 })
|
||||
end)
|
||||
vim.cmd.redraw()
|
||||
end
|
||||
|
||||
---@param opts wk.Win.opts
|
||||
function M.check_overlap(opts)
|
||||
if Config.win.no_overlap == false then
|
||||
return
|
||||
end
|
||||
local row, col = vim.fn.screenrow(), vim.fn.screencol()
|
||||
local overlaps = (row >= opts.row and row <= opts.row + opts.height)
|
||||
and (col >= opts.col and col <= opts.col + opts.width)
|
||||
-- dd(overlaps and "overlaps" or "no overlap", {
|
||||
-- editor = { lines = vim.o.lines, columns = vim.o.columns },
|
||||
-- cursor = { col = col, row = row },
|
||||
-- win = { row = opts.row, col = opts.col, height = opts.height, width = opts.width },
|
||||
-- overlaps = overlaps,
|
||||
-- })
|
||||
if overlaps then
|
||||
opts.row = row + 1
|
||||
opts.height = math.max(vim.o.lines - opts.row, 4)
|
||||
end
|
||||
end
|
||||
|
||||
---@param up boolean
|
||||
function M.scroll(up)
|
||||
return M.view and M.view:scroll(up)
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
Reference in New Issue
Block a user