aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua/vim')
-rw-r--r--runtime/lua/vim/_editor.lua3
-rw-r--r--runtime/lua/vim/_meta.lua2
-rw-r--r--runtime/lua/vim/_meta/api.lua6
-rw-r--r--runtime/lua/vim/_meta/vimfn.lua14
-rw-r--r--runtime/lua/vim/deprecated/health.lua2
-rw-r--r--runtime/lua/vim/diagnostic.lua193
-rw-r--r--runtime/lua/vim/filetype.lua2
-rw-r--r--runtime/lua/vim/filetype/detect.lua24
-rw-r--r--runtime/lua/vim/health.lua120
-rw-r--r--runtime/lua/vim/health/health.lua3
-rw-r--r--runtime/lua/vim/highlight.lua55
-rw-r--r--runtime/lua/vim/lsp.lua3
-rw-r--r--runtime/lua/vim/lsp/buf.lua2
-rw-r--r--runtime/lua/vim/lsp/client.lua3
-rw-r--r--runtime/lua/vim/lsp/health.lua86
-rw-r--r--runtime/lua/vim/lsp/util.lua4
-rw-r--r--runtime/lua/vim/provider/health.lua228
-rw-r--r--runtime/lua/vim/shared.lua76
-rw-r--r--runtime/lua/vim/snippet.lua11
-rw-r--r--runtime/lua/vim/treesitter/dev.lua22
20 files changed, 575 insertions, 284 deletions
diff --git a/runtime/lua/vim/_editor.lua b/runtime/lua/vim/_editor.lua
index 5e9be509c8..9f952db4fc 100644
--- a/runtime/lua/vim/_editor.lua
+++ b/runtime/lua/vim/_editor.lua
@@ -494,6 +494,7 @@ do
vim.t = make_dict_accessor('t')
end
+--- @deprecated
--- Gets a dict of line segment ("chunk") positions for the region from `pos1` to `pos2`.
---
--- Input and output positions are byte positions, (0,0)-indexed. "End of line" column
@@ -507,6 +508,8 @@ end
---@return table region Dict of the form `{linenr = {startcol,endcol}}`. `endcol` is exclusive, and
---whole lines are returned as `{startcol,endcol} = {0,-1}`.
function vim.region(bufnr, pos1, pos2, regtype, inclusive)
+ vim.deprecate('vim.region', 'vim.fn.getregionpos()', '0.13')
+
if not vim.api.nvim_buf_is_loaded(bufnr) then
vim.fn.bufload(bufnr)
end
diff --git a/runtime/lua/vim/_meta.lua b/runtime/lua/vim/_meta.lua
index 731dd5b923..c9f207cb20 100644
--- a/runtime/lua/vim/_meta.lua
+++ b/runtime/lua/vim/_meta.lua
@@ -34,3 +34,5 @@ vim.uri_from_fname = uri.uri_from_fname
vim.uri_from_bufnr = uri.uri_from_bufnr
vim.uri_to_fname = uri.uri_to_fname
vim.uri_to_bufnr = uri.uri_to_bufnr
+
+vim.provider = require('vim.provider')
diff --git a/runtime/lua/vim/_meta/api.lua b/runtime/lua/vim/_meta/api.lua
index 6edf2a5a96..c99eefa4f6 100644
--- a/runtime/lua/vim/_meta/api.lua
+++ b/runtime/lua/vim/_meta/api.lua
@@ -1761,13 +1761,15 @@ function vim.api.nvim_open_term(buffer, opts) end
---
--- • title: Title (optional) in window border, string or list.
--- List should consist of `[text, highlight]` tuples. If
---- string, the default highlight group is `FloatTitle`.
+--- string, or a tuple lacks a highlight, the default
+--- highlight group is `FloatTitle`.
--- • title_pos: Title position. Must be set with `title`
--- option. Value can be one of "left", "center", or "right".
--- Default is `"left"`.
--- • footer: Footer (optional) in window border, string or
--- list. List should consist of `[text, highlight]` tuples.
---- If string, the default highlight group is `FloatFooter`.
+--- If string, or a tuple lacks a highlight, the default
+--- highlight group is `FloatFooter`.
--- • footer_pos: Footer position. Must be set with `footer`
--- option. Value can be one of "left", "center", or "right".
--- Default is `"left"`.
diff --git a/runtime/lua/vim/_meta/vimfn.lua b/runtime/lua/vim/_meta/vimfn.lua
index f4daacfb7d..84bb26a135 100644
--- a/runtime/lua/vim/_meta/vimfn.lua
+++ b/runtime/lua/vim/_meta/vimfn.lua
@@ -1642,7 +1642,7 @@ function vim.fn.execute(command, silent) end
--- If {expr} starts with "./" the |current-directory| is used.
---
--- @param expr any
---- @return any
+--- @return string
function vim.fn.exepath(expr) end
--- The result is a Number, which is |TRUE| if {expr} is
@@ -3536,14 +3536,14 @@ function vim.fn.getreginfo(regname) end
--- The optional argument {opts} is a Dict and supports the
--- following items:
---
---- type Specify the region's selection type
---- (default: "v"):
---- "v" for |charwise| mode
---- "V" for |linewise| mode
---- "<CTRL-V>" for |blockwise-visual| mode
+--- type Specify the region's selection type.
+--- See |getregtype()| for possible values,
+--- except that the width can be omitted
+--- and an empty string cannot be used.
+--- (default: "v")
---
--- exclusive If |TRUE|, use exclusive selection
---- for the end position
+--- for the end position.
--- (default: follow 'selection')
---
--- You can get the last selection type by |visualmode()|.
diff --git a/runtime/lua/vim/deprecated/health.lua b/runtime/lua/vim/deprecated/health.lua
index 0f6b1f578c..64a755b248 100644
--- a/runtime/lua/vim/deprecated/health.lua
+++ b/runtime/lua/vim/deprecated/health.lua
@@ -1,7 +1,7 @@
local M = {}
local health = vim.health
-local deprecated = {}
+local deprecated = {} ---@type {[1]: string, [2]: table, [3]: string}[]
function M.check()
if next(deprecated) == nil then
diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua
index 348204abb7..8e68e9608a 100644
--- a/runtime/lua/vim/diagnostic.lua
+++ b/runtime/lua/vim/diagnostic.lua
@@ -835,21 +835,36 @@ local function filter_highest(diagnostics)
end
end
---- @param position {[1]: integer, [2]: integer}
--- @param search_forward boolean
---- @param bufnr integer
---- @param opts vim.diagnostic.GotoOpts
---- @param namespace integer[]|integer
+--- @param opts vim.diagnostic.JumpOpts?
--- @return vim.Diagnostic?
-local function next_diagnostic(position, search_forward, bufnr, opts, namespace)
+local function next_diagnostic(search_forward, opts)
+ opts = opts or {}
+
+ -- Support deprecated win_id alias
+ if opts.win_id then
+ vim.deprecate('opts.win_id', 'opts.winid', '0.13')
+ opts.winid = opts.win_id
+ opts.win_id = nil
+ end
+
+ -- Support deprecated cursor_position alias
+ if opts.cursor_position then
+ vim.deprecate('opts.cursor_position', 'opts.pos', '0.13')
+ opts.pos = opts.cursor_position
+ opts.cursor_position = nil
+ end
+
+ local winid = opts.winid or api.nvim_get_current_win()
+ local bufnr = api.nvim_win_get_buf(winid)
+ local position = opts.pos or api.nvim_win_get_cursor(winid)
+
+ -- Adjust row to be 0-indexed
position[1] = position[1] - 1
- bufnr = get_bufnr(bufnr)
- local wrap = if_nil(opts.wrap, true)
- local get_opts = vim.deepcopy(opts)
- get_opts.namespace = get_opts.namespace or namespace
+ local wrap = if_nil(opts.wrap, true)
- local diagnostics = get_diagnostics(bufnr, get_opts, true)
+ local diagnostics = get_diagnostics(bufnr, opts, true)
if opts._highest then
filter_highest(diagnostics)
@@ -902,32 +917,41 @@ local function next_diagnostic(position, search_forward, bufnr, opts, namespace)
end
end
---- @param opts vim.diagnostic.GotoOpts?
---- @param pos {[1]:integer,[2]:integer}|false
-local function diagnostic_move_pos(opts, pos)
- opts = opts or {}
-
- local float = if_nil(opts.float, true)
- local win_id = opts.win_id or api.nvim_get_current_win()
-
- if not pos then
+--- Move the cursor to the given diagnostic.
+---
+--- @param diagnostic vim.Diagnostic?
+--- @param opts vim.diagnostic.JumpOpts?
+local function goto_diagnostic(diagnostic, opts)
+ if not diagnostic then
api.nvim_echo({ { 'No more valid diagnostics to move to', 'WarningMsg' } }, true, {})
return
end
- api.nvim_win_call(win_id, function()
+ opts = opts or {}
+
+ -- Support deprecated win_id alias
+ if opts.win_id then
+ vim.deprecate('opts.win_id', 'opts.winid', '0.13')
+ opts.winid = opts.win_id
+ opts.win_id = nil
+ end
+
+ local winid = opts.winid or api.nvim_get_current_win()
+
+ api.nvim_win_call(winid, function()
-- Save position in the window's jumplist
vim.cmd("normal! m'")
- api.nvim_win_set_cursor(win_id, { pos[1] + 1, pos[2] })
+ api.nvim_win_set_cursor(winid, { diagnostic.lnum + 1, diagnostic.col })
-- Open folds under the cursor
vim.cmd('normal! zv')
end)
+ local float = if_nil(opts.float, true)
if float then
local float_opts = type(float) == 'table' and float or {}
vim.schedule(function()
M.open_float(vim.tbl_extend('keep', float_opts, {
- bufnr = api.nvim_win_get_buf(win_id),
+ bufnr = api.nvim_win_get_buf(winid),
scope = 'cursor',
focus = false,
}))
@@ -1114,24 +1138,24 @@ end
--- Get the previous diagnostic closest to the cursor position.
---
----@param opts? vim.diagnostic.GotoOpts
+---@param opts? vim.diagnostic.JumpOpts
---@return vim.Diagnostic? : Previous diagnostic
function M.get_prev(opts)
- opts = opts or {}
-
- local win_id = opts.win_id or api.nvim_get_current_win()
- local bufnr = api.nvim_win_get_buf(win_id)
- local cursor_position = opts.cursor_position or api.nvim_win_get_cursor(win_id)
-
- return next_diagnostic(cursor_position, false, bufnr, opts, opts.namespace)
+ return next_diagnostic(false, opts)
end
--- Return the position of the previous diagnostic in the current buffer.
---
----@param opts? vim.diagnostic.GotoOpts
+---@param opts? vim.diagnostic.JumpOpts
---@return table|false: Previous diagnostic position as a `(row, col)` tuple
--- or `false` if there is no prior diagnostic.
+---@deprecated
function M.get_prev_pos(opts)
+ vim.deprecate(
+ 'vim.diagnostic.get_prev_pos()',
+ 'access the lnum and col fields from get_prev() instead',
+ '0.13'
+ )
local prev = M.get_prev(opts)
if not prev then
return false
@@ -1141,31 +1165,33 @@ function M.get_prev_pos(opts)
end
--- Move to the previous diagnostic in the current buffer.
----@param opts? vim.diagnostic.GotoOpts
+---@param opts? vim.diagnostic.JumpOpts
+---@deprecated
function M.goto_prev(opts)
- return diagnostic_move_pos(opts, M.get_prev_pos(opts))
+ vim.deprecate('vim.diagnostic.goto_prev()', 'vim.diagnostic.jump()', '0.13')
+ goto_diagnostic(M.get_prev(opts), opts)
end
--- Get the next diagnostic closest to the cursor position.
---
----@param opts? vim.diagnostic.GotoOpts
+---@param opts? vim.diagnostic.JumpOpts
---@return vim.Diagnostic? : Next diagnostic
function M.get_next(opts)
- opts = opts or {}
-
- local win_id = opts.win_id or api.nvim_get_current_win()
- local bufnr = api.nvim_win_get_buf(win_id)
- local cursor_position = opts.cursor_position or api.nvim_win_get_cursor(win_id)
-
- return next_diagnostic(cursor_position, true, bufnr, opts, opts.namespace)
+ return next_diagnostic(true, opts)
end
--- Return the position of the next diagnostic in the current buffer.
---
----@param opts? vim.diagnostic.GotoOpts
+---@param opts? vim.diagnostic.JumpOpts
---@return table|false : Next diagnostic position as a `(row, col)` tuple or false if no next
--- diagnostic.
+---@deprecated
function M.get_next_pos(opts)
+ vim.deprecate(
+ 'vim.diagnostic.get_next_pos()',
+ 'access the lnum and col fields from get_next() instead',
+ '0.13'
+ )
local next = M.get_next(opts)
if not next then
return false
@@ -1187,12 +1213,21 @@ end
--- @field severity? vim.diagnostic.SeverityFilter
--- Configuration table with the following keys:
---- @class vim.diagnostic.GotoOpts : vim.diagnostic.GetOpts
+--- @class vim.diagnostic.JumpOpts : vim.diagnostic.GetOpts
+---
+--- The diagnostic to jump to. Mutually exclusive with {count}, {namespace},
+--- and {severity}.
+--- @field diagnostic? vim.Diagnostic
+---
+--- The number of diagnostics to move by, starting from {pos}. A positive
+--- integer moves forward by {count} diagnostics, while a negative integer moves
+--- backward by {count} diagnostics. Mutually exclusive with {diagnostic}.
+--- @field count? integer
---
---- Cursor position as a `(row, col)` tuple.
---- See |nvim_win_get_cursor()|.
---- (default: current cursor position)
---- @field cursor_position? {[1]:integer,[2]:integer}
+--- Cursor position as a `(row, col)` tuple. See |nvim_win_get_cursor()|. Used
+--- to find the nearest diagnostic when {count} is used. Only used when {count}
+--- is non-nil. Default is the current cursor position.
+--- @field pos? {[1]:integer,[2]:integer}
---
--- Whether to loop around file or not. Similar to 'wrapscan'.
--- (default: `true`)
@@ -1214,13 +1249,69 @@ end
---
--- Window ID
--- (default: `0`)
---- @field win_id? integer
+--- @field winid? integer
+
+--- Move to a diagnostic.
+---
+--- @param opts vim.diagnostic.JumpOpts
+--- @return vim.Diagnostic? # The diagnostic that was moved to.
+function M.jump(opts)
+ -- One of "diagnostic" or "count" must be provided
+ assert(
+ opts.diagnostic or opts.count,
+ 'One of "diagnostic" or "count" must be specified in the options to vim.diagnostic.jump()'
+ )
+
+ if opts.diagnostic then
+ goto_diagnostic(opts.diagnostic, opts)
+ return opts.diagnostic
+ end
+
+ local count = opts.count
+ if count == 0 then
+ return nil
+ end
+
+ -- Support deprecated cursor_position alias
+ if opts.cursor_position then
+ vim.deprecate('opts.cursor_position', 'opts.pos', '0.13')
+ opts.pos = opts.cursor_position
+ opts.cursor_position = nil
+ end
+
+ -- Copy the opts table so that we can modify it
+ local opts_ = vim.deepcopy(opts, true)
+
+ local diag = nil
+ while count ~= 0 do
+ local next = next_diagnostic(count > 0, opts_)
+ if not next then
+ break
+ end
+
+ -- Update cursor position
+ opts_.pos = { next.lnum + 1, next.col }
+
+ if count > 0 then
+ count = count - 1
+ else
+ count = count + 1
+ end
+ diag = next
+ end
+
+ goto_diagnostic(diag, opts)
+
+ return diag
+end
--- Move to the next diagnostic.
---
----@param opts? vim.diagnostic.GotoOpts
+---@param opts? vim.diagnostic.JumpOpts
+---@deprecated
function M.goto_next(opts)
- diagnostic_move_pos(opts, M.get_next_pos(opts))
+ vim.deprecate('vim.diagnostic.goto_next()', 'vim.diagnostic.jump()', '0.13')
+ goto_diagnostic(M.get_next(opts), opts)
end
M.handlers.signs = {
@@ -1688,7 +1779,7 @@ end
---
---@param opts vim.diagnostic.Opts.Float?
---@return integer? float_bufnr
----@return integer? win_id
+---@return integer? winid
function M.open_float(opts, ...)
-- Support old (bufnr, opts) signature
local bufnr --- @type integer?
diff --git a/runtime/lua/vim/filetype.lua b/runtime/lua/vim/filetype.lua
index d1fdd0aa16..2ab6cc6059 100644
--- a/runtime/lua/vim/filetype.lua
+++ b/runtime/lua/vim/filetype.lua
@@ -1637,7 +1637,7 @@ local filename = {
['.xsdbcmdhistory'] = 'tcl',
['texmf.cnf'] = 'texmf',
COPYING = 'text',
- README = 'text',
+ README = detect_seq(detect.haredoc, 'text'),
LICENSE = 'text',
AUTHORS = 'text',
tfrc = 'tf',
diff --git a/runtime/lua/vim/filetype/detect.lua b/runtime/lua/vim/filetype/detect.lua
index ba86d8de5a..58d2666564 100644
--- a/runtime/lua/vim/filetype/detect.lua
+++ b/runtime/lua/vim/filetype/detect.lua
@@ -650,6 +650,30 @@ function M.header(_, bufnr)
end
end
+--- Recursively search for Hare source files in a directory and any
+--- subdirectories, up to a given depth.
+--- @param dir string
+--- @param depth number
+--- @return boolean
+local function is_hare_module(dir, depth)
+ depth = math.max(depth, 0)
+ for name, _ in vim.fs.dir(dir, { depth = depth + 1 }) do
+ if name:find('%.ha$') then
+ return true
+ end
+ end
+ return false
+end
+
+--- @type vim.filetype.mapfn
+function M.haredoc(path, _)
+ if vim.g.filetype_haredoc then
+ if is_hare_module(vim.fs.dirname(path), vim.g.haredoc_search_depth or 1) then
+ return 'haredoc'
+ end
+ end
+end
+
--- @type vim.filetype.mapfn
function M.html(_, bufnr)
for _, line in ipairs(getlines(bufnr, 1, 10)) do
diff --git a/runtime/lua/vim/health.lua b/runtime/lua/vim/health.lua
index f40f04a064..236f9da752 100644
--- a/runtime/lua/vim/health.lua
+++ b/runtime/lua/vim/health.lua
@@ -104,10 +104,10 @@ local function filepath_to_healthcheck(path)
local subpath = path:gsub('.*lua/', '')
if vim.fs.basename(subpath) == 'health.lua' then
-- */health.lua
- name = assert(vim.fs.dirname(subpath))
+ name = vim.fs.dirname(subpath)
else
-- */health/init.lua
- name = assert(vim.fs.dirname(assert(vim.fs.dirname(subpath))))
+ name = vim.fs.dirname(vim.fs.dirname(subpath))
end
name = name:gsub('/', '.')
@@ -275,114 +275,6 @@ function M.error(msg, ...)
collect_output(input)
end
-function M._provider_disabled(provider)
- local loaded_var = 'loaded_' .. provider .. '_provider'
- local v = vim.g[loaded_var]
- if v == 0 then
- M.info('Disabled (' .. loaded_var .. '=' .. v .. ').')
- return true
- end
- return false
-end
-
--- Handler for s:system() function.
-local function system_handler(self, _, data, event)
- if event == 'stderr' then
- if self.add_stderr_to_output then
- self.output = self.output .. table.concat(data, '')
- else
- self.stderr = self.stderr .. table.concat(data, '')
- end
- elseif event == 'stdout' then
- self.output = self.output .. table.concat(data, '')
- end
-end
-
--- Attempts to construct a shell command from an args list.
--- Only for display, to help users debug a failed command.
-local function shellify(cmd)
- if type(cmd) ~= 'table' then
- return cmd
- end
- local escaped = {}
- for i, v in ipairs(cmd) do
- if v:match('[^A-Za-z_/.-]') then
- escaped[i] = vim.fn.shellescape(v)
- else
- escaped[i] = v
- end
- end
- return table.concat(escaped, ' ')
-end
-
-function M._cmd_ok(cmd)
- local out = vim.fn.system(cmd)
- return vim.v.shell_error == 0, out
-end
-
---- Run a system command and timeout after 30 seconds.
----
---- @param cmd table List of command arguments to execute
---- @param args? table Optional arguments:
---- - stdin (string): Data to write to the job's stdin
---- - stderr (boolean): Append stderr to stdout
---- - ignore_error (boolean): If true, ignore error output
---- - timeout (number): Number of seconds to wait before timing out (default 30)
-function M._system(cmd, args)
- args = args or {}
- local stdin = args.stdin or ''
- local stderr = vim.F.if_nil(args.stderr, false)
- local ignore_error = vim.F.if_nil(args.ignore_error, false)
-
- local shell_error_code = 0
- local opts = {
- add_stderr_to_output = stderr,
- output = '',
- stderr = '',
- on_stdout = system_handler,
- on_stderr = system_handler,
- on_exit = function(_, data)
- shell_error_code = data
- end,
- }
- local jobid = vim.fn.jobstart(cmd, opts)
-
- if jobid < 1 then
- local message =
- string.format('Command error (job=%d): %s (in %s)', jobid, shellify(cmd), vim.uv.cwd())
- error(message)
- return opts.output, 1
- end
-
- if stdin:find('^%s$') then
- vim.fn.chansend(jobid, stdin)
- end
-
- local res = vim.fn.jobwait({ jobid }, vim.F.if_nil(args.timeout, 30) * 1000)
- if res[1] == -1 then
- error('Command timed out: ' .. shellify(cmd))
- vim.fn.jobstop(jobid)
- elseif shell_error_code ~= 0 and not ignore_error then
- local emsg = string.format(
- 'Command error (job=%d, exit code %d): %s (in %s)',
- jobid,
- shell_error_code,
- shellify(cmd),
- vim.uv.cwd()
- )
- if opts.output:find('%S') then
- emsg = string.format('%s\noutput: %s', emsg, opts.output)
- end
- if opts.stderr:find('%S') then
- emsg = string.format('%s\nstderr: %s', emsg, opts.stderr)
- end
- error(emsg)
- end
-
- -- return opts.output
- return vim.trim(vim.fn.system(cmd)), shell_error_code
-end
-
local path2name = function(path)
if path:match('%.lua$') then
-- Lua: transform "../lua/vim/lsp/health.lua" into "vim.lsp"
@@ -409,11 +301,13 @@ end
local PATTERNS = { '/autoload/health/*.vim', '/lua/**/**/health.lua', '/lua/**/**/health/init.lua' }
--- :checkhealth completion function used by cmdexpand.c get_healthcheck_names()
M._complete = function()
- local unique = vim
+ local unique = vim ---@type table<string,boolean>
+ ---@param pattern string
.iter(vim.tbl_map(function(pattern)
return vim.tbl_map(path2name, vim.api.nvim_get_runtime_file(pattern, true))
end, PATTERNS))
:flatten()
+ ---@param t table<string,boolean>
:fold({}, function(t, name)
t[name] = true -- Remove duplicates
return t
@@ -472,7 +366,7 @@ function M._check(mods, plugin_names)
vim.fn.call(func, {})
else
local f = assert(loadstring(func))
- local ok, output = pcall(f)
+ local ok, output = pcall(f) ---@type boolean, string
if not ok then
M.error(
string.format('Failed to run healthcheck for "%s" plugin. Exception:\n%s\n', name, output)
@@ -499,7 +393,7 @@ function M._check(mods, plugin_names)
end
s_output[#s_output + 1] = ''
s_output = vim.list_extend(header, s_output)
- vim.fn.append('$', s_output)
+ vim.fn.append(vim.fn.line('$'), s_output)
vim.cmd.redraw()
end
diff --git a/runtime/lua/vim/health/health.lua b/runtime/lua/vim/health/health.lua
index 5bc03199ee..235dacb82a 100644
--- a/runtime/lua/vim/health/health.lua
+++ b/runtime/lua/vim/health/health.lua
@@ -239,6 +239,7 @@ local function check_tmux()
return
end
+ ---@param option string
local get_tmux_option = function(option)
local cmd = 'tmux show-option -qvg ' .. option -- try global scope
local out = vim.fn.system(vim.fn.split(cmd))
@@ -378,7 +379,7 @@ local function check_terminal()
'SSH_TTY',
}) do
if vim.env[env_var] then
- health.info(vim.fn.printf('$%s="%s"', env_var, vim.env[env_var]))
+ health.info(string.format('$%s="%s"', env_var, vim.env[env_var]))
end
end
end
diff --git a/runtime/lua/vim/highlight.lua b/runtime/lua/vim/highlight.lua
index f278bd357f..89298ce568 100644
--- a/runtime/lua/vim/highlight.lua
+++ b/runtime/lua/vim/highlight.lua
@@ -20,8 +20,8 @@ M.priorities = {
--- @class vim.highlight.range.Opts
--- @inlinedoc
---
---- Type of range. See [setreg()]
---- (default: `'charwise'`)
+--- Type of range. See [getregtype()]
+--- (default: `'v'` i.e. charwise)
--- @field regtype? string
---
--- Indicates whether the range is end-inclusive
@@ -49,20 +49,49 @@ function M.range(bufnr, ns, higroup, start, finish, opts)
local priority = opts.priority or M.priorities.user
local scoped = opts._scoped or false
- -- TODO: in case of 'v', 'V' (not block), this should calculate equivalent
- -- bounds (row, col, end_row, end_col) as multiline regions are natively
- -- supported now
- local region = vim.region(bufnr, start, finish, regtype, inclusive)
- for linenr, cols in pairs(region) do
- local end_row
- if cols[2] == -1 then
- end_row = linenr + 1
- cols[2] = 0
+ local pos1 = type(start) == 'string' and vim.fn.getpos(start)
+ or { bufnr, start[1] + 1, start[2] + 1, 0 }
+ local pos2 = type(finish) == 'string' and vim.fn.getpos(finish)
+ or { bufnr, finish[1] + 1, finish[2] + 1, 0 }
+
+ local buf_line_count = vim.api.nvim_buf_line_count(bufnr)
+ pos1[2] = math.min(pos1[2], buf_line_count)
+ pos2[2] = math.min(pos2[2], buf_line_count)
+
+ if pos1[2] <= 0 or pos1[3] <= 0 or pos2[2] <= 0 or pos2[3] <= 0 then
+ return
+ end
+
+ vim.api.nvim_buf_call(bufnr, function()
+ local max_col1 = vim.fn.col({ pos1[2], '$' })
+ pos1[3] = math.min(pos1[3], max_col1)
+ local max_col2 = vim.fn.col({ pos2[2], '$' })
+ pos2[3] = math.min(pos2[3], max_col2)
+ end)
+
+ local region = vim.fn.getregionpos(pos1, pos2, {
+ type = regtype,
+ exclusive = not inclusive,
+ eol = true,
+ })
+ -- For non-blockwise selection, use a single extmark.
+ if regtype == 'v' or regtype == 'V' then
+ region = { { region[1][1], region[#region][2] } }
+ end
+
+ for _, res in ipairs(region) do
+ local start_row = res[1][2] - 1
+ local start_col = res[1][3] - 1
+ local end_row = res[2][2] - 1
+ local end_col = res[2][3]
+ if regtype == 'V' then
+ end_row = end_row + 1
+ end_col = 0
end
- api.nvim_buf_set_extmark(bufnr, ns, linenr, cols[1], {
+ api.nvim_buf_set_extmark(bufnr, ns, start_row, start_col, {
hl_group = higroup,
end_row = end_row,
- end_col = cols[2],
+ end_col = end_col,
priority = priority,
strict = false,
scoped = scoped,
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua
index da3d4d91f2..94c31359da 100644
--- a/runtime/lua/vim/lsp.lua
+++ b/runtime/lua/vim/lsp.lua
@@ -577,7 +577,8 @@ local function buf_attach(bufnr)
api.nvim_buf_attach(bufnr, false, {
on_lines = function(_, _, changedtick, firstline, lastline, new_lastline)
if #lsp.get_clients({ bufnr = bufnr }) == 0 then
- return true -- detach
+ -- detach if there are no clients
+ return #lsp.get_clients({ bufnr = bufnr, _uninitialized = true }) == 0
end
util.buf_versions[bufnr] = changedtick
changetracking.send_changes(bufnr, firstline, lastline, new_lastline)
diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua
index 49833eaeec..299b68e134 100644
--- a/runtime/lua/vim/lsp/buf.lua
+++ b/runtime/lua/vim/lsp/buf.lua
@@ -135,7 +135,7 @@ end
---@param mode "v"|"V"
---@return table {start={row,col}, end={row,col}} using (1, 0) indexing
local function range_from_selection(bufnr, mode)
- -- TODO: Use `vim.region()` instead https://github.com/neovim/neovim/pull/13896
+ -- TODO: Use `vim.fn.getregionpos()` instead.
-- [bufnum, lnum, col, off]; both row and column 1-indexed
local start = vim.fn.getpos('v')
diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua
index c8616bf728..327cd19125 100644
--- a/runtime/lua/vim/lsp/client.lua
+++ b/runtime/lua/vim/lsp/client.lua
@@ -182,7 +182,7 @@ local validate = vim.validate
--- It can be `null` if the client supports workspace folders but none are
--- configured.
--- @field workspace_folders lsp.WorkspaceFolder[]?
---- @field root_dir string
+--- @field root_dir string?
---
--- @field attached_buffers table<integer,true>
---
@@ -470,7 +470,6 @@ function Client.create(config)
_on_exit_cbs = ensure_list(config.on_exit),
_on_attach_cbs = ensure_list(config.on_attach),
_on_error_cb = config.on_error,
- _root_dir = config.root_dir,
_trace = get_trace(config.trace),
--- Contains $/progress report messages.
diff --git a/runtime/lua/vim/lsp/health.lua b/runtime/lua/vim/lsp/health.lua
index a79ae76eb9..ffe595ab37 100644
--- a/runtime/lua/vim/lsp/health.lua
+++ b/runtime/lua/vim/lsp/health.lua
@@ -33,16 +33,25 @@ local function check_active_clients()
local clients = vim.lsp.get_clients()
if next(clients) then
for _, client in pairs(clients) do
- local attached_to = table.concat(vim.tbl_keys(client.attached_buffers or {}), ',')
- report_info(
+ local cmd ---@type string
+ if type(client.config.cmd) == 'table' then
+ cmd = table.concat(client.config.cmd --[[@as table]], ' ')
+ elseif type(client.config.cmd) == 'function' then
+ cmd = tostring(client.config.cmd)
+ end
+ report_info(table.concat({
+ string.format('%s (id: %d)', client.name, client.id),
string.format(
- '%s (id=%s, root_dir=%s, attached_to=[%s])',
- client.name,
- client.id,
- vim.fn.fnamemodify(client.root_dir, ':~'),
- attached_to
- )
- )
+ ' Root directory: %s',
+ client.root_dir and vim.fn.fnamemodify(client.root_dir, ':~') or nil
+ ),
+ string.format(' Command: %s', cmd),
+ string.format(' Settings: %s', vim.inspect(client.settings, { newline = '\n ' })),
+ string.format(
+ ' Attached buffers: %s',
+ vim.iter(pairs(client.attached_buffers)):map(tostring):join(', ')
+ ),
+ }, '\n'))
end
else
report_info('No active clients')
@@ -50,7 +59,7 @@ local function check_active_clients()
end
local function check_watcher()
- vim.health.start('vim.lsp: File watcher')
+ vim.health.start('vim.lsp: File Watcher')
-- Only run the check if file watching has been enabled by a client.
local clients = vim.lsp.get_clients()
@@ -94,11 +103,68 @@ local function check_watcher()
end
end
+local function check_position_encodings()
+ vim.health.start('vim.lsp: Position Encodings')
+ local clients = vim.lsp.get_clients()
+ if next(clients) then
+ local position_encodings = {} ---@type table<integer, table<string, integer[]>>
+ for _, client in pairs(clients) do
+ for bufnr in pairs(client.attached_buffers) do
+ if not position_encodings[bufnr] then
+ position_encodings[bufnr] = {}
+ end
+ if not position_encodings[bufnr][client.offset_encoding] then
+ position_encodings[bufnr][client.offset_encoding] = {}
+ end
+ table.insert(position_encodings[bufnr][client.offset_encoding], client.id)
+ end
+ end
+
+ -- Check if any buffers are attached to multiple clients with different position encodings
+ local buffers = {} ---@type integer[]
+ for bufnr, encodings in pairs(position_encodings) do
+ local list = {} ---@type string[]
+ for k in pairs(encodings) do
+ list[#list + 1] = k
+ end
+
+ if #list > 1 then
+ buffers[#buffers + 1] = bufnr
+ end
+ end
+
+ if #buffers > 0 then
+ local lines =
+ { 'Found buffers attached to multiple clients with different position encodings.' }
+ for _, bufnr in ipairs(buffers) do
+ local encodings = position_encodings[bufnr]
+ local parts = {}
+ for encoding, client_ids in pairs(encodings) do
+ table.insert(
+ parts,
+ string.format('%s (client id(s): %s)', encoding:upper(), table.concat(client_ids, ', '))
+ )
+ end
+ table.insert(lines, string.format('- Buffer %d: %s', bufnr, table.concat(parts, ', ')))
+ end
+ report_warn(
+ table.concat(lines, '\n'),
+ 'Use the positionEncodings client capability to ensure all clients use the same position encoding'
+ )
+ else
+ report_info('No buffers contain mixed position encodings')
+ end
+ else
+ report_info('No active clients')
+ end
+end
+
--- Performs a healthcheck for LSP
function M.check()
check_log()
check_active_clients()
check_watcher()
+ check_position_encodings()
end
return M
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index 5a229a1169..0099e82f52 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -616,7 +616,7 @@ function M.rename(old_fname, new_fname, opts)
buf_rename[b] = { from = old_bname, to = new_bname }
end
- local newdir = assert(vim.fs.dirname(new_fname))
+ local newdir = vim.fs.dirname(new_fname)
vim.fn.mkdir(newdir, 'p')
local ok, err = os.rename(old_fname_full, new_fname)
@@ -625,7 +625,7 @@ function M.rename(old_fname, new_fname, opts)
local old_undofile = vim.fn.undofile(old_fname_full)
if uv.fs_stat(old_undofile) ~= nil then
local new_undofile = vim.fn.undofile(new_fname)
- vim.fn.mkdir(assert(vim.fs.dirname(new_undofile)), 'p')
+ vim.fn.mkdir(vim.fs.dirname(new_undofile), 'p')
os.rename(old_undofile, new_undofile)
end
diff --git a/runtime/lua/vim/provider/health.lua b/runtime/lua/vim/provider/health.lua
index 63e0da448a..fa2c452268 100644
--- a/runtime/lua/vim/provider/health.lua
+++ b/runtime/lua/vim/provider/health.lua
@@ -3,6 +3,112 @@ local iswin = vim.uv.os_uname().sysname == 'Windows_NT'
local M = {}
+local function cmd_ok(cmd)
+ local out = vim.fn.system(cmd)
+ return vim.v.shell_error == 0, out
+end
+
+-- Attempts to construct a shell command from an args list.
+-- Only for display, to help users debug a failed command.
+local function shellify(cmd)
+ if type(cmd) ~= 'table' then
+ return cmd
+ end
+ local escaped = {}
+ for i, v in ipairs(cmd) do
+ if v:match('[^A-Za-z_/.-]') then
+ escaped[i] = vim.fn.shellescape(v)
+ else
+ escaped[i] = v
+ end
+ end
+ return table.concat(escaped, ' ')
+end
+
+-- Handler for s:system() function.
+local function system_handler(self, _, data, event)
+ if event == 'stderr' then
+ if self.add_stderr_to_output then
+ self.output = self.output .. table.concat(data, '')
+ else
+ self.stderr = self.stderr .. table.concat(data, '')
+ end
+ elseif event == 'stdout' then
+ self.output = self.output .. table.concat(data, '')
+ end
+end
+
+--- @param cmd table List of command arguments to execute
+--- @param args? table Optional arguments:
+--- - stdin (string): Data to write to the job's stdin
+--- - stderr (boolean): Append stderr to stdout
+--- - ignore_error (boolean): If true, ignore error output
+--- - timeout (number): Number of seconds to wait before timing out (default 30)
+local function system(cmd, args)
+ args = args or {}
+ local stdin = args.stdin or ''
+ local stderr = vim.F.if_nil(args.stderr, false)
+ local ignore_error = vim.F.if_nil(args.ignore_error, false)
+
+ local shell_error_code = 0
+ local opts = {
+ add_stderr_to_output = stderr,
+ output = '',
+ stderr = '',
+ on_stdout = system_handler,
+ on_stderr = system_handler,
+ on_exit = function(_, data)
+ shell_error_code = data
+ end,
+ }
+ local jobid = vim.fn.jobstart(cmd, opts)
+
+ if jobid < 1 then
+ local message =
+ string.format('Command error (job=%d): %s (in %s)', jobid, shellify(cmd), vim.uv.cwd())
+ error(message)
+ return opts.output, 1
+ end
+
+ if stdin:find('^%s$') then
+ vim.fn.chansend(jobid, stdin)
+ end
+
+ local res = vim.fn.jobwait({ jobid }, vim.F.if_nil(args.timeout, 30) * 1000)
+ if res[1] == -1 then
+ error('Command timed out: ' .. shellify(cmd))
+ vim.fn.jobstop(jobid)
+ elseif shell_error_code ~= 0 and not ignore_error then
+ local emsg = string.format(
+ 'Command error (job=%d, exit code %d): %s (in %s)',
+ jobid,
+ shell_error_code,
+ shellify(cmd),
+ vim.uv.cwd()
+ )
+ if opts.output:find('%S') then
+ emsg = string.format('%s\noutput: %s', emsg, opts.output)
+ end
+ if opts.stderr:find('%S') then
+ emsg = string.format('%s\nstderr: %s', emsg, opts.stderr)
+ end
+ error(emsg)
+ end
+
+ return vim.trim(vim.fn.system(cmd)), shell_error_code
+end
+
+---@param provider string
+local function provider_disabled(provider)
+ local loaded_var = 'loaded_' .. provider .. '_provider'
+ local v = vim.g[loaded_var]
+ if v == 0 then
+ health.info('Disabled (' .. loaded_var .. '=' .. v .. ').')
+ return true
+ end
+ return false
+end
+
local function clipboard()
health.start('Clipboard (optional)')
@@ -10,7 +116,7 @@ local function clipboard()
os.getenv('TMUX')
and vim.fn.executable('tmux') == 1
and vim.fn.executable('pbpaste') == 1
- and not health._cmd_ok('pbpaste')
+ and not cmd_ok('pbpaste')
then
local tmux_version = string.match(vim.fn.system('tmux -V'), '%d+%.%d+')
local advice = {
@@ -20,9 +126,9 @@ local function clipboard()
health.error('pbcopy does not work with tmux version: ' .. tmux_version, advice)
end
- local clipboard_tool = vim.fn['provider#clipboard#Executable']()
+ local clipboard_tool = vim.fn['provider#clipboard#Executable']() ---@type string
if vim.g.clipboard ~= nil and clipboard_tool == '' then
- local error_message = vim.fn['provider#clipboard#Error']()
+ local error_message = vim.fn['provider#clipboard#Error']() ---@type string
health.error(
error_message,
"Use the example in :help g:clipboard as a template, or don't set g:clipboard at all."
@@ -40,7 +146,7 @@ end
local function node()
health.start('Node.js provider (optional)')
- if health._provider_disabled('node') then
+ if provider_disabled('node') then
return
end
@@ -60,7 +166,7 @@ local function node()
end
-- local node_v = vim.fn.split(system({'node', '-v'}), "\n")[1] or ''
- local ok, node_v = health._cmd_ok({ 'node', '-v' })
+ local ok, node_v = cmd_ok({ 'node', '-v' })
health.info('Node.js: ' .. node_v)
if not ok or vim.version.lt(node_v, '6.0.0') then
health.warn('Nvim node.js host does not support Node ' .. node_v)
@@ -73,7 +179,7 @@ local function node()
)
end
- local node_detect_table = vim.fn['provider#node#Detect']()
+ local node_detect_table = vim.fn['provider#node#Detect']() ---@type string[]
local host = node_detect_table[1]
if host:find('^%s*$') then
health.warn('Missing "neovim" npm (or yarn, pnpm) package.', {
@@ -97,7 +203,7 @@ local function node()
iswin and 'cmd /c ' .. manager .. ' info neovim --json' or manager .. ' info neovim --json'
)
local latest_npm
- ok, latest_npm = health._cmd_ok(vim.split(latest_npm_cmd, ' '))
+ ok, latest_npm = cmd_ok(vim.split(latest_npm_cmd, ' '))
if not ok or latest_npm:find('^%s$') then
health.error(
'Failed to run: ' .. latest_npm_cmd,
@@ -115,7 +221,7 @@ local function node()
local current_npm_cmd = { 'node', host, '--version' }
local current_npm
- ok, current_npm = health._cmd_ok(current_npm_cmd)
+ ok, current_npm = cmd_ok(current_npm_cmd)
if not ok then
health.error(
'Failed to run: ' .. table.concat(current_npm_cmd, ' '),
@@ -143,7 +249,7 @@ end
local function perl()
health.start('Perl provider (optional)')
- if health._provider_disabled('perl') then
+ if provider_disabled('perl') then
return
end
@@ -162,7 +268,7 @@ local function perl()
-- we cannot use cpanm that is on the path, as it may not be for the perl
-- set with g:perl_host_prog
- local ok = health._cmd_ok({ perl_exec, '-W', '-MApp::cpanminus', '-e', '' })
+ local ok = cmd_ok({ perl_exec, '-W', '-MApp::cpanminus', '-e', '' })
if not ok then
return { perl_exec, '"App::cpanminus" module is not installed' }
end
@@ -174,7 +280,7 @@ local function perl()
'my $app = App::cpanminus::script->new; $app->parse_options ("--info", "-q", "Neovim::Ext"); exit $app->doit',
}
local latest_cpan
- ok, latest_cpan = health._cmd_ok(latest_cpan_cmd)
+ ok, latest_cpan = cmd_ok(latest_cpan_cmd)
if not ok or latest_cpan:find('^%s*$') then
health.error(
'Failed to run: ' .. table.concat(latest_cpan_cmd, ' '),
@@ -184,7 +290,7 @@ local function perl()
elseif latest_cpan[1] == '!' then
local cpanm_errs = vim.split(latest_cpan, '!')
if cpanm_errs[1]:find("Can't write to ") then
- local advice = {}
+ local advice = {} ---@type string[]
for i = 2, #cpanm_errs do
advice[#advice + 1] = cpanm_errs[i]
end
@@ -197,7 +303,7 @@ local function perl()
return
end
end
- latest_cpan = vim.fn.matchstr(latest_cpan, [[\(\.\?\d\)\+]])
+ latest_cpan = tostring(vim.fn.matchstr(latest_cpan, [[\(\.\?\d\)\+]]))
if latest_cpan:find('^%s*$') then
health.error('Cannot parse version number from cpanm output: ' .. latest_cpan)
return
@@ -205,7 +311,7 @@ local function perl()
local current_cpan_cmd = { perl_exec, '-W', '-MNeovim::Ext', '-e', 'print $Neovim::Ext::VERSION' }
local current_cpan
- ok, current_cpan = health._cmd_ok(current_cpan_cmd)
+ ok, current_cpan = cmd_ok(current_cpan_cmd)
if not ok then
health.error(
'Failed to run: ' .. table.concat(current_cpan_cmd, ' '),
@@ -243,9 +349,11 @@ local function python_exepath(invocation)
return vim.fs.normalize(vim.trim(p.stdout))
end
--- Check if pyenv is available and a valid pyenv root can be found, then return
--- their respective paths. If either of those is invalid, return two empty
--- strings, effectively ignoring pyenv.
+--- Check if pyenv is available and a valid pyenv root can be found, then return
+--- their respective paths. If either of those is invalid, return two empty
+--- strings, effectively ignoring pyenv.
+---
+--- @return {[1]: string, [2]: string}
local function check_for_pyenv()
local pyenv_path = vim.fn.resolve(vim.fn.exepath('pyenv'))
@@ -288,11 +396,13 @@ local function check_bin(bin)
return true
end
--- Fetch the contents of a URL.
+--- Fetch the contents of a URL.
+---
+--- @param url string
local function download(url)
local has_curl = vim.fn.executable('curl') == 1
if has_curl and vim.fn.system({ 'curl', '-V' }):find('Protocols:.*https') then
- local out, rc = health._system({ 'curl', '-sL', url }, { stderr = true, ignore_error = true })
+ local out, rc = system({ 'curl', '-sL', url }, { stderr = true, ignore_error = true })
if rc ~= 0 then
return 'curl error with ' .. url .. ': ' .. rc
else
@@ -305,7 +415,7 @@ local function download(url)
from urllib2 import urlopen\n\
response = urlopen('" .. url .. "')\n\
print(response.read().decode('utf8'))\n"
- local out, rc = health._system({ 'python', '-c', script })
+ local out, rc = system({ 'python', '-c', script })
if out == '' and rc ~= 0 then
return 'python urllib.request error: ' .. rc
else
@@ -323,25 +433,24 @@ local function download(url)
return message
end
--- Get the latest Nvim Python client (pynvim) version from PyPI.
+--- Get the latest Nvim Python client (pynvim) version from PyPI.
local function latest_pypi_version()
local pypi_version = 'unable to get pypi response'
local pypi_response = download('https://pypi.python.org/pypi/pynvim/json')
if pypi_response ~= '' then
local pcall_ok, output = pcall(vim.fn.json_decode, pypi_response)
- local pypi_data
- if pcall_ok then
- pypi_data = output
- else
+ if not pcall_ok then
return 'error: ' .. pypi_response
end
+ local pypi_data = output
local pypi_element = pypi_data['info'] or {}
pypi_version = pypi_element['version'] or 'unable to parse'
end
return pypi_version
end
+--- @param s string
local function is_bad_response(s)
local lower = s:lower()
return vim.startswith(lower, 'unable')
@@ -349,20 +458,22 @@ local function is_bad_response(s)
or vim.startswith(lower, 'outdated')
end
--- Get version information using the specified interpreter. The interpreter is
--- used directly in case breaking changes were introduced since the last time
--- Nvim's Python client was updated.
---
--- Returns: {
--- {python executable version},
--- {current nvim version},
--- {current pypi nvim status},
--- {installed version status}
--- }
+--- Get version information using the specified interpreter. The interpreter is
+--- used directly in case breaking changes were introduced since the last time
+--- Nvim's Python client was updated.
+---
+--- @param python string
+---
+--- Returns: {
+--- {python executable version},
+--- {current nvim version},
+--- {current pypi nvim status},
+--- {installed version status}
+--- }
local function version_info(python)
local pypi_version = latest_pypi_version()
- local python_version, rc = health._system({
+ local python_version, rc = system({
python,
'-c',
'import sys; print(".".join(str(x) for x in sys.version_info[:3]))',
@@ -373,7 +484,7 @@ local function version_info(python)
end
local nvim_path
- nvim_path, rc = health._system({
+ nvim_path, rc = system({
python,
'-c',
'import sys; sys.path = [p for p in sys.path if p != ""]; import neovim; print(neovim.__file__)',
@@ -398,7 +509,7 @@ local function version_info(python)
-- Try to get neovim.VERSION (added in 0.1.11dev).
local nvim_version
- nvim_version, rc = health._system({
+ nvim_version, rc = system({
python,
'-c',
'from neovim import VERSION as v; print("{}.{}.{}{}".format(v.major, v.minor, v.patch, v.prerelease))',
@@ -406,9 +517,9 @@ local function version_info(python)
if rc ~= 0 or nvim_version == '' then
nvim_version = 'unable to find pynvim module version'
local base = vim.fs.basename(nvim_path)
- local metas = vim.fn.glob(base .. '-*/METADATA', 1, 1)
- vim.list_extend(metas, vim.fn.glob(base .. '-*/PKG-INFO', 1, 1))
- vim.list_extend(metas, vim.fn.glob(base .. '.egg-info/PKG-INFO', 1, 1))
+ local metas = vim.fn.glob(base .. '-*/METADATA', true, 1)
+ vim.list_extend(metas, vim.fn.glob(base .. '-*/PKG-INFO', true, 1))
+ vim.list_extend(metas, vim.fn.glob(base .. '.egg-info/PKG-INFO', true, 1))
metas = table.sort(metas, compare)
if metas and next(metas) ~= nil then
@@ -438,14 +549,13 @@ end
local function python()
health.start('Python 3 provider (optional)')
- local pyname = 'python3' ---@type string?
local python_exe = ''
local virtual_env = os.getenv('VIRTUAL_ENV')
local venv = virtual_env and vim.fn.resolve(virtual_env) or ''
- local host_prog_var = pyname .. '_host_prog'
- local python_multiple = {}
+ local host_prog_var = 'python3_host_prog'
+ local python_multiple = {} ---@type string[]
- if health._provider_disabled(pyname) then
+ if provider_disabled('python3') then
return
end
@@ -458,8 +568,7 @@ local function python()
health.info(message)
end
- local pythonx_warnings
- pyname, pythonx_warnings = vim.provider.python.detect_by_module('neovim')
+ local pyname, pythonx_warnings = vim.provider.python.detect_by_module('neovim')
if not pyname then
health.warn(
@@ -487,7 +596,7 @@ local function python()
end
if pyenv ~= '' then
- python_exe = health._system({ pyenv, 'which', pyname }, { stderr = true })
+ python_exe = system({ pyenv, 'which', pyname }, { stderr = true })
if python_exe == '' then
health.warn('pyenv could not find ' .. pyname .. '.')
end
@@ -547,12 +656,7 @@ local function python()
)
health.warn('pyenv is not set up optimally.', advice)
elseif venv ~= '' then
- local venv_root
- if pyenv_root ~= '' then
- venv_root = pyenv_root
- else
- venv_root = vim.fs.dirname(venv)
- end
+ local venv_root = pyenv_root ~= '' and pyenv_root or vim.fs.dirname(venv)
if vim.startswith(vim.fn.resolve(python_exe), venv_root .. '/') then
local advice = string.format(
@@ -637,9 +741,9 @@ local function python()
health.ok('no $VIRTUAL_ENV')
return
end
- local errors = {}
+ local errors = {} ---@type string[]
-- Keep hints as dict keys in order to discard duplicates.
- local hints = {}
+ local hints = {} ---@type table<string, boolean>
-- The virtualenv should contain some Python executables, and those
-- executables should be first both on Nvim's $PATH and the $PATH of
-- subshells launched from Nvim.
@@ -710,9 +814,7 @@ local function python()
health.info(msg)
health.info(
'Python version: '
- .. health._system(
- 'python -c "import platform, sys; sys.stdout.write(platform.python_version())"'
- )
+ .. system('python -c "import platform, sys; sys.stdout.write(platform.python_version())"')
)
health.ok('$VIRTUAL_ENV provides :!python.')
end
@@ -721,7 +823,7 @@ end
local function ruby()
health.start('Ruby provider (optional)')
- if health._provider_disabled('ruby') then
+ if provider_disabled('ruby') then
return
end
@@ -732,7 +834,7 @@ local function ruby()
)
return
end
- health.info('Ruby: ' .. health._system({ 'ruby', '-v' }))
+ health.info('Ruby: ' .. system({ 'ruby', '-v' }))
local host, _ = vim.provider.ruby.detect()
if (not host) or host:find('^%s*$') then
@@ -748,7 +850,7 @@ local function ruby()
health.info('Host: ' .. host)
local latest_gem_cmd = (iswin and 'cmd /c gem list -ra "^^neovim$"' or 'gem list -ra ^neovim$')
- local ok, latest_gem = health._cmd_ok(vim.split(latest_gem_cmd, ' '))
+ local ok, latest_gem = cmd_ok(vim.split(latest_gem_cmd, ' '))
if not ok or latest_gem:find('^%s*$') then
health.error(
'Failed to run: ' .. latest_gem_cmd,
@@ -761,7 +863,7 @@ local function ruby()
local current_gem_cmd = { host, '--version' }
local current_gem
- ok, current_gem = health._cmd_ok(current_gem_cmd)
+ ok, current_gem = cmd_ok(current_gem_cmd)
if not ok then
health.error(
'Failed to run: ' .. table.concat(current_gem_cmd, ' '),
diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua
index e9e4326057..2641d1feb0 100644
--- a/runtime/lua/vim/shared.lua
+++ b/runtime/lua/vim/shared.lua
@@ -214,7 +214,7 @@ end
---@param t table<T, any> (table) Table
---@return T[] : List of keys
function vim.tbl_keys(t)
- vim.validate({ t = { t, 't' } })
+ vim.validate('t', t, 'table')
--- @cast t table<any,any>
local keys = {}
@@ -231,7 +231,7 @@ end
---@param t table<any, T> (table) Table
---@return T[] : List of values
function vim.tbl_values(t)
- vim.validate({ t = { t, 't' } })
+ vim.validate('t', t, 'table')
local values = {}
for _, v in
@@ -332,7 +332,7 @@ end
---@param value any Value to compare
---@return boolean `true` if `t` contains `value`
function vim.list_contains(t, value)
- vim.validate({ t = { t, 't' } })
+ vim.validate('t', t, 'table')
--- @cast t table<any,any>
for _, v in ipairs(t) do
@@ -350,7 +350,7 @@ end
---@param t table Table to check
---@return boolean `true` if `t` is empty
function vim.tbl_isempty(t)
- vim.validate({ t = { t, 't' } })
+ vim.validate('t', t, 'table')
return next(t) == nil
end
@@ -580,7 +580,7 @@ end
---@return fun(table: table<K, V>, index?: K):K, V # |for-in| iterator over sorted keys and their values
---@return T
function vim.spairs(t)
- assert(type(t) == 'table', ('expected table, got %s'):format(type(t)))
+ vim.validate('t', t, 'table')
--- @cast t table<any,any>
-- collect the keys
@@ -691,7 +691,7 @@ end
---@param t table Table
---@return integer : Number of non-nil values in table
function vim.tbl_count(t)
- vim.validate({ t = { t, 't' } })
+ vim.validate('t', t, 'table')
--- @cast t table<any,any>
local count = 0
@@ -723,7 +723,7 @@ end
---@param s string String to trim
---@return string String with whitespace removed from its beginning and end
function vim.trim(s)
- vim.validate({ s = { s, 's' } })
+ vim.validate('s', s, 'string')
return s:match('^%s*(.*%S)') or ''
end
@@ -733,7 +733,7 @@ end
---@param s string String to escape
---@return string %-escaped pattern string
function vim.pesc(s)
- vim.validate({ s = { s, 's' } })
+ vim.validate('s', s, 'string')
return (s:gsub('[%(%)%.%%%+%-%*%?%[%]%^%$]', '%%%1'))
end
@@ -743,7 +743,8 @@ end
---@param prefix string Prefix to match
---@return boolean `true` if `prefix` is a prefix of `s`
function vim.startswith(s, prefix)
- vim.validate({ s = { s, 's' }, prefix = { prefix, 's' } })
+ vim.validate('s', s, 'string')
+ vim.validate('prefix', prefix, 'string')
return s:sub(1, #prefix) == prefix
end
@@ -753,7 +754,8 @@ end
---@param suffix string Suffix to match
---@return boolean `true` if `suffix` is a suffix of `s`
function vim.endswith(s, suffix)
- vim.validate({ s = { s, 's' }, suffix = { suffix, 's' } })
+ vim.validate('s', s, 'string')
+ vim.validate('suffix', suffix, 'string')
return #suffix == 0 or s:sub(-#suffix) == suffix
end
@@ -877,8 +879,30 @@ do
return true
end
- --- Validates a parameter specification (types and values). Specs are evaluated in alphanumeric
- --- order, until the first failure.
+ --- Validate function arguments.
+ ---
+ --- This function has two valid forms:
+ ---
+ --- 1. vim.validate(name: str, value: any, type: string, optional?: bool)
+ --- 2. vim.validate(spec: table)
+ ---
+ --- Form 1 validates that argument {name} with value {value} has the type
+ --- {type}. {type} must be a value returned by |lua-type()|. If {optional} is
+ --- true, then {value} may be null. This form is significantly faster and
+ --- should be preferred for simple cases.
+ ---
+ --- Example:
+ ---
+ --- ```lua
+ --- function vim.startswith(s, prefix)
+ --- vim.validate('s', s, 'string')
+ --- vim.validate('prefix', prefix, 'string')
+ --- ...
+ --- end
+ --- ```
+ ---
+ --- Form 2 validates a parameter specification (types and values). Specs are
+ --- evaluated in alphanumeric order, until the first failure.
---
--- Usage example:
---
@@ -930,8 +954,32 @@ do
--- only if the argument is valid. Can optionally return an additional
--- informative error message as the second returned value.
--- - msg: (optional) error string if validation fails
- function vim.validate(opt)
- local ok, err_msg = is_valid(opt)
+ --- @overload fun(name: string, val: any, expected: string, optional?: boolean)
+ function vim.validate(opt, ...)
+ local ok = false
+ local err_msg ---@type string?
+ local narg = select('#', ...)
+ if narg == 0 then
+ ok, err_msg = is_valid(opt)
+ elseif narg >= 2 then
+ -- Overloaded signature for fast/simple cases
+ local name = opt --[[@as string]]
+ local v, expected, optional = ... ---@type string, string, boolean?
+ local actual = type(v)
+
+ ok = (actual == expected) or (v == nil and optional == true)
+ if not ok then
+ err_msg = ('%s: expected %s, got %s%s'):format(
+ name,
+ expected,
+ actual,
+ v and (' (%s)'):format(v) or ''
+ )
+ end
+ else
+ error('invalid arguments')
+ end
+
if not ok then
error(err_msg, 2)
end
diff --git a/runtime/lua/vim/snippet.lua b/runtime/lua/vim/snippet.lua
index 1ec5235d7b..8cd454b908 100644
--- a/runtime/lua/vim/snippet.lua
+++ b/runtime/lua/vim/snippet.lua
@@ -376,7 +376,7 @@ local function select_tabstop(tabstop)
move_cursor_to(range[1] + 1, range[2] + 1)
feedkeys('v')
move_cursor_to(range[3] + 1, range[4])
- feedkeys('o<c-g>')
+ feedkeys('o<c-g><c-r>_')
end
end
@@ -456,6 +456,15 @@ local function setup_autocmds(bufnr)
end
end,
})
+
+ vim.api.nvim_create_autocmd('BufLeave', {
+ group = snippet_group,
+ desc = 'Stop the snippet session when leaving the buffer',
+ buffer = bufnr,
+ callback = function()
+ M.stop()
+ end,
+ })
end
--- Expands the given snippet text.
diff --git a/runtime/lua/vim/treesitter/dev.lua b/runtime/lua/vim/treesitter/dev.lua
index 5c91f101c0..ca8cf85eda 100644
--- a/runtime/lua/vim/treesitter/dev.lua
+++ b/runtime/lua/vim/treesitter/dev.lua
@@ -325,7 +325,10 @@ function M.inspect_tree(opts)
opts = opts or {}
+ -- source buffer
local buf = api.nvim_get_current_buf()
+
+ -- window id for source buffer
local win = api.nvim_get_current_win()
local treeview = assert(TSTreeView:new(buf, opts.lang))
@@ -334,12 +337,14 @@ function M.inspect_tree(opts)
close_win(vim.b[buf].dev_inspect)
end
+ -- window id for tree buffer
local w = opts.winid
if not w then
vim.cmd(opts.command or '60vnew')
w = api.nvim_get_current_win()
end
+ -- tree buffer
local b = opts.bufnr
if b then
api.nvim_win_set_buf(w, b)
@@ -375,6 +380,12 @@ function M.inspect_tree(opts)
callback = function()
local row = api.nvim_win_get_cursor(w)[1]
local lnum, col = treeview:get(row).node:start()
+
+ -- update source window if original was closed
+ if not api.nvim_win_is_valid(win) then
+ win = vim.fn.win_findbuf(buf)[1]
+ end
+
api.nvim_set_current_win(win)
api.nvim_win_set_cursor(win, { lnum + 1, col })
end,
@@ -432,6 +443,7 @@ function M.inspect_tree(opts)
return true
end
+ w = api.nvim_get_current_win()
api.nvim_buf_clear_namespace(buf, treeview.ns, 0, -1)
local row = api.nvim_win_get_cursor(w)[1]
local lnum, col, end_lnum, end_col = treeview:get(row).node:range()
@@ -441,6 +453,11 @@ function M.inspect_tree(opts)
hl_group = 'Visual',
})
+ -- update source window if original was closed
+ if not api.nvim_win_is_valid(win) then
+ win = vim.fn.win_findbuf(buf)[1]
+ end
+
local topline, botline = vim.fn.line('w0', win), vim.fn.line('w$', win)
-- Move the cursor if highlighted range is completely out of view
@@ -506,7 +523,10 @@ function M.inspect_tree(opts)
buffer = buf,
once = true,
callback = function()
- close_win(w)
+ -- close all tree windows
+ for _, window in pairs(vim.fn.win_findbuf(b)) do
+ close_win(window)
+ end
end,
})
end