Regenerate nvim config
This commit is contained in:
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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)
|
||||
@ -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
|
||||
@ -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
|
||||
Reference in New Issue
Block a user