Files
neovim/runtime/lua/vim/_core/util.lua
T
Lewis Russell efdd73c096 feat(watch): add on_error callback
Problem: Watcher failures are reported inconsistently, and callers
cannot detect when a watcher is no longer usable.

Solution: Route startup and event failures and unexpected inotifywait
exits through an optional on_error callback. Log to nvim-watch.log by
default. Ignore disappearing child directories and exits caused by
cancellation. Let LSP log failures and notify once per message, using
INFO for missing roots and ERROR otherwise, without interrupting
registration.

AI-assisted
2026-09-09 11:39:57 +01:00

250 lines
8.2 KiB
Lua

-- Nursery for random things that may later find their way into stdlib if they mature.
local M = {}
-- Generated from async.nvim/lua/async/_errors.lua: start
local nil_error = 'error(nil)'
--- Normalize a failed Lua operation for error slots where `nil` means success.
--- @param err any
--- @return any
--- @private
function M._normalize_error(err)
return err == nil and nil_error or err
end
--- Convert an error to a string without letting its metamethod interrupt cleanup.
--- @param err any
--- @return string
--- @private
function M._stringify_error(err)
local ok, message = pcall(tostring, err)
return ok and message or '<unprintable error>'
end
-- Generated from async.nvim/lua/async/_errors.lua: end
--- Adds one or more blank lines above or below the cursor.
--- @param above? boolean Place blank line(s) above the cursor
local function add_blank(above)
local offset = above and 1 or 0
local repeated = vim.fn['repeat']({ '' }, vim.v.count1)
local linenr = vim.api.nvim_win_get_cursor(0)[1]
vim.api.nvim_buf_set_lines(0, linenr - offset, linenr - offset, true, repeated)
end
function M.space_above()
add_blank(true)
end
function M.space_below()
add_blank()
end
--- Gets a buffer by name
--- @param name string
--- @return integer?
function M.get_buf_by_name(name)
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
if vim.api.nvim_buf_get_name(buf) == name then
return buf
end
end
end
--- Edit a file in a specific window
--- @param winnr number
--- @param file string
--- @return number buffer number of the edited buffer
M.edit_in = function(winnr, file)
local function resolved_path(path)
if not path or path == '' then
return ''
end
return vim.fn.resolve(vim.fs.abspath(path))
end
return vim.api.nvim_win_call(winnr, function()
local current_buf = vim.api.nvim_win_get_buf(winnr)
local current = resolved_path(vim.api.nvim_buf_get_name(current_buf))
-- Check if the current buffer is already the target file
if current == resolved_path(file) then
return current_buf
end
-- Read the file into the buffer
vim.cmd.edit(vim.fn.fnameescape(file))
return vim.api.nvim_get_current_buf()
end)
end
--- :edit, but it respects commands like :hor, :vert, :tab, etc.
--- @param file string
--- @param mods string|vim.api.keyset.cmd_mods Modifier string ("vertical") or structured mods table.
function M.wrapped_edit(file, mods)
assert(mods)
if type(mods) == 'string' then
mods = vim.api.nvim_parse_cmd(mods .. ' edit').mods --[[@as vim.api.keyset.cmd_mods]]
end
--- @cast mods vim.api.keyset.cmd_mods
if (mods.tab or 0) > 0 or (mods.split or '') ~= '' or mods.horizontal or mods.vertical then
local buf = M.get_buf_by_name(file)
if buf == nil then
buf = vim.api.nvim_create_buf(true, false)
end
vim.cmd.sbuffer { buf, mods = mods }
end
vim.cmd.edit { file }
end
--- Read a chunk of data from a file
--- @param file string
--- @param size number
--- @return string? chunk or nil on error
function M.read_chunk(file, size)
local fd = io.open(file, 'rb')
if not fd then
return nil
end
local chunk = fd:read(size)
fd:close()
return tostring(chunk)
end
--- Check if a range in a buffer is inside a Lua codeblock via treesitter injection.
--- Used by :source to detect Lua code in non-Lua files (e.g., vimdoc).
--- @param bufnr integer Buffer number
--- @param line1 integer Start line (1-indexed)
--- @param line2 integer End line (1-indexed)
--- @return boolean True if the range is in a Lua injection
function M.source_is_lua(bufnr, line1, line2)
local parser = vim.treesitter.get_parser(bufnr)
if not parser then
return false
end
-- Parse from buffer start through one line past line2 to include injection closing markers
local range = { line1 - 1, 0, line2 - 1, -1 }
parser:parse({ 0, 0, line2, -1 })
local lang_tree = parser:language_for_range(range)
return lang_tree:lang() == 'lua'
end
--- Returns the exit code string for the current buffer, given:
--- - Channel is attached to the current buffer
--- - Current buffer is a terminal buffer
---
--- @return string
function M.term_exitcode()
local chan_id = vim.bo.channel
if chan_id == 0 or vim.bo.buftype ~= 'terminal' then
return ''
end
local info = vim.api.nvim_get_chan_info(chan_id)
if info.exitcode and info.exitcode >= 0 then
-- use non-breaking space to avoid fillchar
return string.format('[Exit:\226\128\175%d]', info.exitcode)
end
return ''
end
--- Compute a link to a target on a forge host
--- @param repo string URL of repo, usually "https://<domain>/<user>/<name>"
--- @param target string Identifier of a target, like commit hash or tag name
--- @param target_type "commit"|"tag"
--- @return string? # Example: <repo>/releases/tag/<target>
function M.get_forge_url(repo, target, target_type)
-- The structure <host>/<middle>/<target> works for most forges. Like:
-- - https://github.com/neovim/nvim-lspconfig/commit/e146efa
-- - https://github.com/neovim/nvim-lspconfig/releases/tag/v2.8.0
local ref_middles = {
{ pattern = '^https://github%.com/', commit = 'commit', tag = 'releases/tag' },
{ pattern = '^https://gitlab%.com/', commit = '-/commit', tag = '-/tags' },
{ pattern = '^https://git%.sr%.ht/', commit = 'commit', tag = 'refs' },
{ pattern = '^https://tangled%.org/', commit = 'commit', tag = 'tags' },
{ pattern = '^https://bitbucket%.org/', commit = 'commits', tag = 'src' },
-- Fall back to Forgejo style since there is no fixed host
{ pattern = '^https://', commit = 'commit', tag = 'src/tag' },
}
local middle = ''
for _, mid in ipairs(ref_middles) do
if repo:match(mid.pattern) then
middle = mid[target_type]
if middle ~= '' then
break
end
end
end
if middle == '' then
return nil
end
repo = repo:gsub('/+$', '')
return ('%s/%s/%s'):format(repo, middle, target)
end
--- Gets a scrubbed message from a pcall'd command error (drops Lua context/traceback):
--- "…/editor.lua:123: Vim(put):E484: xx" => "E484: xx"
---
--- @param err string
--- @return string
function M.cmd_errmsg(err)
err = err:match('^[^\n]*') or err
--- @type string
err = err:match('Vim%b():%s*(.*)') or err:match('Vim:%s*(.*)') or (err:gsub('^.-:%d+:%s*', ''))
return (err:gsub('^Lua:%s*', ''))
end
--- Utility function for displaying vim error codes (EXX)
--- @param msg string
function M.echo_err(msg)
vim.api.nvim_echo({ { msg } }, true, { err = true })
end
--- Shows a message from a builtin plugin, prefixed with the plugin name.
---
--- Scheduled, so it is safe to call from |api-fast| contexts.
--- @param name string Plugin name, e.g. "zip".
--- @param msg string
--- @param level? integer Level from |vim.log.levels|. Defaults to ERROR.
--- @param once? boolean Only show the message once.
function M.notify(name, msg, level, once)
vim.schedule(function()
local notify = once and vim.notify_once or vim.notify
notify(('%s: %s'):format(name, msg), level or vim.log.levels.ERROR)
end)
end
--- Define event-handlers (autocmds) ergonomically.
---
--- Examples:
--- ```lua
--- local nvim_on = require('vim._core.util').nvim_on
--- nvim_on('BufWritePost', group, function(ev) print(ev.file) end)
--- nvim_on({ 'BufRead', 'BufNew' }, group, { pattern = '*.lua' }, function(ev) end)
--- nvim_on('VimLeavePre', nil, function() end)
--- ```
---
--- @param events vim.api.keyset.events|vim.api.keyset.events[] Event(s) to watch. See |autocmd-events|.
--- @param group string|integer? Group name or id, or `nil`.
--- @param opts_or_fn vim.api.keyset.create_autocmd Options.
--- @param fn fun(ev: vim.api.keyset.create_autocmd.callback_args): boolean? Event handler.
--- @return integer # Autocmd id (see |nvim_create_autocmd()|).
--- @overload fun(events: vim.api.keyset.events|vim.api.keyset.events[], group: string|integer?, fn: fun(ev: vim.api.keyset.create_autocmd.callback_args): boolean?): integer
function M.nvim_on(events, group, opts_or_fn, fn)
vim.validate('opts_or_fn', opts_or_fn, { 'function', 'table' })
local opts --- @type vim.api.keyset.create_autocmd
if type(opts_or_fn) == 'function' then
fn, opts = opts_or_fn, {}
else
opts = opts_or_fn
end
opts.group = group
opts.callback = fn
return vim.api.nvim_create_autocmd(events, opts)
end
return M