1

Refresh generated neovim config

This commit is contained in:
2024-08-15 13:01:03 +02:00
parent 64b51cf53a
commit f5af8e2b28
1836 changed files with 38979 additions and 31094 deletions

View File

@ -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

View File

@ -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,
})

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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