1

Regenerate nvim config

This commit is contained in:
2024-06-02 03:29:20 +02:00
parent 75eea0c030
commit ef2e28883d
5576 changed files with 604886 additions and 503 deletions

View File

@ -0,0 +1,19 @@
local M = {}
local links = {
[""] = "Function",
Separator = "Comment",
Group = "Keyword",
Desc = "Identifier",
Float = "NormalFloat",
Border = "FloatBorder",
Value = "Comment",
}
function M.setup()
for k, v in pairs(links) do
vim.api.nvim_set_hl(0, "WhichKey" .. k, { link = v, default = true })
end
end
return M

View File

@ -0,0 +1,105 @@
local M = {}
M.namespace = vim.api.nvim_create_namespace("WhichKey")
---@class Options
local defaults = {
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
-- the presets plugin, adds help for a bunch of default keybindings in Neovim
-- No actual key bindings are created
spelling = {
enabled = true, -- enabling this will show WhichKey when pressing z= to select spelling suggestions
suggestions = 20, -- how many suggestions should be shown in the list?
},
presets = {
operators = true, -- adds help for operators like d, y, ...
motions = true, -- adds help for motions
text_objects = true, -- help for text objects triggered after entering an operator
windows = true, -- default bindings on <c-w>
nav = true, -- misc bindings to work with windows
z = true, -- bindings for folds, spelling and others prefixed with z
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",
},
motions = {
count = true,
},
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
},
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 specifiy 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 deafult for Telescope
disable = {
buftypes = {},
filetypes = {},
},
}
---@type Options
M.options = {}
---@param options? Options
function M.setup(options)
M.options = vim.tbl_deep_extend("force", {}, defaults, options or {})
end
M.setup()
return M

View File

@ -0,0 +1,57 @@
local Keys = require("which-key.keys")
local M = {}
local start = vim.health.start or vim.health.report_start
local ok = vim.health.ok or vim.health.report_ok
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
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
)
end
if conflicts == 0 then
ok("No conflicting keymaps found")
return
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"
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)
end
info(("old rhs: `%s`"):format(dup.other.rhs or ""))
info(("new rhs: `%s`"):format(dup.cmd or ""))
end
end
return M

View File

@ -0,0 +1,108 @@
local Keys = require("which-key.keys")
local Util = require("which-key.util")
---@class WhichKey
local M = {}
local loaded = false -- once we loaded everything
local scheduled = 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.execute(id)
local func = Keys.functions[id]
return func()
end
function M.show(keys, opts)
opts = opts or {}
if type(opts) == "string" then
opts = { mode = opts }
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)"
)
else
M.show(keys, { mode = mode })
end
end
local queue = {}
-- Defer registering keymaps until VimEnter
function M.register(mappings, opts)
schedule_load()
if loaded then
Keys.register(mappings, opts)
Keys.update()
else
table.insert(queue, { mappings, opts })
end
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
end
function M.reset()
-- local mappings = Keys.mappings
require("plenary.reload").reload_module("which-key")
-- require("which-key.Keys").mappings = mappings
require("which-key").setup()
end
return M

View File

@ -0,0 +1,438 @@
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.functions = {}
M.operators = {}
M.nowait = {}
M.blacklist = {}
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)
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 then
return op_i, op_n
end
end
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.label ~= "which_key_ignore"
and child.mapping.desc ~= "which_key_ignore"
and not (child.mapping.group and vim.tbl_isempty(child.children))
then
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.label 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.label = value.label or "+prefix"
value.label = value.label:gsub("^%+", "")
value.label = Config.options.icons.group .. value.label
elseif not value.label then
value.label = value.desc or value.cmd or ""
for _, v in ipairs(Config.options.hidden) do
value.label = value.label: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 decendents 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.label 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? buffer
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)
if is_nop(keymap) then
skip = true
end
if not skip then
local mapping = {
prefix = keymap.lhs,
cmd = keymap.rhs,
desc = keymap.desc,
callback = keymap.callback,
keys = Util.parse_keys(keymap.lhs),
}
-- 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.label = mapping.desc
end
end
end
end
end
return M

View File

@ -0,0 +1,216 @@
local Config = require("which-key.config")
local Text = require("which-key.text")
local Keys = require("which-key.keys")
local Util = require("which-key.util")
---@class Layout
---@field mapping Mapping
---@field items VisualMapping[]
---@field options Options
---@field text Text
---@field results MappingGroup
local Layout = {}
Layout.__index = Layout
---@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
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])
end
end
return max
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.label) then
node = path[i]
end
local step = self.mapping.keys.notation[i]
if node and node.mapping and node.mapping.label then
step = self.options.icons.group .. node.mapping.label
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" })
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) })
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
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
local max_key_width = self:max_width("key")
local max_label_width = self:max_width("label")
local max_value_width = self:max_width("value")
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
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
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)
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
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])
end
end
start = start + max_value_width + 2
end
local label = item.label
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
end
end
for _ = 1, pad_bot, 1 do
self.text:nl()
end
self:trail()
return self.text
end
return Layout

View File

