aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua')
-rw-r--r--runtime/lua/_vim9script.lua2
-rw-r--r--runtime/lua/coxpcall.lua4
-rw-r--r--runtime/lua/editorconfig.lua6
-rw-r--r--runtime/lua/man.lua698
-rw-r--r--runtime/lua/vim/_defaults.lua14
-rw-r--r--runtime/lua/vim/_editor.lua16
-rw-r--r--runtime/lua/vim/_meta/api.lua94
-rw-r--r--runtime/lua/vim/_meta/api_keysets.lua7
-rw-r--r--runtime/lua/vim/_meta/api_keysets_extra.lua20
-rw-r--r--runtime/lua/vim/_meta/builtin.lua5
-rw-r--r--runtime/lua/vim/_meta/json.lua14
-rw-r--r--runtime/lua/vim/_meta/options.lua48
-rw-r--r--runtime/lua/vim/_meta/vimfn.lua56
-rw-r--r--runtime/lua/vim/_meta/vvars.lua12
-rw-r--r--runtime/lua/vim/_options.lua8
-rw-r--r--runtime/lua/vim/_system.lua3
-rw-r--r--runtime/lua/vim/diagnostic.lua34
-rw-r--r--runtime/lua/vim/filetype.lua38
-rw-r--r--runtime/lua/vim/filetype/detect.lua35
-rw-r--r--runtime/lua/vim/fs.lua144
-rw-r--r--runtime/lua/vim/func.lua14
-rw-r--r--runtime/lua/vim/func/_memoize.lua56
-rw-r--r--runtime/lua/vim/health.lua60
-rw-r--r--runtime/lua/vim/hl.lua2
-rw-r--r--runtime/lua/vim/lsp.lua104
-rw-r--r--runtime/lua/vim/lsp/_folding_range.lua2
-rw-r--r--runtime/lua/vim/lsp/_watchfiles.lua1
-rw-r--r--runtime/lua/vim/lsp/buf.lua19
-rw-r--r--runtime/lua/vim/lsp/client.lua15
-rw-r--r--runtime/lua/vim/lsp/codelens.lua4
-rw-r--r--runtime/lua/vim/lsp/completion.lua18
-rw-r--r--runtime/lua/vim/lsp/diagnostic.lua4
-rw-r--r--runtime/lua/vim/lsp/handlers.lua7
-rw-r--r--runtime/lua/vim/lsp/health.lua5
-rw-r--r--runtime/lua/vim/lsp/inlay_hint.lua4
-rw-r--r--runtime/lua/vim/lsp/semantic_tokens.lua6
-rw-r--r--runtime/lua/vim/lsp/util.lua17
-rw-r--r--runtime/lua/vim/shared.lua4
-rw-r--r--runtime/lua/vim/snippet.lua4
-rw-r--r--runtime/lua/vim/treesitter.lua8
-rw-r--r--runtime/lua/vim/treesitter/_fold.lua248
-rw-r--r--runtime/lua/vim/treesitter/_query_linter.lua2
-rw-r--r--runtime/lua/vim/treesitter/dev.lua10
-rw-r--r--runtime/lua/vim/treesitter/highlighter.lua25
-rw-r--r--runtime/lua/vim/treesitter/language.lua5
-rw-r--r--runtime/lua/vim/treesitter/languagetree.lua145
-rw-r--r--runtime/lua/vim/treesitter/query.lua299
-rw-r--r--runtime/lua/vim/version.lua3
-rw-r--r--runtime/lua/vim/vimhelp.lua2
49 files changed, 1438 insertions, 913 deletions
diff --git a/runtime/lua/_vim9script.lua b/runtime/lua/_vim9script.lua
index 23c078cb87..29bafc53b2 100644
--- a/runtime/lua/_vim9script.lua
+++ b/runtime/lua/_vim9script.lua
@@ -145,7 +145,7 @@ local vim9 = (function()
-- work well for calling ":source X" from within a vimscript/vim9script
-- function
M.make_source_cmd = function()
- local group = vim.api.nvim_create_augroup('vim9script-source', {})
+ local group = vim.api.nvim_create_augroup('nvim.vim9script_source', {})
vim.api.nvim_create_autocmd('SourceCmd', {
pattern = '*.vim',
group = group,
diff --git a/runtime/lua/coxpcall.lua b/runtime/lua/coxpcall.lua
index 6b179f1ef0..75e7a43567 100644
--- a/runtime/lua/coxpcall.lua
+++ b/runtime/lua/coxpcall.lua
@@ -1,6 +1,10 @@
-------------------------------------------------------------------------------
+-- (Not needed for LuaJIT or Lua 5.2+)
+--
-- Coroutine safe xpcall and pcall versions
--
+-- https://keplerproject.github.io/coxpcall/
+--
-- Encapsulates the protected calls with a coroutine based loop, so errors can
-- be dealed without the usual Lua 5.x pcall/xpcall issues with coroutines
-- yielding inside the call to pcall or xpcall.
diff --git a/runtime/lua/editorconfig.lua b/runtime/lua/editorconfig.lua
index e65d267a70..2093c4eb5c 100644
--- a/runtime/lua/editorconfig.lua
+++ b/runtime/lua/editorconfig.lua
@@ -151,7 +151,7 @@ function properties.trim_trailing_whitespace(bufnr, val)
)
if val == 'true' then
vim.api.nvim_create_autocmd('BufWritePre', {
- group = 'editorconfig',
+ group = 'nvim.editorconfig',
buffer = bufnr,
callback = function()
local view = vim.fn.winsaveview()
@@ -163,7 +163,7 @@ function properties.trim_trailing_whitespace(bufnr, val)
else
vim.api.nvim_clear_autocmds({
event = 'BufWritePre',
- group = 'editorconfig',
+ group = 'nvim.editorconfig',
buffer = bufnr,
})
end
@@ -180,7 +180,7 @@ function properties.insert_final_newline(bufnr, val)
local endofline = val == 'true'
if vim.bo[bufnr].endofline ~= endofline then
vim.api.nvim_create_autocmd('BufWritePre', {
- group = 'editorconfig',
+ group = 'nvim.editorconfig',
buffer = bufnr,
once = true,
callback = function()
diff --git a/runtime/lua/man.lua b/runtime/lua/man.lua
index 114c94f9e5..e322a1f680 100644
--- a/runtime/lua/man.lua
+++ b/runtime/lua/man.lua
@@ -1,93 +1,84 @@
local api, fn = vim.api, vim.fn
-local FIND_ARG = '-w'
-local localfile_arg = true -- Always use -l if possible. #6683
-
----@type table[]
-local buf_hls = {}
-
local M = {}
-local function man_error(msg)
- M.errormsg = 'man.lua: ' .. vim.inspect(msg)
- error(M.errormsg)
-end
-
--- Run a system command and timeout after 30 seconds.
----@param cmd string[]
----@param silent boolean?
----@param env? table<string,string|number>
----@return string
+--- Run a system command and timeout after 10 seconds.
+--- @param cmd string[]
+--- @param silent boolean?
+--- @param env? table<string,string|number>
+--- @return string
local function system(cmd, silent, env)
local r = vim.system(cmd, { env = env, timeout = 10000 }):wait()
- if r.code ~= 0 and not silent then
- local cmd_str = table.concat(cmd, ' ')
- man_error(string.format("command error '%s': %s", cmd_str, r.stderr))
+ if not silent then
+ if r.code ~= 0 then
+ local cmd_str = table.concat(cmd, ' ')
+ error(string.format("command error '%s': %s", cmd_str, r.stderr))
+ end
+ assert(r.stdout ~= '')
end
return assert(r.stdout)
end
----@param line string
----@param linenr integer
-local function highlight_line(line, linenr)
- ---@type string[]
+--- @enum Man.Attribute
+local Attrs = {
+ None = 0,
+ Bold = 1,
+ Underline = 2,
+ Italic = 3,
+}
+
+--- @param line string
+--- @param row integer
+--- @param hls {attr:Man.Attribute,row:integer,start:integer,final:integer}[]
+--- @return string
+local function render_line(line, row, hls)
+ --- @type string[]
local chars = {}
local prev_char = ''
local overstrike, escape, osc8 = false, false, false
- ---@type table<integer,{attr:integer,start:integer,final:integer}>
- local hls = {} -- Store highlight groups as { attr, start, final }
-
- local NONE, BOLD, UNDERLINE, ITALIC = 0, 1, 2, 3
- local hl_groups = { [BOLD] = 'manBold', [UNDERLINE] = 'manUnderline', [ITALIC] = 'manItalic' }
- local attr = NONE
+ local attr = Attrs.None
local byte = 0 -- byte offset
- local function end_attr_hl(attr_)
- for i, hl in ipairs(hls) do
- if hl.attr == attr_ and hl.final == -1 then
- hl.final = byte
- hls[i] = hl
- end
- end
- end
+ local hls_start = #hls + 1
+ --- @param code integer
local function add_attr_hl(code)
local continue_hl = true
if code == 0 then
- attr = NONE
+ attr = Attrs.None
continue_hl = false
elseif code == 1 then
- attr = BOLD
+ attr = Attrs.Bold
elseif code == 22 then
- attr = BOLD
+ attr = Attrs.Bold
continue_hl = false
elseif code == 3 then
- attr = ITALIC
+ attr = Attrs.Italic
elseif code == 23 then
- attr = ITALIC
+ attr = Attrs.Italic
continue_hl = false
elseif code == 4 then
- attr = UNDERLINE
+ attr = Attrs.Underline
elseif code == 24 then
- attr = UNDERLINE
+ attr = Attrs.Underline
continue_hl = false
else
- attr = NONE
+ attr = Attrs.None
return
end
if continue_hl then
- hls[#hls + 1] = { attr = attr, start = byte, final = -1 }
+ hls[#hls + 1] = { attr = attr, row = row, start = byte, final = -1 }
else
- if attr == NONE then
- for a, _ in pairs(hl_groups) do
- end_attr_hl(a)
+ for _, a in pairs(attr == Attrs.None and Attrs or { attr }) do
+ for i = hls_start, #hls do
+ if hls[i].attr == a and hls[i].final == -1 then
+ hls[i].final = byte
+ end
end
- else
- end_attr_hl(attr)
end
end
end
@@ -100,11 +91,11 @@ local function highlight_line(line, linenr)
if overstrike then
local last_hl = hls[#hls]
if char == prev_char then
- if char == '_' and attr == ITALIC and last_hl and last_hl.final == byte then
+ if char == '_' and attr == Attrs.Italic and last_hl and last_hl.final == byte then
-- This underscore is in the middle of an italic word
- attr = ITALIC
+ attr = Attrs.Italic
else
- attr = BOLD
+ attr = Attrs.Bold
end
elseif prev_char == '_' then
-- Even though underline is strictly what this should be. <bs>_ was used by nroff to
@@ -113,26 +104,26 @@ local function highlight_line(line, linenr)
-- See:
-- - https://unix.stackexchange.com/questions/274658/purpose-of-ascii-text-with-overstriking-file-format/274795#274795
-- - https://cmd.inp.nsk.su/old/cmd2/manuals/unix/UNIX_Unleashed/ch08.htm
- -- attr = UNDERLINE
- attr = ITALIC
+ -- attr = Attrs.Underline
+ attr = Attrs.Italic
elseif prev_char == '+' and char == 'o' then
-- bullet (overstrike text '+^Ho')
- attr = BOLD
+ attr = Attrs.Bold
char = '·'
elseif prev_char == '·' and char == 'o' then
-- bullet (additional handling for '+^H+^Ho^Ho')
- attr = BOLD
+ attr = Attrs.Bold
char = '·'
else
-- use plain char
- attr = NONE
+ attr = Attrs.None
end
-- Grow the previous highlight group if possible
if last_hl and last_hl.attr == attr and last_hl.final == byte then
last_hl.final = byte + #char
else
- hls[#hls + 1] = { attr = attr, start = byte, final = byte + #char }
+ hls[#hls + 1] = { attr = attr, row = row, start = byte, final = byte + #char }
end
overstrike = false
@@ -151,14 +142,15 @@ local function highlight_line(line, linenr)
-- We only want to match against SGR sequences, which consist of ESC
-- followed by '[', then a series of parameter and intermediate bytes in
-- the range 0x20 - 0x3f, then 'm'. (See ECMA-48, sections 5.4 & 8.3.117)
- ---@type string?
+ --- @type string?
local sgr = prev_char:match('^%[([\032-\063]*)m$')
-- Ignore escape sequences with : characters, as specified by ITU's T.416
-- Open Document Architecture and interchange format.
- if sgr and not string.find(sgr, ':') then
- local match ---@type string?
+ if sgr and not sgr:find(':') then
+ local match --- @type string?
while sgr and #sgr > 0 do
-- Match against SGR parameters, which may be separated by ';'
+ --- @type string?, string?
match, sgr = sgr:match('^(%d*);?(.*)')
add_attr_hl(match + 0) -- coerce to number
end
@@ -184,55 +176,40 @@ local function highlight_line(line, linenr)
end
end
- for _, hl in ipairs(hls) do
- if hl.attr ~= NONE then
- buf_hls[#buf_hls + 1] = {
- 0,
- -1,
- hl_groups[hl.attr],
- linenr - 1,
- hl.start,
- hl.final,
- }
- end
- end
-
return table.concat(chars, '')
end
+local HlGroups = {
+ [Attrs.Bold] = 'manBold',
+ [Attrs.Underline] = 'manUnderline',
+ [Attrs.Italic] = 'manItalic',
+}
+
local function highlight_man_page()
local mod = vim.bo.modifiable
vim.bo.modifiable = true
local lines = api.nvim_buf_get_lines(0, 0, -1, false)
+
+ --- @type {attr:Man.Attribute,row:integer,start:integer,final:integer}[]
+ local hls = {}
+
for i, line in ipairs(lines) do
- lines[i] = highlight_line(line, i)
+ lines[i] = render_line(line, i - 1, hls)
end
+
api.nvim_buf_set_lines(0, 0, -1, false, lines)
- for _, args in ipairs(buf_hls) do
- api.nvim_buf_add_highlight(unpack(args))
+ for _, hl in ipairs(hls) do
+ api.nvim_buf_add_highlight(0, -1, HlGroups[hl.attr], hl.row, hl.start, hl.final)
end
- buf_hls = {}
vim.bo.modifiable = mod
end
--- replace spaces in a man page name with underscores
--- intended for PostgreSQL, which has man pages like 'CREATE_TABLE(7)';
--- while editing SQL source code, it's nice to visually select 'CREATE TABLE'
--- and hit 'K', which requires this transformation
----@param str string
----@return string
-local function spaces_to_underscores(str)
- local res = str:gsub('%s', '_')
- return res
-end
-
----@param sect string|nil
----@param name string|nil
----@param silent boolean
-local function get_path(sect, name, silent)
+--- @param name? string
+--- @param sect? string
+local function get_path(name, sect)
name = name or ''
sect = sect or ''
-- Some man implementations (OpenBSD) return all available paths from the
@@ -253,14 +230,14 @@ local function get_path(sect, name, silent)
--
-- Finally, we can avoid relying on -S or -s here since they are very
-- inconsistently supported. Instead, call -w with a section and a name.
- local cmd ---@type string[]
+ local cmd --- @type string[]
if sect == '' then
- cmd = { 'man', FIND_ARG, name }
+ cmd = { 'man', '-w', name }
else
- cmd = { 'man', FIND_ARG, sect, name }
+ cmd = { 'man', '-w', sect, name }
end
- local lines = system(cmd, silent)
+ local lines = system(cmd, true)
local results = vim.split(lines, '\n', { trimempty = true })
if #results == 0 then
@@ -276,87 +253,73 @@ local function get_path(sect, name, silent)
end
-- find any that match the specified name
- ---@param v string
+ --- @param v string
local namematches = vim.tbl_filter(function(v)
local tail = fn.fnamemodify(v, ':t')
- return string.find(tail, name, 1, true)
+ return tail:find(name, 1, true) ~= nil
end, results) or {}
local sectmatches = {}
if #namematches > 0 and sect ~= '' then
- ---@param v string
+ --- @param v string
sectmatches = vim.tbl_filter(function(v)
return fn.fnamemodify(v, ':e') == sect
end, namematches)
end
- return fn.substitute(sectmatches[1] or namematches[1] or results[1], [[\n\+$]], '', '')
+ return (sectmatches[1] or namematches[1] or results[1]):gsub('\n+$', '')
end
----@param text string
----@param pat_or_re string
-local function matchstr(text, pat_or_re)
- local re = type(pat_or_re) == 'string' and vim.regex(pat_or_re) or pat_or_re
-
- ---@type integer, integer
- local s, e = re:match_str(text)
-
- if s == nil then
- return
+--- Attempt to extract the name and sect out of 'name(sect)'
+--- otherwise just return the largest string of valid characters in ref
+--- @param ref string
+--- @return string? name
+--- @return string? sect
+--- @return string? err
+local function parse_ref(ref)
+ if ref == '' or ref:sub(1, 1) == '-' then
+ return nil, nil, ('invalid manpage reference "%s"'):format(ref)
end
- return text:sub(vim.str_utfindex(text, 'utf-32', s) + 1, vim.str_utfindex(text, 'utf-32', e))
-end
+ -- match "<name>(<sect>)"
+ -- note: name can contain spaces
+ local name, sect = ref:match('([^()]+)%(([^()]+)%)')
+ if name then
+ -- see ':Man 3X curses' on why tolower.
+ -- TODO(nhooyr) Not sure if this is portable across OSs
+ -- but I have not seen a single uppercase section.
+ return name, sect:lower()
+ end
--- attempt to extract the name and sect out of 'name(sect)'
--- otherwise just return the largest string of valid characters in ref
----@param ref string
----@return string, string
-local function extract_sect_and_name_ref(ref)
- ref = ref or ''
- if ref:sub(1, 1) == '-' then -- try ':Man -pandoc' with this disabled.
- man_error("manpage name cannot start with '-'")
- end
- local ref1 = ref:match('[^()]+%([^()]+%)')
- if not ref1 then
- local name = ref:match('[^()]+')
- if not name then
- man_error('manpage reference cannot contain only parentheses: ' .. ref)
- end
- return '', name
- end
- local parts = vim.split(ref1, '(', { plain = true })
- -- see ':Man 3X curses' on why tolower.
- -- TODO(nhooyr) Not sure if this is portable across OSs
- -- but I have not seen a single uppercase section.
- local sect = vim.split(parts[2] or '', ')', { plain = true })[1]:lower()
- local name = parts[1]
- return sect, name
+ name = ref:match('[^()]+')
+ if not name then
+ return nil, nil, ('invalid manpage reference "%s"'):format(ref)
+ end
+ return name
end
--- find_path attempts to find the path to a manpage
--- based on the passed section and name.
---
--- 1. If manpage could not be found with the given sect and name,
--- then try all the sections in b:man_default_sects.
--- 2. If it still could not be found, then we try again without a section.
--- 3. If still not found but $MANSECT is set, then we try again with $MANSECT
--- unset.
--- 4. If a path still wasn't found, return nil.
----@param sect string?
----@param name string
-function M.find_path(sect, name)
+--- Attempts to find the path to a manpage based on the passed section and name.
+---
+--- 1. If manpage could not be found with the given sect and name,
+--- then try all the sections in b:man_default_sects.
+--- 2. If it still could not be found, then we try again without a section.
+--- 3. If still not found but $MANSECT is set, then we try again with $MANSECT
+--- unset.
+--- 4. If a path still wasn't found, return nil.
+--- @param name string?
+--- @param sect string?
+--- @return string? path
+function M._find_path(name, sect)
if sect and sect ~= '' then
- local ret = get_path(sect, name, true)
+ local ret = get_path(name, sect)
if ret then
return ret
end
end
if vim.b.man_default_sects ~= nil then
- local sects = vim.split(vim.b.man_default_sects, ',', { plain = true, trimempty = true })
- for _, sec in ipairs(sects) do
- local ret = get_path(sec, name, true)
+ for sec in vim.gsplit(vim.b.man_default_sects, ',', { trimempty = true }) do
+ local ret = get_path(name, sec)
if ret then
return ret
end
@@ -364,17 +327,18 @@ function M.find_path(sect, name)
end
-- if none of the above worked, we will try with no section
- local res_empty_sect = get_path('', name, true)
- if res_empty_sect then
- return res_empty_sect
+ local ret = get_path(name)
+ if ret then
+ return ret
end
-- if that still didn't work, we will check for $MANSECT and try again with it
-- unset
if vim.env.MANSECT then
+ --- @type string
local mansect = vim.env.MANSECT
vim.env.MANSECT = nil
- local res = get_path('', name, true)
+ local res = get_path(name)
vim.env.MANSECT = mansect
if res then
return res
@@ -385,24 +349,27 @@ function M.find_path(sect, name)
return nil
end
-local EXT_RE = vim.regex([[\.\%([glx]z\|bz2\|lzma\|Z\)$]])
-
--- Extracts the name/section from the 'path/name.sect', because sometimes the actual section is
--- more specific than what we provided to `man` (try `:Man 3 App::CLI`).
--- Also on linux, name seems to be case-insensitive. So for `:Man PRIntf`, we
--- still want the name of the buffer to be 'printf'.
----@param path string
----@return string, string
-local function extract_sect_and_name_path(path)
+--- Extracts the name/section from the 'path/name.sect', because sometimes the
+--- actual section is more specific than what we provided to `man`
+--- (try `:Man 3 App::CLI`). Also on linux, name seems to be case-insensitive.
+--- So for `:Man PRIntf`, we still want the name of the buffer to be 'printf'.
+--- @param path string
+--- @return string name
+--- @return string sect
+local function parse_path(path)
local tail = fn.fnamemodify(path, ':t')
- if EXT_RE:match_str(path) then -- valid extensions
+ if
+ path:match('%.[glx]z$')
+ or path:match('%.bz2$')
+ or path:match('%.lzma$')
+ or path:match('%.Z$')
+ then
tail = fn.fnamemodify(tail, ':r')
end
- local name, sect = tail:match('^(.+)%.([^.]+)$')
- return sect, name
+ return tail:match('^(.+)%.([^.]+)$')
end
----@return boolean
+--- @return boolean
local function find_man()
if vim.bo.filetype == 'man' then
return true
@@ -430,22 +397,32 @@ local function set_options()
vim.bo.filetype = 'man'
end
----@param path string
----@param silent boolean?
----@return string
+--- Always use -l if possible. #6683
+--- @type boolean?
+local localfile_arg
+
+--- @param path string
+--- @param silent boolean?
+--- @return string
local function get_page(path, silent)
-- Disable hard-wrap by using a big $MANWIDTH (max 1000 on some systems #9065).
-- Soft-wrap: ftplugin/man.lua sets wrap/breakindent/….
-- Hard-wrap: driven by `man`.
- local manwidth ---@type integer|string
+ local manwidth --- @type integer|string
if (vim.g.man_hardwrap or 1) ~= 1 then
manwidth = 999
elseif vim.env.MANWIDTH then
- manwidth = vim.env.MANWIDTH
+ manwidth = vim.env.MANWIDTH --- @type string|integer
else
manwidth = api.nvim_win_get_width(0) - vim.o.wrapmargin
end
+ if localfile_arg == nil then
+ local mpath = get_path('man')
+ -- Check for -l support.
+ localfile_arg = (mpath and system({ 'man', '-l', mpath }, true) or '') ~= ''
+ end
+
local cmd = localfile_arg and { 'man', '-l', path } or { 'man', path }
-- Force MANPAGER=cat to ensure Vim is not recursively invoked (by man-db).
@@ -458,47 +435,17 @@ local function get_page(path, silent)
})
end
----@param lnum integer
----@return string
-local function getline(lnum)
- ---@diagnostic disable-next-line
- return fn.getline(lnum)
-end
-
----@param page string
-local function put_page(page)
- vim.bo.modifiable = true
- vim.bo.readonly = false
- vim.bo.swapfile = false
-
- api.nvim_buf_set_lines(0, 0, -1, false, vim.split(page, '\n'))
-
- while getline(1):match('^%s*$') do
- api.nvim_buf_set_lines(0, 0, 1, false, {})
- end
- -- XXX: nroff justifies text by filling it with whitespace. That interacts
- -- badly with our use of $MANWIDTH=999. Hack around this by using a fixed
- -- size for those whitespace regions.
- -- Use try/catch to avoid setting v:errmsg.
- vim.cmd([[
- try
- keeppatterns keepjumps %s/\s\{199,}/\=repeat(' ', 10)/g
- catch
- endtry
- ]])
- vim.cmd('1') -- Move cursor to first line
- highlight_man_page()
- set_options()
-end
-
+--- @param path string
+--- @param psect string
local function format_candidate(path, psect)
- if matchstr(path, [[\.\%(pdf\|in\)$]]) then -- invalid extensions
+ if vim.endswith(path, '.pdf') or vim.endswith(path, '.in') then
+ -- invalid extensions
return ''
end
- local sect, name = extract_sect_and_name_path(path)
+ local name, sect = parse_path(path)
if sect == psect then
return name
- elseif sect and name and matchstr(sect, psect .. '.\\+$') then -- invalid extensions
+ elseif sect:match(psect .. '.+$') then -- invalid extensions
-- We include the section if the user provided section is a prefix
-- of the actual section.
return ('%s(%s)'):format(name, sect)
@@ -506,63 +453,54 @@ local function format_candidate(path, psect)
return ''
end
----@generic T
----@param list T[]
----@param elem T
----@return T[]
-local function move_elem_to_head(list, elem)
- ---@diagnostic disable-next-line:no-unknown
- local list1 = vim.tbl_filter(function(v)
- return v ~= elem
- end, list)
- return { elem, unpack(list1) }
-end
-
----@param sect string
----@param name string
----@return string[]
-local function get_paths(sect, name)
+--- @param name string
+--- @param sect? string
+--- @return string[] paths
+--- @return string? err
+local function get_paths(name, sect)
-- Try several sources for getting the list man directories:
- -- 1. `man -w` (works on most systems)
- -- 2. `manpath`
+ -- 1. `manpath -q`
+ -- 2. `man -w` (works on most systems)
-- 3. $MANPATH
- local mandirs_raw = vim.F.npcall(system, { 'man', FIND_ARG })
- or vim.F.npcall(system, { 'manpath', '-q' })
+ --
+ -- Note we prefer `manpath -q` because `man -w`:
+ -- - does not work on MacOS 14 and later.
+ -- - only returns '/usr/bin/man' on MacOS 13 and earlier.
+ --- @type string?
+ local mandirs_raw = vim.F.npcall(system, { 'manpath', '-q' })
+ or vim.F.npcall(system, { 'man', '-w' })
or vim.env.MANPATH
if not mandirs_raw then
- man_error("Could not determine man directories from: 'man -w', 'manpath' or $MANPATH")
+ return {}, "Could not determine man directories from: 'man -w', 'manpath' or $MANPATH"
end
local mandirs = table.concat(vim.split(mandirs_raw, '[:\n]', { trimempty = true }), ',')
- ---@type string[]
+
+ sect = sect or ''
+
+ --- @type string[]
local paths = fn.globpath(mandirs, 'man[^\\/]*/' .. name .. '*.' .. sect .. '*', false, true)
-- Prioritize the result from find_path as it obeys b:man_default_sects.
- local first = M.find_path(sect, name)
+ local first = M._find_path(name, sect)
if first then
- paths = move_elem_to_head(paths, first)
+ --- @param v string
+ paths = vim.tbl_filter(function(v)
+ return v ~= first
+ end, paths)
+ table.insert(paths, 1, first)
end
return paths
end
----@param sect string
----@param psect string
----@param name string
----@return string[]
-local function complete(sect, psect, name)
- local pages = get_paths(sect, name)
- -- We remove duplicates in case the same manpage in different languages was found.
- return fn.uniq(fn.sort(vim.tbl_map(function(v)
- return format_candidate(v, psect)
- end, pages) or {}, 'i'))
-end
-
--- see extract_sect_and_name_ref on why tolower(sect)
----@param arg_lead string
----@param cmd_line string
-function M.man_complete(arg_lead, cmd_line, _)
+--- @param arg_lead string
+--- @param cmd_line string
+--- @return string? sect
+--- @return string? psect
+--- @return string? name
+local function parse_cmdline(arg_lead, cmd_line)
local args = vim.split(cmd_line, '%s+', { trimempty = true })
local cmd_offset = fn.index(args, 'Man')
if cmd_offset > 0 then
@@ -572,13 +510,13 @@ function M.man_complete(arg_lead, cmd_line, _)
end
if #args > 3 then
- return {}
+ return
end
if #args == 1 then
-- returning full completion is laggy. Require some arg_lead to complete
- -- return complete('', '', '')
- return {}
+ -- return '', '', ''
+ return
end
if arg_lead:match('^[^()]+%([^()]*$') then
@@ -587,18 +525,19 @@ function M.man_complete(arg_lead, cmd_line, _)
-- It will offer 'priclass.d(1m)' even though section is specified as 1.
local tmp = vim.split(arg_lead, '(', { plain = true })
local name = tmp[1]
+ -- See extract_sect_and_name_ref on why :lower()
local sect = (tmp[2] or ''):lower()
- return complete(sect, '', name)
+ return sect, '', name
end
if not args[2]:match('^[^()]+$') then
-- cursor (|) is at ':Man 3() |' or ':Man (3|' or ':Man 3() pri|'
-- or ':Man 3() pri |'
- return {}
+ return
end
if #args == 2 then
- ---@type string, string
+ --- @type string, string
local name, sect
if arg_lead == '' then
-- cursor (|) is at ':Man 1 |'
@@ -614,52 +553,77 @@ function M.man_complete(arg_lead, cmd_line, _)
name = arg_lead
sect = ''
end
- return complete(sect, sect, name)
+ return sect, sect, name
end
if not arg_lead:match('[^()]+$') then
-- cursor (|) is at ':Man 3 printf |' or ':Man 3 (pr)i|'
- return {}
+ return
end
-- cursor (|) is at ':Man 3 pri|'
- local name = arg_lead
- local sect = args[2]:lower()
- return complete(sect, sect, name)
+ local name, sect = arg_lead, args[2]:lower()
+ return sect, sect, name
end
----@param pattern string
----@return {name:string,filename:string,cmd:string}[]
-function M.goto_tag(pattern, _, _)
- local sect, name = extract_sect_and_name_ref(pattern)
+--- @param arg_lead string
+--- @param cmd_line string
+function M.man_complete(arg_lead, cmd_line)
+ local sect, psect, name = parse_cmdline(arg_lead, cmd_line)
+ if not (sect and psect and name) then
+ return {}
+ end
- local paths = get_paths(sect, name)
- ---@type {name:string,title:string}[]
- local structured = {}
+ local pages = get_paths(name, sect)
- for _, path in ipairs(paths) do
- sect, name = extract_sect_and_name_path(path)
- if sect and name then
- structured[#structured + 1] = {
- name = name,
- title = name .. '(' .. sect .. ')',
- }
+ -- We check for duplicates in case the same manpage in different languages
+ -- was found.
+ local pages_fmt = {} --- @type string[]
+ local pages_fmt_keys = {} --- @type table<string,true>
+ for _, v in ipairs(pages) do
+ local x = format_candidate(v, psect)
+ local xl = x:lower() -- ignore case when searching avoiding duplicates
+ if not pages_fmt_keys[xl] then
+ pages_fmt[#pages_fmt + 1] = x
+ pages_fmt_keys[xl] = true
end
end
+ table.sort(pages_fmt)
+
+ return pages_fmt
+end
+
+--- @param pattern string
+--- @return {name:string,filename:string,cmd:string}[]
+function M.goto_tag(pattern, _, _)
+ local name, sect, err = parse_ref(pattern)
+ if err then
+ error(err)
+ end
+
+ local paths, err2 = get_paths(assert(name), sect)
+ if err2 then
+ error(err2)
+ end
+
+ --- @type table[]
+ local ret = {}
- ---@param entry {name:string,title:string}
- return vim.tbl_map(function(entry)
- return {
- name = entry.name,
- filename = 'man://' .. entry.title,
+ for _, path in ipairs(paths) do
+ local pname, psect = parse_path(path)
+ ret[#ret + 1] = {
+ name = pname,
+ filename = ('man://%s(%s)'):format(pname, psect),
cmd = '1',
}
- end, structured)
+ end
+
+ return ret
end
--- Called when Nvim is invoked as $MANPAGER.
+--- Called when Nvim is invoked as $MANPAGER.
function M.init_pager()
- if getline(1):match('^%s*$') then
+ if fn.getline(1):match('^%s*$') then
api.nvim_buf_set_lines(0, 0, 1, false, {})
else
vim.cmd('keepjumps 1')
@@ -667,9 +631,10 @@ function M.init_pager()
highlight_man_page()
-- Guess the ref from the heading (which is usually uppercase, so we cannot
-- know the correct casing, cf. `man glDrawArraysInstanced`).
- local ref = fn.substitute(matchstr(getline(1), [[^[^)]\+)]]) or '', ' ', '_', 'g')
- local ok, res = pcall(extract_sect_and_name_ref, ref)
- vim.b.man_sect = ok and res or ''
+ --- @type string
+ local ref = (fn.getline(1):match('^[^)]+%)') or ''):gsub(' ', '_')
+ local _, sect, err = pcall(parse_ref, ref)
+ vim.b.man_sect = err ~= nil and sect or ''
if not fn.bufname('%'):match('man://') then -- Avoid duplicate buffers, E95.
vim.cmd.file({ 'man://' .. fn.fnameescape(ref):lower(), mods = { silent = true } })
@@ -678,50 +643,64 @@ function M.init_pager()
set_options()
end
----@param count integer
----@param args string[]
+--- Combine the name and sect into a manpage reference so that all
+--- verification/extraction can be kept in a single function.
+--- @param args string[]
+--- @return string? ref
+local function ref_from_args(args)
+ if #args <= 1 then
+ return args[1]
+ elseif args[1]:match('^%d$') or args[1]:match('^%d%a') or args[1]:match('^%a$') then
+ -- NB: Valid sections are not only digits, but also:
+ -- - <digit><word> (see POSIX mans),
+ -- - and even <letter> and <word> (see, for example, by tcl/tk)
+ -- NB2: don't optimize to :match("^%d"), as it will match manpages like
+ -- 441toppm and others whose name starts with digit
+ local sect = args[1]
+ table.remove(args, 1)
+ local name = table.concat(args, ' ')
+ return ('%s(%s)'):format(name, sect)
+ end
+
+ return table.concat(args, ' ')
+end
+
+--- @param count integer
+--- @param args string[]
+--- @return string? err
function M.open_page(count, smods, args)
- local ref ---@type string
- if #args == 0 then
+ local ref = ref_from_args(args)
+ if not ref then
ref = vim.bo.filetype == 'man' and fn.expand('<cWORD>') or fn.expand('<cword>')
if ref == '' then
- man_error('no identifier under cursor')
- end
- elseif #args == 1 then
- ref = args[1]
- else
- -- Combine the name and sect into a manpage reference so that all
- -- verification/extraction can be kept in a single function.
- if args[1]:match('^%d$') or args[1]:match('^%d%a') or args[1]:match('^%a$') then
- -- NB: Valid sections are not only digits, but also:
- -- - <digit><word> (see POSIX mans),
- -- - and even <letter> and <word> (see, for example, by tcl/tk)
- -- NB2: don't optimize to :match("^%d"), as it will match manpages like
- -- 441toppm and others whose name starts with digit
- local sect = args[1]
- table.remove(args, 1)
- local name = table.concat(args, ' ')
- ref = ('%s(%s)'):format(name, sect)
- else
- ref = table.concat(args, ' ')
+ return 'no identifier under cursor'
end
end
- local sect, name = extract_sect_and_name_ref(ref)
+ local name, sect, err = parse_ref(ref)
+ if err then
+ return err
+ end
+ assert(name)
+
if count >= 0 then
sect = tostring(count)
end
-- Try both spaces and underscores, use the first that exists.
- local path = M.find_path(sect, name)
- if path == nil then
- path = M.find_path(sect, spaces_to_underscores(name))
- if path == nil then
- man_error('no manual entry for ' .. name)
+ local path = M._find_path(name, sect)
+ if not path then
+ --- Replace spaces in a man page name with underscores
+ --- intended for PostgreSQL, which has man pages like 'CREATE_TABLE(7)';
+ --- while editing SQL source code, it's nice to visually select 'CREATE TABLE'
+ --- and hit 'K', which requires this transformation
+ path = M._find_path(name:gsub('%s', '_'), sect)
+ if not path then
+ return 'no manual entry for ' .. name
end
end
- sect, name = extract_sect_and_name_path(path)
+ name, sect = parse_path(path)
local buf = api.nvim_get_current_buf()
local save_tfu = vim.bo[buf].tagfunc
vim.bo[buf].tagfunc = "v:lua.require'man'.goto_tag"
@@ -744,24 +723,51 @@ function M.open_page(count, smods, args)
if not ok then
error(ret)
- else
- set_options()
end
+ set_options()
vim.b.man_sect = sect
end
--- Called when a man:// buffer is opened.
+--- Called when a man:// buffer is opened.
+--- @return string? err
function M.read_page(ref)
- local sect, name = extract_sect_and_name_ref(ref)
- local path = M.find_path(sect, name)
- if path == nil then
- man_error('no manual entry for ' .. name)
+ local name, sect, err = parse_ref(ref)
+ if err then
+ return err
+ end
+
+ local path = M._find_path(name, sect)
+ if not path then
+ return 'no manual entry for ' .. name
end
- sect = extract_sect_and_name_path(path)
+
+ local _, sect1 = parse_path(path)
local page = get_page(path)
- vim.b.man_sect = sect
- put_page(page)
+
+ vim.b.man_sect = sect1
+ vim.bo.modifiable = true
+ vim.bo.readonly = false
+ vim.bo.swapfile = false
+
+ api.nvim_buf_set_lines(0, 0, -1, false, vim.split(page, '\n'))
+
+ while fn.getline(1):match('^%s*$') do
+ api.nvim_buf_set_lines(0, 0, 1, false, {})
+ end
+ -- XXX: nroff justifies text by filling it with whitespace. That interacts
+ -- badly with our use of $MANWIDTH=999. Hack around this by using a fixed
+ -- size for those whitespace regions.
+ -- Use try/catch to avoid setting v:errmsg.
+ vim.cmd([[
+ try
+ keeppatterns keepjumps %s/\s\{199,}/\=repeat(' ', 10)/g
+ catch
+ endtry
+ ]])
+ vim.cmd('1') -- Move cursor to first line
+ highlight_man_page()
+ set_options()
end
function M.show_toc()
@@ -773,29 +779,18 @@ function M.show_toc()
return
end
- ---@type {bufnr:integer, lnum:integer, text:string}[]
+ --- @type {bufnr:integer, lnum:integer, text:string}[]
local toc = {}
local lnum = 2
local last_line = fn.line('$') - 1
- local section_title_re = vim.regex([[^\%( \{3\}\)\=\S.*$]])
- local flag_title_re = vim.regex([[^\s\+\%(+\|-\)\S\+]])
while lnum and lnum < last_line do
- local text = getline(lnum)
- if section_title_re:match_str(text) then
- -- if text is a section title
+ local text = fn.getline(lnum)
+ if text:match('^%s+[-+]%S') or text:match('^ %S') or text:match('^%S') then
toc[#toc + 1] = {
bufnr = bufnr,
lnum = lnum,
- text = text,
- }
- elseif flag_title_re:match_str(text) then
- -- if text is a flag title. we strip whitespaces and prepend two
- -- spaces to have a consistent format in the loclist.
- toc[#toc + 1] = {
- bufnr = bufnr,
- lnum = lnum,
- text = ' ' .. fn.substitute(text, [[^\s*\(.\{-}\)\s*$]], [[\1]], ''),
+ text = text:gsub('^%s+', ''):gsub('%s+$', ''),
}
end
lnum = fn.nextnonblank(lnum + 1)
@@ -807,19 +802,4 @@ function M.show_toc()
vim.w.qf_toc = bufname
end
-local function init()
- local path = get_path('', 'man', true)
- local page ---@type string?
- if path ~= nil then
- -- Check for -l support.
- page = get_page(path, true)
- end
-
- if page == '' or page == nil then
- localfile_arg = false
- end
-end
-
-init()
-
return M
diff --git a/runtime/lua/vim/_defaults.lua b/runtime/lua/vim/_defaults.lua
index 0b8a54e957..28f1542f64 100644
--- a/runtime/lua/vim/_defaults.lua
+++ b/runtime/lua/vim/_defaults.lua
@@ -224,7 +224,7 @@ do
local function cmd(opts)
local ok, err = pcall(vim.api.nvim_cmd, opts, {})
if not ok then
- vim.api.nvim_err_writeln(err:sub(#'Vim:' + 1))
+ vim.api.nvim_echo({ { err:sub(#'Vim:' + 1) } }, true, { err = true })
end
end
@@ -412,7 +412,7 @@ do
end
end
- local nvim_popupmenu_augroup = vim.api.nvim_create_augroup('nvim_popupmenu', {})
+ local nvim_popupmenu_augroup = vim.api.nvim_create_augroup('nvim.popupmenu', {})
vim.api.nvim_create_autocmd('MenuPopup', {
pattern = '*',
group = nvim_popupmenu_augroup,
@@ -429,13 +429,13 @@ end
--- Default autocommands. See |default-autocmds|
do
- local nvim_terminal_augroup = vim.api.nvim_create_augroup('nvim_terminal', {})
+ local nvim_terminal_augroup = vim.api.nvim_create_augroup('nvim.terminal', {})
vim.api.nvim_create_autocmd('BufReadCmd', {
pattern = 'term://*',
group = nvim_terminal_augroup,
desc = 'Treat term:// buffers as terminal buffers',
nested = true,
- command = "if !exists('b:term_title')|call termopen(matchstr(expand(\"<amatch>\"), '\\c\\mterm://\\%(.\\{-}//\\%(\\d\\+:\\)\\?\\)\\?\\zs.*'), {'cwd': expand(get(matchlist(expand(\"<amatch>\"), '\\c\\mterm://\\(.\\{-}\\)//'), 1, ''))})",
+ command = "if !exists('b:term_title')|call jobstart(matchstr(expand(\"<amatch>\"), '\\c\\mterm://\\%(.\\{-}//\\%(\\d\\+:\\)\\?\\)\\?\\zs.*'), {'term': v:true, 'cwd': expand(get(matchlist(expand(\"<amatch>\"), '\\c\\mterm://\\(.\\{-}\\)//'), 1, ''))})",
})
vim.api.nvim_create_autocmd({ 'TermClose' }, {
@@ -509,14 +509,14 @@ do
vim.api.nvim_create_autocmd('CmdwinEnter', {
pattern = '[:>]',
desc = 'Limit syntax sync to maxlines=1 in the command window',
- group = vim.api.nvim_create_augroup('nvim_cmdwin', {}),
+ group = vim.api.nvim_create_augroup('nvim.cmdwin', {}),
command = 'syntax sync minlines=1 maxlines=1',
})
vim.api.nvim_create_autocmd('SwapExists', {
pattern = '*',
desc = 'Skip the swapfile prompt when the swapfile is owned by a running Nvim process',
- group = vim.api.nvim_create_augroup('nvim_swapfile', {}),
+ group = vim.api.nvim_create_augroup('nvim.swapfile', {}),
callback = function()
local info = vim.fn.swapinfo(vim.v.swapname)
local user = vim.uv.os_get_passwd().username
@@ -543,7 +543,7 @@ do
end
if tty then
- local group = vim.api.nvim_create_augroup('nvim_tty', {})
+ local group = vim.api.nvim_create_augroup('nvim.tty', {})
--- Set an option after startup (so that OptionSet is fired), but only if not
--- already set by the user.
diff --git a/runtime/lua/vim/_editor.lua b/runtime/lua/vim/_editor.lua
index 44f17b3f85..4b28b63746 100644
--- a/runtime/lua/vim/_editor.lua
+++ b/runtime/lua/vim/_editor.lua
@@ -58,6 +58,7 @@ vim._extra = {
--- @private
vim.log = {
+ --- @enum vim.log.levels
levels = {
TRACE = 0,
DEBUG = 1,
@@ -92,7 +93,7 @@ local utfs = {
---
--- -- Runs synchronously:
--- local obj = vim.system({'echo', 'hello'}, { text = true }):wait()
---- -- { code = 0, signal = 0, stdout = 'hello', stderr = '' }
+--- -- { code = 0, signal = 0, stdout = 'hello\n', stderr = '' }
---
--- ```
---
@@ -390,7 +391,7 @@ end
local VIM_CMD_ARG_MAX = 20
---- Executes Vim script commands.
+--- Executes Vimscript (|Ex-commands|).
---
--- Note that `vim.cmd` can be indexed with a command name to return a callable function to the
--- command.
@@ -425,7 +426,7 @@ local VIM_CMD_ARG_MAX = 20
--- ```
---
---@param command string|table Command(s) to execute.
---- If a string, executes multiple lines of Vim script at once. In this
+--- If a string, executes multiple lines of Vimscript at once. In this
--- case, it is an alias to |nvim_exec2()|, where `opts.output` is set
--- to false. Thus it works identical to |:source|.
--- If a table, executes a single command. In this case, it is an alias
@@ -620,13 +621,8 @@ end
---@param opts table|nil Optional parameters. Unused by default.
---@diagnostic disable-next-line: unused-local
function vim.notify(msg, level, opts) -- luacheck: no unused args
- if level == vim.log.levels.ERROR then
- vim.api.nvim_err_writeln(msg)
- elseif level == vim.log.levels.WARN then
- vim.api.nvim_echo({ { msg, 'WarningMsg' } }, true, {})
- else
- vim.api.nvim_echo({ { msg } }, true, {})
- end
+ local chunks = { { msg, level == vim.log.levels.WARN and 'WarningMsg' or nil } }
+ vim.api.nvim_echo(chunks, true, { err = level == vim.log.levels.ERROR })
end
do
diff --git a/runtime/lua/vim/_meta/api.lua b/runtime/lua/vim/_meta/api.lua
index b2385197bd..670e867c1e 100644
--- a/runtime/lua/vim/_meta/api.lua
+++ b/runtime/lua/vim/_meta/api.lua
@@ -272,12 +272,12 @@ function vim.api.nvim_buf_attach(buffer, send_buffer, opts) end
--- This temporarily switches current buffer to "buffer".
--- If the current window already shows "buffer", the window is not switched.
--- If a window inside the current tabpage (including a float) already shows the
---- buffer, then one of these windows will be set as current window temporarily.
+--- buffer, then one of those windows will be set as current window temporarily.
--- Otherwise a temporary scratch window (called the "autocmd window" for
--- historical reasons) will be used.
---
--- This is useful e.g. to call Vimscript functions that only work with the
---- current buffer/window currently, like `termopen()`.
+--- current buffer/window currently, like `jobstart(…, {'term': v:true})`.
---
--- @param buffer integer Buffer handle, or 0 for current buffer
--- @param fun function Function to call inside the buffer (currently Lua callable
@@ -452,7 +452,7 @@ function vim.api.nvim_buf_get_extmarks(buffer, ns_id, start, end_, opts) end
---
--- @param buffer integer Buffer handle, or 0 for current buffer
--- @param mode string Mode short-name ("n", "i", "v", ...)
---- @return vim.api.keyset.keymap[] # Array of |maparg()|-like dictionaries describing mappings.
+--- @return vim.api.keyset.get_keymap[] # Array of |maparg()|-like dictionaries describing mappings.
--- The "buffer" key holds the associated buffer handle.
function vim.api.nvim_buf_get_keymap(buffer, mode) end
@@ -885,10 +885,8 @@ function vim.api.nvim_cmd(cmd, opts) end
---
--- On execution error: fails with Vimscript error, updates v:errmsg.
---
---- Prefer using `nvim_cmd()` or `nvim_exec2()` over this. To evaluate multiple lines of Vim script
---- or an Ex command directly, use `nvim_exec2()`. To construct an Ex command using a structured
---- format and then execute it, use `nvim_cmd()`. To modify an Ex command before evaluating it, use
---- `nvim_parse_cmd()` in conjunction with `nvim_cmd()`.
+--- Prefer `nvim_cmd()` or `nvim_exec2()` instead. To modify an Ex command in a structured way
+--- before executing it, modify the result of `nvim_parse_cmd()` then pass it to `nvim_cmd()`.
---
--- @param command string Ex command string
function vim.api.nvim_command(command) end
@@ -1099,29 +1097,28 @@ function vim.api.nvim_del_user_command(name) end
--- @param name string Variable name
function vim.api.nvim_del_var(name) end
---- Echo a message.
+--- Prints a message given by a list of `[text, hl_group]` "chunks".
---
---- @param chunks any[] A list of `[text, hl_group]` arrays, each representing a
---- text chunk with specified highlight group name or ID.
---- `hl_group` element can be omitted for no highlight.
+--- Example:
+--- ```lua
+--- vim.api.nvim_echo({ { 'chunk1-line1\nchunk1-line2\n' }, { 'chunk2-line1' } }, true, {})
+--- ```
+---
+--- @param chunks any[] List of `[text, hl_group]` pairs, where each is a `text` string highlighted by
+--- the (optional) name or ID `hl_group`.
--- @param history boolean if true, add to `message-history`.
--- @param opts vim.api.keyset.echo_opts Optional parameters.
---- - verbose: Message is printed as a result of 'verbose' option.
---- If Nvim was invoked with -V3log_file, the message will be
---- redirected to the log_file and suppressed from direct output.
+--- - err: Treat the message like `:echoerr`. Sets `hl_group` to `hl-ErrorMsg` by default.
+--- - verbose: Message is controlled by the 'verbose' option. Nvim invoked with `-V3log`
+--- will write the message to the "log" file instead of standard output.
function vim.api.nvim_echo(chunks, history, opts) end
---- Writes a message to the Vim error buffer. Does not append "\n", the
---- message is buffered (won't display) until a linefeed is written.
----
---- @param str string Message
+--- @deprecated
+--- @param str string
function vim.api.nvim_err_write(str) end
---- Writes a message to the Vim error buffer. Appends "\n", so the buffer is
---- flushed (and displayed).
----
---- @see vim.api.nvim_err_write
---- @param str string Message
+--- @deprecated
+--- @param str string
function vim.api.nvim_err_writeln(str) end
--- Evaluates a Vimscript `expression`. Dicts and Lists are recursively expanded.
@@ -1281,6 +1278,8 @@ function vim.api.nvim_get_autocmds(opts) end
--- Gets information about a channel.
---
+--- See `nvim_list_uis()` for an example of how to get channel info.
+---
--- @param chan integer channel_id, or 0 for current channel
--- @return table<string,any> # Channel info dict with these keys:
--- - "id" Channel id.
@@ -1298,8 +1297,8 @@ function vim.api.nvim_get_autocmds(opts) end
--- "/dev/pts/1". If unknown, the key will still be present if a pty is used (e.g.
--- for conpty on Windows).
--- - "buffer" (optional) Buffer connected to |terminal| instance.
---- - "client" (optional) Info about the peer (client on the other end of the RPC channel),
---- which it provided via |nvim_set_client_info()|.
+--- - "client" (optional) Info about the peer (client on the other end of the channel), as set
+--- by |nvim_set_client_info()|.
---
function vim.api.nvim_get_chan_info(chan) end
@@ -1416,7 +1415,7 @@ function vim.api.nvim_get_hl_ns(opts) end
--- Gets a list of global (non-buffer-local) `mapping` definitions.
---
--- @param mode string Mode short-name ("n", "i", "v", ...)
---- @return vim.api.keyset.keymap[] # Array of |maparg()|-like dictionaries describing mappings.
+--- @return vim.api.keyset.get_keymap[] # Array of |maparg()|-like dictionaries describing mappings.
--- The "buffer" key is always zero.
function vim.api.nvim_get_keymap(mode) end
@@ -1621,6 +1620,14 @@ function vim.api.nvim_list_tabpages() end
--- Gets a list of dictionaries representing attached UIs.
---
+--- Example: The Nvim builtin `TUI` sets its channel info as described in `startup-tui`. In
+--- particular, it sets `client.name` to "nvim-tui". So you can check if the TUI is running by
+--- inspecting the client name of each UI:
+---
+--- ```lua
+--- vim.print(vim.api.nvim_get_chan_info(vim.api.nvim_list_uis()[1].chan).client.name)
+--- ```
+---
--- @return any[] # Array of UI dictionaries, each with these keys:
--- - "height" Requested height of the UI
--- - "width" Requested width of the UI
@@ -1640,14 +1647,10 @@ function vim.api.nvim_list_wins() end
--- @return any
function vim.api.nvim_load_context(dict) end
---- Notify the user with a message
----
---- Relays the call to vim.notify . By default forwards your message in the
---- echo area but can be overridden to trigger desktop notifications.
----
---- @param msg string Message to display to the user
---- @param log_level integer The log level
---- @param opts table<string,any> Reserved for future use.
+--- @deprecated
+--- @param msg string
+--- @param log_level integer
+--- @param opts table<string,any>
--- @return any
function vim.api.nvim_notify(msg, log_level, opts) end
@@ -1666,7 +1669,8 @@ function vim.api.nvim_notify(msg, log_level, opts) end
--- in a virtual terminal having the intended size.
---
--- Example: this `TermHl` command can be used to display and highlight raw ANSI termcodes, so you
---- can use Nvim as a "scrollback pager" (for terminals like kitty): [terminal-scrollback-pager]()
+--- can use Nvim as a "scrollback pager" (for terminals like kitty): [ansi-colorize]()
+--- [terminal-scrollback-pager]()
---
--- ```lua
--- vim.api.nvim_create_user_command('TermHl', function()
@@ -1750,10 +1754,12 @@ function vim.api.nvim_open_term(buffer, opts) end
--- @param config vim.api.keyset.win_config Map defining the window configuration. Keys:
--- - relative: Sets the window layout to "floating", placed at (row,col)
--- coordinates relative to:
---- - "editor" The global editor grid
---- - "win" Window given by the `win` field, or current window.
---- - "cursor" Cursor position in current window.
---- - "mouse" Mouse position
+--- - "cursor" Cursor position in current window.
+--- - "editor" The global editor grid.
+--- - "laststatus" 'laststatus' if present, or last row.
+--- - "mouse" Mouse position.
+--- - "tabline" Tabline if present, or first row.
+--- - "win" Window given by the `win` field, or current window.
--- - win: `window-ID` window to split, or relative window when creating a
--- float (relative="win").
--- - anchor: Decides which corner of the float to place at (row,col):
@@ -1861,10 +1867,8 @@ function vim.api.nvim_open_term(buffer, opts) end
--- @return integer # Window handle, or 0 on error
function vim.api.nvim_open_win(buffer, enter, config) end
---- Writes a message to the Vim output buffer. Does not append "\n", the
---- message is buffered (won't display) until a linefeed is written.
----
---- @param str string Message
+--- @deprecated
+--- @param str string
function vim.api.nvim_out_write(str) end
--- Parse command line.
@@ -2136,8 +2140,8 @@ function vim.api.nvim_set_current_win(window) end
--- ```
--- ["start", tick]
--- ```
---- - on_buf: called for each buffer being redrawn (before
---- window callbacks)
+--- - on_buf: called for each buffer being redrawn (once per edit,
+--- before window callbacks)
--- ```
--- ["buf", bufnr, tick]
--- ```
diff --git a/runtime/lua/vim/_meta/api_keysets.lua b/runtime/lua/vim/_meta/api_keysets.lua
index e11dddb2d3..98e916115e 100644
--- a/runtime/lua/vim/_meta/api_keysets.lua
+++ b/runtime/lua/vim/_meta/api_keysets.lua
@@ -88,6 +88,7 @@ error('Cannot require a meta file')
--- @field pattern? string|string[]
--- @class vim.api.keyset.echo_opts
+--- @field err? boolean
--- @field verbose? boolean
--- @class vim.api.keyset.empty
@@ -227,10 +228,10 @@ error('Cannot require a meta file')
--- @field do_source? boolean
--- @class vim.api.keyset.set_decoration_provider
---- @field on_start? fun(_: "start", tick: integer)
+--- @field on_start? fun(_: "start", tick: integer): boolean?
--- @field on_buf? fun(_: "buf", bufnr: integer, tick: integer)
---- @field on_win? fun(_: "win", winid: integer, bufnr: integer, toprow: integer, botrow: integer)
---- @field on_line? fun(_: "line", winid: integer, bufnr: integer, row: integer)
+--- @field on_win? fun(_: "win", winid: integer, bufnr: integer, toprow: integer, botrow: integer): boolean?
+--- @field on_line? fun(_: "line", winid: integer, bufnr: integer, row: integer): boolean?
--- @field on_end? fun(_: "end", tick: integer)
--- @field _on_hl_def? fun(_: "hl_def")
--- @field _on_spell_nav? fun(_: "spell_nav")
diff --git a/runtime/lua/vim/_meta/api_keysets_extra.lua b/runtime/lua/vim/_meta/api_keysets_extra.lua
index 806b3e49c0..fbef6fa3bc 100644
--- a/runtime/lua/vim/_meta/api_keysets_extra.lua
+++ b/runtime/lua/vim/_meta/api_keysets_extra.lua
@@ -173,6 +173,26 @@ error('Cannot require a meta file')
--- @field force? true
--- @field cterm? vim.api.keyset.hl_info.cterm
+--- @class vim.api.keyset.get_keymap
+--- @field abbr? 0|1
+--- @field buffer? 0|1
+--- @field callback? function
+--- @field desc? string
+--- @field expr? 0|1
+--- @field lhs? string
+--- @field lhsraw? string
+--- @field lhsrawalt? string
+--- @field lnum? integer
+--- @field mode? string
+--- @field mode_bits? integer
+--- @field noremap? 0|1
+--- @field nowait? 0|1
+--- @field rhs? string
+--- @field script? 0|1
+--- @field scriptversion? integer
+--- @field sid? integer
+--- @field silent? 0|1
+
--- @class vim.api.keyset.get_mode
--- @field blocking boolean
--- @field mode string
diff --git a/runtime/lua/vim/_meta/builtin.lua b/runtime/lua/vim/_meta/builtin.lua
index b8779b66fe..9fa2e242c4 100644
--- a/runtime/lua/vim/_meta/builtin.lua
+++ b/runtime/lua/vim/_meta/builtin.lua
@@ -233,9 +233,8 @@ function vim.wait(time, callback, interval, fast_only) end
--- {callback} receives event name plus additional parameters. See |ui-popupmenu|
--- and the sections below for event format for respective events.
---
---- Callbacks for `msg_show` events are executed in |api-fast| context unless
---- Nvim will wait for input, in which case messages should be shown
---- immediately.
+--- Callbacks for `msg_show` events are executed in |api-fast| context; showing
+--- the message should be scheduled.
---
--- Excessive errors inside the callback will result in forced detachment.
---
diff --git a/runtime/lua/vim/_meta/json.lua b/runtime/lua/vim/_meta/json.lua
index 1a7e87db9c..0d59de5fa6 100644
--- a/runtime/lua/vim/_meta/json.lua
+++ b/runtime/lua/vim/_meta/json.lua
@@ -25,18 +25,18 @@ vim.json = {}
---
---@param str string Stringified JSON data.
---@param opts? table<string,any> Options table with keys:
---- - luanil: (table) Table with keys:
---- * object: (boolean) When true, converts `null` in JSON objects
---- to Lua `nil` instead of |vim.NIL|.
---- * array: (boolean) When true, converts `null` in JSON arrays
---- to Lua `nil` instead of |vim.NIL|.
+--- - luanil: (table) Table with keys:
+--- - object: (boolean) When true, converts `null` in JSON objects
+--- to Lua `nil` instead of |vim.NIL|.
+--- - array: (boolean) When true, converts `null` in JSON arrays
+--- to Lua `nil` instead of |vim.NIL|.
---@return any
function vim.json.decode(str, opts) end
--- Encodes (or "packs") Lua object {obj} as JSON in a Lua string.
---@param obj any
---@param opts? table<string,any> Options table with keys:
---- - escape_slash: (boolean) (default false) When true, escapes `/`
---- character in JSON strings
+--- - escape_slash: (boolean) (default false) Escape slash
+--- characters "/" in string values.
---@return string
function vim.json.encode(obj, opts) end
diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua
index 63bf0df5f6..107b1ffdfb 100644
--- a/runtime/lua/vim/_meta/options.lua
+++ b/runtime/lua/vim/_meta/options.lua
@@ -52,7 +52,7 @@ vim.go.ari = vim.go.allowrevins
--- set to one of CJK locales. See Unicode Standard Annex #11
--- (https://www.unicode.org/reports/tr11).
---
---- @type string
+--- @type 'single'|'double'
vim.o.ambiwidth = "single"
vim.o.ambw = vim.o.ambiwidth
vim.go.ambiwidth = vim.o.ambiwidth
@@ -208,7 +208,7 @@ vim.go.awa = vim.go.autowriteall
--- will change. To use other settings, place ":highlight" commands AFTER
--- the setting of the 'background' option.
---
---- @type string
+--- @type 'light'|'dark'
vim.o.background = "dark"
vim.o.bg = vim.o.background
vim.go.background = vim.o.background
@@ -595,7 +595,7 @@ vim.wo.briopt = vim.wo.breakindentopt
--- This option is used together with 'buftype' and 'swapfile' to specify
--- special kinds of buffers. See `special-buffers`.
---
---- @type string
+--- @type ''|'hide'|'unload'|'delete'|'wipe'
vim.o.bufhidden = ""
vim.o.bh = vim.o.bufhidden
vim.bo.bufhidden = vim.o.bufhidden
@@ -658,7 +658,7 @@ vim.bo.bl = vim.bo.buflisted
--- without saving. For writing there must be matching `BufWriteCmd|,
--- |FileWriteCmd` or `FileAppendCmd` autocommands.
---
---- @type string
+--- @type ''|'acwrite'|'help'|'nofile'|'nowrite'|'quickfix'|'terminal'|'prompt'
vim.o.buftype = ""
vim.o.bt = vim.o.buftype
vim.bo.buftype = vim.o.buftype
@@ -1087,8 +1087,8 @@ vim.go.cia = vim.go.completeitemalign
--- "menu" or "menuone". No effect if "longest" is present.
---
--- noselect Same as "noinsert", except that no menu item is
---- pre-selected. If both "noinsert" and "noselect" are present,
---- "noselect" has precedence.
+--- pre-selected. If both "noinsert" and "noselect" are
+--- present, "noselect" has precedence.
---
--- fuzzy Enable `fuzzy-matching` for completion candidates. This
--- allows for more flexible and intuitive matching, where
@@ -1118,7 +1118,7 @@ vim.go.cot = vim.go.completeopt
--- For Insert mode completion the buffer-local value is used. For
--- command line completion the global value is used.
---
---- @type string
+--- @type ''|'slash'|'backslash'
vim.o.completeslash = ""
vim.o.csl = vim.o.completeslash
vim.bo.completeslash = vim.o.completeslash
@@ -1824,7 +1824,7 @@ vim.go.dy = vim.go.display
--- hor horizontally, height of windows is not affected
--- both width and height of windows is affected
---
---- @type string
+--- @type 'both'|'ver'|'hor'
vim.o.eadirection = "both"
vim.o.ead = vim.o.eadirection
vim.go.eadirection = vim.o.eadirection
@@ -2126,7 +2126,7 @@ vim.go.fencs = vim.go.fileencodings
--- option is set, because the file would be different when written.
--- This option cannot be changed when 'modifiable' is off.
---
---- @type string
+--- @type 'unix'|'dos'|'mac'
vim.o.fileformat = "unix"
vim.o.ff = vim.o.fileformat
vim.bo.fileformat = vim.o.fileformat
@@ -2382,7 +2382,7 @@ vim.go.fcl = vim.go.foldclose
--- "[1-9]": to display a fixed number of columns
--- See `folding`.
---
---- @type string
+--- @type 'auto'|'auto:1'|'auto:2'|'auto:3'|'auto:4'|'auto:5'|'auto:6'|'auto:7'|'auto:8'|'auto:9'|'0'|'1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'
vim.o.foldcolumn = "0"
vim.o.fdc = vim.o.foldcolumn
vim.wo.foldcolumn = vim.o.foldcolumn
@@ -2479,7 +2479,7 @@ vim.wo.fmr = vim.wo.foldmarker
--- `fold-syntax` syntax Syntax highlighting items specify folds.
--- `fold-diff` diff Fold text that is not changed.
---
---- @type string
+--- @type 'manual'|'expr'|'marker'|'indent'|'syntax'|'diff'
vim.o.foldmethod = "manual"
vim.o.fdm = vim.o.foldmethod
vim.wo.foldmethod = vim.o.foldmethod
@@ -2783,6 +2783,7 @@ vim.go.gp = vim.go.grepprg
--- ci Command-line Insert mode
--- cr Command-line Replace mode
--- sm showmatch in Insert mode
+--- t Terminal mode
--- a all modes
--- The argument-list is a dash separated list of these arguments:
--- hor{N} horizontal bar, {N} percent of the character height
@@ -2802,7 +2803,8 @@ vim.go.gp = vim.go.grepprg
--- ```vim
--- set guicursor=n:blinkon0
--- ```
---- - Default is "blinkon0" for each mode.
+---
+--- Default is "blinkon0" for each mode.
--- {group-name}
--- Highlight group that decides the color and font of the
--- cursor.
@@ -2848,7 +2850,7 @@ vim.go.gp = vim.go.grepprg
---
---
--- @type string
-vim.o.guicursor = "n-v-c-sm:block,i-ci-ve:ver25,r-cr-o:hor20"
+vim.o.guicursor = "n-v-c-sm:block,i-ci-ve:ver25,r-cr-o:hor20,t:block-blinkon500-blinkoff500-TermCursor"
vim.o.gcr = vim.o.guicursor
vim.go.guicursor = vim.o.guicursor
vim.go.gcr = vim.go.guicursor
@@ -3142,7 +3144,7 @@ vim.bo.ims = vim.bo.imsearch
--- 'redrawtime') then 'inccommand' is automatically disabled until
--- `Command-line-mode` is done.
---
---- @type string
+--- @type 'nosplit'|'split'|''
vim.o.inccommand = "nosplit"
vim.o.icm = vim.o.inccommand
vim.go.inccommand = vim.o.inccommand
@@ -4352,7 +4354,7 @@ vim.go.mh = vim.go.mousehide
--- "g<LeftMouse>" is "<C-LeftMouse> (jump to tag under mouse click)
--- "g<RightMouse>" is "<C-RightMouse> ("CTRL-T")
---
---- @type string
+--- @type 'extend'|'popup'|'popup_setpos'
vim.o.mousemodel = "popup_setpos"
vim.o.mousem = vim.o.mousemodel
vim.go.mousemodel = vim.o.mousemodel
@@ -4843,8 +4845,8 @@ vim.go.redrawdebug = vim.o.redrawdebug
vim.go.rdb = vim.go.redrawdebug
--- Time in milliseconds for redrawing the display. Applies to
---- 'hlsearch', 'inccommand', `:match` highlighting and syntax
---- highlighting.
+--- 'hlsearch', 'inccommand', `:match` highlighting, syntax highlighting,
+--- and async `LanguageTree:parse()`.
--- When redrawing takes more than this many milliseconds no further
--- matches will be highlighted.
--- For syntax highlighting the time applies per window. When over the
@@ -5220,7 +5222,7 @@ vim.go.sect = vim.go.sections
--- backwards, you cannot include the last character of a line, when
--- starting in Normal mode and 'virtualedit' empty.
---
---- @type string
+--- @type 'inclusive'|'exclusive'|'old'
vim.o.selection = "inclusive"
vim.o.sel = vim.o.selection
vim.go.selection = vim.o.selection
@@ -5786,7 +5788,7 @@ vim.go.sc = vim.go.showcmd
--- place the text. Without a custom 'statusline' or 'tabline' it will be
--- displayed in a convenient location.
---
---- @type string
+--- @type 'last'|'statusline'|'tabline'
vim.o.showcmdloc = "last"
vim.o.sloc = vim.o.showcmdloc
vim.go.showcmdloc = vim.o.showcmdloc
@@ -5918,7 +5920,7 @@ vim.go.siso = vim.go.sidescrolloff
--- "number" display signs in the 'number' column. If the number
--- column is not present, then behaves like "auto".
---
---- @type string
+--- @type 'yes'|'no'|'auto'|'auto:1'|'auto:2'|'auto:3'|'auto:4'|'auto:5'|'auto:6'|'auto:7'|'auto:8'|'auto:9'|'yes:1'|'yes:2'|'yes:3'|'yes:4'|'yes:5'|'yes:6'|'yes:7'|'yes:8'|'yes:9'|'number'
vim.o.signcolumn = "auto"
vim.o.scl = vim.o.signcolumn
vim.wo.signcolumn = vim.o.signcolumn
@@ -6226,7 +6228,7 @@ vim.go.sb = vim.go.splitbelow
--- with the previous cursor position. For "screen", the text cannot always
--- be kept on the same screen line when 'wrap' is enabled.
---
---- @type string
+--- @type 'cursor'|'screen'|'topline'
vim.o.splitkeep = "cursor"
vim.o.spk = vim.o.splitkeep
vim.go.splitkeep = vim.o.splitkeep
@@ -6874,7 +6876,7 @@ vim.go.tbs = vim.go.tagbsearch
--- match Match case
--- smart Ignore case unless an upper case letter is used
---
---- @type string
+--- @type 'followic'|'ignore'|'match'|'followscs'|'smart'
vim.o.tagcase = "followic"
vim.o.tc = vim.o.tagcase
vim.bo.tagcase = vim.o.tagcase
@@ -7756,7 +7758,7 @@ vim.go.wop = vim.go.wildoptions
--- key is never used for the menu.
--- This option is not used for <F10>; on Win32.
---
---- @type string
+--- @type 'yes'|'menu'|'no'
vim.o.winaltkeys = "menu"
vim.o.wak = vim.o.winaltkeys
vim.go.winaltkeys = vim.o.winaltkeys
diff --git a/runtime/lua/vim/_meta/vimfn.lua b/runtime/lua/vim/_meta/vimfn.lua
index cf1beda15f..4b5b276023 100644
--- a/runtime/lua/vim/_meta/vimfn.lua
+++ b/runtime/lua/vim/_meta/vimfn.lua
@@ -1023,16 +1023,22 @@ function vim.fn.complete_check() end
--- See |complete_info_mode| for the values.
--- pum_visible |TRUE| if popup menu is visible.
--- See |pumvisible()|.
---- items List of completion matches. Each item is a
---- dictionary containing the entries "word",
+--- items List of all completion candidates. Each item
+--- is a dictionary containing the entries "word",
--- "abbr", "menu", "kind", "info" and "user_data".
--- See |complete-items|.
+--- matches Same as "items", but only returns items that
+--- are matching current query. If both "matches"
+--- and "items" are in "what", the returned list
+--- will still be named "items", but each item
+--- will have an additional "match" field.
--- selected Selected item index. First index is zero.
--- Index is -1 if no item is selected (showing
--- typed text only, or the last completion after
--- no item is selected when using the <Up> or
--- <Down> keys)
---- inserted Inserted string. [NOT IMPLEMENTED YET]
+--- completed Return a dictionary containing the entries of
+--- the currently selected index item.
--- preview_winid Info floating preview window id.
--- preview_bufnr Info floating preview buffer id.
---
@@ -3765,6 +3771,20 @@ function vim.fn.getregtype(regname) end
--- @return vim.fn.getscriptinfo.ret[]
function vim.fn.getscriptinfo(opts) end
+--- Returns the current stack trace of Vim scripts.
+--- Stack trace is a |List|, of which each item is a |Dictionary|
+--- with the following items:
+--- funcref The funcref if the stack is at a function,
+--- otherwise this item is omitted.
+--- event The string of the event description if the
+--- stack is at an autocmd event, otherwise this
+--- item is omitted.
+--- lnum The line number in the script on the stack.
+--- filepath The file path of the script on the stack.
+---
+--- @return table[]
+function vim.fn.getstacktrace() end
+
--- If {tabnr} is not specified, then information about all the
--- tab pages is returned as a |List|. Each List item is a
--- |Dictionary|. Otherwise, {tabnr} specifies the tab page
@@ -4805,7 +4825,7 @@ function vim.fn.jobresize(job, width, height) end
--- @return any
function vim.fn.jobsend(...) end
---- Note: Prefer |vim.system()| in Lua (unless using the `pty` option).
+--- Note: Prefer |vim.system()| in Lua (unless using `rpc`, `pty`, or `term`).
---
--- Spawns {cmd} as a job.
--- If {cmd} is a List it runs directly (no 'shell').
@@ -4813,8 +4833,11 @@ function vim.fn.jobsend(...) end
--- call jobstart(split(&shell) + split(&shellcmdflag) + ['{cmd}'])
--- <(See |shell-unquoting| for details.)
---
---- Example: >vim
---- call jobstart('nvim -h', {'on_stdout':{j,d,e->append(line('.'),d)}})
+--- Example: start a job and handle its output: >vim
+--- call jobstart(['nvim', '-h'], {'on_stdout':{j,d,e->append(line('.'),d)}})
+--- <
+--- Example: start a job in a |terminal| connected to the current buffer: >vim
+--- call jobstart(['nvim', '-h'], {'term':v:true})
--- <
--- Returns |job-id| on success, 0 on invalid arguments (or job
--- table is full), -1 if {cmd}[0] or 'shell' is not executable.
@@ -4879,6 +4902,10 @@ function vim.fn.jobsend(...) end
--- stdin: (string) Either "pipe" (default) to connect the
--- job's stdin to a channel or "null" to disconnect
--- stdin.
+--- term: (boolean) Spawns {cmd} in a new pseudo-terminal session
+--- connected to the current (unmodified) buffer. Implies "pty".
+--- Default "height" and "width" are set to the current window
+--- dimensions. |jobstart()|. Defaults $TERM to "xterm-256color".
--- width: (number) Width of the `pty` terminal.
---
--- {opts} is passed as |self| dictionary to the callback; the
@@ -7511,7 +7538,7 @@ function vim.fn.screenstring(row, col) end
--- @param stopline? integer
--- @param timeout? integer
--- @param skip? string|function
---- @return any
+--- @return integer
function vim.fn.search(pattern, flags, stopline, timeout, skip) end
--- Get or update the last search count, like what is displayed
@@ -10168,19 +10195,8 @@ function vim.fn.tanh(expr) end
--- @return string
function vim.fn.tempname() end
---- Spawns {cmd} in a new pseudo-terminal session connected
---- to the current (unmodified) buffer. Parameters and behavior
---- are the same as |jobstart()| except "pty", "width", "height",
---- and "TERM" are ignored: "height" and "width" are taken from
---- the current window. Note that termopen() implies a "pty" arg
---- to jobstart(), and thus has the implications documented at
---- |jobstart()|.
----
---- Returns the same values as jobstart().
----
---- Terminal environment is initialized as in |jobstart-env|,
---- except $TERM is set to "xterm-256color". Full behavior is
---- described in |terminal|.
+--- @deprecated
+--- Use |jobstart()| with `{term: v:true}` instead.
---
--- @param cmd string|string[]
--- @param opts? table
diff --git a/runtime/lua/vim/_meta/vvars.lua b/runtime/lua/vim/_meta/vvars.lua
index 445da4e02f..c1b8695bbf 100644
--- a/runtime/lua/vim/_meta/vvars.lua
+++ b/runtime/lua/vim/_meta/vvars.lua
@@ -203,7 +203,8 @@ vim.v.errors = ...
vim.v.event = ...
--- The value of the exception most recently caught and not
---- finished. See also `v:throwpoint` and `throw-variables`.
+--- finished. See also `v:stacktrace`, `v:throwpoint`, and
+--- `throw-variables`.
--- Example:
---
--- ```vim
@@ -616,6 +617,13 @@ vim.v.servername = ...
--- @type integer
vim.v.shell_error = ...
+--- The stack trace of the exception most recently caught and
+--- not finished. Refer to `getstacktrace()` for the structure of
+--- stack trace. See also `v:exception`, `v:throwpoint`, and
+--- `throw-variables`.
+--- @type table[]
+vim.v.stacktrace = ...
+
--- Last given status message.
--- Modifiable (can be set).
--- @type string
@@ -718,7 +726,7 @@ vim.v.this_session = ...
--- The point where the exception most recently caught and not
--- finished was thrown. Not set when commands are typed. See
---- also `v:exception` and `throw-variables`.
+--- also `v:exception`, `v:stacktrace`, and `throw-variables`.
--- Example:
---
--- ```vim
diff --git a/runtime/lua/vim/_options.lua b/runtime/lua/vim/_options.lua
index 77d7054626..8338c5ead7 100644
--- a/runtime/lua/vim/_options.lua
+++ b/runtime/lua/vim/_options.lua
@@ -229,10 +229,8 @@ end
--- global value of a |global-local| option, see |:setglobal|.
--- </pre>
---- Get or set |options|. Like `:set`. Invalid key is an error.
----
---- Note: this works on both buffer-scoped and window-scoped options using the
---- current buffer and window.
+--- Get or set |options|. Works like `:set`, so buffer/window-scoped options target the current
+--- buffer/window. Invalid key is an error.
---
--- Example:
---
@@ -770,7 +768,7 @@ end
---
---
--- A special interface |vim.opt| exists for conveniently interacting with list-
---- and map-style option from Lua: It allows accessing them as Lua tables and
+--- and map-style options from Lua: It allows accessing them as Lua tables and
--- offers object-oriented method for adding and removing entries.
---
--- Examples: ~
diff --git a/runtime/lua/vim/_system.lua b/runtime/lua/vim/_system.lua
index c0a0570e13..157172447a 100644
--- a/runtime/lua/vim/_system.lua
+++ b/runtime/lua/vim/_system.lua
@@ -79,7 +79,8 @@ function SystemObj:_timeout(signal)
self:kill(signal or SIG.TERM)
end
-local MAX_TIMEOUT = 2 ^ 31
+-- Use max 32-bit signed int value to avoid overflow on 32-bit systems. #31633
+local MAX_TIMEOUT = 2 ^ 31 - 1
--- @param timeout? integer
--- @return vim.SystemCompleted
diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua
index ded7a8f89d..ead75f7d51 100644
--- a/runtime/lua/vim/diagnostic.lua
+++ b/runtime/lua/vim/diagnostic.lua
@@ -70,7 +70,7 @@ end
--- Use virtual text for diagnostics. If multiple diagnostics are set for a
--- namespace, one prefix per diagnostic + the last diagnostic message are
--- shown.
---- (default: `true`)
+--- (default: `false`)
--- @field virtual_text? boolean|vim.diagnostic.Opts.VirtualText|fun(namespace: integer, bufnr:integer): vim.diagnostic.Opts.VirtualText
---
--- Use signs for diagnostics |diagnostic-signs|.
@@ -312,7 +312,7 @@ M.severity = {
local global_diagnostic_options = {
signs = true,
underline = true,
- virtual_text = true,
+ virtual_text = false,
float = true,
update_in_insert = false,
severity_sort = false,
@@ -356,7 +356,7 @@ local bufnr_and_namespace_cacher_mt = {
-- bufnr -> ns -> Diagnostic[]
local diagnostic_cache = {} --- @type table<integer,table<integer,vim.Diagnostic[]>>
do
- local group = api.nvim_create_augroup('DiagnosticBufWipeout', {})
+ local group = api.nvim_create_augroup('nvim.diagnostic.buf_wipeout', {})
setmetatable(diagnostic_cache, {
--- @param t table<integer,vim.Diagnostic[]>
--- @param bufnr integer
@@ -871,10 +871,10 @@ local function set_list(loclist, opts)
end
if open then
- if qf_id then
+ if not loclist then
-- First navigate to the diagnostics quickfix list.
local nr = vim.fn.getqflist({ id = qf_id, nr = 0 }).nr
- api.nvim_command(nr .. 'chistory')
+ api.nvim_command(('silent %dchistory'):format(nr))
-- Now open the quickfix list.
api.nvim_command('botright cwindow')
@@ -1395,10 +1395,6 @@ M.handlers.signs = {
return
end
- if opts.signs and opts.signs.severity then
- diagnostics = filter_by_severity(opts.signs.severity, diagnostics)
- end
-
-- 10 is the default sign priority when none is explicitly specified
local priority = opts.signs and opts.signs.priority or 10
local get_priority = severity_to_extmark_priority(priority, opts)
@@ -1406,7 +1402,7 @@ M.handlers.signs = {
local ns = M.get_namespace(namespace)
if not ns.user_data.sign_ns then
ns.user_data.sign_ns =
- api.nvim_create_namespace(string.format('%s/diagnostic/signs', ns.name))
+ api.nvim_create_namespace(string.format('nvim.%s.diagnostic.signs', ns.name))
end
-- Handle legacy diagnostic sign definitions
@@ -1501,14 +1497,10 @@ M.handlers.underline = {
return
end
- if opts.underline and opts.underline.severity then
- diagnostics = filter_by_severity(opts.underline.severity, diagnostics)
- end
-
local ns = M.get_namespace(namespace)
if not ns.user_data.underline_ns then
ns.user_data.underline_ns =
- api.nvim_create_namespace(string.format('%s/diagnostic/underline', ns.name))
+ api.nvim_create_namespace(string.format('nvim.%s.diagnostic.underline', ns.name))
end
local underline_ns = ns.user_data.underline_ns
@@ -1565,7 +1557,6 @@ M.handlers.virtual_text = {
return
end
- local severity --- @type vim.diagnostic.SeverityFilter?
if opts.virtual_text then
if opts.virtual_text.format then
diagnostics = reformat_diagnostics(opts.virtual_text.format, diagnostics)
@@ -1576,23 +1567,17 @@ M.handlers.virtual_text = {
then
diagnostics = prefix_source(diagnostics)
end
- if opts.virtual_text.severity then
- severity = opts.virtual_text.severity
- end
end
local ns = M.get_namespace(namespace)
if not ns.user_data.virt_text_ns then
ns.user_data.virt_text_ns =
- api.nvim_create_namespace(string.format('%s/diagnostic/virtual_text', ns.name))
+ api.nvim_create_namespace(string.format('nvim.%s.diagnostic.virtual_text', ns.name))
end
local virt_text_ns = ns.user_data.virt_text_ns
local buffer_line_diagnostics = diagnostic_lines(diagnostics)
for line, line_diagnostics in pairs(buffer_line_diagnostics) do
- if severity then
- line_diagnostics = filter_by_severity(severity, line_diagnostics)
- end
local virt_texts = M._get_virt_text_chunks(line_diagnostics, opts.virtual_text)
if virt_texts then
@@ -1797,7 +1782,8 @@ function M.show(namespace, bufnr, diagnostics, opts)
for handler_name, handler in pairs(M.handlers) do
if handler.show and opts_res[handler_name] then
- handler.show(namespace, bufnr, diagnostics, opts_res)
+ local filtered = filter_by_severity(opts_res[handler_name].severity, diagnostics)
+ handler.show(namespace, bufnr, filtered, opts_res)
end
end
end
diff --git a/runtime/lua/vim/filetype.lua b/runtime/lua/vim/filetype.lua
index 83e82392de..efc41269f8 100644
--- a/runtime/lua/vim/filetype.lua
+++ b/runtime/lua/vim/filetype.lua
@@ -353,6 +353,7 @@ local extension = {
cql = 'cqlang',
crm = 'crm',
cr = 'crystal',
+ cake = 'cs',
csx = 'cs',
cs = 'cs',
csc = 'csc',
@@ -504,6 +505,7 @@ local extension = {
gdshader = 'gdshader',
shader = 'gdshader',
ged = 'gedcom',
+ gel = 'gel',
gmi = 'gemtext',
gemini = 'gemtext',
gift = 'gift',
@@ -592,6 +594,7 @@ local extension = {
hw = detect.hw,
module = detect.hw,
pkg = detect.hw,
+ hy = 'hy',
iba = 'ibasic',
ibi = 'ibasic',
icn = 'icon',
@@ -616,6 +619,7 @@ local extension = {
janet = 'janet',
jav = 'java',
java = 'java',
+ jsh = 'java',
jj = 'javacc',
jjt = 'javacc',
es = 'javascript',
@@ -628,7 +632,7 @@ local extension = {
clp = 'jess',
jgr = 'jgraph',
jinja = 'jinja',
- jjdescription = 'jj',
+ jjdescription = 'jjdescription',
j73 = 'jovial',
jov = 'jovial',
jovial = 'jovial',
@@ -698,7 +702,6 @@ local extension = {
el = 'lisp',
lsp = 'lisp',
asd = 'lisp',
- stsg = 'lisp',
lt = 'lite',
lite = 'lite',
livemd = 'livebook',
@@ -734,7 +737,7 @@ local extension = {
mk = detect.make,
mak = detect.make,
page = 'mallard',
- map = 'map',
+ map = detect_line1('^%*+$', 'lnkmap', 'map'),
mws = 'maple',
mpl = 'maple',
mv = 'maple',
@@ -1061,16 +1064,17 @@ local extension = {
builder = 'ruby',
rake = 'ruby',
rs = 'rust',
+ sa = detect.sa,
sage = 'sage',
sls = 'salt',
sas = 'sas',
sass = 'sass',
- sa = 'sather',
sbt = 'sbt',
scala = 'scala',
ss = 'scheme',
scm = 'scheme',
sld = 'scheme',
+ stsg = 'scheme',
sce = 'scilab',
sci = 'scilab',
scss = 'scss',
@@ -1093,6 +1097,7 @@ local extension = {
la = 'sh',
lai = 'sh',
mdd = 'sh',
+ slang = 'shaderslang',
sieve = 'sieve',
siv = 'sieve',
sig = detect.sig,
@@ -1334,6 +1339,7 @@ local extension = {
xlb = 'xml',
xlc = 'xml',
xba = 'xml',
+ slnx = 'xml',
xpm = detect_line1('XPM2', 'xpm2', 'xpm'),
xpm2 = 'xpm2',
xqy = 'xquery',
@@ -1389,7 +1395,7 @@ local extension = {
txt = detect.txt,
xml = detect.xml,
y = detect.y,
- cmd = detect_line1('^/%*', 'rexx', 'dosbatch'),
+ cmd = detect.cmd,
rul = detect.rul,
cpy = detect_line1('^##', 'python', 'cobol'),
dsl = detect_line1('^%s*<!', 'dsl', 'structurizr'),
@@ -1438,6 +1444,7 @@ local filename = {
['/etc/asound.conf'] = 'alsaconf',
['build.xml'] = 'ant',
['.htaccess'] = 'apache',
+ APKBUILD = 'apkbuild',
['apt.conf'] = 'aptconf',
['/.aptitude/config'] = 'aptconf',
['=tagging-method'] = 'arch',
@@ -1544,6 +1551,8 @@ local filename = {
['filter-rules'] = 'elmfilt',
['exim.conf'] = 'exim',
exports = 'exports',
+ fennelrc = 'fennel',
+ ['.fennelrc'] = 'fennel',
['.fetchmailrc'] = 'fetchmail',
fvSchemes = detect.foam,
fvSolution = detect.foam,
@@ -1565,6 +1574,12 @@ local filename = {
['.gitmodules'] = 'gitconfig',
['.gitattributes'] = 'gitattributes',
['.gitignore'] = 'gitignore',
+ ['.ignore'] = 'gitignore',
+ ['.dockerignore'] = 'gitignore',
+ ['.fdignore'] = 'gitignore',
+ ['.npmignore'] = 'gitignore',
+ ['.rgignore'] = 'gitignore',
+ ['.vscodeignore'] = 'gitignore',
['gitolite.conf'] = 'gitolite',
['git-rebase-todo'] = 'gitrebase',
gkrellmrc = 'gkrellmrc',
@@ -1599,6 +1614,7 @@ local filename = {
['/etc/host.conf'] = 'hostconf',
['/etc/hosts.allow'] = 'hostsaccess',
['/etc/hosts.deny'] = 'hostsaccess',
+ ['.hy-history'] = 'hy',
['hyprland.conf'] = 'hyprlang',
['hyprpaper.conf'] = 'hyprlang',
['hypridle.conf'] = 'hyprlang',
@@ -1620,6 +1636,7 @@ local filename = {
['.lintstagedrc'] = 'json',
['deno.lock'] = 'json',
['flake.lock'] = 'json',
+ ['.swcrc'] = 'json',
['.babelrc'] = 'jsonc',
['.eslintrc'] = 'jsonc',
['.hintrc'] = 'jsonc',
@@ -1629,6 +1646,7 @@ local filename = {
['.luaurc'] = 'jsonc',
['.swrc'] = 'jsonc',
['.vsconfig'] = 'jsonc',
+ ['bun.lock'] = 'jsonc',
['.justfile'] = 'just',
['justfile'] = 'just',
['Justfile'] = 'just',
@@ -1774,6 +1792,7 @@ local filename = {
['Rantfile'] = 'ruby',
Vagrantfile = 'ruby',
['smb.conf'] = 'samba',
+ ['.lips_repl_history'] = 'scheme',
screenrc = 'screen',
['.screenrc'] = 'screen',
['/etc/sensors3.conf'] = 'sensors',
@@ -1795,7 +1814,6 @@ local filename = {
['.kshrc'] = detect.ksh,
['.profile'] = detect.sh,
['/etc/profile'] = detect.sh,
- APKBUILD = detect.bash,
PKGBUILD = detect.bash,
['.tcshrc'] = detect.tcsh,
['tcsh.login'] = detect.tcsh,
@@ -1872,6 +1890,9 @@ local filename = {
['/etc/blkid.tab'] = 'xml',
['/etc/blkid.tab.old'] = 'xml',
['fonts.conf'] = 'xml',
+ ['Directory.Packages.props'] = 'xml',
+ ['Directory.Build.props'] = 'xml',
+ ['Directory.Build.targets'] = 'xml',
['.clangd'] = 'yaml',
['.clang-format'] = 'yaml',
['.clang-tidy'] = 'yaml',
@@ -2156,8 +2177,8 @@ local pattern = {
['/gitolite%-admin/conf/'] = starsetf('gitolite'),
['/%.i3/config$'] = 'i3config',
['/i3/config$'] = 'i3config',
- ['/supertux2/config$'] = 'lisp',
['/%.mplayer/config$'] = 'mplayerconf',
+ ['/supertux2/config$'] = 'scheme',
['/neofetch/config%.conf$'] = 'sh',
['/%.ssh/config$'] = 'sshconfig',
['/%.sway/config$'] = 'swayconfig',
@@ -2225,6 +2246,7 @@ local pattern = {
['^dictd.*%.conf$'] = 'dictdconf',
['/lxqt/.*%.conf$'] = 'dosini',
['/screengrab/.*%.conf$'] = 'dosini',
+ ['/%.config/fd/ignore$'] = 'gitignore',
['^${GNUPGHOME}/gpg%.conf$'] = 'gpg',
['/boot/grub/grub%.conf$'] = 'grub',
['/hypr/.*%.conf$'] = 'hyprlang',
@@ -2355,6 +2377,8 @@ local pattern = {
['%.html%.m4$'] = 'htmlm4',
['^JAM.*%.'] = starsetf('jam'),
['^Prl.*%.'] = starsetf('jam'),
+ ['^${HOME}/.*/Code/User/.*%.json$'] = 'jsonc',
+ ['^${HOME}/.*/VSCodium/User/.*%.json$'] = 'jsonc',
['%.properties_..$'] = 'jproperties',
['%.properties_.._..$'] = 'jproperties',
['%.properties_.._.._'] = starsetf('jproperties'),
diff --git a/runtime/lua/vim/filetype/detect.lua b/runtime/lua/vim/filetype/detect.lua
index 4f2fef5b1f..31c88c80bd 100644
--- a/runtime/lua/vim/filetype/detect.lua
+++ b/runtime/lua/vim/filetype/detect.lua
@@ -34,6 +34,12 @@ local matchregex = vim.filetype._matchregex
-- can be detected from the first five lines of the file.
--- @type vim.filetype.mapfn
function M.asm(path, bufnr)
+ -- tiasm uses `* commment`
+ local lines = table.concat(getlines(bufnr, 1, 10), '\n')
+ if findany(lines, { '^%*', '\n%*', 'Texas Instruments Incorporated' }) then
+ return 'tiasm'
+ end
+
local syntax = vim.b[bufnr].asmsyntax
if not syntax or syntax == '' then
syntax = M.asm_syntax(path, bufnr)
@@ -219,6 +225,24 @@ function M.cls(_, bufnr)
return 'st'
end
+--- *.cmd is close to a Batch file, but on OS/2 Rexx files and TI linker command files also use *.cmd.
+--- lnk: `/* comment */`, `// comment`, and `--linker-option=value`
+--- rexx: `/* comment */`, `-- comment`
+--- @type vim.filetype.mapfn
+function M.cmd(_, bufnr)
+ local lines = table.concat(getlines(bufnr, 1, 20))
+ if matchregex(lines, [[MEMORY\|SECTIONS\|\%(^\|\n\)--\S\|\%(^\|\n\)//]]) then
+ return 'lnk'
+ else
+ local line1 = getline(bufnr, 1)
+ if line1:find('^/%*') then
+ return 'rexx'
+ else
+ return 'dosbatch'
+ end
+ end
+end
+
--- @type vim.filetype.mapfn
function M.conf(path, bufnr)
if fn.did_filetype() ~= 0 or path:find(vim.g.ft_ignore_pat) then
@@ -733,7 +757,7 @@ function M.html(_, bufnr)
if
matchregex(
line,
- [[@\(if\|for\|defer\|switch\)\|\*\(ngIf\|ngFor\|ngSwitch\|ngTemplateOutlet\)\|ng-template\|ng-content\|{{.*}}]]
+ [[@\(if\|for\|defer\|switch\)\|\*\(ngIf\|ngFor\|ngSwitch\|ngTemplateOutlet\)\|ng-template\|ng-content]]
)
then
return 'htmlangular'
@@ -1406,6 +1430,15 @@ function M.sig(_, bufnr)
end
end
+--- @type vim.filetype.mapfn
+function M.sa(_, bufnr)
+ local lines = table.concat(getlines(bufnr, 1, 4), '\n')
+ if findany(lines, { '^;', '\n;' }) then
+ return 'tiasm'
+ end
+ return 'sather'
+end
+
-- This function checks the first 25 lines of file extension "sc" to resolve
-- detection between scala and SuperCollider
--- @type vim.filetype.mapfn
diff --git a/runtime/lua/vim/fs.lua b/runtime/lua/vim/fs.lua
index 2f007d97c3..5940fa4386 100644
--- a/runtime/lua/vim/fs.lua
+++ b/runtime/lua/vim/fs.lua
@@ -105,14 +105,23 @@ function M.basename(file)
return file:match('/$') and '' or (file:match('[^/]*$'))
end
---- Concatenate directories and/or file paths into a single path with normalization
---- (e.g., `"foo/"` and `"bar"` get joined to `"foo/bar"`)
+--- Concatenates partial paths (one absolute or relative path followed by zero or more relative
+--- paths). Slashes are normalized: redundant slashes are removed, and (on Windows) backslashes are
+--- replaced with forward-slashes.
+---
+--- Examples:
+--- - "foo/", "/bar" => "foo/bar"
+--- - Windows: "a\foo\", "\bar" => "a/foo/bar"
---
---@since 12
---@param ... string
---@return string
function M.joinpath(...)
- return (table.concat({ ... }, '/'):gsub('//+', '/'))
+ local path = table.concat({ ... }, '/')
+ if iswin then
+ path = path:gsub('\\', '/')
+ end
+ return (path:gsub('//+', '/'))
end
---@alias Iterator fun(): string?, string?
@@ -127,6 +136,7 @@ end
--- - skip: (fun(dir_name: string): boolean)|nil Predicate
--- to control traversal. Return false to stop searching the current directory.
--- Only useful when depth > 1
+--- - follow: boolean|nil Follow symbolic links. (default: true)
---
---@return Iterator over items in {path}. Each iteration yields two values: "name" and "type".
--- "name" is the basename of the item relative to {path}.
@@ -138,6 +148,7 @@ function M.dir(path, opts)
vim.validate('path', path, 'string')
vim.validate('depth', opts.depth, 'number', true)
vim.validate('skip', opts.skip, 'function', true)
+ vim.validate('follow', opts.follow, 'boolean', true)
path = M.normalize(path)
if not opts.depth or opts.depth == 1 then
@@ -168,7 +179,9 @@ function M.dir(path, opts)
if
opts.depth
and level < opts.depth
- and t == 'directory'
+ and (t == 'directory' or (t == 'link' and opts.follow ~= false and (vim.uv.fs_stat(
+ M.joinpath(path, f)
+ ) or {}).type == 'directory'))
and (not opts.skip or opts.skip(f) ~= false)
then
dirs[#dirs + 1] = { f, level + 1 }
@@ -202,6 +215,10 @@ end
--- Use `math.huge` to place no limit on the number of matches.
--- (default: `1`)
--- @field limit? number
+---
+--- Follow symbolic links.
+--- (default: `true`)
+--- @field follow? boolean
--- Find files or directories (or other items as specified by `opts.type`) in the given path.
---
@@ -225,7 +242,7 @@ end
---
--- -- get all files ending with .cpp or .hpp inside lib/
--- local cpp_hpp = vim.fs.find(function(name, path)
---- return name:match('.*%.[ch]pp$') and path:match('[/\\\\]lib$')
+--- return name:match('.*%.[ch]pp$') and path:match('[/\\]lib$')
--- end, {limit = math.huge, type = 'file'})
--- ```
---
@@ -235,6 +252,7 @@ end
--- If {names} is a function, it is called for each traversed item with args:
--- - name: base name of the current item
--- - path: full path of the current item
+---
--- The function should return `true` if the given item is considered a match.
---
---@param opts vim.fs.find.Opts Optional keyword arguments:
@@ -247,6 +265,7 @@ function M.find(names, opts)
vim.validate('stop', opts.stop, 'string', true)
vim.validate('type', opts.type, 'string', true)
vim.validate('limit', opts.limit, 'number', true)
+ vim.validate('follow', opts.follow, 'boolean', true)
if type(names) == 'string' then
names = { names }
@@ -336,7 +355,14 @@ function M.find(names, opts)
end
end
- if type_ == 'directory' then
+ if
+ type_ == 'directory'
+ or (
+ type_ == 'link'
+ and opts.follow ~= false
+ and (vim.uv.fs_stat(f) or {}).type == 'directory'
+ )
+ then
dirs[#dirs + 1] = f
end
end
@@ -505,6 +531,27 @@ local function path_resolve_dot(path)
return (is_path_absolute and '/' or '') .. table.concat(new_path_components, '/')
end
+--- Expand tilde (~) character at the beginning of the path to the user's home directory.
+---
+--- @param path string Path to expand.
+--- @param sep string|nil Path separator to use. Uses os_sep by default.
+--- @return string Expanded path.
+local function expand_home(path, sep)
+ sep = sep or os_sep
+
+ if vim.startswith(path, '~') then
+ local home = uv.os_homedir() or '~' --- @type string
+
+ if home:sub(-1) == sep then
+ home = home:sub(1, -2)
+ end
+
+ path = home .. path:sub(2)
+ end
+
+ return path
+end
+
--- @class vim.fs.normalize.Opts
--- @inlinedoc
---
@@ -568,14 +615,8 @@ function M.normalize(path, opts)
return ''
end
- -- Expand ~ to users home directory
- if vim.startswith(path, '~') then
- local home = uv.os_homedir() or '~'
- if home:sub(-1) == os_sep_local then
- home = home:sub(1, -2)
- end
- path = home .. path:sub(2)
- end
+ -- Expand ~ to user's home directory
+ path = expand_home(path, os_sep_local)
-- Expand environment variables if `opts.expand_env` isn't `false`
if opts.expand_env == nil or opts.expand_env then
@@ -605,8 +646,8 @@ function M.normalize(path, opts)
return prefix .. path
end
- -- Remove extraneous slashes from the prefix
- prefix = prefix:gsub('/+', '/')
+ -- Ensure capital drive and remove extraneous slashes from the prefix
+ prefix = prefix:gsub('^%a:', string.upper):gsub('/+', '/')
end
if not opts._fast then
@@ -679,4 +720,75 @@ function M.rm(path, opts)
end
end
+--- Convert path to an absolute path. A tilde (~) character at the beginning of the path is expanded
+--- to the user's home directory. Does not check if the path exists, normalize the path, resolve
+--- symlinks or hardlinks (including `.` and `..`), or expand environment variables. If the path is
+--- already absolute, it is returned unchanged. Also converts `\` path separators to `/`.
+---
+--- @param path string Path
+--- @return string Absolute path
+function M.abspath(path)
+ vim.validate('path', path, 'string')
+
+ -- Expand ~ to user's home directory
+ path = expand_home(path)
+
+ -- Convert path separator to `/`
+ path = path:gsub(os_sep, '/')
+
+ local prefix = ''
+
+ if iswin then
+ prefix, path = split_windows_path(path)
+ end
+
+ if prefix == '//' or vim.startswith(path, '/') then
+ -- Path is already absolute, do nothing
+ return prefix .. path
+ end
+
+ -- Windows allows paths like C:foo/bar, these paths are relative to the current working directory
+ -- of the drive specified in the path
+ local cwd = (iswin and prefix:match('^%w:$')) and uv.fs_realpath(prefix) or uv.cwd()
+ assert(cwd ~= nil)
+ -- Convert cwd path separator to `/`
+ cwd = cwd:gsub(os_sep, '/')
+
+ -- Prefix is not needed for expanding relative paths, as `cwd` already contains it.
+ return M.joinpath(cwd, path)
+end
+
+--- Gets `target` path relative to `base`, or `nil` if `base` is not an ancestor.
+---
+--- Example:
+---
+--- ```lua
+--- vim.fs.relpath('/var', '/var/lib') -- 'lib'
+--- vim.fs.relpath('/var', '/usr/bin') -- nil
+--- ```
+---
+--- @param base string
+--- @param target string
+--- @param opts table? Reserved for future use
+--- @return string|nil
+function M.relpath(base, target, opts)
+ vim.validate('base', base, 'string')
+ vim.validate('target', target, 'string')
+ vim.validate('opts', opts, 'table', true)
+
+ base = vim.fs.normalize(vim.fs.abspath(base))
+ target = vim.fs.normalize(vim.fs.abspath(target))
+ if base == target then
+ return '.'
+ end
+
+ local prefix = ''
+ if iswin then
+ prefix, base = split_windows_path(base)
+ end
+ base = prefix .. base .. (base ~= '/' and '/' or '')
+
+ return vim.startswith(target, base) and target:sub(#base + 1) or nil
+end
+
return M
diff --git a/runtime/lua/vim/func.lua b/runtime/lua/vim/func.lua
index f71659ffb4..fc8fa62c71 100644
--- a/runtime/lua/vim/func.lua
+++ b/runtime/lua/vim/func.lua
@@ -3,9 +3,6 @@ local M = {}
-- TODO(lewis6991): Private for now until:
-- - There are other places in the codebase that could benefit from this
-- (e.g. LSP), but might require other changes to accommodate.
--- - Invalidation of the cache needs to be controllable. Using weak tables
--- is an acceptable invalidation policy, but it shouldn't be the only
--- one.
-- - I don't think the story around `hash` is completely thought out. We
-- may be able to have a good default hash by hashing each argument,
-- so basically a better 'concat'.
@@ -17,6 +14,10 @@ local M = {}
--- Internally uses a |lua-weaktable| to cache the results of {fn} meaning the
--- cache will be invalidated whenever Lua does garbage collection.
---
+--- The cache can also be manually invalidated by calling `:clear()` on the returned object.
+--- Calling this function with no arguments clears the entire cache; otherwise, the arguments will
+--- be interpreted as function inputs, and only the cache entry at their hash will be cleared.
+---
--- The memoized function returns shared references so be wary about
--- mutating return values.
---
@@ -32,11 +33,12 @@ local M = {}
--- first n arguments passed to {fn}.
---
--- @param fn F Function to memoize.
---- @param strong? boolean Do not use a weak table
+--- @param weak? boolean Use a weak table (default `true`)
--- @return F # Memoized version of {fn}
--- @nodoc
-function M._memoize(hash, fn, strong)
- return require('vim.func._memoize')(hash, fn, strong)
+function M._memoize(hash, fn, weak)
+ -- this is wrapped in a function to lazily require the module
+ return require('vim.func._memoize')(hash, fn, weak)
end
return M
diff --git a/runtime/lua/vim/func/_memoize.lua b/runtime/lua/vim/func/_memoize.lua
index 6e557905a7..c46f878067 100644
--- a/runtime/lua/vim/func/_memoize.lua
+++ b/runtime/lua/vim/func/_memoize.lua
@@ -1,5 +1,7 @@
--- Module for private utility functions
+--- @alias vim.func.MemoObj { _hash: (fun(...): any), _weak: boolean?, _cache: table<any> }
+
--- @param argc integer?
--- @return fun(...): any
local function concat_hash(argc)
@@ -33,29 +35,49 @@ local function resolve_hash(hash)
return hash
end
+--- @param weak boolean?
+--- @return table
+local create_cache = function(weak)
+ return setmetatable({}, {
+ __mode = weak ~= false and 'kv',
+ })
+end
+
--- @generic F: function
--- @param hash integer|string|fun(...): any
--- @param fn F
---- @param strong? boolean
+--- @param weak? boolean
--- @return F
-return function(hash, fn, strong)
+return function(hash, fn, weak)
vim.validate('hash', hash, { 'number', 'string', 'function' })
vim.validate('fn', fn, 'function')
+ vim.validate('weak', weak, 'boolean', true)
- ---@type table<any,table<any,any>>
- local cache = {}
- if not strong then
- setmetatable(cache, { __mode = 'kv' })
- end
-
- hash = resolve_hash(hash)
+ --- @type vim.func.MemoObj
+ local obj = {
+ _cache = create_cache(weak),
+ _hash = resolve_hash(hash),
+ _weak = weak,
+ --- @param self vim.func.MemoObj
+ clear = function(self, ...)
+ if select('#', ...) == 0 then
+ self._cache = create_cache(self._weak)
+ return
+ end
+ local key = self._hash(...)
+ self._cache[key] = nil
+ end,
+ }
- return function(...)
- local key = hash(...)
- if cache[key] == nil then
- cache[key] = vim.F.pack_len(fn(...))
- end
-
- return vim.F.unpack_len(cache[key])
- end
+ return setmetatable(obj, {
+ --- @param self vim.func.MemoObj
+ __call = function(self, ...)
+ local key = self._hash(...)
+ local cache = self._cache
+ if cache[key] == nil then
+ cache[key] = vim.F.pack_len(fn(...))
+ end
+ return vim.F.unpack_len(cache[key])
+ end,
+ })
end
diff --git a/runtime/lua/vim/health.lua b/runtime/lua/vim/health.lua
index 52a7a13966..ee376f3a11 100644
--- a/runtime/lua/vim/health.lua
+++ b/runtime/lua/vim/health.lua
@@ -11,7 +11,7 @@
--- <
--- Plugin authors are encouraged to write new healthchecks. |health-dev|
---
---- Commands *health-commands*
+--- COMMANDS *health-commands*
---
--- *:che* *:checkhealth*
--- :che[ckhealth] Run all healthchecks.
@@ -39,6 +39,23 @@
--- :checkhealth vim*
--- <
---
+--- USAGE *health-usage*
+---
+--- Local mappings in the healthcheck buffer:
+---
+--- q Closes the window.
+---
+--- Global configuration:
+---
+--- *g:health*
+--- g:health Dictionary with the following optional keys:
+--- - `style` (`'float'|nil`) Set to "float" to display :checkhealth in
+--- a floating window instead of the default behavior.
+---
+--- Example: >lua
+--- vim.g.health = { style = 'float' }
+---
+--- --------------------------------------------------------------------------------
--- Create a healthcheck *health-dev*
---
--- Healthchecks are functions that check the user environment, configuration, or
@@ -331,13 +348,31 @@ function M._check(mods, plugin_names)
local emptybuf = vim.fn.bufnr('$') == 1 and vim.fn.getline(1) == '' and 1 == vim.fn.line('$')
- -- When no command modifiers are used:
- -- - If the current buffer is empty, open healthcheck directly.
- -- - If not specified otherwise open healthcheck in a tab.
- local buf_cmd = #mods > 0 and (mods .. ' sbuffer') or emptybuf and 'buffer' or 'tab sbuffer'
-
local bufnr = vim.api.nvim_create_buf(true, true)
- vim.cmd(buf_cmd .. ' ' .. bufnr)
+ if
+ vim.g.health
+ and type(vim.g.health) == 'table'
+ and vim.tbl_get(vim.g.health, 'style') == 'float'
+ then
+ local max_height = math.floor(vim.o.lines * 0.8)
+ local max_width = 80
+ local float_bufnr, float_winid = vim.lsp.util.open_floating_preview({}, '', {
+ height = max_height,
+ width = max_width,
+ offset_x = math.floor((vim.o.columns - max_width) / 2),
+ offset_y = math.floor((vim.o.lines - max_height) / 2) - 1,
+ relative = 'editor',
+ })
+ vim.api.nvim_set_current_win(float_winid)
+ vim.bo[float_bufnr].modifiable = true
+ vim.wo[float_winid].list = false
+ else
+ -- When no command modifiers are used:
+ -- - If the current buffer is empty, open healthcheck directly.
+ -- - If not specified otherwise open healthcheck in a tab.
+ local buf_cmd = #mods > 0 and (mods .. ' sbuffer') or emptybuf and 'buffer' or 'tab sbuffer'
+ vim.cmd(buf_cmd .. ' ' .. bufnr)
+ end
if vim.fn.bufexists('health://') == 1 then
vim.cmd.bwipe('health://')
@@ -407,6 +442,17 @@ function M._check(mods, plugin_names)
-- Clear the 'Running healthchecks...' message.
vim.cmd.redraw()
vim.print('')
+
+ -- Quit with 'q' inside healthcheck buffers.
+ vim.keymap.set('n', 'q', function()
+ local ok, _ = pcall(vim.cmd.close)
+ if not ok then
+ vim.cmd.bdelete()
+ end
+ end, { buffer = bufnr, silent = true, noremap = true, nowait = true })
+
+ -- Once we're done writing checks, set nomodifiable.
+ vim.bo[bufnr].modifiable = false
end
return M
diff --git a/runtime/lua/vim/hl.lua b/runtime/lua/vim/hl.lua
index 099efa3c61..f5ace7fdc5 100644
--- a/runtime/lua/vim/hl.lua
+++ b/runtime/lua/vim/hl.lua
@@ -115,7 +115,7 @@ function M.range(bufnr, ns, higroup, start, finish, opts)
end
end
-local yank_ns = api.nvim_create_namespace('hlyank')
+local yank_ns = api.nvim_create_namespace('nvim.hlyank')
local yank_timer --- @type uv.uv_timer_t?
local yank_cancel --- @type fun()?
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua
index 6a8c3d1ff3..5226c8ae37 100644
--- a/runtime/lua/vim/lsp.lua
+++ b/runtime/lua/vim/lsp.lua
@@ -211,7 +211,7 @@ local function reuse_client_default(client, config)
for _, config_folder in ipairs(config_folders) do
local found = false
- for _, client_folder in ipairs(client.workspace_folders) do
+ for _, client_folder in ipairs(client.workspace_folders or {}) do
if config_folder.uri == client_folder.uri then
found = true
break
@@ -334,6 +334,11 @@ end
--- rootUri, and rootPath on initialization. Unused if `root_dir` is provided.
--- @field root_markers? string[]
---
+--- Directory where the LSP server will base its workspaceFolders, rootUri, and rootPath on
+--- initialization. If a function, it accepts a single callback argument which must be called with
+--- the value of root_dir to use. The LSP server will not be started until the callback is called.
+--- @field root_dir? string|fun(cb:fun(string))
+---
--- Predicate used to decide if a client should be re-used. Used on all
--- running clients. The default implementation re-uses a client if name and
--- root_dir matches.
@@ -415,9 +420,33 @@ lsp.config = setmetatable({ _configs = {} }, {
--- @return vim.lsp.Config
__index = function(self, name)
validate('name', name, 'string')
- invalidate_enabled_config(name)
+
+ local rconfig = lsp._enabled_configs[name] or {}
self._configs[name] = self._configs[name] or {}
- return self._configs[name]
+
+ if not rconfig.resolved_config then
+ -- Resolve configs from lsp/*.lua
+ -- Calls to vim.lsp.config in lsp/* have a lower precedence than calls from other sites.
+ local rtp_config = {} ---@type vim.lsp.Config
+ for _, v in ipairs(api.nvim_get_runtime_file(('lsp/%s.lua'):format(name), true)) do
+ local config = assert(loadfile(v))() ---@type any?
+ if type(config) == 'table' then
+ rtp_config = vim.tbl_deep_extend('force', rtp_config, config)
+ else
+ log.warn(string.format('%s does not return a table, ignoring', v))
+ end
+ end
+
+ rconfig.resolved_config = vim.tbl_deep_extend(
+ 'force',
+ lsp.config._configs['*'] or {},
+ rtp_config,
+ lsp.config._configs[name] or {}
+ )
+ rconfig.resolved_config.name = name
+ end
+
+ return rconfig.resolved_config
end,
--- @param self vim.lsp.config
@@ -441,40 +470,6 @@ lsp.config = setmetatable({ _configs = {} }, {
end,
})
---- @private
---- @param name string
---- @return vim.lsp.Config
-function lsp._resolve_config(name)
- local econfig = lsp._enabled_configs[name] or {}
-
- if not econfig.resolved_config then
- -- Resolve configs from lsp/*.lua
- -- Calls to vim.lsp.config in lsp/* have a lower precedence than calls from other sites.
- local orig_configs = lsp.config._configs
- lsp.config._configs = {}
- pcall(vim.cmd.runtime, { ('lsp/%s.lua'):format(name), bang = true })
- local rtp_configs = lsp.config._configs
- lsp.config._configs = orig_configs
-
- local config = vim.tbl_deep_extend(
- 'force',
- lsp.config._configs['*'] or {},
- rtp_configs[name] or {},
- lsp.config._configs[name] or {}
- )
-
- config.name = name
-
- validate('cmd', config.cmd, { 'function', 'table' })
- validate('cmd', config.reuse_client, 'function', true)
- -- All other fields are validated in client.create
-
- econfig.resolved_config = config
- end
-
- return assert(econfig.resolved_config)
-end
-
local lsp_enable_autocmd_id --- @type integer?
--- @param bufnr integer
@@ -495,19 +490,36 @@ local function lsp_enable_callback(bufnr)
return true
end
+ --- @param config vim.lsp.Config
+ local function start(config)
+ return vim.lsp.start(config, {
+ bufnr = bufnr,
+ reuse_client = config.reuse_client,
+ _root_markers = config.root_markers,
+ })
+ end
+
for name in vim.spairs(lsp._enabled_configs) do
- local config = lsp._resolve_config(name)
+ local config = lsp.config[name]
+ validate('cmd', config.cmd, { 'function', 'table' })
+ validate('cmd', config.reuse_client, 'function', true)
if can_start(config) then
-- Deepcopy config so changes done in the client
-- do not propagate back to the enabled configs.
config = vim.deepcopy(config)
- vim.lsp.start(config, {
- bufnr = bufnr,
- reuse_client = config.reuse_client,
- _root_markers = config.root_markers,
- })
+ if type(config.root_dir) == 'function' then
+ ---@param root_dir string
+ config.root_dir(function(root_dir)
+ config.root_dir = root_dir
+ vim.schedule(function()
+ start(config)
+ end)
+ end)
+ else
+ start(config)
+ end
end
end
end
@@ -534,7 +546,7 @@ function lsp.enable(name, enable)
if nm == '*' then
error('Invalid name')
end
- lsp._enabled_configs[nm] = enable == false and nil or {}
+ lsp._enabled_configs[nm] = enable ~= false and {} or nil
end
if not next(lsp._enabled_configs) then
@@ -831,7 +843,7 @@ local function buf_attach(bufnr)
attached_buffers[bufnr] = true
local uri = vim.uri_from_bufnr(bufnr)
- local augroup = ('lsp_b_%d_save'):format(bufnr)
+ local augroup = ('nvim.lsp.b_%d_save'):format(bufnr)
local group = api.nvim_create_augroup(augroup, { clear = true })
api.nvim_create_autocmd('BufWritePre', {
group = group,
@@ -1379,7 +1391,7 @@ end
--- |LspAttach| autocommand. Example:
---
--- ```lua
---- vim.api.nvim_create_autocommand('LspAttach', {
+--- vim.api.nvim_create_autocmd('LspAttach', {
--- callback = function(args)
--- local client = vim.lsp.get_client_by_id(args.data.client_id)
--- if client:supports_method('textDocument/foldingRange') then
diff --git a/runtime/lua/vim/lsp/_folding_range.lua b/runtime/lua/vim/lsp/_folding_range.lua
index 2f1767aaf5..66eb81db6e 100644
--- a/runtime/lua/vim/lsp/_folding_range.lua
+++ b/runtime/lua/vim/lsp/_folding_range.lua
@@ -171,7 +171,7 @@ end
-- 1. Implement clearing `bufstate` and event hooks
-- when no clients in the buffer support the corresponding method.
-- 2. Then generalize this state management to other LSP modules.
-local augroup_setup = api.nvim_create_augroup('vim_lsp_folding_range/setup', {})
+local augroup_setup = api.nvim_create_augroup('nvim.lsp.folding_range.setup', {})
--- Initialize `bufstate` and event hooks, then request folding ranges.
--- Manage their lifecycle within this function.
diff --git a/runtime/lua/vim/lsp/_watchfiles.lua b/runtime/lua/vim/lsp/_watchfiles.lua
index 248969885c..4711b3cc9b 100644
--- a/runtime/lua/vim/lsp/_watchfiles.lua
+++ b/runtime/lua/vim/lsp/_watchfiles.lua
@@ -174,6 +174,7 @@ function M.cancel(client_id)
cancel()
end
end
+ cancels[client_id] = nil
end
return M
diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua
index 1926a0228d..8efc6996dd 100644
--- a/runtime/lua/vim/lsp/buf.lua
+++ b/runtime/lua/vim/lsp/buf.lua
@@ -20,7 +20,7 @@ local function client_positional_params(params)
end
end
-local hover_ns = api.nvim_create_namespace('vim_lsp_hover_range')
+local hover_ns = api.nvim_create_namespace('nvim.lsp.hover_range')
--- @class vim.lsp.buf.hover.Opts : vim.lsp.util.open_floating_preview.Opts
--- @field silent? boolean
@@ -252,13 +252,13 @@ end
--- vim.lsp.buf.definition({ on_list = on_list })
--- vim.lsp.buf.references(nil, { on_list = on_list })
--- ```
+--- @field on_list? fun(t: vim.lsp.LocationOpts.OnList)
---
---- If you prefer loclist instead of qflist:
+--- Whether to use the |location-list| or the |quickfix| list.
--- ```lua
--- vim.lsp.buf.definition({ loclist = true })
---- vim.lsp.buf.references(nil, { loclist = true })
+--- vim.lsp.buf.references(nil, { loclist = false })
--- ```
---- @field on_list? fun(t: vim.lsp.LocationOpts.OnList)
--- @field loclist? boolean
--- @class vim.lsp.LocationOpts.OnList
@@ -324,12 +324,11 @@ local function process_signature_help_results(results)
return signatures
end
-local sig_help_ns = api.nvim_create_namespace('vim_lsp_signature_help')
+local sig_help_ns = api.nvim_create_namespace('nvim.lsp.signature_help')
--- @class vim.lsp.buf.signature_help.Opts : vim.lsp.util.open_floating_preview.Opts
--- @field silent? boolean
--- TODO(lewis6991): support multiple clients
--- Displays signature information about the symbol under the cursor in a
--- floating window.
--- @param config? vim.lsp.buf.signature_help.Opts
@@ -356,6 +355,7 @@ function M.signature_help(config)
local ft = vim.bo[ctx.bufnr].filetype
local total = #signatures
+ local can_cycle = total > 1 and config.focusable
local idx = 0
--- @param update_win? integer
@@ -371,7 +371,7 @@ function M.signature_help(config)
return
end
- local sfx = total > 1 and string.format(' (%d/%d) (<C-s> to cycle)', idx, total) or ''
+ local sfx = can_cycle and string.format(' (%d/%d) (<C-s> to cycle)', idx, total) or ''
local title = string.format('Signature Help: %s%s', client.name, sfx)
if config.border then
config.title = title
@@ -402,7 +402,7 @@ function M.signature_help(config)
local fbuf, fwin = show_signature()
- if total > 1 then
+ if can_cycle then
vim.keymap.set('n', '<C-s>', function()
show_signature(fwin)
end, {
@@ -796,9 +796,10 @@ function M.references(context, opts)
end
end
---- Lists all symbols in the current buffer in the quickfix window.
+--- Lists all symbols in the current buffer in the |location-list|.
--- @param opts? vim.lsp.ListOpts
function M.document_symbol(opts)
+ opts = vim.tbl_deep_extend('keep', opts or {}, { loclist = true })
local params = { textDocument = util.make_text_document_params() }
request_with_opts(ms.textDocument_documentSymbol, params, opts)
end
diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua
index 72043c18dd..a082613bb0 100644
--- a/runtime/lua/vim/lsp/client.lua
+++ b/runtime/lua/vim/lsp/client.lua
@@ -174,6 +174,10 @@ local validate = vim.validate
--- capabilities.
--- @field server_capabilities lsp.ServerCapabilities?
---
+--- Response from the server sent on `initialize` describing information about
+--- the server.
+--- @field server_info lsp.ServerInfo?
+---
--- A ring buffer (|vim.ringbuf()|) containing progress messages
--- sent by the server.
--- @field progress vim.lsp.Client.Progress
@@ -556,6 +560,8 @@ function Client:initialize()
self.offset_encoding = self.server_capabilities.positionEncoding
end
+ self.server_info = result.serverInfo
+
if next(self.settings) then
self:notify(ms.workspace_didChangeConfiguration, { settings = self.settings })
end
@@ -696,14 +702,14 @@ local wait_result_reason = { [-1] = 'timeout', [-2] = 'interrupted', [-3] = 'err
---
--- @param ... string List to write to the buffer
local function err_message(...)
- local message = table.concat(vim.iter({ ... }):flatten():totable())
+ local chunks = { { table.concat(vim.iter({ ... }):flatten():totable()) } }
if vim.in_fast_event() then
vim.schedule(function()
- api.nvim_err_writeln(message)
+ api.nvim_echo(chunks, true, { err = true })
api.nvim_command('redraw')
end)
else
- api.nvim_err_writeln(message)
+ api.nvim_echo(chunks, true, { err = true })
api.nvim_command('redraw')
end
end
@@ -799,6 +805,8 @@ function Client:stop(force)
return
end
+ vim.lsp._watchfiles.cancel(self.id)
+
if force or not self.initialized or self._graceful_shutdown_failed then
rpc.terminate()
return
@@ -813,7 +821,6 @@ function Client:stop(force)
rpc.terminate()
self._graceful_shutdown_failed = true
end
- vim.lsp._watchfiles.cancel(self.id)
end)
end
diff --git a/runtime/lua/vim/lsp/codelens.lua b/runtime/lua/vim/lsp/codelens.lua
index 3ccd165d0b..e36d8fee27 100644
--- a/runtime/lua/vim/lsp/codelens.lua
+++ b/runtime/lua/vim/lsp/codelens.lua
@@ -21,7 +21,7 @@ local lens_cache_by_buf = setmetatable({}, {
---client_id -> namespace
local namespaces = setmetatable({}, {
__index = function(t, key)
- local value = api.nvim_create_namespace('vim_lsp_codelens:' .. key)
+ local value = api.nvim_create_namespace('nvim.lsp.codelens:' .. key)
rawset(t, key, value)
return value
end,
@@ -30,7 +30,7 @@ local namespaces = setmetatable({}, {
---@private
M.__namespaces = namespaces
-local augroup = api.nvim_create_augroup('vim_lsp_codelens', {})
+local augroup = api.nvim_create_augroup('nvim.lsp.codelens', {})
api.nvim_create_autocmd('LspDetach', {
group = augroup,
diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua
index dbf0a62eeb..9902c52c33 100644
--- a/runtime/lua/vim/lsp/completion.lua
+++ b/runtime/lua/vim/lsp/completion.lua
@@ -127,8 +127,10 @@ end
--- See https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
---
--- @param item lsp.CompletionItem
+--- @param prefix string
+--- @param match fun(text: string, prefix: string):boolean
--- @return string
-local function get_completion_word(item)
+local function get_completion_word(item, prefix, match)
if item.insertTextFormat == protocol.InsertTextFormat.Snippet then
if item.textEdit then
-- Use label instead of text if text has different starting characters.
@@ -146,7 +148,12 @@ local function get_completion_word(item)
--
-- Typing `i` would remove the candidate because newText starts with `t`.
local text = parse_snippet(item.insertText or item.textEdit.newText)
- return #text < #item.label and vim.fn.matchstr(text, '\\k*') or item.label
+ local word = #text < #item.label and vim.fn.matchstr(text, '\\k*') or item.label
+ if item.filterText and not match(word, prefix) then
+ return item.filterText
+ else
+ return word
+ end
elseif item.insertText and item.insertText ~= '' then
return parse_snippet(item.insertText)
else
@@ -224,6 +231,9 @@ end
---@param prefix string
---@return boolean
local function match_item_by_value(value, prefix)
+ if prefix == '' then
+ return true
+ end
if vim.o.completeopt:find('fuzzy') ~= nil then
return next(vim.fn.matchfuzzy({ value }, prefix)) ~= nil
end
@@ -276,7 +286,7 @@ function M._lsp_to_complete_items(result, prefix, client_id)
local user_convert = vim.tbl_get(buf_handles, bufnr, 'convert')
for _, item in ipairs(items) do
if matches(item) then
- local word = get_completion_word(item)
+ local word = get_completion_word(item, prefix, match_item_by_value)
local hl_group = ''
if
item.deprecated
@@ -630,7 +640,7 @@ local function enable_completions(client_id, bufnr, opts)
-- Set up autocommands.
local group =
- api.nvim_create_augroup(string.format('vim/lsp/completion-%d', bufnr), { clear = true })
+ api.nvim_create_augroup(string.format('nvim.lsp.completion_%d', bufnr), { clear = true })
api.nvim_create_autocmd('CompleteDone', {
group = group,
buffer = bufnr,
diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua
index 9a879d9f38..cf39338cc1 100644
--- a/runtime/lua/vim/lsp/diagnostic.lua
+++ b/runtime/lua/vim/lsp/diagnostic.lua
@@ -5,7 +5,7 @@ local api = vim.api
local M = {}
-local augroup = api.nvim_create_augroup('vim_lsp_diagnostic', {})
+local augroup = api.nvim_create_augroup('nvim.lsp.diagnostic', {})
local DEFAULT_CLIENT_ID = -1
@@ -208,7 +208,7 @@ end
--- @param uri string
--- @param client_id? integer
---- @param diagnostics vim.Diagnostic[]
+--- @param diagnostics lsp.Diagnostic[]
--- @param is_pull boolean
local function handle_diagnostics(uri, client_id, diagnostics, is_pull)
local fname = vim.uri_to_fname(uri)
diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua
index 1945040bda..5da4033f89 100644
--- a/runtime/lua/vim/lsp/handlers.lua
+++ b/runtime/lua/vim/lsp/handlers.lua
@@ -382,7 +382,7 @@ end
--- @diagnostic disable-next-line: deprecated
RCS[ms.textDocument_hover] = M.hover
-local sig_help_ns = api.nvim_create_namespace('vim_lsp_signature_help')
+local sig_help_ns = api.nvim_create_namespace('nvim.lsp.signature_help')
--- @deprecated remove in 0.13
--- |lsp-handler| for the method "textDocument/signatureHelp".
@@ -582,9 +582,8 @@ NSC['window/showMessage'] = function(_, params, ctx)
if message_type == protocol.MessageType.Error then
err_message('LSP[', client_name, '] ', message)
else
- --- @type string
- local message_type_name = protocol.MessageType[message_type]
- api.nvim_out_write(string.format('LSP[%s][%s] %s\n', client_name, message_type_name, message))
+ message = ('LSP[%s][%s] %s\n'):format(client_name, protocol.MessageType[message_type], message)
+ api.nvim_echo({ { message } }, true, {})
end
return params
end
diff --git a/runtime/lua/vim/lsp/health.lua b/runtime/lua/vim/lsp/health.lua
index d2cf888d89..8af9f2f791 100644
--- a/runtime/lua/vim/lsp/health.lua
+++ b/runtime/lua/vim/lsp/health.lua
@@ -40,6 +40,8 @@ local function check_active_clients()
local clients = vim.lsp.get_clients()
if next(clients) then
for _, client in pairs(clients) do
+ local server_version = vim.tbl_get(client, 'server_info', 'version')
+ or '? (no serverInfo.version response)'
local cmd ---@type string
local ccmd = client.config.cmd
if type(ccmd) == 'table' then
@@ -62,6 +64,7 @@ local function check_active_clients()
end
report_info(table.concat({
string.format('%s (id: %d)', client.name, client.id),
+ string.format('- Version: %s', server_version),
dirs_info,
string.format('- Command: %s', cmd),
string.format('- Settings: %s', vim.inspect(client.settings, { newline = '\n ' })),
@@ -181,7 +184,7 @@ local function check_enabled_configs()
vim.health.start('vim.lsp: Enabled Configurations')
for name in vim.spairs(vim.lsp._enabled_configs) do
- local config = vim.lsp._resolve_config(name)
+ local config = vim.lsp.config[name]
local text = {} --- @type string[]
text[#text + 1] = ('%s:'):format(name)
for k, v in
diff --git a/runtime/lua/vim/lsp/inlay_hint.lua b/runtime/lua/vim/lsp/inlay_hint.lua
index 50cf9f5f29..37e1202d1d 100644
--- a/runtime/lua/vim/lsp/inlay_hint.lua
+++ b/runtime/lua/vim/lsp/inlay_hint.lua
@@ -29,8 +29,8 @@ local bufstates = vim.defaulttable(function(_)
})
end)
-local namespace = api.nvim_create_namespace('vim_lsp_inlayhint')
-local augroup = api.nvim_create_augroup('vim_lsp_inlayhint', {})
+local namespace = api.nvim_create_namespace('nvim.lsp.inlayhint')
+local augroup = api.nvim_create_augroup('nvim.lsp.inlayhint', {})
--- |lsp-handler| for the method `textDocument/inlayHint`
--- Store hints for a specific buffer and client
diff --git a/runtime/lua/vim/lsp/semantic_tokens.lua b/runtime/lua/vim/lsp/semantic_tokens.lua
index 7cc3f95aed..a31202553b 100644
--- a/runtime/lua/vim/lsp/semantic_tokens.lua
+++ b/runtime/lua/vim/lsp/semantic_tokens.lua
@@ -166,7 +166,7 @@ function STHighlighter.new(bufnr)
local self = setmetatable({}, { __index = STHighlighter })
self.bufnr = bufnr
- self.augroup = api.nvim_create_augroup('vim_lsp_semantic_tokens:' .. bufnr, { clear = true })
+ self.augroup = api.nvim_create_augroup('nvim.lsp.semantic_tokens:' .. bufnr, { clear = true })
self.client_state = {}
STHighlighter.active[bufnr] = self
@@ -225,7 +225,7 @@ function STHighlighter:attach(client_id)
local state = self.client_state[client_id]
if not state then
state = {
- namespace = api.nvim_create_namespace('vim_lsp_semantic_tokens:' .. client_id),
+ namespace = api.nvim_create_namespace('nvim.lsp.semantic_tokens:' .. client_id),
active_request = {},
current_result = {},
}
@@ -805,7 +805,7 @@ function M._refresh(err, _, ctx)
return vim.NIL
end
-local namespace = api.nvim_create_namespace('vim_lsp_semantic_tokens')
+local namespace = api.nvim_create_namespace('nvim.lsp.semantic_tokens')
api.nvim_set_decoration_provider(namespace, {
on_win = function(_, _, bufnr, topline, botline)
local highlighter = STHighlighter.active[bufnr]
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index 6bee5bc31f..4e0adf3366 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -875,15 +875,16 @@ function M.make_floating_popup_options(width, height, opts)
return {
anchor = anchor,
+ row = row + (opts.offset_y or 0),
col = col + (opts.offset_x or 0),
height = height,
focusable = opts.focusable,
- relative = opts.relative == 'mouse' and 'mouse' or 'cursor',
- row = row + (opts.offset_y or 0),
+ relative = (opts.relative == 'mouse' or opts.relative == 'editor') and opts.relative
+ or 'cursor',
style = 'minimal',
width = width,
border = opts.border or default_border,
- zindex = opts.zindex or 50,
+ zindex = opts.zindex or (api.nvim_win_get_config(0).zindex or 49) + 1,
title = title,
title_pos = title_pos,
}
@@ -1356,7 +1357,7 @@ end
---@param bufnrs table list of buffers where the preview window will remain visible
---@see autocmd-events
local function close_preview_autocmd(events, winnr, bufnrs)
- local augroup = api.nvim_create_augroup('preview_window_' .. winnr, {
+ local augroup = api.nvim_create_augroup('nvim.preview_window_' .. winnr, {
clear = true,
})
@@ -1431,7 +1432,7 @@ function M._make_floating_popup_size(contents, opts)
if vim.tbl_isempty(line_widths) then
for _, line in ipairs(contents) do
local line_width = vim.fn.strdisplaywidth(line:gsub('%z', '\n'))
- height = height + math.ceil(line_width / wrap_at)
+ height = height + math.max(1, math.ceil(line_width / wrap_at))
end
else
for i = 1, #contents do
@@ -1494,7 +1495,7 @@ end
--- @field title_pos? 'left'|'center'|'right'
---
--- (default: `'cursor'`)
---- @field relative? 'mouse'|'cursor'
+--- @field relative? 'mouse'|'cursor'|'editor'
---
--- - "auto": place window based on which side of the cursor has more lines
--- - "above": place the window above the cursor unless there are not enough lines
@@ -1618,7 +1619,7 @@ function M.open_floating_preview(contents, syntax, opts)
api.nvim_buf_set_var(bufnr, 'lsp_floating_preview', floating_winnr)
end
- local augroup_name = ('closing_floating_preview_%d'):format(floating_winnr)
+ local augroup_name = ('nvim.closing_floating_preview_%d'):format(floating_winnr)
local ok =
pcall(api.nvim_get_autocmds, { group = augroup_name, pattern = tostring(floating_winnr) })
if not ok then
@@ -1649,7 +1650,7 @@ function M.open_floating_preview(contents, syntax, opts)
end
do --[[ References ]]
- local reference_ns = api.nvim_create_namespace('vim_lsp_references')
+ local reference_ns = api.nvim_create_namespace('nvim.lsp.references')
--- Removes document highlights from a buffer.
---
diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua
index 24c3f243e5..02b12490af 100644
--- a/runtime/lua/vim/shared.lua
+++ b/runtime/lua/vim/shared.lua
@@ -959,7 +959,7 @@ do
--- function vim.startswith(s, prefix)
--- vim.validate('s', s, 'string')
--- vim.validate('prefix', prefix, 'string')
- --- ...
+ --- -- ...
--- end
--- ```
---
@@ -979,7 +979,7 @@ do
--- age={age, 'number'},
--- hobbies={hobbies, 'table'},
--- }
- --- ...
+ --- -- ...
--- end
--- ```
---
diff --git a/runtime/lua/vim/snippet.lua b/runtime/lua/vim/snippet.lua
index af7e3c6d33..bfd439181e 100644
--- a/runtime/lua/vim/snippet.lua
+++ b/runtime/lua/vim/snippet.lua
@@ -1,6 +1,6 @@
local G = vim.lsp._snippet_grammar
-local snippet_group = vim.api.nvim_create_augroup('vim/snippet', {})
-local snippet_ns = vim.api.nvim_create_namespace('vim/snippet')
+local snippet_group = vim.api.nvim_create_augroup('nvim.snippet', {})
+local snippet_ns = vim.api.nvim_create_namespace('nvim.snippet')
local hl_group = 'SnippetTabstop'
local jump_forward_key = '<tab>'
local jump_backward_key = '<s-tab>'
diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua
index 89dc4e289a..0269699dfd 100644
--- a/runtime/lua/vim/treesitter.lua
+++ b/runtime/lua/vim/treesitter.lua
@@ -61,8 +61,6 @@ function M._create_parser(bufnr, lang, opts)
{ on_bytes = bytes_cb, on_detach = detach_cb, on_reload = reload_cb, preview = true }
)
- self:parse()
-
return self
end
@@ -397,6 +395,8 @@ end
--- Note: By default, disables regex syntax highlighting, which may be required for some plugins.
--- In this case, add `vim.bo.syntax = 'on'` after the call to `start`.
---
+--- Note: By default, the highlighter parses code asynchronously, using a segment time of 3ms.
+---
--- Example:
---
--- ```lua
@@ -408,8 +408,8 @@ end
--- })
--- ```
---
----@param bufnr (integer|nil) Buffer to be highlighted (default: current buffer)
----@param lang (string|nil) Language of the parser (default: from buffer filetype)
+---@param bufnr integer? Buffer to be highlighted (default: current buffer)
+---@param lang string? Language of the parser (default: from buffer filetype)
function M.start(bufnr, lang)
bufnr = vim._resolve_bufnr(bufnr)
local parser = assert(M.get_parser(bufnr, lang, { error = false }))
diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua
index 0cb5b497c7..4a571bbaf7 100644
--- a/runtime/lua/vim/treesitter/_fold.lua
+++ b/runtime/lua/vim/treesitter/_fold.lua
@@ -19,14 +19,19 @@ local api = vim.api
---The range on which to evaluate foldexpr.
---When in insert mode, the evaluation is deferred to InsertLeave.
---@field foldupdate_range? Range2
+---
+---The treesitter parser associated with this buffer.
+---@field parser? vim.treesitter.LanguageTree
local FoldInfo = {}
FoldInfo.__index = FoldInfo
---@private
-function FoldInfo.new()
+---@param bufnr integer
+function FoldInfo.new(bufnr)
return setmetatable({
levels0 = {},
levels = {},
+ parser = ts.get_parser(bufnr, nil, { error = false }),
}, FoldInfo)
end
@@ -64,111 +69,122 @@ end
---@param info TS.FoldInfo
---@param srow integer?
---@param erow integer? 0-indexed, exclusive
----@param parse_injections? boolean
-local function compute_folds_levels(bufnr, info, srow, erow, parse_injections)
+---@param callback function?
+local function compute_folds_levels(bufnr, info, srow, erow, callback)
srow = srow or 0
erow = erow or api.nvim_buf_line_count(bufnr)
- local parser = assert(ts.get_parser(bufnr, nil, { error = false }))
-
- parser:parse(parse_injections and { srow, erow } or nil)
-
- local enter_counts = {} ---@type table<integer, integer>
- local leave_counts = {} ---@type table<integer, integer>
- local prev_start = -1
- local prev_stop = -1
+ local parser = info.parser
+ if not parser then
+ return
+ end
- parser:for_each_tree(function(tree, ltree)
- local query = ts.query.get(ltree:lang(), 'folds')
- if not query then
+ parser:parse(nil, function(_, trees)
+ if not trees then
return
end
- -- Collect folds starting from srow - 1, because we should first subtract the folds that end at
- -- srow - 1 from the level of srow - 1 to get accurate level of srow.
- for _, match, metadata in query:iter_matches(tree:root(), bufnr, math.max(srow - 1, 0), erow) do
- for id, nodes in pairs(match) do
- if query.captures[id] == 'fold' then
- local range = ts.get_range(nodes[1], bufnr, metadata[id])
- local start, _, stop, stop_col = Range.unpack4(range)
-
- if #nodes > 1 then
- -- assumes nodes are ordered by range
- local end_range = ts.get_range(nodes[#nodes], bufnr, metadata[id])
- local _, _, end_stop, end_stop_col = Range.unpack4(end_range)
- stop = end_stop
- stop_col = end_stop_col
- end
+ local enter_counts = {} ---@type table<integer, integer>
+ local leave_counts = {} ---@type table<integer, integer>
+ local prev_start = -1
+ local prev_stop = -1
- if stop_col == 0 then
- stop = stop - 1
- end
+ parser:for_each_tree(function(tree, ltree)
+ local query = ts.query.get(ltree:lang(), 'folds')
+ if not query then
+ return
+ end
- local fold_length = stop - start + 1
-
- -- Fold only multiline nodes that are not exactly the same as previously met folds
- -- Checking against just the previously found fold is sufficient if nodes
- -- are returned in preorder or postorder when traversing tree
- if
- fold_length > vim.wo.foldminlines and not (start == prev_start and stop == prev_stop)
- then
- enter_counts[start + 1] = (enter_counts[start + 1] or 0) + 1
- leave_counts[stop + 1] = (leave_counts[stop + 1] or 0) + 1
- prev_start = start
- prev_stop = stop
+ -- Collect folds starting from srow - 1, because we should first subtract the folds that end at
+ -- srow - 1 from the level of srow - 1 to get accurate level of srow.
+ for _, match, metadata in query:iter_matches(tree:root(), bufnr, math.max(srow - 1, 0), erow) do
+ for id, nodes in pairs(match) do
+ if query.captures[id] == 'fold' then
+ local range = ts.get_range(nodes[1], bufnr, metadata[id])
+ local start, _, stop, stop_col = Range.unpack4(range)
+
+ if #nodes > 1 then
+ -- assumes nodes are ordered by range
+ local end_range = ts.get_range(nodes[#nodes], bufnr, metadata[id])
+ local _, _, end_stop, end_stop_col = Range.unpack4(end_range)
+ stop = end_stop
+ stop_col = end_stop_col
+ end
+
+ if stop_col == 0 then
+ stop = stop - 1
+ end
+
+ local fold_length = stop - start + 1
+
+ -- Fold only multiline nodes that are not exactly the same as previously met folds
+ -- Checking against just the previously found fold is sufficient if nodes
+ -- are returned in preorder or postorder when traversing tree
+ if
+ fold_length > vim.wo.foldminlines and not (start == prev_start and stop == prev_stop)
+ then
+ enter_counts[start + 1] = (enter_counts[start + 1] or 0) + 1
+ leave_counts[stop + 1] = (leave_counts[stop + 1] or 0) + 1
+ prev_start = start
+ prev_stop = stop
+ end
end
end
end
- end
- end)
+ end)
- local nestmax = vim.wo.foldnestmax
- local level0_prev = info.levels0[srow] or 0
- local leave_prev = leave_counts[srow] or 0
-
- -- We now have the list of fold opening and closing, fill the gaps and mark where fold start
- for lnum = srow + 1, erow do
- local enter_line = enter_counts[lnum] or 0
- local leave_line = leave_counts[lnum] or 0
- local level0 = level0_prev - leave_prev + enter_line
-
- -- Determine if it's the start/end of a fold
- -- NB: vim's fold-expr interface does not have a mechanism to indicate that
- -- two (or more) folds start at this line, so it cannot distinguish between
- -- ( \n ( \n )) \n (( \n ) \n )
- -- versus
- -- ( \n ( \n ) \n ( \n ) \n )
- -- Both are represented by ['>1', '>2', '2', '>2', '2', '1'], and
- -- vim interprets as the second case.
- -- If it did have such a mechanism, (clamped - clamped_prev)
- -- would be the correct number of starts to pass on.
- local adjusted = level0 ---@type integer
- local prefix = ''
- if enter_line > 0 then
- prefix = '>'
- if leave_line > 0 then
- -- If this line ends a fold f1 and starts a fold f2, then move f1's end to the previous line
- -- so that f2 gets the correct level on this line. This may reduce the size of f1 below
- -- foldminlines, but we don't handle it for simplicity.
- adjusted = level0 - leave_line
- leave_line = 0
+ local nestmax = vim.wo.foldnestmax
+ local level0_prev = info.levels0[srow] or 0
+ local leave_prev = leave_counts[srow] or 0
+
+ -- We now have the list of fold opening and closing, fill the gaps and mark where fold start
+ for lnum = srow + 1, erow do
+ local enter_line = enter_counts[lnum] or 0
+ local leave_line = leave_counts[lnum] or 0
+ local level0 = level0_prev - leave_prev + enter_line
+
+ -- Determine if it's the start/end of a fold
+ -- NB: vim's fold-expr interface does not have a mechanism to indicate that
+ -- two (or more) folds start at this line, so it cannot distinguish between
+ -- ( \n ( \n )) \n (( \n ) \n )
+ -- versus
+ -- ( \n ( \n ) \n ( \n ) \n )
+ -- Both are represented by ['>1', '>2', '2', '>2', '2', '1'], and
+ -- vim interprets as the second case.
+ -- If it did have such a mechanism, (clamped - clamped_prev)
+ -- would be the correct number of starts to pass on.
+ local adjusted = level0 ---@type integer
+ local prefix = ''
+ if enter_line > 0 then
+ prefix = '>'
+ if leave_line > 0 then
+ -- If this line ends a fold f1 and starts a fold f2, then move f1's end to the previous line
+ -- so that f2 gets the correct level on this line. This may reduce the size of f1 below
+ -- foldminlines, but we don't handle it for simplicity.
+ adjusted = level0 - leave_line
+ leave_line = 0
+ end
end
- end
- -- Clamp at foldnestmax.
- local clamped = adjusted
- if adjusted > nestmax then
- prefix = ''
- clamped = nestmax
- end
+ -- Clamp at foldnestmax.
+ local clamped = adjusted
+ if adjusted > nestmax then
+ prefix = ''
+ clamped = nestmax
+ end
- -- Record the "real" level, so that it can be used as "base" of later compute_folds_levels().
- info.levels0[lnum] = adjusted
- info.levels[lnum] = prefix .. tostring(clamped)
+ -- Record the "real" level, so that it can be used as "base" of later compute_folds_levels().
+ info.levels0[lnum] = adjusted
+ info.levels[lnum] = prefix .. tostring(clamped)
- leave_prev = leave_line
- level0_prev = adjusted
- end
+ leave_prev = leave_line
+ level0_prev = adjusted
+ end
+
+ if callback then
+ callback()
+ end
+ end)
end
local M = {}
@@ -176,7 +192,7 @@ local M = {}
---@type table<integer,TS.FoldInfo>
local foldinfos = {}
-local group = api.nvim_create_augroup('treesitter/fold', {})
+local group = api.nvim_create_augroup('nvim.treesitter.fold', {})
--- Update the folds in the windows that contain the buffer and use expr foldmethod (assuming that
--- the user doesn't use different foldexpr for the same buffer).
@@ -259,6 +275,8 @@ local function on_changedtree(bufnr, foldinfo, tree_changes)
schedule_if_loaded(bufnr, function()
local srow_upd, erow_upd ---@type integer?, integer?
local max_erow = api.nvim_buf_line_count(bufnr)
+ -- TODO(ribru17): Replace this with a proper .all() awaiter once #19624 is resolved
+ local iterations = 0
for _, change in ipairs(tree_changes) do
local srow, _, erow, ecol = Range.unpack4(change)
-- If a parser doesn't have any ranges explicitly set, treesitter will
@@ -272,12 +290,14 @@ local function on_changedtree(bufnr, foldinfo, tree_changes)
end
-- Start from `srow - foldminlines`, because this edit may have shrunken the fold below limit.
srow = math.max(srow - vim.wo.foldminlines, 0)
- compute_folds_levels(bufnr, foldinfo, srow, erow)
srow_upd = srow_upd and math.min(srow_upd, srow) or srow
erow_upd = erow_upd and math.max(erow_upd, erow) or erow
- end
- if #tree_changes > 0 then
- foldinfo:foldupdate(bufnr, srow_upd, erow_upd)
+ compute_folds_levels(bufnr, foldinfo, srow, erow, function()
+ iterations = iterations + 1
+ if iterations == #tree_changes then
+ foldinfo:foldupdate(bufnr, srow_upd, erow_upd)
+ end
+ end)
end
end)
end
@@ -335,8 +355,9 @@ local function on_bytes(bufnr, foldinfo, start_row, start_col, old_row, old_col,
foldinfo.on_bytes_range = nil
-- Start from `srow - foldminlines`, because this edit may have shrunken the fold below limit.
srow = math.max(srow - vim.wo.foldminlines, 0)
- compute_folds_levels(bufnr, foldinfo, srow, erow)
- foldinfo:foldupdate(bufnr, srow, erow)
+ compute_folds_levels(bufnr, foldinfo, srow, erow, function()
+ foldinfo:foldupdate(bufnr, srow, erow)
+ end)
end)
end
end
@@ -347,13 +368,21 @@ function M.foldexpr(lnum)
lnum = lnum or vim.v.lnum
local bufnr = api.nvim_get_current_buf()
- local parser = ts.get_parser(bufnr, nil, { error = false })
- if not parser then
- return '0'
- end
-
if not foldinfos[bufnr] then
- foldinfos[bufnr] = FoldInfo.new()
+ foldinfos[bufnr] = FoldInfo.new(bufnr)
+ api.nvim_create_autocmd('BufUnload', {
+ buffer = bufnr,
+ once = true,
+ callback = function()
+ foldinfos[bufnr] = nil
+ end,
+ })
+
+ local parser = foldinfos[bufnr].parser
+ if not parser then
+ return '0'
+ end
+
compute_folds_levels(bufnr, foldinfos[bufnr])
parser:register_cbs({
@@ -378,10 +407,17 @@ api.nvim_create_autocmd('OptionSet', {
pattern = { 'foldminlines', 'foldnestmax' },
desc = 'Refresh treesitter folds',
callback = function()
- for bufnr, _ in pairs(foldinfos) do
- foldinfos[bufnr] = FoldInfo.new()
- compute_folds_levels(bufnr, foldinfos[bufnr])
- foldinfos[bufnr]:foldupdate(bufnr, 0, api.nvim_buf_line_count(bufnr))
+ local buf = api.nvim_get_current_buf()
+ local bufs = vim.v.option_type == 'global' and vim.tbl_keys(foldinfos)
+ or foldinfos[buf] and { buf }
+ or {}
+ for _, bufnr in ipairs(bufs) do
+ foldinfos[bufnr] = FoldInfo.new(bufnr)
+ api.nvim_buf_call(bufnr, function()
+ compute_folds_levels(bufnr, foldinfos[bufnr], nil, nil, function()
+ foldinfos[bufnr]:foldupdate(bufnr, 0, api.nvim_buf_line_count(bufnr))
+ end)
+ end)
end
end,
})
diff --git a/runtime/lua/vim/treesitter/_query_linter.lua b/runtime/lua/vim/treesitter/_query_linter.lua
index a825505378..f6645beb28 100644
--- a/runtime/lua/vim/treesitter/_query_linter.lua
+++ b/runtime/lua/vim/treesitter/_query_linter.lua
@@ -1,6 +1,6 @@
local api = vim.api
-local namespace = api.nvim_create_namespace('vim.treesitter.query_linter')
+local namespace = api.nvim_create_namespace('nvim.treesitter.query_linter')
local M = {}
diff --git a/runtime/lua/vim/treesitter/dev.lua b/runtime/lua/vim/treesitter/dev.lua
index 26817cdba5..42c25dbdad 100644
--- a/runtime/lua/vim/treesitter/dev.lua
+++ b/runtime/lua/vim/treesitter/dev.lua
@@ -119,7 +119,7 @@ function TSTreeView:new(bufnr, lang)
end
local t = {
- ns = api.nvim_create_namespace('treesitter/dev-inspect'),
+ ns = api.nvim_create_namespace('nvim.treesitter.dev_inspect'),
nodes = nodes,
named = named,
---@type vim.treesitter.dev.TSTreeViewOpts
@@ -135,7 +135,7 @@ function TSTreeView:new(bufnr, lang)
return t
end
-local decor_ns = api.nvim_create_namespace('ts.dev')
+local decor_ns = api.nvim_create_namespace('nvim.treesitter.dev')
---@param range Range4
---@return string
@@ -442,7 +442,7 @@ function M.inspect_tree(opts)
end,
})
- local group = api.nvim_create_augroup('treesitter/dev', {})
+ local group = api.nvim_create_augroup('nvim.treesitter.dev', {})
api.nvim_create_autocmd('CursorMoved', {
group = group,
@@ -547,7 +547,7 @@ function M.inspect_tree(opts)
})
end
-local edit_ns = api.nvim_create_namespace('treesitter/dev-edit')
+local edit_ns = api.nvim_create_namespace('nvim.treesitter.dev_edit')
---@param query_win integer
---@param base_win integer
@@ -633,7 +633,7 @@ function M.edit_query(lang)
-- can infer the language later.
api.nvim_buf_set_name(query_buf, string.format('%s/query_editor.scm', lang))
- local group = api.nvim_create_augroup('treesitter/dev-edit', {})
+ local group = api.nvim_create_augroup('nvim.treesitter.dev_edit', {})
api.nvim_create_autocmd({ 'TextChanged', 'InsertLeave' }, {
group = group,
buffer = query_buf,
diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua
index 8ce8652f7d..c11fa1999d 100644
--- a/runtime/lua/vim/treesitter/highlighter.lua
+++ b/runtime/lua/vim/treesitter/highlighter.lua
@@ -2,7 +2,7 @@ local api = vim.api
local query = vim.treesitter.query
local Range = require('vim.treesitter._range')
-local ns = api.nvim_create_namespace('treesitter/highlighter')
+local ns = api.nvim_create_namespace('nvim.treesitter.highlighter')
---@alias vim.treesitter.highlighter.Iter fun(end_line: integer|nil): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch
@@ -69,6 +69,7 @@ end
---@field private _queries table<string,vim.treesitter.highlighter.Query>
---@field tree vim.treesitter.LanguageTree
---@field private redraw_count integer
+---@field parsing boolean true if we are parsing asynchronously
local TSHighlighter = {
active = {},
}
@@ -147,8 +148,6 @@ function TSHighlighter.new(tree, opts)
vim.opt_local.spelloptions:append('noplainbuffer')
end)
- self.tree:parse()
-
return self
end
@@ -299,6 +298,8 @@ local function on_line_impl(self, buf, line, is_spell_nav)
state.highlighter_query:query():iter_captures(root_node, self.bufnr, line, root_end_row + 1)
end
+ local captures = state.highlighter_query:query().captures
+
while line >= state.next_row do
local capture, node, metadata, match = state.iter(line)
@@ -311,7 +312,7 @@ local function on_line_impl(self, buf, line, is_spell_nav)
if capture then
local hl = state.highlighter_query:get_hl_from_capture(capture)
- local capture_name = state.highlighter_query:query().captures[capture]
+ local capture_name = captures[capture]
local spell, spell_pri_offset = get_spell(capture_name)
@@ -382,19 +383,23 @@ function TSHighlighter._on_spell_nav(_, _, buf, srow, _, erow, _)
end
---@private
----@param _win integer
---@param buf integer
---@param topline integer
---@param botline integer
-function TSHighlighter._on_win(_, _win, buf, topline, botline)
+function TSHighlighter._on_win(_, _, buf, topline, botline)
local self = TSHighlighter.active[buf]
- if not self then
+ if not self or self.parsing then
return false
end
- self.tree:parse({ topline, botline + 1 })
- self:prepare_highlight_states(topline, botline + 1)
+ self.parsing = self.tree:parse({ topline, botline + 1 }, function(_, trees)
+ if trees and self.parsing then
+ self.parsing = false
+ api.nvim__redraw({ buf = buf, valid = false, flush = false })
+ end
+ end) == nil
self.redraw_count = self.redraw_count + 1
- return true
+ self:prepare_highlight_states(topline, botline)
+ return #self._highlight_states > 0
end
api.nvim_set_decoration_provider(ns, {
diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua
index 446051dfd7..238a078703 100644
--- a/runtime/lua/vim/treesitter/language.lua
+++ b/runtime/lua/vim/treesitter/language.lua
@@ -133,8 +133,9 @@ function M.add(lang, opts)
path = paths[1]
end
- return loadparser(path, lang, symbol_name) or nil,
- string.format('Cannot load parser %s for language "%s"', path, lang)
+ local res = loadparser(path, lang, symbol_name)
+ return res,
+ res == nil and string.format('Cannot load parser %s for language "%s"', path, lang) or nil
end
--- @param x string|string[]
diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua
index 4b42164dc8..35a77f1afc 100644
--- a/runtime/lua/vim/treesitter/languagetree.lua
+++ b/runtime/lua/vim/treesitter/languagetree.lua
@@ -44,6 +44,8 @@ local query = require('vim.treesitter.query')
local language = require('vim.treesitter.language')
local Range = require('vim.treesitter._range')
+local default_parse_timeout_ms = 3
+
---@alias TSCallbackName
---| 'changedtree'
---| 'bytes'
@@ -76,6 +78,10 @@ local TSCallbackNames = {
---@field private _injections_processed boolean
---@field private _opts table Options
---@field private _parser TSParser Parser for language
+---Table of regions for which the tree is currently running an async parse
+---@field private _ranges_being_parsed table<string, boolean>
+---Table of callback queues, keyed by each region for which the callbacks should be run
+---@field private _cb_queues table<string, fun(err?: string, trees?: table<integer, TSTree>)[]>
---@field private _has_regions boolean
---@field private _regions table<integer, Range6[]>?
---List of regions this tree should manage and parse. If nil then regions are
@@ -130,6 +136,8 @@ function LanguageTree.new(source, lang, opts)
_injections_processed = false,
_valid = false,
_parser = vim._create_ts_parser(lang),
+ _ranges_being_parsed = {},
+ _cb_queues = {},
_callbacks = {},
_callbacks_rec = {},
}
@@ -232,6 +240,7 @@ end
---@param reload boolean|nil
function LanguageTree:invalidate(reload)
self._valid = false
+ self._parser:reset()
-- buffer was reloaded, reparse all trees
if reload then
@@ -334,10 +343,12 @@ end
--- @private
--- @param range boolean|Range?
+--- @param timeout integer?
--- @return Range6[] changes
--- @return integer no_regions_parsed
--- @return number total_parse_time
-function LanguageTree:_parse_regions(range)
+--- @return boolean finished whether async parsing still needs time
+function LanguageTree:_parse_regions(range, timeout)
local changes = {}
local no_regions_parsed = 0
local total_parse_time = 0
@@ -357,9 +368,14 @@ function LanguageTree:_parse_regions(range)
)
then
self._parser:set_included_ranges(ranges)
+ self._parser:set_timeout(timeout and timeout * 1000 or 0) -- ms -> micros
local parse_time, tree, tree_changes =
tcall(self._parser.parse, self._parser, self._trees[i], self._source, true)
+ if not tree then
+ return changes, no_regions_parsed, total_parse_time, false
+ end
+
-- Pass ranges if this is an initial parse
local cb_changes = self._trees[i] and tree_changes or tree:included_ranges(true)
@@ -373,7 +389,7 @@ function LanguageTree:_parse_regions(range)
end
end
- return changes, no_regions_parsed, total_parse_time
+ return changes, no_regions_parsed, total_parse_time, true
end
--- @private
@@ -409,6 +425,82 @@ function LanguageTree:_add_injections()
return query_time
end
+--- @param range boolean|Range?
+--- @return string
+local function range_to_string(range)
+ return type(range) == 'table' and table.concat(range, ',') or tostring(range)
+end
+
+--- @private
+--- @param range boolean|Range?
+--- @param callback fun(err?: string, trees?: table<integer, TSTree>)
+function LanguageTree:_push_async_callback(range, callback)
+ local key = range_to_string(range)
+ self._cb_queues[key] = self._cb_queues[key] or {}
+ local queue = self._cb_queues[key]
+ queue[#queue + 1] = callback
+end
+
+--- @private
+--- @param range boolean|Range?
+--- @param err? string
+--- @param trees? table<integer, TSTree>
+function LanguageTree:_run_async_callbacks(range, err, trees)
+ local key = range_to_string(range)
+ for _, cb in ipairs(self._cb_queues[key]) do
+ cb(err, trees)
+ end
+ self._ranges_being_parsed[key] = nil
+ self._cb_queues[key] = nil
+end
+
+--- Run an asynchronous parse, calling {on_parse} when complete.
+---
+--- @private
+--- @param range boolean|Range?
+--- @param on_parse fun(err?: string, trees?: table<integer, TSTree>)
+--- @return table<integer, TSTree>? trees the list of parsed trees, if parsing completed synchronously
+function LanguageTree:_async_parse(range, on_parse)
+ self:_push_async_callback(range, on_parse)
+
+ -- If we are already running an async parse, just queue the callback.
+ local range_string = range_to_string(range)
+ if not self._ranges_being_parsed[range_string] then
+ self._ranges_being_parsed[range_string] = true
+ else
+ return
+ end
+
+ local buf = vim.b[self._source]
+ local ct = buf.changedtick
+ local total_parse_time = 0
+ local redrawtime = vim.o.redrawtime
+ local timeout = not vim.g._ts_force_sync_parsing and default_parse_timeout_ms or nil
+
+ local function step()
+ -- If buffer was changed in the middle of parsing, reset parse state
+ if buf.changedtick ~= ct then
+ ct = buf.changedtick
+ total_parse_time = 0
+ end
+
+ local parse_time, trees, finished = tcall(self._parse, self, range, timeout)
+ total_parse_time = total_parse_time + parse_time
+
+ if finished then
+ self:_run_async_callbacks(range, nil, trees)
+ return trees
+ elseif total_parse_time > redrawtime then
+ self:_run_async_callbacks(range, 'TIMEOUT', nil)
+ return nil
+ else
+ vim.schedule(step)
+ end
+ end
+
+ return step()
+end
+
--- Recursively parse all regions in the language tree using |treesitter-parsers|
--- for the corresponding languages and run injection queries on the parsed trees
--- to determine whether child trees should be created and parsed.
@@ -420,11 +512,33 @@ end
--- Set to `true` to run a complete parse of the source (Note: Can be slow!)
--- Set to `false|nil` to only parse regions with empty ranges (typically
--- only the root tree without injections).
---- @return table<integer, TSTree>
-function LanguageTree:parse(range)
+--- @param on_parse fun(err?: string, trees?: table<integer, TSTree>)? Function invoked when parsing completes.
+--- When provided and `vim.g._ts_force_sync_parsing` is not set, parsing will run
+--- asynchronously. The first argument to the function is a string respresenting the error type,
+--- in case of a failure (currently only possible for timeouts). The second argument is the list
+--- of trees returned by the parse (upon success), or `nil` if the parse timed out (determined
+--- by 'redrawtime').
+---
+--- If parsing was still able to finish synchronously (within 3ms), `parse()` returns the list
+--- of trees. Otherwise, it returns `nil`.
+--- @return table<integer, TSTree>?
+function LanguageTree:parse(range, on_parse)
+ if on_parse then
+ return self:_async_parse(range, on_parse)
+ end
+ local trees, _ = self:_parse(range)
+ return trees
+end
+
+--- @private
+--- @param range boolean|Range|nil
+--- @param timeout integer?
+--- @return table<integer, TSTree> trees
+--- @return boolean finished
+function LanguageTree:_parse(range, timeout)
if self:is_valid() then
self:_log('valid')
- return self._trees
+ return self._trees, true
end
local changes --- @type Range6[]?
@@ -433,17 +547,22 @@ function LanguageTree:parse(range)
local no_regions_parsed = 0
local query_time = 0
local total_parse_time = 0
+ local is_finished --- @type boolean
-- At least 1 region is invalid
if not self:is_valid(true) then
- changes, no_regions_parsed, total_parse_time = self:_parse_regions(range)
+ changes, no_regions_parsed, total_parse_time, is_finished = self:_parse_regions(range, timeout)
+ timeout = timeout and math.max(timeout - total_parse_time, 0)
+ if not is_finished then
+ return self._trees, is_finished
+ end
-- Need to run injections when we parsed something
if no_regions_parsed > 0 then
self._injections_processed = false
end
end
- if not self._injections_processed and range ~= false and range ~= nil then
+ if not self._injections_processed and range then
query_time = self:_add_injections()
self._injections_processed = true
end
@@ -457,10 +576,17 @@ function LanguageTree:parse(range)
})
for _, child in pairs(self._children) do
- child:parse(range)
+ if timeout == 0 then
+ return self._trees, false
+ end
+ local ctime, _, child_finished = tcall(child._parse, child, range, timeout)
+ timeout = timeout and math.max(timeout - ctime, 0)
+ if not child_finished then
+ return self._trees, child_finished
+ end
end
- return self._trees
+ return self._trees, true
end
--- Invokes the callback for each |LanguageTree| recursively.
@@ -907,6 +1033,7 @@ function LanguageTree:_edit(
)
end
+ self._parser:reset()
self._regions = nil
local changed_range = {
diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua
index dbe3d54c2f..e43d0a8ad4 100644
--- a/runtime/lua/vim/treesitter/query.lua
+++ b/runtime/lua/vim/treesitter/query.lua
@@ -1,17 +1,74 @@
+--- @brief This Lua |treesitter-query| interface allows you to create queries and use them to parse
+--- text. See |vim.treesitter.query.parse()| for a working example.
+
local api = vim.api
local language = require('vim.treesitter.language')
local memoize = vim.func._memoize
local M = {}
+local function is_directive(name)
+ return string.sub(name, -1) == '!'
+end
+
+---@nodoc
+---@class vim.treesitter.query.ProcessedPredicate
+---@field [1] string predicate name
+---@field [2] boolean should match
+---@field [3] (integer|string)[] the original predicate
+
+---@alias vim.treesitter.query.ProcessedDirective (integer|string)[]
+
+---@nodoc
+---@class vim.treesitter.query.ProcessedPattern {
+---@field predicates vim.treesitter.query.ProcessedPredicate[]
+---@field directives vim.treesitter.query.ProcessedDirective[]
+
+--- Splits the query patterns into predicates and directives.
+---@param patterns table<integer, (integer|string)[][]>
+---@return table<integer, vim.treesitter.query.ProcessedPattern>
+local function process_patterns(patterns)
+ ---@type table<integer, vim.treesitter.query.ProcessedPattern>
+ local processed_patterns = {}
+
+ for k, pattern_list in pairs(patterns) do
+ ---@type vim.treesitter.query.ProcessedPredicate[]
+ local predicates = {}
+ ---@type vim.treesitter.query.ProcessedDirective[]
+ local directives = {}
+
+ for _, pattern in ipairs(pattern_list) do
+ -- Note: tree-sitter strips the leading # from predicates for us.
+ local pred_name = pattern[1]
+ ---@cast pred_name string
+
+ if is_directive(pred_name) then
+ table.insert(directives, pattern)
+ else
+ local should_match = true
+ if pred_name:match('^not%-') then
+ pred_name = pred_name:sub(5)
+ should_match = false
+ end
+ table.insert(predicates, { pred_name, should_match, pattern })
+ end
+ end
+
+ processed_patterns[k] = { predicates = predicates, directives = directives }
+ end
+
+ return processed_patterns
+end
+
---@nodoc
---Parsed query, see |vim.treesitter.query.parse()|
---
---@class vim.treesitter.Query
----@field lang string name of the language for this parser
+---@field lang string parser language name
---@field captures string[] list of (unique) capture names defined in query
----@field info vim.treesitter.QueryInfo contains information used in the query (e.g. captures, predicates, directives)
+---@field info vim.treesitter.QueryInfo query context (e.g. captures, predicates, directives)
---@field query TSQuery userdata query object
+---@field private _processed_patterns table<integer, vim.treesitter.query.ProcessedPattern>
local Query = {}
Query.__index = Query
@@ -30,6 +87,7 @@ function Query.new(lang, ts_query)
patterns = query_info.patterns,
}
self.captures = self.info.captures
+ self._processed_patterns = process_patterns(self.info.patterns)
return self
end
@@ -204,6 +262,7 @@ local explicit_queries = setmetatable({}, {
---@param query_name string Name of the query (e.g., "highlights")
---@param text string Query text (unparsed).
function M.set(lang, query_name, text)
+ M.get:clear(lang, query_name)
explicit_queries[lang][query_name] = M.parse(lang, text)
end
@@ -226,22 +285,38 @@ M.get = memoize('concat-2', function(lang, query_name)
end
return M.parse(lang, query_string)
-end)
+end, false)
---- Parse {query} as a string. (If the query is in a file, the caller
---- should read the contents into a string before calling).
----
---- Returns a `Query` (see |lua-treesitter-query|) object which can be used to
---- search nodes in the syntax tree for the patterns defined in {query}
---- using the `iter_captures` and `iter_matches` methods.
+api.nvim_create_autocmd('OptionSet', {
+ pattern = { 'runtimepath' },
+ group = api.nvim_create_augroup('nvim.treesitter.query_cache_reset', { clear = true }),
+ callback = function()
+ M.get:clear()
+ end,
+})
+
+--- Parses a {query} string and returns a `Query` object (|lua-treesitter-query|), which can be used
+--- to search the tree for the query patterns (via |Query:iter_captures()|, |Query:iter_matches()|),
+--- or inspect the query via these fields:
+--- - `captures`: a list of unique capture names defined in the query (alias: `info.captures`).
+--- - `info.patterns`: information about predicates.
---
---- Exposes `info` and `captures` with additional context about {query}.
---- - `captures` contains the list of unique capture names defined in {query}.
---- - `info.captures` also points to `captures`.
---- - `info.patterns` contains information about predicates.
+--- Example (to try it, use `g==` or select the code then run `:'<,'>lua`):
+--- ```lua
+--- local query = vim.treesitter.query.parse('vimdoc', [[
+--- ; query
+--- ((h1) @str
+--- (#trim! @str 1 1 1 1))
+--- ]])
+--- local tree = vim.treesitter.get_parser():parse()[1]
+--- for id, node, metadata in query:iter_captures(tree:root(), 0) do
+--- -- Print the node name and source text.
+--- vim.print({node:type(), vim.treesitter.get_node_text(node, vim.api.nvim_get_current_buf())})
+--- end
+--- ```
---
---@param lang string Language to use for the query
----@param query string Query in s-expr syntax
+---@param query string Query text, in s-expr syntax
---
---@return vim.treesitter.Query : Parsed query
---
@@ -250,7 +325,7 @@ M.parse = memoize('concat-2', function(lang, query)
assert(language.add(lang))
local ts_query = vim._ts_parse_query(lang, query)
return Query.new(lang, ts_query)
-end)
+end, false)
--- Implementations of predicates that can optionally be prefixed with "any-".
---
@@ -740,84 +815,50 @@ function M.list_predicates()
return vim.tbl_keys(predicate_handlers)
end
-local function xor(x, y)
- return (x or y) and not (x and y)
-end
-
-local function is_directive(name)
- return string.sub(name, -1) == '!'
-end
-
---@private
----@param match TSQueryMatch
+---@param pattern_i integer
+---@param predicates vim.treesitter.query.ProcessedPredicate[]
+---@param captures table<integer, TSNode[]>
---@param source integer|string
-function Query:match_preds(match, source)
- local _, pattern = match:info()
- local preds = self.info.patterns[pattern]
-
- if not preds then
- return true
- end
-
- local captures = match:captures()
-
- for _, pred in pairs(preds) do
- -- Here we only want to return if a predicate DOES NOT match, and
- -- continue on the other case. This way unknown predicates will not be considered,
- -- which allows some testing and easier user extensibility (#12173).
- -- Also, tree-sitter strips the leading # from predicates for us.
- local is_not = false
-
- -- Skip over directives... they will get processed after all the predicates.
- if not is_directive(pred[1]) then
- local pred_name = pred[1]
- if pred_name:match('^not%-') then
- pred_name = pred_name:sub(5)
- is_not = true
- end
-
- local handler = predicate_handlers[pred_name]
-
- if not handler then
- error(string.format('No handler for %s', pred[1]))
- return false
- end
-
- local pred_matches = handler(captures, pattern, source, pred)
+---@return boolean whether the predicates match
+function Query:_match_predicates(predicates, pattern_i, captures, source)
+ for _, predicate in ipairs(predicates) do
+ local processed_name = predicate[1]
+ local should_match = predicate[2]
+ local orig_predicate = predicate[3]
+
+ local handler = predicate_handlers[processed_name]
+ if not handler then
+ error(string.format('No handler for %s', orig_predicate[1]))
+ return false
+ end
- if not xor(is_not, pred_matches) then
- return false
- end
+ local does_match = handler(captures, pattern_i, source, orig_predicate)
+ if does_match ~= should_match then
+ return false
end
end
return true
end
---@private
----@param match TSQueryMatch
+---@param pattern_i integer
+---@param directives vim.treesitter.query.ProcessedDirective[]
+---@param source integer|string
+---@param captures table<integer, TSNode[]>
---@return vim.treesitter.query.TSMetadata metadata
-function Query:apply_directives(match, source)
+function Query:_apply_directives(directives, pattern_i, captures, source)
---@type vim.treesitter.query.TSMetadata
local metadata = {}
- local _, pattern = match:info()
- local preds = self.info.patterns[pattern]
-
- if not preds then
- return metadata
- end
- local captures = match:captures()
+ for _, directive in pairs(directives) do
+ local handler = directive_handlers[directive[1]]
- for _, pred in pairs(preds) do
- if is_directive(pred[1]) then
- local handler = directive_handlers[pred[1]]
-
- if not handler then
- error(string.format('No handler for %s', pred[1]))
- end
-
- handler(captures, pattern, source, pred, metadata)
+ if not handler then
+ error(string.format('No handler for %s', directive[1]))
end
+
+ handler(captures, pattern_i, source, directive, metadata)
end
return metadata
@@ -841,26 +882,22 @@ local function value_or_node_range(start, stop, node)
return start, stop
end
---- @param match TSQueryMatch
---- @return integer
-local function match_id_hash(_, match)
- return (match:info())
-end
-
---- Iterate over all captures from all matches inside {node}
+--- Iterates over all captures from all matches in {node}.
---
---- {source} is needed if the query contains predicates; then the caller
+--- {source} is required if the query contains predicates; then the caller
--- must ensure to use a freshly parsed tree consistent with the current
--- text of the buffer (if relevant). {start} and {stop} can be used to limit
--- matches inside a row range (this is typically used with root node
--- as the {node}, i.e., to get syntax highlight matches in the current
--- viewport). When omitted, the {start} and {stop} row values are used from the given node.
---
---- The iterator returns four values: a numeric id identifying the capture,
---- the captured node, metadata from any directives processing the match,
---- and the match itself.
---- The following example shows how to get captures by name:
+--- The iterator returns four values:
+--- 1. the numeric id identifying the capture
+--- 2. the captured node
+--- 3. metadata from any directives processing the match
+--- 4. the match itself
---
+--- Example: how to get captures by name:
--- ```lua
--- for id, node, metadata, match in query:iter_captures(tree:root(), bufnr, first, last) do
--- local name = query.captures[id] -- name of the capture in the query
@@ -876,8 +913,8 @@ end
---@param start? integer Starting line for the search. Defaults to `node:start()`.
---@param stop? integer Stopping line for the search (end-exclusive). Defaults to `node:end_()`.
---
----@return (fun(end_line: integer|nil): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch):
---- capture id, capture node, metadata, match
+---@return (fun(end_line: integer|nil): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch, TSTree):
+--- capture id, capture node, metadata, match, tree
---
---@note Captures are only returned if the query pattern of a specific capture contained predicates.
function Query:iter_captures(node, source, start, stop)
@@ -887,10 +924,14 @@ function Query:iter_captures(node, source, start, stop)
start, stop = value_or_node_range(start, stop, node)
+ -- Copy the tree to ensure it is valid during the entire lifetime of the iterator
+ local tree = node:tree():copy()
local cursor = vim._create_ts_querycursor(node, self.query, start, stop, { match_limit = 256 })
- local apply_directives = memoize(match_id_hash, self.apply_directives, true)
- local match_preds = memoize(match_id_hash, self.match_preds, true)
+ -- For faster checks that a match is not in the cache.
+ local highest_cached_match_id = -1
+ ---@type table<integer, vim.treesitter.query.TSMetadata>
+ local match_cache = {}
local function iter(end_line)
local capture, captured_node, match = cursor:next_capture()
@@ -899,18 +940,39 @@ function Query:iter_captures(node, source, start, stop)
return
end
- if not match_preds(self, match, source) then
- local match_id = match:info()
- cursor:remove_match(match_id)
- if end_line and captured_node:range() > end_line then
- return nil, captured_node, nil, nil
- end
- return iter(end_line) -- tail call: try next match
+ local match_id, pattern_i = match:info()
+
+ --- @type vim.treesitter.query.TSMetadata
+ local metadata
+ if match_id <= highest_cached_match_id then
+ metadata = match_cache[match_id]
end
- local metadata = apply_directives(self, match, source)
+ if not metadata then
+ metadata = {}
+
+ local processed_pattern = self._processed_patterns[pattern_i]
+ if processed_pattern then
+ local captures = match:captures()
- return capture, captured_node, metadata, match
+ local predicates = processed_pattern.predicates
+ if not self:_match_predicates(predicates, pattern_i, captures, source) then
+ cursor:remove_match(match_id)
+ if end_line and captured_node:range() > end_line then
+ return nil, captured_node, nil, nil
+ end
+ return iter(end_line) -- tail call: try next match
+ end
+
+ local directives = processed_pattern.directives
+ metadata = self:_apply_directives(directives, pattern_i, captures, source)
+ end
+
+ highest_cached_match_id = math.max(highest_cached_match_id, match_id)
+ match_cache[match_id] = metadata
+ end
+
+ return capture, captured_node, metadata, match, tree
end
return iter
end
@@ -932,7 +994,7 @@ end
--- -- `node` was captured by the `name` capture in the match
---
--- local node_data = metadata[id] -- Node level metadata
---- ... use the info here ...
+--- -- ... use the info here ...
--- end
--- end
--- end
@@ -951,7 +1013,7 @@ end
--- (last) node instead of the full list of matching nodes. This option is only for backward
--- compatibility and will be removed in a future release.
---
----@return (fun(): integer, table<integer, TSNode[]>, vim.treesitter.query.TSMetadata): pattern id, match, metadata
+---@return (fun(): integer, table<integer, TSNode[]>, vim.treesitter.query.TSMetadata, TSTree): pattern id, match, metadata, tree
function Query:iter_matches(node, source, start, stop, opts)
opts = opts or {}
opts.match_limit = opts.match_limit or 256
@@ -962,6 +1024,8 @@ function Query:iter_matches(node, source, start, stop, opts)
start, stop = value_or_node_range(start, stop, node)
+ -- Copy the tree to ensure it is valid during the entire lifetime of the iterator
+ local tree = node:tree():copy()
local cursor = vim._create_ts_querycursor(node, self.query, start, stop, opts)
local function iter()
@@ -971,17 +1035,22 @@ function Query:iter_matches(node, source, start, stop, opts)
return
end
- local match_id, pattern = match:info()
+ local match_id, pattern_i = match:info()
+ local processed_pattern = self._processed_patterns[pattern_i]
+ local captures = match:captures()
- if not self:match_preds(match, source) then
- cursor:remove_match(match_id)
- return iter() -- tail call: try next match
+ --- @type vim.treesitter.query.TSMetadata
+ local metadata = {}
+ if processed_pattern then
+ local predicates = processed_pattern.predicates
+ if not self:_match_predicates(predicates, pattern_i, captures, source) then
+ cursor:remove_match(match_id)
+ return iter() -- tail call: try next match
+ end
+ local directives = processed_pattern.directives
+ metadata = self:_apply_directives(directives, pattern_i, captures, source)
end
- local metadata = self:apply_directives(match, source)
-
- local captures = match:captures()
-
if opts.all == false then
-- Convert the match table into the old buggy version for backward
-- compatibility. This is slow, but we only do it when the caller explicitly opted into it by
@@ -990,11 +1059,11 @@ function Query:iter_matches(node, source, start, stop, opts)
for k, v in pairs(captures or {}) do
old_match[k] = v[#v]
end
- return pattern, old_match, metadata
+ return pattern_i, old_match, metadata
end
-- TODO(lewis6991): create a new function that returns {match, metadata}
- return pattern, captures, metadata
+ return pattern_i, captures, metadata, tree
end
return iter
end
diff --git a/runtime/lua/vim/version.lua b/runtime/lua/vim/version.lua
index d64ef98d2d..06c54ac033 100644
--- a/runtime/lua/vim/version.lua
+++ b/runtime/lua/vim/version.lua
@@ -227,8 +227,7 @@ end
---@field to? vim.Version
local VersionRange = {}
---- @private
----
+---@nodoc
---@param version string|vim.Version
function VersionRange:has(version)
if type(version) == 'string' then
diff --git a/runtime/lua/vim/vimhelp.lua b/runtime/lua/vim/vimhelp.lua
index 5579cc0174..a494d311b1 100644
--- a/runtime/lua/vim/vimhelp.lua
+++ b/runtime/lua/vim/vimhelp.lua
@@ -7,7 +7,7 @@ local M = {}
--- Note: {patterns} is assumed to be sorted by occurrence in the file.
--- @param patterns {start:string,stop:string,match:string}[]
function M.highlight_groups(patterns)
- local ns = vim.api.nvim_create_namespace('vimhelp')
+ local ns = vim.api.nvim_create_namespace('nvim.vimhelp')
vim.api.nvim_buf_clear_namespace(0, ns, 0, -1)
local save_cursor = vim.fn.getcurpos()