Meh I'll figure out submodules later

This commit is contained in:
mustard 2025-09-16 01:01:02 +02:00
parent 4ca9d44a90
commit 8cb281f436
352 changed files with 66107 additions and 0 deletions

View file

@ -0,0 +1,478 @@
local buf_storage = require("nui.utils.buf_storage")
local is_type = require("nui.utils").is_type
local feature = require("nui.utils")._.feature
local autocmd = {
event = {
-- after adding a buffer to the buffer list
BufAdd = "BufAdd",
-- deleting a buffer from the buffer list
BufDelete = "BufDelete",
-- after entering a buffer
BufEnter = "BufEnter",
-- after renaming a buffer
BufFilePost = "BufFilePost",
-- before renaming a buffer
BufFilePre = "BufFilePre",
-- just after buffer becomes hidden
BufHidden = "BufHidden",
-- before leaving a buffer
BufLeave = "BufLeave",
-- after the 'modified' state of a buffer changes
BufModifiedSet = "BufModifiedSet",
-- after creating any buffer
BufNew = "BufNew",
-- when creating a buffer for a new file
BufNewFile = "BufNewFile",
-- read buffer using command
BufReadCmd = "BufReadCmd",
-- after reading a buffer
BufReadPost = "BufReadPost",
-- before reading a buffer
BufReadPre = "BufReadPre",
-- just before unloading a buffer
BufUnload = "BufUnload",
-- after showing a buffer in a window
BufWinEnter = "BufWinEnter",
-- just after buffer removed from window
BufWinLeave = "BufWinLeave",
-- just before really deleting a buffer
BufWipeout = "BufWipeout",
-- write buffer using command
BufWriteCmd = "BufWriteCmd",
-- after writing a buffer
BufWritePost = "BufWritePost",
-- before writing a buffer
BufWritePre = "BufWritePre",
-- info was received about channel
ChanInfo = "ChanInfo",
-- channel was opened
ChanOpen = "ChanOpen",
-- command undefined
CmdUndefined = "CmdUndefined",
-- command line was modified
CmdlineChanged = "CmdlineChanged",
-- after entering cmdline mode
CmdlineEnter = "CmdlineEnter",
-- before leaving cmdline mode
CmdlineLeave = "CmdlineLeave",
-- after entering the cmdline window
CmdWinEnter = "CmdwinEnter",
-- before leaving the cmdline window
CmdWinLeave = "CmdwinLeave",
-- after loading a colorscheme
ColorScheme = "ColorScheme",
-- before loading a colorscheme
ColorSchemePre = "ColorSchemePre",
-- after popup menu changed
CompleteChanged = "CompleteChanged",
-- after finishing insert complete
CompleteDone = "CompleteDone",
-- idem, before clearing info
CompleteDonePre = "CompleteDonePre",
-- cursor in same position for a while
CursorHold = "CursorHold",
-- idem, in Insert mode
CursorHoldI = "CursorHoldI",
-- cursor was moved
CursorMoved = "CursorMoved",
-- cursor was moved in Insert mode
CursorMovedI = "CursorMovedI",
-- diffs have been updated
DiffUpdated = "DiffUpdated",
-- directory changed
DirChanged = "DirChanged",
-- after changing the 'encoding' option
EncodingChanged = "EncodingChanged",
-- before exiting
ExitPre = "ExitPre",
-- append to a file using command
FileAppendCmd = "FileAppendCmd",
-- after appending to a file
FileAppendPost = "FileAppendPost",
-- before appending to a file
FileAppendPre = "FileAppendPre",
-- before first change to read-only file
FileChangedRO = "FileChangedRO",
-- after shell command that changed file
FileChangedShell = "FileChangedShell",
-- after (not) reloading changed file
FileChangedShellPost = "FileChangedShellPost",
-- read from a file using command
FileReadCmd = "FileReadCmd",
-- after reading a file
FileReadPost = "FileReadPost",
-- before reading a file
FileReadPre = "FileReadPre",
-- new file type detected (user defined)
FileType = "FileType",
-- write to a file using command
FileWriteCmd = "FileWriteCmd",
-- after writing a file
FileWritePost = "FileWritePost",
-- before writing a file
FileWritePre = "FileWritePre",
-- after reading from a filter
FilterReadPost = "FilterReadPost",
-- before reading from a filter
FilterReadPre = "FilterReadPre",
-- after writing to a filter
FilterWritePost = "FilterWritePost",
-- before writing to a filter
FilterWritePre = "FilterWritePre",
-- got the focus
FocusGained = "FocusGained",
-- lost the focus to another app
FocusLost = "FocusLost",
-- if calling a function which doesn't exist
FuncUndefined = "FuncUndefined",
-- after starting the GUI
GUIEnter = "GUIEnter",
-- after starting the GUI failed
GUIFailed = "GUIFailed",
-- when changing Insert/Replace mode
InsertChange = "InsertChange",
-- before inserting a char
InsertCharPre = "InsertCharPre",
-- when entering Insert mode
InsertEnter = "InsertEnter",
-- just after leaving Insert mode
InsertLeave = "InsertLeave",
-- just before leaving Insert mode
InsertLeavePre = "InsertLeavePre",
-- just before popup menu is displayed
MenuPopup = "MenuPopup",
-- after changing the mode
ModeChanged = "ModeChanged",
-- after setting any option
OptionSet = "OptionSet",
-- after :make, :grep etc.
QuickFixCmdPost = "QuickFixCmdPost",
-- before :make, :grep etc.
QuickFixCmdPre = "QuickFixCmdPre",
-- before :quit
QuitPre = "QuitPre",
-- upon string reception from a remote vim
RemoteReply = "RemoteReply",
-- when the search wraps around the document
SearchWrapped = "SearchWrapped",
-- after loading a session file
SessionLoadPost = "SessionLoadPost",
-- after ":!cmd"
ShellCmdPost = "ShellCmdPost",
-- after ":1,2!cmd", ":w !cmd", ":r !cmd".
ShellFilterPost = "ShellFilterPost",
-- after nvim process received a signal
Signal = "Signal",
-- sourcing a Vim script using command
SourceCmd = "SourceCmd",
-- after sourcing a Vim script
SourcePost = "SourcePost",
-- before sourcing a Vim script
SourcePre = "SourcePre",
-- spell file missing
SpellFileMissing = "SpellFileMissing",
-- after reading from stdin
StdinReadPost = "StdinReadPost",
-- before reading from stdin
StdinReadPre = "StdinReadPre",
-- found existing swap file
SwapExists = "SwapExists",
-- syntax selected
Syntax = "Syntax",
-- a tab has closed
TabClosed = "TabClosed",
-- after entering a tab page
TabEnter = "TabEnter",
-- before leaving a tab page
TabLeave = "TabLeave",
-- when creating a new tab
TabNew = "TabNew",
-- after entering a new tab
TabNewEntered = "TabNewEntered",
-- after changing 'term'
TermChanged = "TermChanged",
-- after the process exits
TermClose = "TermClose",
-- after entering Terminal mode
TermEnter = "TermEnter",
-- after leaving Terminal mode
TermLeave = "TermLeave",
-- after opening a terminal buffer
TermOpen = "TermOpen",
-- after setting "v:termresponse"
TermResponse = "TermResponse",
-- text was modified
TextChanged = "TextChanged",
-- text was modified in Insert mode(no popup)
TextChangedI = "TextChangedI",
-- text was modified in Insert mode(popup)
TextChangedP = "TextChangedP",
-- after a yank or delete was done (y, d, c)
TextYankPost = "TextYankPost",
-- after UI attaches
UIEnter = "UIEnter",
-- after UI detaches
UILeave = "UILeave",
-- user defined autocommand
User = "User",
-- whenthe user presses the same key 42 times
UserGettingBored = "UserGettingBored",
-- after starting Vim
VimEnter = "VimEnter",
-- before exiting Vim
VimLeave = "VimLeave",
-- before exiting Vim and writing ShaDa file
VimLeavePre = "VimLeavePre",
-- after Vim window was resized
VimResized = "VimResized",
-- after Nvim is resumed
VimResume = "VimResume",
-- before Nvim is suspended
VimSuspend = "VimSuspend",
-- after closing a window
WinClosed = "WinClosed",
-- after entering a window
WinEnter = "WinEnter",
-- before leaving a window
WinLeave = "WinLeave",
-- when entering a new window
WinNew = "WinNew",
-- after scrolling a window
WinScrolled = "WinScrolled",
-- alias for `BufAdd`
BufCreate = "BufAdd",
-- alias for `BufReadPost`
BufRead = "BufReadPost",
-- alias for `BufWritePre`
BufWrite = "BufWritePre",
-- alias for `EncodingChanged`
FileEncoding = "EncodingChanged",
},
buf = {
storage = buf_storage.create("nui.utils.autocmd", { _next_handler_id = 1 }),
},
}
---@param callback fun(event: table): nil
---@param bufnr integer
local function to_stored_handler(callback, bufnr)
local handler_id = autocmd.buf.storage[bufnr]._next_handler_id
autocmd.buf.storage[bufnr]._next_handler_id = handler_id + 1
autocmd.buf.storage[bufnr][handler_id] = callback
local command = string.format(":lua require('nui.utils.autocmd').execute_stored_handler(%s, %s)", bufnr, handler_id)
return command
end
---@param bufnr integer
---@param handler_id number
function autocmd.execute_stored_handler(bufnr, handler_id)
local handler = autocmd.buf.storage[bufnr][handler_id]
if is_type("function", handler) then
handler()
end
end
---@param name string
---@param opts { clear?: boolean }
function autocmd.create_group(name, opts)
if feature.lua_autocmd then
return vim.api.nvim_create_augroup(name, opts)
end
vim.cmd(string.format(
[[
augroup %s
%s
augroup end
]],
name,
opts.clear and "autocmd!" or ""
))
end
---@param name string
function autocmd.delete_group(name)
if feature.lua_autocmd then
return vim.api.nvim_del_augroup_by_name(name)
end
vim.cmd(string.format(
[[
autocmd! %s
augroup! %s
]],
name,
name
))
end
---@param event string|string[]
---@param opts table
---@param bufnr? integer # to store callback if lua autocmd is not available
function autocmd.create(event, opts, bufnr)
if feature.lua_autocmd then
return vim.api.nvim_create_autocmd(event, opts)
end
event = type(event) == "table" and table.concat(event, ",") or event --[[@as string]]
local pattern = is_type("table", opts.pattern) and table.concat(opts.pattern, ",") or opts.pattern
if opts.buffer then
pattern = string.format("<buffer=%s>", opts.buffer)
end
if opts.callback then
local buffer = opts.buffer or bufnr
if not buffer then
error("[nui.utils.autocmd] missing param: bufnr")
end
opts.command = to_stored_handler(opts.callback, buffer)
end
vim.cmd(
string.format(
"autocmd %s %s %s %s %s %s",
opts.group or "",
event,
pattern,
opts.once and "++once" or "",
opts.nested and "++nested" or "",
opts.command
)
)
end
---@param opts table
function autocmd.delete(opts)
if feature.lua_autocmd then
for _, item in ipairs(vim.api.nvim_get_autocmds(opts)) do
if item.id then
vim.api.nvim_del_autocmd(item.id)
end
end
return
end
local event = is_type("table", opts.event) and table.concat(opts.event, ",") or opts.event
local pattern = is_type("table", opts.pattern) and table.concat(opts.pattern, ",") or opts.pattern
if opts.buffer then
pattern = string.format("<buffer=%s>", opts.buffer)
end
vim.cmd(string.format("autocmd! %s %s %s", opts.group or "", event or "*", pattern or ""))
end
---@param event string|string[]
---@param opts table
function autocmd.exec(event, opts)
local events = type(event) == "table" and event or { event } --[=[@as string[]]=]
if feature.lua_autocmd then
vim.api.nvim_exec_autocmds(events, {
group = opts.group,
pattern = opts.pattern,
buffer = opts.buffer,
modeline = opts.modeline,
data = opts.data,
})
return
end
for _, event_name in ipairs(events) do
local command = string.format(
[[doautocmd %s %s %s %s]],
opts.modeline == false and "<nomodeline>" or "",
opts.group or "",
event_name,
opts.pattern or ""
)
if opts.buffer then
vim.api.nvim_buf_call(opts.buffer, function()
vim.cmd(command)
end)
else
vim.cmd(command)
end
end
end
-- @deprecated
---@deprecated
---@param event string | string[]
---@param pattern string | string[]
---@param cmd string
---@param options nil | table<"'once'" | "'nested'", boolean>
function autocmd.define(event, pattern, cmd, options)
local opts = options or {}
opts.pattern = pattern
opts.command = cmd
autocmd.create(event, opts)
end
-- @deprecated
---@deprecated
---@param group_name string
---@param auto_clear boolean
---@param definitions table<"'event'" | "'pattern'" | "'cmd'" | "'options'", any>
function autocmd.define_grouped(group_name, auto_clear, definitions)
if not is_type("boolean", auto_clear) then
error("invalid param type: auto_clear, expected boolean")
end
autocmd.create_group(group_name, { clear = auto_clear })
for _, definition in ipairs(definitions) do
autocmd.define(definition.event, definition.pattern, definition.cmd, definition.options)
end
end
-- @deprecated
---@deprecated
---@param group_name nil | string
---@param event nil | string | string[]
---@param pattern nil | string | string[]
function autocmd.remove(group_name, event, pattern)
autocmd.delete({
event = event,
group = group_name,
pattern = pattern,
})
end
---@param bufnr number
---@param event string | string[]
---@param handler string | function
---@param options nil | table<"'once'" | "'nested'", boolean>
function autocmd.buf.define(bufnr, event, handler, options)
local opts = options or {}
opts.buffer = bufnr
if is_type("function", handler) then
opts.callback = handler
else
opts.command = handler
end
autocmd.create(event, opts, bufnr)
end
---@param bufnr number
---@param group_name nil | string
---@param event nil | string | string[]
function autocmd.buf.remove(bufnr, group_name, event)
autocmd.delete({
buffer = bufnr,
event = event,
group = group_name,
})
end
return autocmd