@ -0,0 +1,231 @@
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
local mapargs = {
"noremap",
"desc",
"expr",
"silent",
"nowait",
"script",
"unique",
"callback",
"replace_keycodes", -- TODO: add config setting for default value
}
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
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
end
function M._parse(value, mappings, opts)
if type(value) ~= "table" then
value = { value }
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 = 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
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
end
mapping.buf = mapping.buffer
mapping.buffer = nil
mapping.mode = mapping.mode or "n"
mapping.label = mapping.desc or mapping.name
mapping.keys = Util.parse_keys(mapping.prefix or "")
local opts = {}
for _, o in ipairs(mapargs) do
opts[o] = mapping[o]
mapping[o] = nil
end
if vim.fn.has("nvim-0.7.0") == 0 then
opts.replace_keycodes = nil
-- Neovim < 0.7.0 doesn't support descriptions
opts.desc = nil
-- use lua functions proxy for Neovim < 0.7.0
if opts.callback then
local functions = require("which-key.keys").functions
table.insert(functions, opts.callback)
if opts.expr then
opts.cmd = string.format([[luaeval('require("which-key").execute(%d)')]], #functions)
else
opts.cmd = string.format([[<cmd>lua require("which-key").execute(%d)<cr>]], #functions)
end
opts.callback = nil
end
end
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)
end
end
---@return Mapping[]
function M.parse(mappings, 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)
end
return M

View File

@ -0,0 +1,59 @@
local Keys = require("which-key.keys")
local Util = require("which-key.util")
local Config = require("which-key.config")
local M = {}
M.plugins = {}
function M.setup()
for name, opts in pairs(Config.options.plugins) do
-- only setup plugin if we didnt load it before
if not M.plugins[name] then
if type(opts) == "boolean" then
opts = { enabled = opts }
end
opts.enabled = opts.enabled ~= false
if opts.enabled then
M.plugins[name] = require("which-key.plugins." .. name)
M._setup(M.plugins[name], opts)
end
end
end
end
---@param plugin 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
end
if plugin.setup then
plugin.setup(require("which-key"), opts, Config.options)
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
return ret
end
return M

View File

@ -0,0 +1,66 @@
local M = {}
M.name = "marks"
M.actions = {
{ trigger = "`", mode = "n" },
{ trigger = "'", mode = "n" },
{ trigger = "g`", mode = "n" },
{ trigger = "g'", mode = "n" },
}
function M.setup(_wk, _config, options) end
local labels = {
["^"] = "Last position of cursor in insert mode",
["."] = "Last change in current buffer",
['"'] = "Last exited current buffer",
["0"] = "In last file edited",
["'"] = "Back to line in current buffer where jumped from",
["`"] = "Back to position in current buffer where jumped from",
["["] = "To beginning of previously changed or yanked text",
["]"] = "To end of previously changed or yanked text",
["<lt>"] = "To beginning of last visual selection",
[">"] = "To end of last visual selection",
}
---@type Plugin
---@return PluginItem[]
function M.run(_trigger, _mode, buf)
local items = {}
local marks = {}
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
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
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,
label = labels[key] or file and ("file: " .. file) or "",
value = value,
highlights = { { 1, 5, "Number" } },
})
end
return items
end
return M

View File

@ -0,0 +1,107 @@
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

@ -0,0 +1,97 @@
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",
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
end
return M

View File

@ -0,0 +1,52 @@
---@type 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" },
}
function M.setup(_wk, _config, options) end
M.registers = '*+"-:.%/#=_abcdefghijklmnopqrstuvwxyz0123456789'
local labels = {
['"'] = "last deleted, changed, or yanked content",
["0"] = "last yank",
["-"] = "deleted or changed content smaller than one line",
["."] = "last inserted text",
["%"] = "name of the current file",
[":"] = "most recent executed command",
["#"] = "alternate buffer",
["="] = "result of an expression",
["+"] = "synchronized with the system clipboard",
["*"] = "synchronized with the selection clipboard",
["_"] = "black hole",
["/"] = "last search pattern",
}
---@type Plugin
---@return PluginItem[]
function M.run(_trigger, _mode, _buf)
local items = {}
for i = 1, #M.registers, 1 do
local key = M.registers:sub(i, i)
local ok, value = pcall(vim.fn.getreg, key, 1)
if not ok then
value = ""
end
if value ~= "" then
table.insert(items, { key = key, label = labels[key] or "", value = value })
end
end
return items
end
return M

View File

@ -0,0 +1,51 @@
local M = {}
M.name = "spelling"
M.actions = { { trigger = "z=", mode = "n" } }
M.opts = {}
function M.setup(_, config, options)
M.opts = config
end
---@type Plugin
---@return PluginItem[]
function M.run()
-- if started with a count, let the default keybinding work
local count = vim.api.nvim_get_vvar("count")
if count and count > 0 then
return {}
end
---@diagnostic disable-next-line: missing-parameter
local cursor_word = vim.fn.expand("<cword>")
-- get a misspellled 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 suggestions = vim.fn.spellsuggest(word, M.opts.suggestions or 20, bad[2] == "caps" and 1 or 0)
local items = {}
local keys = "1234567890abcdefghijklmnopqrstuvwxyz"
for i, label in ipairs(suggestions) do
local key = keys:sub(i, i)
table.insert(items, {
key = key,
label = label,
fn = function()
vim.cmd("norm! ciw" .. label)
end,
})
end
return items
end
return M

