mirror of
https://github.com/neovim/neovim.git
synced 2026-11-06 13:26:57 +00:00
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
250 lines
8.2 KiB
Lua
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
|