View file

@ -0,0 +1,33 @@
local defaults = require("nui.utils").defaults
local buf_storage = {
_registry = {},
}
---@param storage_name string
---@param default_value any
---@return table<number, any>
function buf_storage.create(storage_name, default_value)
local storage = setmetatable({}, {
__index = function(tbl, bufnr)
rawset(tbl, bufnr, vim.deepcopy(defaults(default_value, {})))
-- TODO: can `buf_storage.cleanup` be automatically (and reliably) triggered on `BufWipeout`?
return tbl[bufnr]
end,
})
buf_storage._registry[storage_name] = storage
return storage
end
---@param bufnr number
function buf_storage.cleanup(bufnr)
for _, storage in pairs(buf_storage._registry) do
rawset(storage, bufnr, nil)
end
end
return buf_storage

View file

@ -0,0 +1,387 @@
local ok_nvim_version, nvim_version = pcall(vim.version)
if not ok_nvim_version then
nvim_version = {}
end
-- internal utils
local _ = {
feature = {
lua_keymap = type(vim.keymap) ~= "nil",
lua_autocmd = type(vim.api.nvim_create_autocmd) ~= "nil",
v0_10 = nvim_version.minor >= 10,
v0_11 = nvim_version.minor >= 11,
},
}
local utils = {
_ = _,
}
function utils.get_editor_size()
return {
width = vim.o.columns,
height = vim.o.lines,
}
end
function utils.get_window_size(winid)
winid = winid or 0
return {
width = vim.api.nvim_win_get_width(winid),
height = vim.api.nvim_win_get_height(winid),
}
end
function utils.defaults(v, default_value)
return type(v) == "nil" and default_value or v
end
-- luacheck: push no max comment line length
---@param type_name "'nil'" | "'number'" | "'string'" | "'boolean'" | "'table'" | "'function'" | "'thread'" | "'userdata'" | "'list'" | '"map"'
---@return boolean
function utils.is_type(type_name, v)
-- `vim.tbl_islist` will be removed in the future
local islist = vim.islist or vim.tbl_islist
if type_name == "list" then
return islist(v)
end
if type_name == "map" then
return type(v) == "table" and not islist(v)
end
return type(v) == type_name
end
-- luacheck: pop
---@param v string | number
function utils.parse_number_input(v)
local parsed = {}
parsed.is_percentage = type(v) == "string" and string.sub(v, -1) == "%"
if parsed.is_percentage then
parsed.value = tonumber(string.sub(v, 1, #v - 1)) / 100
else
parsed.value = tonumber(v)
parsed.is_percentage = parsed.value and 0 < parsed.value and parsed.value < 1
end
return parsed
end
---@param prefix? string
---@return (fun(): string) get_next_id
local function get_id_generator(prefix)
prefix = prefix or ""
local id = 0
return function()
id = id + 1
return prefix .. id
end
end
_.get_next_id = get_id_generator("nui_")
---@private
---@param bufnr number
---@param linenr number line number (1-indexed)
---@param char_start number start character position (0-indexed)
---@param char_end number end character position (0-indexed)
---@return number[] byte_range
function _.char_to_byte_range(bufnr, linenr, char_start, char_end)
local line = vim.api.nvim_buf_get_lines(bufnr, linenr - 1, linenr, false)[1]
local skipped_part = vim.fn.strcharpart(line, 0, char_start)
local target_part = vim.fn.strcharpart(line, char_start, char_end - char_start)
local byte_start = vim.fn.strlen(skipped_part)
local byte_end = math.min(byte_start + vim.fn.strlen(target_part), vim.fn.strlen(line))
return { byte_start, byte_end }
end
---@type integer
local fallback_namespace_id = vim.api.nvim_create_namespace("nui.nvim")
---@private
---@param ns_id integer
---@return integer
function _.ensure_namespace_id(ns_id)
return ns_id == -1 and fallback_namespace_id or ns_id
end
---@private
---@param ns_id? integer|string
---@return integer ns_id namespace id
function _.normalize_namespace_id(ns_id)
if utils.is_type("string", ns_id) then
---@cast ns_id string
return vim.api.nvim_create_namespace(ns_id)
end
---@cast ns_id integer
return ns_id or fallback_namespace_id
end
---@private
---@param bufnr integer
---@param ns_id integer
---@param linenr_start? integer (1-indexed)
---@param linenr_end? integer (1-indexed,inclusive)
function _.clear_namespace(bufnr, ns_id, linenr_start, linenr_end)
linenr_start = linenr_start or 1
linenr_end = linenr_end and linenr_end + 1 or 0
vim.api.nvim_buf_clear_namespace(bufnr, ns_id, linenr_start - 1, linenr_end - 1)
end
-- luacov: disable
local nvim_buf_set_option = vim.api.nvim_buf_set_option
---@param bufnr integer
---@param name string
---@param value any
local function set_buf_option(bufnr, name, value)
nvim_buf_set_option(bufnr, name, value)
end
local nvim_win_set_option = vim.api.nvim_win_set_option
---@param winid integer
---@param name string
---@param value any
local function set_win_option(winid, name, value)
nvim_win_set_option(winid, name, value)
end
-- luacov: enable
if _.feature.v0_10 then
function set_buf_option(bufnr, name, value)
vim.api.nvim_set_option_value(name, value, { buf = bufnr })
end
function set_win_option(winid, name, value)
vim.api.nvim_set_option_value(name, value, { win = winid, scope = "local" })
end
end
_.set_buf_option = set_buf_option
_.set_win_option = set_win_option
---@private
---@param bufnr number
---@param buf_options table<string, any>
function _.set_buf_options(bufnr, buf_options)
for name, value in pairs(buf_options) do
set_buf_option(bufnr, name, value)
end
end
---@private
---@param winid number
---@param win_options table<string, any>
function _.set_win_options(winid, win_options)
for name, value in pairs(win_options) do
set_win_option(winid, name, value)
end
end
---@private
---@param dimension number | string
---@param container_dimension number
---@return nil | number
function _.normalize_dimension(dimension, container_dimension)
local number = utils.parse_number_input(dimension)
if not number.value then
return nil
end
if number.is_percentage then
return math.floor(container_dimension * number.value)
end
return number.value
end
local strchars, strcharpart, strdisplaywidth = vim.fn.strchars, vim.fn.strcharpart, vim.fn.strdisplaywidth
---@param text string
---@param max_length number
---@return string
function _.truncate_text(text, max_length)
if strdisplaywidth(text) <= max_length then
return text
end
local low, high = 0, strchars(text)
local mid
while low < high do
mid = math.floor((low + high + 1) / 2)
if strdisplaywidth(strcharpart(text, 0, mid)) < max_length then
low = mid
else
high = mid - 1
end
end
return strcharpart(text, 0, low) .. ""
end
---@param text NuiText
---@param max_width number
function _.truncate_nui_text(text, max_width)
text:set(_.truncate_text(text:content(), max_width))
end
---@param line NuiLine
---@param max_width number
function _.truncate_nui_line(line, max_width)
local width = line:width()
local last_part_idx = #line._texts
while width > max_width do
local extra_width = width - max_width
local last_part = line._texts[last_part_idx]
if last_part:width() <= extra_width then
width = width - last_part:width()
line._texts[last_part_idx] = nil
last_part_idx = last_part_idx - 1
-- need to add truncate indicator in previous part
if last_part:width() == extra_width then
last_part = line._texts[last_part_idx]
last_part:set(_.truncate_text(last_part:content() .. " ", last_part:width()))
end
else
last_part:set(_.truncate_text(last_part:content(), last_part:width() - extra_width))
width = width - extra_width
end
end
end
---@param align "'left'" | "'center'" | "'right'"
---@param total_width number
---@param text_width number
---@return number left_gap_width, number right_gap_width
function _.calculate_gap_width(align, total_width, text_width)
local gap_width = total_width - text_width
if align == "left" then
return 0, gap_width
elseif align == "center" then
return math.floor(gap_width / 2), math.ceil(gap_width / 2)
elseif align == "right" then
return gap_width, 0
end
error("invalid value align=" .. align)
end
---@param lines (string|NuiLine)[]
---@param bufnr number
---@param ns_id number
---@param linenr_start integer (1-indexed)
---@param linenr_end? integer (1-indexed,inclusive)
---@param byte_start? integer (0-indexed)
---@param byte_end? integer (0-indexed,exclusive)
function _.render_lines(lines, bufnr, ns_id, linenr_start, linenr_end, byte_start, byte_end)
local row_start = linenr_start - 1
local row_end = linenr_end or row_start + 1
local content = vim.tbl_map(function(line)
if type(line) == "string" then
return line
end
return line:content()
end, lines)
if byte_start then
local col_start = byte_start
local col_end = byte_end or #vim.api.nvim_buf_get_lines(bufnr, row_start, row_end, false)[1]
vim.api.nvim_buf_set_text(bufnr, row_start, col_start, row_end - 1, col_end, content)
else
vim.api.nvim_buf_set_lines(bufnr, row_start, row_end, false, content)
end
for linenr, line in ipairs(lines) do
if type(line) ~= "string" then
line:highlight(bufnr, ns_id, linenr + row_start, byte_start)
end
end
end
---@param bufnr integer
---@param linenr_start integer (1-indexed)
---@param linenr_end integer (1-indexed,inclusive)
function _.clear_lines(bufnr, linenr_start, linenr_end)
local count = linenr_end - linenr_start + 1
if count < 1 then
return
end
local lines = {}
for i = 1, count do
lines[i] = ""
end
vim.api.nvim_buf_set_lines(bufnr, linenr_start - 1, linenr_end, false, lines)
end
function _.normalize_layout_options(options)
if utils.is_type("string", options.relative) then
options.relative = {
type = options.relative,
}
end
if options.position and not utils.is_type("table", options.position) then
options.position = {
row = options.position,
col = options.position,
}
end
if options.size and not utils.is_type("table", options.size) then
options.size = {
width = options.size,
height = options.size,
}
end
return options
end
---@param winhighlight string
---@return table<string, string> highlight_map
function _.parse_winhighlight(winhighlight)
local highlight = {}
local parts = vim.split(winhighlight, ",", { plain = true, trimempty = true })
for _, part in ipairs(parts) do
local key, value = part:match("(.+):(.+)")
highlight[key] = value
end
return highlight
end
---@param highlight_map table<string, string>
---@return string winhighlight
function _.serialize_winhighlight(highlight_map)
local parts = vim.tbl_map(function(key)
return key .. ":" .. highlight_map[key]
end, vim.tbl_keys(highlight_map))
table.sort(parts)
return table.concat(parts, ",")
end
function _.get_default_winborder()
return "none"
end
if _.feature.v0_11 then
function _.get_default_winborder()
local style = vim.api.nvim_get_option_value("winborder", {})
if style == "" then
return "none"
end
return style
end
end
return utils

View file

@ -0,0 +1,154 @@
local buf_storage = require("nui.utils.buf_storage")
local is_type = require("nui.utils").is_type
local feature = require("nui.utils")._.feature
local keymap = {
storage = buf_storage.create("nui.utils.keymap", { _next_handler_id = 1, keys = {}, handlers = {} }),
}
---@param mode string
---@param key string
---@return string key_id
local function get_key_id(mode, key)
return string.format("%s---%s", mode, vim.api.nvim_replace_termcodes(key, true, true, true))
end
---@param bufnr number
---@param key_id string
---@return integer|nil handler_id
local function get_handler_id(bufnr, key_id)
return keymap.storage[bufnr].keys[key_id]
end
---@param bufnr number
---@param key_id string
---@return integer handler_id
local function next_handler_id(bufnr, key_id)
local handler_id = keymap.storage[bufnr]._next_handler_id
keymap.storage[bufnr].keys[key_id] = handler_id
keymap.storage[bufnr]._next_handler_id = handler_id + 1
return handler_id
end
---@param bufnr number
---@param mode string
---@param key string
---@param handler string|fun(): nil
---@return { rhs: string, callback?: fun(): nil }|nil
local function get_keymap_info(bufnr, mode, key, handler, overwrite)
local key_id = get_key_id(mode, key)
-- luacov: disable
if get_handler_id(bufnr, key_id) and not overwrite then
return nil
end
-- luacov: enable
local handler_id = next_handler_id(bufnr, key_id)
local rhs, callback = "", nil
if type(handler) == "function" then
if feature.lua_keymap then
callback = handler
else
keymap.storage[bufnr].handlers[handler_id] = handler
rhs = string.format("<cmd>lua require('nui.utils.keymap').execute(%s, %s)<CR>", bufnr, handler_id)
end
else
rhs = handler
end
return {
rhs = rhs,
callback = callback,
}
end
---@param bufnr number
---@param handler_id number
function keymap.execute(bufnr, handler_id)
local handler = keymap.storage[bufnr].handlers[handler_id]
if is_type("function", handler) then
handler(bufnr)
end
end
---@param bufnr number
---@param mode string
---@param lhs string|string[]
---@param handler string|fun(): nil
---@param opts? table<"'expr'"|"'noremap'"|"'nowait'"|"'remap'"|"'script'"|"'silent'"|"'unique'", boolean>
---@return nil
function keymap.set(bufnr, mode, lhs, handler, opts, force)
if feature.lua_keymap and not is_type("boolean", force) then
force = true
end
local keys = lhs
if type(lhs) ~= "table" then
keys = { lhs }
end
---@cast keys -string
opts = opts or {}
if not is_type("nil", opts.remap) then
opts.noremap = not opts.remap
opts.remap = nil
end
for _, key in ipairs(keys) do
local keymap_info = get_keymap_info(bufnr, mode, key, handler, force)
-- luacov: disable
if not keymap_info then
return false
end
-- luacov: enable
local options = vim.deepcopy(opts)
options.callback = keymap_info.callback
vim.api.nvim_buf_set_keymap(bufnr, mode, key, keymap_info.rhs, options)
end
return true
end
---@param bufnr number
---@param mode string
---@param lhs string|string[]
---@return nil
function keymap._del(bufnr, mode, lhs, force)
if feature.lua_keymap and not is_type("boolean", force) then
force = true
end
local keys = lhs
if type(lhs) ~= "table" then
keys = { lhs }
end
---@cast keys -string
for _, key in ipairs(keys) do
local key_id = get_key_id(mode, key)
local handler_id = get_handler_id(bufnr, key_id)
-- luacov: disable
if not handler_id and not force then
return false
---@cast handler_id -nil
end
-- luacov: enable
keymap.storage[bufnr].keys[key_id] = nil
keymap.storage[bufnr].handlers[handler_id] = nil
vim.api.nvim_buf_del_keymap(bufnr, mode, key)
end
return true
end
return keymap