View File

@ -0,0 +1,72 @@
---@class Highlight
---@field group string
---@field line number
---@field from number
---@field to number
---@class Text
---@field lines string[]
---@field hl Highlight[]
---@field lineNr number
---@field current string
local Text = {}
Text.__index = Text
function Text.len(str)
return vim.fn.strwidth(str)
end
function Text:new()
local this = { lines = {}, hl = {}, lineNr = 0, current = "" }
setmetatable(this, self)
return this
end
function Text:fix_nl(line)
return line:gsub("[\n]", "")
end
function Text:nl()
local line = self:fix_nl(self.current)
table.insert(self.lines, line)
self.current = ""
self.lineNr = self.lineNr + 1
end
function Text:set(row, col, str, group)
str = self:fix_nl(str)
-- extend lines if needed
for i = 1, row, 1 do
if not self.lines[i] then
self.lines[i] = ""
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)
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 })
end
return Text

View File

@ -0,0 +1,108 @@
local Util = require("which-key.util")
---@class Tree
---@field root Node
---@field nodes table<string, Node>
local Tree = {}
Tree.__index = Tree
---@class Node
---@field mapping Mapping
---@field prefix_i string
---@field prefix_n string
---@field children table<string, Node>
-- selene: allow(unused_variable)
local Node
---@return Tree
function Tree:new()
local this = { root = { children = {}, prefix_i = "", prefix_n = "" }, nodes = {} }
setmetatable(this, self)
return this
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
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
end
return path
end
---@param mapping Mapping
---@param opts? {cache?: boolean}
---@return Node
function Tree:add(mapping, 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 }
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

View File

@ -0,0 +1,74 @@
---@meta
--# 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 label string
---@field desc string
---@field prefix string
---@field cmd string
---@field opts MappingOptions
---@field keys KeyCodes
---@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
---@class VisualMapping : Mapping
---@field key string
---@field highlights table
---@field value string
---@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
---@field name string
---@field actions PluginAction[]
---@field run fun(trigger:string, mode:string, buf:number):PluginItem[]
---@field setup fun(wk, opts, Options)

View File

@ -0,0 +1,196 @@
---@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
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]
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
local Tokens = {
["<"] = strbyte("<"),
[">"] = strbyte(">"),
["-"] = strbyte("-"),
}
---@return KeyCodes
function M.parse_keys(keystr)
M.check_cache()
if cache[keystr] then
return cache[keystr]
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
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
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
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
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
end
return keys
end
function M.warn(msg)
vim.notify(msg, vim.log.levels.WARN, { title = "WhichKey" })
end
function M.error(msg)
vim.notify(msg, vim.log.levels.ERROR, { title = "WhichKey" })
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))
return false
end
return true
end
return M

View File

@ -0,0 +1,350 @@
local Keys = require("which-key.keys")
local config = require("which-key.config")
local Layout = require("which-key.layout")
local Util = require("which-key.util")
local highlight = vim.api.nvim_buf_add_highlight
---@class View
local M = {}
M.keys = ""
M.mode = "n"
M.reg = nil
M.auto = false
M.count = 0
M.buf = nil
M.win = nil
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
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)
end
end
margins[i] = m
end
local opts = {
relative = "editor",
width = vim.o.columns
- margins[2]
- margins[4]
- (vim.fn.has("nvim-0.6") == 0 and config.options.window.border ~= "none" and 2 or 0),
height = config.options.layout.height.min,
focusable = false,
anchor = "SW",
border = config.options.window.border,
row = vim.o.lines
- margins[3]
- (vim.fn.has("nvim-0.6") == 0 and config.options.window.border ~= "none" and 2 or 0)
+ ((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.api.nvim_buf_set_option(M.buf, "filetype", "WhichKey")
vim.api.nvim_buf_set_option(M.buf, "buftype", "nofile")
vim.api.nvim_buf_set_option(M.buf, "bufhidden", "wipe")
vim.api.nvim_buf_set_option(M.buf, "modifiable", true)
local winhl = "NormalFloat:WhichKeyFloat"
if vim.fn.hlexists("FloatBorder") == 1 then
winhl = winhl .. ",FloatBorder:WhichKeyBorder"
end
vim.api.nvim_win_set_option(M.win, "winhighlight", winhl)
vim.api.nvim_win_set_option(M.win, "foldmethod", "manual")
vim.api.nvim_win_set_option(M.win, "winblend", config.options.window.winblend)
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)
end
end
end
-- make sure we remove all WK hooks before executing the sequence
-- this is to make existing keybindongs 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.api.nvim_buf_get_option(buf, "buftype")
for _, bt in ipairs(config.options.disable.buftypes) do
if bt == buftype then
return false
end
end
local filetype = vim.api.nvim_buf_get_option(buf, "filetype")
for _, bt in ipairs(config.options.disable.filetypes) do
if bt == filetype then
return false
end
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)
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
end
return M