aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua')
-rw-r--r--runtime/lua/_vim9script.lua650
-rw-r--r--runtime/lua/editorconfig.lua246
-rw-r--r--runtime/lua/man.lua35
-rw-r--r--runtime/lua/man/health.lua20
-rw-r--r--runtime/lua/nvim/health.lua406
-rw-r--r--runtime/lua/vim/_editor.lua21
-rw-r--r--runtime/lua/vim/_init_packages.lua3
-rw-r--r--runtime/lua/vim/_inspector.lua238
-rw-r--r--runtime/lua/vim/_meta.lua2
-rw-r--r--runtime/lua/vim/diagnostic.lua319
-rw-r--r--runtime/lua/vim/filetype.lua69
-rw-r--r--runtime/lua/vim/filetype/detect.lua66
-rw-r--r--runtime/lua/vim/fs.lua127
-rw-r--r--runtime/lua/vim/health.lua15
-rw-r--r--runtime/lua/vim/highlight.lua1
-rw-r--r--runtime/lua/vim/keymap.lua8
-rw-r--r--runtime/lua/vim/lsp.lua385
-rw-r--r--runtime/lua/vim/lsp/buf.lua50
-rw-r--r--runtime/lua/vim/lsp/codelens.lua27
-rw-r--r--runtime/lua/vim/lsp/diagnostic.lua2
-rw-r--r--runtime/lua/vim/lsp/handlers.lua106
-rw-r--r--runtime/lua/vim/lsp/health.lua16
-rw-r--r--runtime/lua/vim/lsp/log.lua17
-rw-r--r--runtime/lua/vim/lsp/protocol.lua95
-rw-r--r--runtime/lua/vim/lsp/rpc.lua74
-rw-r--r--runtime/lua/vim/lsp/semantic_tokens.lua702
-rw-r--r--runtime/lua/vim/lsp/sync.lua2
-rw-r--r--runtime/lua/vim/lsp/util.lua37
-rw-r--r--runtime/lua/vim/secure.lua188
-rw-r--r--runtime/lua/vim/shared.lua97
-rw-r--r--runtime/lua/vim/treesitter.lua210
-rw-r--r--runtime/lua/vim/treesitter/health.lua19
-rw-r--r--runtime/lua/vim/treesitter/highlighter.lua30
-rw-r--r--runtime/lua/vim/treesitter/languagetree.lua4
-rw-r--r--runtime/lua/vim/treesitter/playground.lua186
-rw-r--r--runtime/lua/vim/treesitter/query.lua14
-rw-r--r--runtime/lua/vim/ui.lua21
37 files changed, 3907 insertions, 601 deletions
diff --git a/runtime/lua/_vim9script.lua b/runtime/lua/_vim9script.lua
new file mode 100644
index 0000000000..363d061451
--- /dev/null
+++ b/runtime/lua/_vim9script.lua
@@ -0,0 +1,650 @@
+-------------------------------------------------------------------------------
+-- This file is auto generated by vim9jit. Do not edit by hand.
+-- All content is in the source repository.
+-- Bugs should be reported to: github.com/tjdevries/vim9jit
+--
+-- In addition, this file is considered "private" by neovim. You should
+-- not expect any of the APIs, functions, etc to be stable. They are subject
+-- to change at any time.
+-------------------------------------------------------------------------------
+
+local vim9 = (function()
+ local M = {}
+
+ M.ternary = function(cond, if_true, if_false)
+ if cond then
+ if type(if_true) == 'function' then
+ return if_true()
+ else
+ return if_true
+ end
+ else
+ if type(if_false) == 'function' then
+ return if_false()
+ else
+ return if_false
+ end
+ end
+ end
+
+ M.fn_ref = function(module, name, copied, ...)
+ for _, val in ipairs({ ... }) do
+ table.insert(copied, val)
+ end
+
+ local funcref = name
+ if type(funcref) == 'function' then
+ return funcref(unpack(copied))
+ elseif type(funcref) == 'string' then
+ if vim.fn.exists('*' .. funcref) == 1 then
+ return vim.fn[funcref](unpack(copied))
+ end
+
+ if module[funcref] then
+ module[funcref](unpack(copied))
+ end
+
+ error('unknown function: ' .. funcref)
+ else
+ error(string.format('unable to call funcref: %s', funcref))
+ end
+ end
+
+ M.fn_mut = function(name, args, info)
+ local result = vim.fn._Vim9ScriptFn(name, args)
+ for idx, val in pairs(result[2]) do
+ M.replace(args[idx], val)
+ end
+
+ -- Substitute returning the reference to the
+ -- returned value
+ if info.replace then
+ return args[info.replace + 1]
+ end
+
+ return result[1]
+ end
+
+ M.replace = function(orig, new)
+ if type(orig) == 'table' and type(new) == 'table' then
+ for k in pairs(orig) do
+ orig[k] = nil
+ end
+
+ for k, v in pairs(new) do
+ orig[k] = v
+ end
+
+ return orig
+ end
+
+ return new
+ end
+
+ M.index = function(obj, idx)
+ if vim.tbl_islist(obj) then
+ if idx < 0 then
+ return obj[#obj + idx + 1]
+ else
+ return obj[idx + 1]
+ end
+ elseif type(obj) == 'table' then
+ return obj[idx]
+ elseif type(obj) == 'string' then
+ return string.sub(obj, idx + 1, idx + 1)
+ end
+
+ error('invalid type for indexing: ' .. vim.inspect(obj))
+ end
+
+ M.index_expr = function(idx)
+ if type(idx) == 'string' then
+ return idx
+ elseif type(idx) == 'number' then
+ return idx + 1
+ else
+ error(string.format('not yet handled: %s', vim.inspect(idx)))
+ end
+ end
+
+ M.slice = function(obj, start, finish)
+ if start == nil then
+ start = 0
+ end
+
+ if start < 0 then
+ start = #obj + start
+ end
+ assert(type(start) == 'number')
+
+ if finish == nil then
+ finish = #obj
+ end
+
+ if finish < 0 then
+ finish = #obj + finish
+ end
+ assert(type(finish) == 'number')
+
+ local slicer
+ if vim.tbl_islist(obj) then
+ slicer = vim.list_slice
+ elseif type(obj) == 'string' then
+ slicer = string.sub
+ else
+ error('invalid type for slicing: ' .. vim.inspect(obj))
+ end
+
+ return slicer(obj, start + 1, finish + 1)
+ end
+
+ -- Currently unused, but this could be used to embed vim9jit within a
+ -- running nvim application and transpile "on the fly" as files are
+ -- sourced. There would still need to be some work done to make that
+ -- work correctly with imports and what not, but overall it could
+ -- 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', {})
+ vim.api.nvim_create_autocmd('SourceCmd', {
+ pattern = '*.vim',
+ group = group,
+ callback = function(a)
+ local file = vim.fn.readfile(a.file)
+ for _, line in ipairs(file) do
+ -- TODO: Or starts with def <something>
+ -- You can use def in legacy vim files
+ if vim.startswith(line, 'vim9script') then
+ -- TODO: Use the rust lib to actually
+ -- generate the corresponding lua code and then
+ -- execute that (instead of sourcing it directly)
+ return
+ end
+ end
+
+ vim.api.nvim_exec(table.concat(file, '\n'), false)
+ end,
+ })
+ end
+
+ M.iter = function(expr)
+ if vim.tbl_islist(expr) then
+ return ipairs(expr)
+ else
+ return pairs(expr)
+ end
+ end
+
+ M.ITER_DEFAULT = 0
+ M.ITER_CONTINUE = 1
+ M.ITER_BREAK = 2
+ M.ITER_RETURN = 3
+
+ return M
+end)()
+
+vim.cmd([[
+function! _Vim9ScriptFn(name, args) abort
+ try
+ let ret = function(a:name, a:args)()
+ catch
+ echo "Failed..."
+ echo a:name
+ echo a:args
+
+ throw v:errmsg
+ endtry
+
+ return [ret, a:args]
+endfunction
+]])
+
+vim9['autoload'] = (function()
+ return function(path)
+ return loadfile(path)()
+ end
+end)()
+vim9['bool'] = (function()
+ return function(...)
+ return vim9.convert.to_vim_bool(...)
+ end
+end)()
+vim9['convert'] = (function()
+ local M = {}
+
+ M.decl_bool = function(val)
+ if type(val) == 'boolean' then
+ return val
+ elseif type(val) == 'number' then
+ if val == 0 then
+ return false
+ elseif val == 1 then
+ return true
+ else
+ error(string.format('bad number passed to bool declaration: %s', val))
+ end
+ end
+
+ error(string.format('invalid bool declaration: %s', vim.inspect(val)))
+ end
+
+ M.decl_dict = function(val)
+ if type(val) == 'nil' then
+ return vim.empty_dict()
+ elseif type(val) == 'table' then
+ if vim.tbl_isempty(val) then
+ return vim.empty_dict()
+ elseif vim.tbl_islist(val) then
+ error(string.format('Cannot pass list to dictionary? %s', vim.inspect(val)))
+ else
+ return val
+ end
+ end
+
+ error(string.format('invalid dict declaration: %s', vim.inspect(val)))
+ end
+
+ M.to_vim_bool = function(val)
+ if type(val) == 'boolean' then
+ return val
+ elseif type(val) == 'number' then
+ return val ~= 0
+ elseif type(val) == 'string' then
+ return string.len(val) ~= 0
+ elseif type(val) == 'table' then
+ return not vim.tbl_isempty(val)
+ elseif val == nil then
+ return false
+ end
+
+ error('unhandled type: ' .. vim.inspect(val))
+ end
+
+ return M
+end)()
+vim9['fn'] = (function()
+ local M = {}
+
+ M.insert = function(list, item, idx)
+ if idx == nil then
+ idx = 1
+ end
+
+ table.insert(list, idx + 1, item)
+
+ return list
+ end
+
+ M.extend = function(left, right, expr3)
+ if expr3 ~= nil then
+ error("haven't written this code yet")
+ end
+
+ if vim.tbl_islist(right) then
+ vim.list_extend(left, right)
+ return left
+ else
+ -- local result = vim.tbl_extend(left, right)
+ for k, v in pairs(right) do
+ left[k] = v
+ end
+
+ return left
+ end
+ end
+
+ M.add = function(list, item)
+ table.insert(list, item)
+ return list
+ end
+
+ M.has_key = function(obj, key)
+ return not not obj[key]
+ end
+
+ M.prop_type_add = function(...)
+ local args = { ... }
+ print('[prop_type_add]', vim.inspect(args))
+ end
+
+ do
+ local has_overrides = {
+ -- We do have vim9script ;) that's this plugin
+ ['vim9script'] = true,
+
+ -- Include some vim patches that are sometimes required by variuos vim9script plugins
+ -- that we implement via vim9jit
+ [ [[patch-8.2.2261]] ] = true,
+ [ [[patch-8.2.4257]] ] = true,
+ }
+
+ M.has = function(patch)
+ if has_overrides[patch] then
+ return true
+ end
+
+ return vim.fn.has(patch)
+ end
+ end
+
+ --[=[
+Currently missing patch, can be removed in the future.
+
+readdirex({directory} [, {expr} [, {dict}]]) *readdirex()*
+ Extended version of |readdir()|.
+ Return a list of Dictionaries with file and directory
+ information in {directory}.
+ This is useful if you want to get the attributes of file and
+ directory at the same time as getting a list of a directory.
+ This is much faster than calling |readdir()| then calling
+ |getfperm()|, |getfsize()|, |getftime()| and |getftype()| for
+ each file and directory especially on MS-Windows.
+ The list will by default be sorted by name (case sensitive),
+ the sorting can be changed by using the optional {dict}
+ argument, see |readdir()|.
+
+ The Dictionary for file and directory information has the
+ following items:
+ group Group name of the entry. (Only on Unix)
+ name Name of the entry.
+ perm Permissions of the entry. See |getfperm()|.
+ size Size of the entry. See |getfsize()|.
+ time Timestamp of the entry. See |getftime()|.
+ type Type of the entry.
+ On Unix, almost same as |getftype()| except:
+ Symlink to a dir "linkd"
+ Other symlink "link"
+ On MS-Windows:
+ Normal file "file"
+ Directory "dir"
+ Junction "junction"
+ Symlink to a dir "linkd"
+ Other symlink "link"
+ Other reparse point "reparse"
+ user User name of the entry's owner. (Only on Unix)
+ On Unix, if the entry is a symlink, the Dictionary includes
+ the information of the target (except the "type" item).
+ On MS-Windows, it includes the information of the symlink
+ itself because of performance reasons.
+--]=]
+ M.readdirex = function(dir)
+ local files = vim.fn.readdir(dir)
+ local direx = {}
+ for _, f in ipairs(files) do
+ table.insert(direx, {
+ name = f,
+ type = vim.fn.getftype(f),
+ })
+ end
+
+ return direx
+ end
+
+ M.mapnew = function(tbl, expr)
+ return vim.fn.map(tbl, expr)
+ end
+
+ M.typename = function(val)
+ local ty = type(val)
+ if ty == 'string' then
+ return 'string'
+ elseif ty == 'boolean' then
+ return 'bool'
+ elseif ty == 'number' then
+ return 'number'
+ else
+ error(string.format('typename: %s', val))
+ end
+ end
+
+ -- Popup menu stuff: Could be rolled into other plugin later
+ -- but currently is here for testing purposes (and implements
+ -- some very simple compat layers at the moment)
+ do
+ local pos_map = {
+ topleft = 'NW',
+ topright = 'NE',
+ botleft = 'SW',
+ botright = 'SE',
+ }
+
+ M.popup_menu = function(_, options)
+ -- print "OPTIONS:"
+
+ local buf = vim.api.nvim_create_buf(false, true)
+ local win = vim.api.nvim_open_win(buf, true, {
+ relative = 'editor',
+ style = 'minimal',
+ anchor = pos_map[options.pos],
+ height = options.maxheight or options.minheight,
+ width = options.maxwidth or options.minwidth,
+ row = options.line,
+ col = options.col,
+ })
+
+ if options.filter then
+ local loop
+ loop = function()
+ vim.cmd([[redraw!]])
+ local ok, ch = pcall(vim.fn.getcharstr)
+ if not ok then
+ return
+ end -- interrupted
+
+ if ch == '<C-C>' then
+ return
+ end
+
+ if not require('vim9script').bool(options.filter(nil, ch)) then
+ vim.cmd.normal(ch)
+ end
+
+ vim.schedule(loop)
+ end
+
+ vim.schedule(loop)
+ end
+
+ return win
+ end
+
+ M.popup_settext = function(id, text)
+ if type(text) == 'string' then
+ -- text = vim.split(text, "\n")
+ error("Haven't handled string yet")
+ end
+
+ local lines = {}
+ for _, obj in ipairs(text) do
+ table.insert(lines, obj.text)
+ end
+
+ vim.api.nvim_buf_set_lines(vim.api.nvim_win_get_buf(id), 0, -1, false, lines)
+ end
+
+ M.popup_filter_menu = function()
+ print('ok, just pretend we filtered the menu')
+ end
+
+ M.popup_setoptions = function(id, _)
+ print('setting options...', id)
+ end
+ end
+
+ M = setmetatable(M, {
+ __index = vim.fn,
+ })
+
+ return M
+end)()
+vim9['heredoc'] = (function()
+ local M = {}
+
+ M.trim = function(lines)
+ local min_whitespace = 9999
+ for _, line in ipairs(lines) do
+ local _, finish = string.find(line, '^%s*')
+ min_whitespace = math.min(min_whitespace, finish)
+ end
+
+ local trimmed_lines = {}
+ for _, line in ipairs(lines) do
+ table.insert(trimmed_lines, string.sub(line, min_whitespace + 1))
+ end
+
+ return trimmed_lines
+ end
+
+ return M
+end)()
+vim9['import'] = (function()
+ local imported = {}
+ imported.autoload = setmetatable({}, {
+ __index = function(_, name)
+ local luaname = 'autoload/' .. string.gsub(name, '%.vim$', '.lua')
+ local runtime_file = vim.api.nvim_get_runtime_file(luaname, false)[1]
+ if not runtime_file then
+ error('unable to find autoload file:' .. name)
+ end
+
+ return imported.absolute[vim.fn.fnamemodify(runtime_file, ':p')]
+ end,
+ })
+
+ imported.absolute = setmetatable({}, {
+ __index = function(self, name)
+ if vim.loop.fs_stat(name) then
+ local result = loadfile(name)()
+ rawset(self, name, result)
+
+ return result
+ end
+
+ error(string.format('unabled to find absolute file: %s', name))
+ end,
+ })
+
+ return function(info)
+ local name = info.name
+
+ if info.autoload then
+ return imported.autoload[info.name]
+ end
+
+ local debug_info = debug.getinfo(2, 'S')
+ local sourcing_path = vim.fn.fnamemodify(string.sub(debug_info.source, 2), ':p')
+
+ -- Relative paths
+ if vim.startswith(name, '../') or vim.startswith(name, './') then
+ local luaname = string.gsub(name, '%.vim$', '.lua')
+ local directory = vim.fn.fnamemodify(sourcing_path, ':h')
+ local search = directory .. '/' .. luaname
+ return imported.absolute[search]
+ end
+
+ if vim.startswith(name, '/') then
+ error('absolute path')
+ -- local luaname = string.gsub(name, "%.vim", ".lua")
+ -- local runtime_file = vim.api.nvim_get_runtime_file(luaname, false)[1]
+ -- if runtime_file then
+ -- runtime_file = vim.fn.fnamemodify(runtime_file, ":p")
+ -- return loadfile(runtime_file)()
+ -- end
+ end
+
+ error('Unhandled case' .. vim.inspect(info) .. vim.inspect(debug_info))
+ end
+end)()
+vim9['ops'] = (function()
+ local lib = vim9
+
+ local M = {}
+
+ M['And'] = function(left, right)
+ return lib.bool(left) and lib.bool(right)
+ end
+
+ M['Or'] = function(left, right)
+ return lib.bool(left) or lib.bool(right)
+ end
+
+ M['Plus'] = function(left, right)
+ return left + right
+ end
+
+ M['Multiply'] = function(left, right)
+ return left * right
+ end
+
+ M['Divide'] = function(left, right)
+ return left / right
+ end
+
+ M['StringConcat'] = function(left, right)
+ return left .. right
+ end
+
+ M['EqualTo'] = function(left, right)
+ return left == right
+ end
+
+ M['NotEqualTo'] = function(left, right)
+ return not M['EqualTo'](left, right)
+ end
+
+ M['LessThan'] = function(left, right)
+ return left < right
+ end
+
+ M['LessThanOrEqual'] = function(left, right)
+ return left <= right
+ end
+
+ M['GreaterThan'] = function(left, right)
+ return left > right
+ end
+
+ M['GreaterThanOrEqual'] = function(left, right)
+ return left >= right
+ end
+
+ M['RegexpMatches'] = function(left, right)
+ return not not vim.regex(right):match_str(left)
+ end
+
+ M['RegexpMatchesIns'] = function(left, right)
+ return not not vim.regex('\\c' .. right):match_str(left)
+ end
+
+ M['NotRegexpMatches'] = function(left, right)
+ return not M['RegexpMatches'](left, right)
+ end
+
+ M['Modulo'] = function(left, right)
+ return left % right
+ end
+
+ M['Minus'] = function(left, right)
+ -- TODO: This is not right :)
+ return left - right
+ end
+
+ return M
+end)()
+vim9['prefix'] = (function()
+ local lib = vim9
+
+ local M = {}
+
+ M['Minus'] = function(right)
+ return -right
+ end
+
+ M['Bang'] = function(right)
+ return not lib.bool(right)
+ end
+
+ return M
+end)()
+
+return vim9
diff --git a/runtime/lua/editorconfig.lua b/runtime/lua/editorconfig.lua
new file mode 100644
index 0000000000..2079006234
--- /dev/null
+++ b/runtime/lua/editorconfig.lua
@@ -0,0 +1,246 @@
+local M = {}
+
+M.properties = {}
+
+--- Modified version of the builtin assert that does not include error position information
+---
+---@param v any Condition
+---@param message string Error message to display if condition is false or nil
+---@return any v if not false or nil, otherwise an error is displayed
+---
+---@private
+local function assert(v, message)
+ return v or error(message, 0)
+end
+
+--- Show a warning message
+---
+---@param msg string Message to show
+---
+---@private
+local function warn(msg, ...)
+ vim.notify(string.format(msg, ...), vim.log.levels.WARN, {
+ title = 'editorconfig',
+ })
+end
+
+function M.properties.charset(bufnr, val)
+ assert(
+ vim.tbl_contains({ 'utf-8', 'utf-8-bom', 'latin1', 'utf-16be', 'utf-16le' }, val),
+ 'charset must be one of "utf-8", "utf-8-bom", "latin1", "utf-16be", or "utf-16le"'
+ )
+ if val == 'utf-8' or val == 'utf-8-bom' then
+ vim.bo[bufnr].fileencoding = 'utf-8'
+ vim.bo[bufnr].bomb = val == 'utf-8-bom'
+ elseif val == 'utf-16be' then
+ vim.bo[bufnr].fileencoding = 'utf-16'
+ else
+ vim.bo[bufnr].fileencoding = val
+ end
+end
+
+function M.properties.end_of_line(bufnr, val)
+ vim.bo[bufnr].fileformat = assert(
+ ({ lf = 'unix', crlf = 'dos', cr = 'mac' })[val],
+ 'end_of_line must be one of "lf", "crlf", or "cr"'
+ )
+end
+
+function M.properties.indent_style(bufnr, val, opts)
+ assert(val == 'tab' or val == 'space', 'indent_style must be either "tab" or "space"')
+ vim.bo[bufnr].expandtab = val == 'space'
+ if val == 'tab' and not opts.indent_size then
+ vim.bo[bufnr].shiftwidth = 0
+ vim.bo[bufnr].softtabstop = 0
+ end
+end
+
+function M.properties.indent_size(bufnr, val, opts)
+ if val == 'tab' then
+ vim.bo[bufnr].shiftwidth = 0
+ vim.bo[bufnr].softtabstop = 0
+ else
+ local n = assert(tonumber(val), 'indent_size must be a number')
+ vim.bo[bufnr].shiftwidth = n
+ vim.bo[bufnr].softtabstop = -1
+ if not opts.tab_width then
+ vim.bo[bufnr].tabstop = n
+ end
+ end
+end
+
+function M.properties.tab_width(bufnr, val)
+ vim.bo[bufnr].tabstop = assert(tonumber(val), 'tab_width must be a number')
+end
+
+function M.properties.max_line_length(bufnr, val)
+ local n = tonumber(val)
+ if n then
+ vim.bo[bufnr].textwidth = n
+ else
+ assert(val == 'off', 'max_line_length must be a number or "off"')
+ vim.bo[bufnr].textwidth = 0
+ end
+end
+
+function M.properties.trim_trailing_whitespace(bufnr, val)
+ assert(
+ val == 'true' or val == 'false',
+ 'trim_trailing_whitespace must be either "true" or "false"'
+ )
+ if val == 'true' then
+ vim.api.nvim_create_autocmd('BufWritePre', {
+ group = 'editorconfig',
+ buffer = bufnr,
+ callback = function()
+ local view = vim.fn.winsaveview()
+ vim.api.nvim_command('silent! undojoin')
+ vim.api.nvim_command('silent keepjumps keeppatterns %s/\\s\\+$//e')
+ vim.fn.winrestview(view)
+ end,
+ })
+ else
+ vim.api.nvim_clear_autocmds({
+ event = 'BufWritePre',
+ group = 'editorconfig',
+ buffer = bufnr,
+ })
+ end
+end
+
+function M.properties.insert_final_newline(bufnr, val)
+ assert(val == 'true' or val == 'false', 'insert_final_newline must be either "true" or "false"')
+ vim.bo[bufnr].fixendofline = val == 'true'
+ vim.bo[bufnr].endofline = val == 'true'
+end
+
+--- Modified version of |glob2regpat()| that does not match path separators on *.
+---
+--- This function replaces single instances of * with the regex pattern [^/]*. However, the star in
+--- the replacement pattern also gets interpreted by glob2regpat, so we insert a placeholder, pass
+--- it through glob2regpat, then replace the placeholder with the actual regex pattern.
+---
+---@param glob string Glob to convert into a regular expression
+---@return string Regular expression
+---
+---@private
+local function glob2regpat(glob)
+ local placeholder = '@@PLACEHOLDER@@'
+ return (
+ string.gsub(
+ vim.fn.glob2regpat(
+ vim.fn.substitute(
+ string.gsub(glob, '{(%d+)%.%.(%d+)}', '[%1-%2]'),
+ '\\*\\@<!\\*\\*\\@!',
+ placeholder,
+ 'g'
+ )
+ ),
+ placeholder,
+ '[^/]*'
+ )
+ )
+end
+
+--- Parse a single line in an EditorConfig file
+---
+---@param line string Line
+---@return string|nil If the line contains a pattern, the glob pattern
+---@return string|nil If the line contains a key-value pair, the key
+---@return string|nil If the line contains a key-value pair, the value
+---
+---@private
+local function parse_line(line)
+ if line:find('^%s*[^ #;]') then
+ local glob = (line:match('%b[]') or ''):match('^%s*%[(.*)%]%s*$')
+ if glob then
+ return glob, nil, nil
+ end
+
+ local key, val = line:match('^%s*([^:= ][^:=]-)%s*[:=]%s*(.-)%s*$')
+ if key ~= nil and val ~= nil then
+ return nil, key:lower(), val:lower()
+ end
+ end
+end
+
+--- Parse options from an .editorconfig file
+---
+---@param filepath string File path of the file to apply EditorConfig settings to
+---@param dir string Current directory
+---@return table Table of options to apply to the given file
+---
+---@private
+local function parse(filepath, dir)
+ local pat = nil
+ local opts = {}
+ local f = io.open(dir .. '/.editorconfig')
+ if f then
+ for line in f:lines() do
+ local glob, key, val = parse_line(line)
+ if glob then
+ glob = glob:find('/') and (dir .. '/' .. glob:gsub('^/', '')) or ('**/' .. glob)
+ local ok, regpat = pcall(glob2regpat, glob)
+ if ok then
+ pat = vim.regex(regpat)
+ else
+ pat = nil
+ warn('editorconfig: Error occurred while parsing glob pattern "%s": %s', glob, regpat)
+ end
+ elseif key ~= nil and val ~= nil then
+ if key == 'root' then
+ opts.root = val == 'true'
+ elseif pat and pat:match_str(filepath) then
+ opts[key] = val
+ end
+ end
+ end
+ f:close()
+ end
+ return opts
+end
+
+--- Configure the given buffer with options from an .editorconfig file
+---
+---@param bufnr number Buffer number to configure
+---
+---@private
+function M.config(bufnr)
+ bufnr = bufnr or vim.api.nvim_get_current_buf()
+ local path = vim.fs.normalize(vim.api.nvim_buf_get_name(bufnr))
+ if vim.bo[bufnr].buftype ~= '' or not vim.bo[bufnr].modifiable or path == '' then
+ return
+ end
+
+ local opts = {}
+ for parent in vim.fs.parents(path) do
+ for k, v in pairs(parse(path, parent)) do
+ if opts[k] == nil then
+ opts[k] = v
+ end
+ end
+
+ if opts.root then
+ break
+ end
+ end
+
+ local applied = {}
+ for opt, val in pairs(opts) do
+ if val ~= 'unset' then
+ local func = M.properties[opt]
+ if func then
+ local ok, err = pcall(func, bufnr, val, opts)
+ if ok then
+ applied[opt] = val
+ else
+ warn('editorconfig: invalid value for option %s: %s. %s', opt, val, err)
+ end
+ end
+ end
+ end
+
+ vim.b[bufnr].editorconfig = applied
+end
+
+return M
diff --git a/runtime/lua/man.lua b/runtime/lua/man.lua
index 6477786dbb..732a4ab92e 100644
--- a/runtime/lua/man.lua
+++ b/runtime/lua/man.lua
@@ -12,7 +12,7 @@ local function man_error(msg)
end
-- Run a system command and timeout after 30 seconds.
-local function system(cmd, silent, env)
+local function system(cmd_, silent, env)
local stdout_data = {}
local stderr_data = {}
local stdout = vim.loop.new_pipe(false)
@@ -21,11 +21,23 @@ local function system(cmd, silent, env)
local done = false
local exit_code
+ -- We use the `env` command here rather than the env option to vim.loop.spawn since spawn will
+ -- completely overwrite the environment when we just want to modify the existing one.
+ --
+ -- Overwriting mainly causes problems NixOS which relies heavily on a non-standard environment.
+ local cmd
+ if env then
+ cmd = { 'env' }
+ vim.list_extend(cmd, env)
+ vim.list_extend(cmd, cmd_)
+ else
+ cmd = cmd_
+ end
+
local handle
handle = vim.loop.spawn(cmd[1], {
args = vim.list_slice(cmd, 2),
stdio = { nil, stdout, stderr },
- env = env,
}, function(code)
exit_code = code
stdout:close()
@@ -283,6 +295,14 @@ local function get_path(sect, name, silent)
return
end
+ -- `man -w /some/path` will return `/some/path` for any existent file, which
+ -- stops us from actually determining if a path has a corresponding man file.
+ -- Since `:Man /some/path/to/man/file` isn't supported anyway, we should just
+ -- error out here if we detect this is the case.
+ if sect == '' and #results == 1 and results[1] == name then
+ return
+ end
+
-- find any that match the specified name
local namematches = vim.tbl_filter(function(v)
return fn.fnamemodify(v, ':t'):match(name)
@@ -400,6 +420,10 @@ local function extract_sect_and_name_path(path)
end
local function find_man()
+ if vim.bo.filetype == 'man' then
+ return true
+ end
+
local win = 1
while win <= fn.winnr('$') do
local buf = fn.winbufnr(win)
@@ -449,7 +473,7 @@ local function get_page(path, silent)
end
local function put_page(page)
- vim.bo.modified = true
+ vim.bo.modifiable = true
vim.bo.readonly = false
vim.bo.swapfile = false
@@ -604,11 +628,6 @@ function M.goto_tag(pattern, _, _)
end
end
- if vim.o.cscopetag then
- -- return only a single entry so we work well with :cstag (#11675)
- structured = { structured[1] }
- end
-
return vim.tbl_map(function(entry)
return {
name = entry.name,
diff --git a/runtime/lua/man/health.lua b/runtime/lua/man/health.lua
deleted file mode 100644
index 11d7148216..0000000000
--- a/runtime/lua/man/health.lua
+++ /dev/null
@@ -1,20 +0,0 @@
-local M = {}
-
-local report_ok = vim.fn['health#report_ok']
-local report_error = vim.fn['health#report_error']
-
-local function check_runtime_file(name)
- local path = vim.env.VIMRUNTIME .. '/' .. name
- if vim.loop.fs_stat(path) then
- report_error(string.format('%s detected. Please delete %s', name, path))
- else
- report_ok(string.format('%s not in $VIMRUNTIME', name))
- end
-end
-
-function M.check()
- check_runtime_file('plugin/man.vim')
- check_runtime_file('autoload/man.vim')
-end
-
-return M
diff --git a/runtime/lua/nvim/health.lua b/runtime/lua/nvim/health.lua
new file mode 100644
index 0000000000..b76106f241
--- /dev/null
+++ b/runtime/lua/nvim/health.lua
@@ -0,0 +1,406 @@
+local M = {}
+local health = require('vim.health')
+
+local fn_bool = function(key)
+ return function(...)
+ return vim.fn[key](...) == 1
+ end
+end
+
+local has = fn_bool('has')
+local executable = fn_bool('executable')
+local empty = fn_bool('empty')
+local filereadable = fn_bool('filereadable')
+local filewritable = fn_bool('filewritable')
+
+local shell_error = function()
+ return vim.v.shell_error ~= 0
+end
+
+local suggest_faq = 'https://github.com/neovim/neovim/wiki/Building-Neovim#optimized-builds'
+
+local function check_runtime()
+ health.report_start('Runtime')
+ -- Files from an old installation.
+ local bad_files = {
+ ['plugin/man.vim'] = false,
+ ['scripts.vim'] = false,
+ ['autoload/man.vim'] = false,
+ }
+ local bad_files_msg = ''
+ for k, _ in pairs(bad_files) do
+ local path = ('%s/%s'):format(vim.env.VIMRUNTIME, k)
+ if vim.loop.fs_stat(path) then
+ bad_files[k] = true
+ bad_files_msg = ('%s%s\n'):format(bad_files_msg, path)
+ end
+ end
+
+ local ok = (bad_files_msg == '')
+ local info = ok and health.report_ok or health.report_info
+ info(string.format('$VIMRUNTIME: %s', vim.env.VIMRUNTIME))
+ if not ok then
+ health.report_error(
+ string.format(
+ '$VIMRUNTIME has files from an old installation (this can cause weird behavior):\n%s',
+ bad_files_msg
+ ),
+ { 'Delete $VIMRUNTIME (or uninstall Nvim), then reinstall Nvim.' }
+ )
+ end
+end
+
+local function check_config()
+ health.report_start('Configuration')
+ local ok = true
+
+ local vimrc = (
+ empty(vim.env.MYVIMRC) and vim.fn.stdpath('config') .. '/init.vim' or vim.env.MYVIMRC
+ )
+ if not filereadable(vimrc) then
+ ok = false
+ local has_vim = filereadable(vim.fn.expand('~/.vimrc'))
+ health.report_warn(
+ (-1 == vim.fn.getfsize(vimrc) and 'Missing' or 'Unreadable') .. ' user config file: ' .. vimrc,
+ { has_vim and ':help nvim-from-vim' or ':help init.vim' }
+ )
+ end
+
+ -- If $VIM is empty we don't care. Else make sure it is valid.
+ if not empty(vim.env.VIM) and not filereadable(vim.env.VIM .. '/runtime/doc/nvim.txt') then
+ ok = false
+ health.report_error('$VIM is invalid: ' .. vim.env.VIM)
+ end
+
+ if vim.env.NVIM_TUI_ENABLE_CURSOR_SHAPE then
+ ok = false
+ health.report_warn('$NVIM_TUI_ENABLE_CURSOR_SHAPE is ignored in Nvim 0.2+', {
+ "Use the 'guicursor' option to configure cursor shape. :help 'guicursor'",
+ 'https://github.com/neovim/neovim/wiki/Following-HEAD#20170402',
+ })
+ end
+
+ if vim.v.ctype == 'C' then
+ ok = false
+ health.report_error(
+ 'Locale does not support UTF-8. Unicode characters may not display correctly.'
+ .. ('\n$LANG=%s $LC_ALL=%s $LC_CTYPE=%s'):format(
+ vim.env.LANG,
+ vim.env.LC_ALL,
+ vim.env.LC_CTYPE
+ ),
+ {
+ 'If using tmux, try the -u option.',
+ 'Ensure that your terminal/shell/tmux/etc inherits the environment, or set $LANG explicitly.',
+ 'Configure your system locale.',
+ }
+ )
+ end
+
+ if vim.o.paste == 1 then
+ ok = false
+ health.report_error(
+ "'paste' is enabled. This option is only for pasting text.\nIt should not be set in your config.",
+ {
+ 'Remove `set paste` from your init.vim, if applicable.',
+ 'Check `:verbose set paste?` to see if a plugin or script set the option.',
+ }
+ )
+ end
+
+ local writeable = true
+ local shadaopt = vim.fn.split(vim.o.shada, ',')
+ local shadafile = (
+ empty(vim.o.shada) and vim.o.shada
+ or vim.fn.substitute(vim.fn.matchstr(shadaopt[#shadaopt], '^n.\\+'), '^n', '', '')
+ )
+ shadafile = (
+ empty(vim.o.shadafile)
+ and (empty(shadafile) and vim.fn.stdpath('state') .. '/shada/main.shada' or vim.fn.expand(
+ shadafile
+ ))
+ or (vim.o.shadafile == 'NONE' and '' or vim.o.shadafile)
+ )
+ if not empty(shadafile) and empty(vim.fn.glob(shadafile)) then
+ -- Since this may be the first time Nvim has been run, try to create a shada file.
+ if not pcall(vim.cmd.wshada) then
+ writeable = false
+ end
+ end
+ if
+ not writeable
+ or (not empty(shadafile) and (not filereadable(shadafile) or not filewritable(shadafile)))
+ then
+ ok = false
+ health.report_error(
+ 'shada file is not '
+ .. ((not writeable or filereadable(shadafile)) and 'writeable' or 'readable')
+ .. ':\n'
+ .. shadafile
+ )
+ end
+
+ if ok then
+ health.report_ok('no issues found')
+ end
+end
+
+local function check_performance()
+ health.report_start('Performance')
+
+ -- Check buildtype
+ local buildtype = vim.fn.matchstr(vim.fn.execute('version'), [[\v\cbuild type:?\s*[^\n\r\t ]+]])
+ if empty(buildtype) then
+ health.report_error('failed to get build type from :version')
+ elseif vim.regex([[\v(MinSizeRel|Release|RelWithDebInfo)]]):match_str(buildtype) then
+ health.report_ok(buildtype)
+ else
+ health.report_info(buildtype)
+ health.report_warn(
+ 'Non-optimized ' .. (has('debug') and '(DEBUG) ' or '') .. 'build. Nvim will be slower.',
+ {
+ 'Install a different Nvim package, or rebuild with `CMAKE_BUILD_TYPE=RelWithDebInfo`.',
+ suggest_faq,
+ }
+ )
+ end
+
+ -- check for slow shell invocation
+ local slow_cmd_time = 1.5
+ local start_time = vim.fn.reltime()
+ vim.fn.system('echo')
+ local elapsed_time = vim.fn.reltimefloat(vim.fn.reltime(start_time))
+ if elapsed_time > slow_cmd_time then
+ health.report_warn(
+ 'Slow shell invocation (took ' .. vim.fn.printf('%.2f', elapsed_time) .. ' seconds).'
+ )
+ end
+end
+
+-- Load the remote plugin manifest file and check for unregistered plugins
+local function check_rplugin_manifest()
+ health.report_start('Remote Plugins')
+
+ local existing_rplugins = {}
+ for _, item in ipairs(vim.fn['remote#host#PluginsForHost']('python')) do
+ existing_rplugins[item.path] = 'python'
+ end
+
+ for item in ipairs(vim.fn['remote#host#PluginsForHost']('python3')) do
+ existing_rplugins[item.path] = 'python3'
+ end
+
+ local require_update = false
+ local handle_path = function(path)
+ local python_glob = vim.fn.glob(path .. '/rplugin/python*', true, true)
+ if empty(python_glob) then
+ return
+ end
+
+ local python_dir = python_glob[1]
+ local python_version = vim.fn.fnamemodify(python_dir, ':t')
+
+ local scripts = vim.fn.glob(python_dir .. '/*.py', true, true)
+ vim.list_extend(scripts, vim.fn.glob(python_dir .. '/*/__init__.py', true, true))
+
+ for script in ipairs(scripts) do
+ local contents = vim.fn.join(vim.fn.readfile(script))
+ if vim.regex([[\<\%(from\|import\)\s\+neovim\>]]):match_str(contents) then
+ if vim.regex([[[\/]__init__\.py$]]):match_str(script) then
+ script = vim.fn.tr(vim.fn.fnamemodify(script, ':h'), '\\', '/')
+ end
+ if not existing_rplugins[script] then
+ local msg = vim.fn.printf('"%s" is not registered.', vim.fn.fnamemodify(path, ':t'))
+ if python_version == 'pythonx' then
+ if not has('python3') then
+ msg = msg .. ' (python3 not available)'
+ end
+ elseif not has(python_version) then
+ msg = msg .. vim.fn.printf(' (%s not available)', python_version)
+ else
+ require_update = true
+ end
+
+ health.report_warn(msg)
+ end
+
+ break
+ end
+ end
+ end
+
+ for _, path in ipairs(vim.fn.map(vim.fn.split(vim.o.runtimepath, ','), 'resolve(v:val)')) do
+ handle_path(path)
+ end
+
+ if require_update then
+ health.report_warn('Out of date', { 'Run `:UpdateRemotePlugins`' })
+ else
+ health.report_ok('Up to date')
+ end
+end
+
+local function check_tmux()
+ if empty(vim.env.TMUX) or not executable('tmux') then
+ return
+ end
+
+ local get_tmux_option = function(option)
+ local cmd = 'tmux show-option -qvg ' .. option -- try global scope
+ local out = vim.fn.system(vim.fn.split(cmd))
+ local val = vim.fn.substitute(out, [[\v(\s|\r|\n)]], '', 'g')
+ if shell_error() then
+ health.report_error('command failed: ' .. cmd .. '\n' .. out)
+ return 'error'
+ elseif empty(val) then
+ cmd = 'tmux show-option -qvgs ' .. option -- try session scope
+ out = vim.fn.system(vim.fn.split(cmd))
+ val = vim.fn.substitute(out, [[\v(\s|\r|\n)]], '', 'g')
+ if shell_error() then
+ health.report_error('command failed: ' .. cmd .. '\n' .. out)
+ return 'error'
+ end
+ end
+ return val
+ end
+
+ health.report_start('tmux')
+
+ -- check escape-time
+ local suggestions =
+ { 'set escape-time in ~/.tmux.conf:\nset-option -sg escape-time 10', suggest_faq }
+ local tmux_esc_time = get_tmux_option('escape-time')
+ if tmux_esc_time ~= 'error' then
+ if empty(tmux_esc_time) then
+ health.report_error('`escape-time` is not set', suggestions)
+ elseif tonumber(tmux_esc_time) > 300 then
+ health.report_error(
+ '`escape-time` (' .. tmux_esc_time .. ') is higher than 300ms',
+ suggestions
+ )
+ else
+ health.report_ok('escape-time: ' .. tmux_esc_time)
+ end
+ end
+
+ -- check focus-events
+ local tmux_focus_events = get_tmux_option('focus-events')
+ if tmux_focus_events ~= 'error' then
+ if empty(tmux_focus_events) or tmux_focus_events ~= 'on' then
+ health.report_warn(
+ "`focus-events` is not enabled. |'autoread'| may not work.",
+ { '(tmux 1.9+ only) Set `focus-events` in ~/.tmux.conf:\nset-option -g focus-events on' }
+ )
+ else
+ health.report_ok('focus-events: ' .. tmux_focus_events)
+ end
+ end
+
+ -- check default-terminal and $TERM
+ health.report_info('$TERM: ' .. vim.env.TERM)
+ local cmd = 'tmux show-option -qvg default-terminal'
+ local out = vim.fn.system(vim.fn.split(cmd))
+ local tmux_default_term = vim.fn.substitute(out, [[\v(\s|\r|\n)]], '', 'g')
+ if empty(tmux_default_term) then
+ cmd = 'tmux show-option -qvgs default-terminal'
+ out = vim.fn.system(vim.fn.split(cmd))
+ tmux_default_term = vim.fn.substitute(out, [[\v(\s|\r|\n)]], '', 'g')
+ end
+
+ if shell_error() then
+ health.report_error('command failed: ' .. cmd .. '\n' .. out)
+ elseif tmux_default_term ~= vim.env.TERM then
+ health.report_info('default-terminal: ' .. tmux_default_term)
+ health.report_error(
+ '$TERM differs from the tmux `default-terminal` setting. Colors might look wrong.',
+ { '$TERM may have been set by some rc (.bashrc, .zshrc, ...).' }
+ )
+ elseif not vim.regex([[\v(tmux-256color|screen-256color)]]):match_str(vim.env.TERM) then
+ health.report_error(
+ '$TERM should be "screen-256color" or "tmux-256color" in tmux. Colors might look wrong.',
+ {
+ 'Set default-terminal in ~/.tmux.conf:\nset-option -g default-terminal "screen-256color"',
+ suggest_faq,
+ }
+ )
+ end
+
+ -- check for RGB capabilities
+ local info = vim.fn.system({ 'tmux', 'display-message', '-p', '#{client_termfeatures}' })
+ info = vim.split(vim.trim(info), ',', { trimempty = true })
+ if not vim.tbl_contains(info, 'RGB') then
+ local has_rgb = false
+ if #info == 0 then
+ -- client_termfeatures may not be supported; fallback to checking show-messages
+ info = vim.fn.system({ 'tmux', 'show-messages', '-JT' })
+ has_rgb = info:find(' Tc: (flag) true', 1, true) or info:find(' RGB: (flag) true', 1, true)
+ end
+ if not has_rgb then
+ health.report_warn(
+ "Neither Tc nor RGB capability set. True colors are disabled. |'termguicolors'| won't work properly.",
+ {
+ "Put this in your ~/.tmux.conf and replace XXX by your $TERM outside of tmux:\nset-option -sa terminal-features ',XXX:RGB'",
+ "For older tmux versions use this instead:\nset-option -ga terminal-overrides ',XXX:Tc'",
+ }
+ )
+ end
+ end
+end
+
+local function check_terminal()
+ if not executable('infocmp') then
+ return
+ end
+
+ health.report_start('terminal')
+ local cmd = 'infocmp -L'
+ local out = vim.fn.system(vim.fn.split(cmd))
+ local kbs_entry = vim.fn.matchstr(out, 'key_backspace=[^,[:space:]]*')
+ local kdch1_entry = vim.fn.matchstr(out, 'key_dc=[^,[:space:]]*')
+
+ if
+ shell_error()
+ and (
+ not has('win32')
+ or empty(
+ vim.fn.matchstr(
+ out,
+ [[infocmp: couldn't open terminfo file .\+\%(conemu\|vtpcon\|win32con\)]]
+ )
+ )
+ )
+ then
+ health.report_error('command failed: ' .. cmd .. '\n' .. out)
+ else
+ health.report_info(
+ vim.fn.printf(
+ 'key_backspace (kbs) terminfo entry: `%s`',
+ (empty(kbs_entry) and '? (not found)' or kbs_entry)
+ )
+ )
+
+ health.report_info(
+ vim.fn.printf(
+ 'key_dc (kdch1) terminfo entry: `%s`',
+ (empty(kbs_entry) and '? (not found)' or kdch1_entry)
+ )
+ )
+ end
+
+ for env_var in ipairs({ 'XTERM_VERSION', 'VTE_VERSION', 'TERM_PROGRAM', 'COLORTERM', 'SSH_TTY' }) do
+ if vim.env[env_var] then
+ health.report_info(vim.fn.printf('$%s="%s"', env_var, vim.env[env_var]))
+ end
+ end
+end
+
+function M.check()
+ check_config()
+ check_runtime()
+ check_performance()
+ check_rplugin_manifest()
+ check_terminal()
+ check_tmux()
+end
+
+return M
diff --git a/runtime/lua/vim/_editor.lua b/runtime/lua/vim/_editor.lua
index 28e5897aa9..da8764fbd4 100644
--- a/runtime/lua/vim/_editor.lua
+++ b/runtime/lua/vim/_editor.lua
@@ -36,6 +36,7 @@ for k, v in pairs({
ui = true,
health = true,
fs = true,
+ secure = true,
}) do
vim._submodules[k] = v
end
@@ -122,7 +123,7 @@ do
--- (such as the |TUI|) pastes text into the editor.
---
--- Example: To remove ANSI color codes when pasting:
- --- <pre>
+ --- <pre>lua
--- vim.paste = (function(overridden)
--- return function(lines, phase)
--- for i,line in ipairs(lines) do
@@ -167,7 +168,8 @@ do
local line1 = lines[1]:gsub('(%c)', '\022%1')
-- nvim_input() is affected by mappings,
-- so use nvim_feedkeys() with "n" flag to ignore mappings.
- vim.api.nvim_feedkeys(line1, 'n', true)
+ -- "t" flag is also needed so the pasted text is saved in cmdline history.
+ vim.api.nvim_feedkeys(line1, 'nt', true)
end
return true
end
@@ -278,7 +280,7 @@ end
--- command.
---
--- Example:
---- <pre>
+--- <pre>lua
--- vim.cmd('echo 42')
--- vim.cmd([[
--- augroup My_group
@@ -389,7 +391,7 @@ end
---@param pos2 integer[] (line, column) tuple marking end of region
---@param regtype string type of selection, see |setreg()|
---@param inclusive boolean indicating whether the selection is end-inclusive
----@return table<integer, {}> region lua table of the form {linenr = {startcol,endcol}}
+---@return table region Table of the form `{linenr = {startcol,endcol}}`
function vim.region(bufnr, pos1, pos2, regtype, inclusive)
if not vim.api.nvim_buf_is_loaded(bufnr) then
vim.fn.bufload(bufnr)
@@ -416,11 +418,16 @@ function vim.region(bufnr, pos1, pos2, regtype, inclusive)
c2 = c1 + regtype:sub(2)
-- and adjust for non-ASCII characters
bufline = vim.api.nvim_buf_get_lines(bufnr, l, l + 1, true)[1]
- if c1 < #bufline then
+ local utflen = vim.str_utfindex(bufline, #bufline)
+ if c1 <= utflen then
c1 = vim.str_byteindex(bufline, c1)
+ else
+ c1 = #bufline + 1
end
- if c2 < #bufline then
+ if c2 <= utflen then
c2 = vim.str_byteindex(bufline, c2)
+ else
+ c2 = #bufline + 1
end
else
c1 = (l == pos1[1]) and pos1[2] or 0
@@ -739,7 +746,7 @@ end
---Prints given arguments in human-readable format.
---Example:
----<pre>
+---<pre>lua
--- -- Print highlight group Normal and store it's contents in a variable.
--- local hl_normal = vim.pretty_print(vim.api.nvim_get_hl_by_name("Normal", true))
---</pre>
diff --git a/runtime/lua/vim/_init_packages.lua b/runtime/lua/vim/_init_packages.lua
index 19c8608732..0c4ee8636d 100644
--- a/runtime/lua/vim/_init_packages.lua
+++ b/runtime/lua/vim/_init_packages.lua
@@ -56,6 +56,9 @@ setmetatable(vim, {
if vim._submodules[key] then
t[key] = require('vim.' .. key)
return t[key]
+ elseif key == 'inspect_pos' or key == 'show_pos' then
+ require('vim._inspector')
+ return t[key]
elseif vim.startswith(key, 'uri_') then
local val = require('vim.uri')[key]
if val ~= nil then
diff --git a/runtime/lua/vim/_inspector.lua b/runtime/lua/vim/_inspector.lua
new file mode 100644
index 0000000000..f46a525910
--- /dev/null
+++ b/runtime/lua/vim/_inspector.lua
@@ -0,0 +1,238 @@
+---@class InspectorFilter
+---@field syntax boolean include syntax based highlight groups (defaults to true)
+---@field treesitter boolean include treesitter based highlight groups (defaults to true)
+---@field extmarks boolean|"all" include extmarks. When `all`, then extmarks without a `hl_group` will also be included (defaults to true)
+---@field semantic_tokens boolean include semantic tokens (defaults to true)
+local defaults = {
+ syntax = true,
+ treesitter = true,
+ extmarks = true,
+ semantic_tokens = true,
+}
+
+---Get all the items at a given buffer position.
+---
+---Can also be pretty-printed with `:Inspect!`. *:Inspect!*
+---
+---@param bufnr? number defaults to the current buffer
+---@param row? number row to inspect, 0-based. Defaults to the row of the current cursor
+---@param col? number col to inspect, 0-based. Defaults to the col of the current cursor
+---@param filter? InspectorFilter (table|nil) a table with key-value pairs to filter the items
+--- - syntax (boolean): include syntax based highlight groups (defaults to true)
+--- - treesitter (boolean): include treesitter based highlight groups (defaults to true)
+--- - extmarks (boolean|"all"): include extmarks. When `all`, then extmarks without a `hl_group` will also be included (defaults to true)
+--- - semantic_tokens (boolean): include semantic tokens (defaults to true)
+---@return {treesitter:table,syntax:table,extmarks:table,semantic_tokens:table,buffer:number,col:number,row:number} (table) a table with the following key-value pairs. Items are in "traversal order":
+--- - treesitter: a list of treesitter captures
+--- - syntax: a list of syntax groups
+--- - semantic_tokens: a list of semantic tokens
+--- - extmarks: a list of extmarks
+--- - buffer: the buffer used to get the items
+--- - row: the row used to get the items
+--- - col: the col used to get the items
+function vim.inspect_pos(bufnr, row, col, filter)
+ filter = vim.tbl_deep_extend('force', defaults, filter or {})
+
+ bufnr = bufnr or 0
+ if row == nil or col == nil then
+ -- get the row/col from the first window displaying the buffer
+ local win = bufnr == 0 and vim.api.nvim_get_current_win() or vim.fn.bufwinid(bufnr)
+ if win == -1 then
+ error('row/col is required for buffers not visible in a window')
+ end
+ local cursor = vim.api.nvim_win_get_cursor(win)
+ row, col = cursor[1] - 1, cursor[2]
+ end
+ bufnr = bufnr == 0 and vim.api.nvim_get_current_buf() or bufnr
+
+ local results = {
+ treesitter = {},
+ syntax = {},
+ extmarks = {},
+ semantic_tokens = {},
+ buffer = bufnr,
+ row = row,
+ col = col,
+ }
+
+ -- resolve hl links
+ ---@private
+ local function resolve_hl(data)
+ if data.hl_group then
+ local hlid = vim.api.nvim_get_hl_id_by_name(data.hl_group)
+ local name = vim.fn.synIDattr(vim.fn.synIDtrans(hlid), 'name')
+ data.hl_group_link = name
+ end
+ return data
+ end
+
+ -- treesitter
+ if filter.treesitter then
+ for _, capture in pairs(vim.treesitter.get_captures_at_pos(bufnr, row, col)) do
+ capture.hl_group = '@' .. capture.capture
+ table.insert(results.treesitter, resolve_hl(capture))
+ end
+ end
+
+ -- syntax
+ if filter.syntax then
+ for _, i1 in ipairs(vim.fn.synstack(row + 1, col + 1)) do
+ table.insert(results.syntax, resolve_hl({ hl_group = vim.fn.synIDattr(i1, 'name') }))
+ end
+ end
+
+ -- semantic tokens
+ if filter.semantic_tokens then
+ for _, token in ipairs(vim.lsp.semantic_tokens.get_at_pos(bufnr, row, col) or {}) do
+ token.hl_groups = {
+ type = resolve_hl({ hl_group = '@' .. token.type }),
+ modifiers = vim.tbl_map(function(modifier)
+ return resolve_hl({ hl_group = '@' .. modifier })
+ end, token.modifiers or {}),
+ }
+ table.insert(results.semantic_tokens, token)
+ end
+ end
+
+ -- extmarks
+ if filter.extmarks then
+ for ns, nsid in pairs(vim.api.nvim_get_namespaces()) do
+ if ns:find('vim_lsp_semantic_tokens') ~= 1 then
+ local extmarks = vim.api.nvim_buf_get_extmarks(bufnr, nsid, 0, -1, { details = true })
+ for _, extmark in ipairs(extmarks) do
+ extmark = {
+ ns_id = nsid,
+ ns = ns,
+ id = extmark[1],
+ row = extmark[2],
+ col = extmark[3],
+ opts = resolve_hl(extmark[4]),
+ }
+ local end_row = extmark.opts.end_row or extmark.row -- inclusive
+ local end_col = extmark.opts.end_col or (extmark.col + 1) -- exclusive
+ if
+ (filter.extmarks == 'all' or extmark.opts.hl_group) -- filter hl_group
+ and (row >= extmark.row and row <= end_row) -- within the rows of the extmark
+ and (row > extmark.row or col >= extmark.col) -- either not the first row, or in range of the col
+ and (row < end_row or col < end_col) -- either not in the last row or in range of the col
+ then
+ table.insert(results.extmarks, extmark)
+ end
+ end
+ end
+ end
+ end
+ return results
+end
+
+---Show all the items at a given buffer position.
+---
+---Can also be shown with `:Inspect`. *:Inspect*
+---
+---@param bufnr? number defaults to the current buffer
+---@param row? number row to inspect, 0-based. Defaults to the row of the current cursor
+---@param col? number col to inspect, 0-based. Defaults to the col of the current cursor
+---@param filter? InspectorFilter (table|nil) see |vim.inspect_pos()|
+function vim.show_pos(bufnr, row, col, filter)
+ local items = vim.inspect_pos(bufnr, row, col, filter)
+
+ local lines = { {} }
+
+ ---@private
+ local function append(str, hl)
+ table.insert(lines[#lines], { str, hl })
+ end
+
+ ---@private
+ local function nl()
+ table.insert(lines, {})
+ end
+
+ ---@private
+ local function item(data, comment)
+ append(' - ')
+ append(data.hl_group, data.hl_group)
+ append(' ')
+ if data.hl_group ~= data.hl_group_link then
+ append('links to ', 'MoreMsg')
+ append(data.hl_group_link, data.hl_group_link)
+ append(' ')
+ end
+ if comment then
+ append(comment, 'Comment')
+ end
+ nl()
+ end
+
+ -- treesitter
+ if #items.treesitter > 0 then
+ append('Treesitter', 'Title')
+ nl()
+ for _, capture in ipairs(items.treesitter) do
+ item(capture, capture.lang)
+ end
+ nl()
+ end
+
+ if #items.semantic_tokens > 0 then
+ append('Semantic Tokens', 'Title')
+ nl()
+ for _, token in ipairs(items.semantic_tokens) do
+ local client = vim.lsp.get_client_by_id(token.client_id)
+ client = client and (' (' .. client.name .. ')') or ''
+ item(token.hl_groups.type, 'type' .. client)
+ for _, modifier in ipairs(token.hl_groups.modifiers) do
+ item(modifier, 'modifier' .. client)
+ end
+ end
+ nl()
+ end
+
+ -- syntax
+ if #items.syntax > 0 then
+ append('Syntax', 'Title')
+ nl()
+ for _, syn in ipairs(items.syntax) do
+ item(syn)
+ end
+ nl()
+ end
+ -- extmarks
+ if #items.extmarks > 0 then
+ append('Extmarks', 'Title')
+ nl()
+ for _, extmark in ipairs(items.extmarks) do
+ if extmark.opts.hl_group then
+ item(extmark.opts, extmark.ns)
+ else
+ append(' - ')
+ append(extmark.ns, 'Comment')
+ nl()
+ end
+ end
+ nl()
+ end
+
+ if #lines[#lines] == 0 then
+ table.remove(lines)
+ end
+
+ local chunks = {}
+ for _, line in ipairs(lines) do
+ vim.list_extend(chunks, line)
+ table.insert(chunks, { '\n' })
+ end
+ if #chunks == 0 then
+ chunks = {
+ {
+ 'No items found at position '
+ .. items.row
+ .. ','
+ .. items.col
+ .. ' in buffer '
+ .. items.buffer,
+ },
+ }
+ end
+ vim.api.nvim_echo(chunks, false, {})
+end
diff --git a/runtime/lua/vim/_meta.lua b/runtime/lua/vim/_meta.lua
index 9c7972873e..104f29c4c0 100644
--- a/runtime/lua/vim/_meta.lua
+++ b/runtime/lua/vim/_meta.lua
@@ -526,7 +526,7 @@ local function create_option_accessor(scope)
return setmetatable({}, {
__index = function(_, k)
- return make_option(k, a.nvim_get_option_value(k, { scope = scope }))
+ return make_option(k, a.nvim_get_option_value(k, {}))
end,
__newindex = function(_, k, v)
diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua
index 84091fbf0e..6fd000a029 100644
--- a/runtime/lua/vim/diagnostic.lua
+++ b/runtime/lua/vim/diagnostic.lua
@@ -1,7 +1,8 @@
-local if_nil = vim.F.if_nil
+local api, if_nil = vim.api, vim.F.if_nil
local M = {}
+---@enum DiagnosticSeverity
M.severity = {
ERROR = 1,
WARN = 2,
@@ -47,11 +48,11 @@ local bufnr_and_namespace_cacher_mt = {
local diagnostic_cache
do
- local group = vim.api.nvim_create_augroup('DiagnosticBufWipeout', {})
+ local group = api.nvim_create_augroup('DiagnosticBufWipeout', {})
diagnostic_cache = setmetatable({}, {
__index = function(t, bufnr)
assert(bufnr > 0, 'Invalid buffer number')
- vim.api.nvim_create_autocmd('BufWipeout', {
+ api.nvim_create_autocmd('BufWipeout', {
group = group,
buffer = bufnr,
callback = function()
@@ -245,25 +246,12 @@ end)()
---@private
local function get_bufnr(bufnr)
if not bufnr or bufnr == 0 then
- return vim.api.nvim_get_current_buf()
+ return api.nvim_get_current_buf()
end
return bufnr
end
---@private
-local function is_disabled(namespace, bufnr)
- local ns = M.get_namespace(namespace)
- if ns.disabled then
- return true
- end
-
- if type(diagnostic_disabled[bufnr]) == 'table' then
- return diagnostic_disabled[bufnr][namespace]
- end
- return diagnostic_disabled[bufnr]
-end
-
----@private
local function diagnostic_lines(diagnostics)
if not diagnostics then
return {}
@@ -299,7 +287,7 @@ end
---@private
local function restore_extmarks(bufnr, last)
for ns, extmarks in pairs(diagnostic_cache_extmarks[bufnr]) do
- local extmarks_current = vim.api.nvim_buf_get_extmarks(bufnr, ns, 0, -1, { details = true })
+ local extmarks_current = api.nvim_buf_get_extmarks(bufnr, ns, 0, -1, { details = true })
local found = {}
for _, extmark in ipairs(extmarks_current) do
-- nvim_buf_set_lines will move any extmark to the line after the last
@@ -312,7 +300,7 @@ local function restore_extmarks(bufnr, last)
if not found[extmark[1]] then
local opts = extmark[4]
opts.id = extmark[1]
- pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, extmark[2], extmark[3], opts)
+ pcall(api.nvim_buf_set_extmark, bufnr, ns, extmark[2], extmark[3], opts)
end
end
end
@@ -322,7 +310,7 @@ end
local function save_extmarks(namespace, bufnr)
bufnr = get_bufnr(bufnr)
if not diagnostic_attached_buffers[bufnr] then
- vim.api.nvim_buf_attach(bufnr, false, {
+ api.nvim_buf_attach(bufnr, false, {
on_lines = function(_, _, _, _, _, last)
restore_extmarks(bufnr, last - 1)
end,
@@ -333,7 +321,7 @@ local function save_extmarks(namespace, bufnr)
diagnostic_attached_buffers[bufnr] = true
end
diagnostic_cache_extmarks[bufnr][namespace] =
- vim.api.nvim_buf_get_extmarks(bufnr, namespace, 0, -1, { details = true })
+ api.nvim_buf_get_extmarks(bufnr, namespace, 0, -1, { details = true })
end
local registered_autocmds = {}
@@ -366,8 +354,8 @@ local function schedule_display(namespace, bufnr, args)
local key = make_augroup_key(namespace, bufnr)
if not registered_autocmds[key] then
- local group = vim.api.nvim_create_augroup(key, { clear = true })
- vim.api.nvim_create_autocmd(insert_leave_auto_cmds, {
+ local group = api.nvim_create_augroup(key, { clear = true })
+ api.nvim_create_autocmd(insert_leave_auto_cmds, {
group = group,
buffer = bufnr,
callback = function()
@@ -384,7 +372,7 @@ local function clear_scheduled_display(namespace, bufnr)
local key = make_augroup_key(namespace, bufnr)
if registered_autocmds[key] then
- vim.api.nvim_del_augroup_by_name(key)
+ api.nvim_del_augroup_by_name(key)
registered_autocmds[key] = nil
end
end
@@ -399,7 +387,7 @@ local function get_diagnostics(bufnr, opts, clamp)
-- Memoized results of buf_line_count per bufnr
local buf_line_count = setmetatable({}, {
__index = function(t, k)
- t[k] = vim.api.nvim_buf_line_count(k)
+ t[k] = api.nvim_buf_line_count(k)
return rawget(t, k)
end,
})
@@ -407,7 +395,7 @@ local function get_diagnostics(bufnr, opts, clamp)
---@private
local function add(b, d)
if not opts.lnum or d.lnum == opts.lnum then
- if clamp and vim.api.nvim_buf_is_loaded(b) then
+ if clamp and api.nvim_buf_is_loaded(b) then
local line_count = buf_line_count[b] - 1
if
d.lnum > line_count
@@ -428,32 +416,31 @@ local function get_diagnostics(bufnr, opts, clamp)
end
end
+ ---@private
+ local function add_all_diags(buf, diags)
+ for _, diagnostic in pairs(diags) do
+ add(buf, diagnostic)
+ end
+ end
+
if namespace == nil and bufnr == nil then
for b, t in pairs(diagnostic_cache) do
for _, v in pairs(t) do
- for _, diagnostic in pairs(v) do
- add(b, diagnostic)
- end
+ add_all_diags(b, v)
end
end
elseif namespace == nil then
bufnr = get_bufnr(bufnr)
for iter_namespace in pairs(diagnostic_cache[bufnr]) do
- for _, diagnostic in pairs(diagnostic_cache[bufnr][iter_namespace]) do
- add(bufnr, diagnostic)
- end
+ add_all_diags(bufnr, diagnostic_cache[bufnr][iter_namespace])
end
elseif bufnr == nil then
for b, t in pairs(diagnostic_cache) do
- for _, diagnostic in pairs(t[namespace] or {}) do
- add(b, diagnostic)
- end
+ add_all_diags(b, t[namespace] or {})
end
else
bufnr = get_bufnr(bufnr)
- for _, diagnostic in pairs(diagnostic_cache[bufnr][namespace] or {}) do
- add(bufnr, diagnostic)
- end
+ add_all_diags(bufnr, diagnostic_cache[bufnr][namespace] or {})
end
if opts.severity then
@@ -471,7 +458,7 @@ local function set_list(loclist, opts)
local winnr = opts.winnr or 0
local bufnr
if loclist then
- bufnr = vim.api.nvim_win_get_buf(winnr)
+ bufnr = api.nvim_win_get_buf(winnr)
end
-- Don't clamp line numbers since the quickfix list can already handle line
-- numbers beyond the end of the buffer
@@ -483,7 +470,7 @@ local function set_list(loclist, opts)
vim.fn.setqflist({}, ' ', { title = title, items = items })
end
if open then
- vim.api.nvim_command(loclist and 'lopen' or 'botright copen')
+ api.nvim_command(loclist and 'lwindow' or 'botright cwindow')
end
end
@@ -492,7 +479,7 @@ local function next_diagnostic(position, search_forward, bufnr, opts, namespace)
position[1] = position[1] - 1
bufnr = get_bufnr(bufnr)
local wrap = vim.F.if_nil(opts.wrap, true)
- local line_count = vim.api.nvim_buf_line_count(bufnr)
+ local line_count = api.nvim_buf_line_count(bufnr)
local diagnostics =
get_diagnostics(bufnr, vim.tbl_extend('keep', opts, { namespace = namespace }), true)
local line_diagnostics = diagnostic_lines(diagnostics)
@@ -506,7 +493,7 @@ local function next_diagnostic(position, search_forward, bufnr, opts, namespace)
lnum = (lnum + line_count) % line_count
end
if line_diagnostics[lnum] and not vim.tbl_isempty(line_diagnostics[lnum]) then
- local line_length = #vim.api.nvim_buf_get_lines(bufnr, lnum, lnum + 1, true)[1]
+ local line_length = #api.nvim_buf_get_lines(bufnr, lnum, lnum + 1, true)[1]
local sort_diagnostics, is_next
if search_forward then
sort_diagnostics = function(a, b)
@@ -542,17 +529,17 @@ local function diagnostic_move_pos(opts, pos)
opts = opts or {}
local float = vim.F.if_nil(opts.float, true)
- local win_id = opts.win_id or vim.api.nvim_get_current_win()
+ local win_id = opts.win_id or api.nvim_get_current_win()
if not pos then
- vim.api.nvim_echo({ { 'No more valid diagnostics to move to', 'WarningMsg' } }, true, {})
+ api.nvim_echo({ { 'No more valid diagnostics to move to', 'WarningMsg' } }, true, {})
return
end
- vim.api.nvim_win_call(win_id, function()
+ api.nvim_win_call(win_id, function()
-- Save position in the window's jumplist
vim.cmd("normal! m'")
- vim.api.nvim_win_set_cursor(win_id, { pos[1] + 1, pos[2] })
+ api.nvim_win_set_cursor(win_id, { pos[1] + 1, pos[2] })
-- Open folds under the cursor
vim.cmd('normal! zv')
end)
@@ -561,7 +548,7 @@ local function diagnostic_move_pos(opts, pos)
local float_opts = type(float) == 'table' and float or {}
vim.schedule(function()
M.open_float(vim.tbl_extend('keep', float_opts, {
- bufnr = vim.api.nvim_win_get_buf(win_id),
+ bufnr = api.nvim_win_get_buf(win_id),
scope = 'cursor',
focus = false,
}))
@@ -578,12 +565,12 @@ end
--- followed by namespace configuration, and finally global configuration.
---
--- For example, if a user enables virtual text globally with
---- <pre>
+--- <pre>lua
--- vim.diagnostic.config({ virtual_text = true })
--- </pre>
---
--- and a diagnostic producer sets diagnostics with
---- <pre>
+--- <pre>lua
--- vim.diagnostic.set(ns, 0, diagnostics, { virtual_text = false })
--- </pre>
---
@@ -613,16 +600,20 @@ end
--- * spacing: (number) Amount of empty spaces inserted at the beginning
--- of the virtual text.
--- * prefix: (string) Prepend diagnostic message with prefix.
+--- * suffix: (string or function) Append diagnostic message with suffix.
+--- If a function, it must have the signature (diagnostic) ->
+--- string, where {diagnostic} is of type |diagnostic-structure|.
+--- This can be used to render an LSP diagnostic error code.
--- * format: (function) A function that takes a diagnostic as input and
--- returns a string. The return value is the text used to display
--- the diagnostic. Example:
---- <pre>
---- function(diagnostic)
---- if diagnostic.severity == vim.diagnostic.severity.ERROR then
---- return string.format("E: %s", diagnostic.message)
+--- <pre>lua
+--- function(diagnostic)
+--- if diagnostic.severity == vim.diagnostic.severity.ERROR then
+--- return string.format("E: %s", diagnostic.message)
+--- end
+--- return diagnostic.message
--- end
---- return diagnostic.message
---- end
--- </pre>
--- - signs: (default true) Use signs for diagnostics. Options:
--- * severity: Only show signs for diagnostics matching the given severity
@@ -666,13 +657,13 @@ function M.config(opts, namespace)
if namespace then
for bufnr, v in pairs(diagnostic_cache) do
- if vim.api.nvim_buf_is_loaded(bufnr) and v[namespace] then
+ if api.nvim_buf_is_loaded(bufnr) and v[namespace] then
M.show(namespace, bufnr)
end
end
else
for bufnr, v in pairs(diagnostic_cache) do
- if vim.api.nvim_buf_is_loaded(bufnr) then
+ if api.nvim_buf_is_loaded(bufnr) then
for ns in pairs(v) do
M.show(ns, bufnr)
end
@@ -707,11 +698,11 @@ function M.set(namespace, bufnr, diagnostics, opts)
set_diagnostic_cache(namespace, bufnr, diagnostics)
end
- if vim.api.nvim_buf_is_loaded(bufnr) then
+ if api.nvim_buf_is_loaded(bufnr) then
M.show(namespace, bufnr, nil, opts)
end
- vim.api.nvim_exec_autocmds('DiagnosticChanged', {
+ api.nvim_exec_autocmds('DiagnosticChanged', {
modeline = false,
buffer = bufnr,
data = { diagnostics = diagnostics },
@@ -726,7 +717,7 @@ function M.get_namespace(namespace)
vim.validate({ namespace = { namespace, 'n' } })
if not all_namespaces[namespace] then
local name
- for k, v in pairs(vim.api.nvim_get_namespaces()) do
+ for k, v in pairs(api.nvim_get_namespaces()) do
if namespace == v then
name = k
break
@@ -751,6 +742,18 @@ function M.get_namespaces()
return vim.deepcopy(all_namespaces)
end
+---@class Diagnostic
+---@field buffer number
+---@field lnum number 0-indexed
+---@field end_lnum nil|number 0-indexed
+---@field col number 0-indexed
+---@field end_col nil|number 0-indexed
+---@field severity DiagnosticSeverity
+---@field message string
+---@field source nil|string
+---@field code nil|string
+---@field user_data nil|any arbitrary data plugins can add
+
--- Get current diagnostics.
---
---@param bufnr number|nil Buffer number to get diagnostics from. Use 0 for
@@ -759,7 +762,7 @@ end
--- - namespace: (number) Limit diagnostics to the given namespace.
--- - lnum: (number) Limit diagnostics to the given line number.
--- - severity: See |diagnostic-severity|.
----@return table A list of diagnostic items |diagnostic-structure|.
+---@return Diagnostic[] table A list of diagnostic items |diagnostic-structure|.
function M.get(bufnr, opts)
vim.validate({
bufnr = { bufnr, 'n', true },
@@ -771,22 +774,23 @@ end
--- Get the previous diagnostic closest to the cursor position.
---
----@param opts table See |vim.diagnostic.goto_next()|
----@return table Previous diagnostic
+---@param opts nil|table See |vim.diagnostic.goto_next()|
+---@return Diagnostic|nil Previous diagnostic
function M.get_prev(opts)
opts = opts or {}
- local win_id = opts.win_id or vim.api.nvim_get_current_win()
- local bufnr = vim.api.nvim_win_get_buf(win_id)
- local cursor_position = opts.cursor_position or vim.api.nvim_win_get_cursor(win_id)
+ local win_id = opts.win_id or api.nvim_get_current_win()
+ local bufnr = api.nvim_win_get_buf(win_id)
+ local cursor_position = opts.cursor_position or api.nvim_win_get_cursor(win_id)
return next_diagnostic(cursor_position, false, bufnr, opts, opts.namespace)
end
--- Return the position of the previous diagnostic in the current buffer.
---
----@param opts table See |vim.diagnostic.goto_next()|
----@return table Previous diagnostic position as a (row, col) tuple.
+---@param opts table|nil See |vim.diagnostic.goto_next()|
+---@return table|false Previous diagnostic position as a (row, col) tuple or false if there is no
+--- prior diagnostic
function M.get_prev_pos(opts)
local prev = M.get_prev(opts)
if not prev then
@@ -797,29 +801,30 @@ function M.get_prev_pos(opts)
end
--- Move to the previous diagnostic in the current buffer.
----@param opts table See |vim.diagnostic.goto_next()|
+---@param opts table|nil See |vim.diagnostic.goto_next()|
function M.goto_prev(opts)
return diagnostic_move_pos(opts, M.get_prev_pos(opts))
end
--- Get the next diagnostic closest to the cursor position.
---
----@param opts table See |vim.diagnostic.goto_next()|
----@return table Next diagnostic
+---@param opts table|nil See |vim.diagnostic.goto_next()|
+---@return Diagnostic|nil Next diagnostic
function M.get_next(opts)
opts = opts or {}
- local win_id = opts.win_id or vim.api.nvim_get_current_win()
- local bufnr = vim.api.nvim_win_get_buf(win_id)
- local cursor_position = opts.cursor_position or vim.api.nvim_win_get_cursor(win_id)
+ local win_id = opts.win_id or api.nvim_get_current_win()
+ local bufnr = api.nvim_win_get_buf(win_id)
+ local cursor_position = opts.cursor_position or api.nvim_win_get_cursor(win_id)
return next_diagnostic(cursor_position, true, bufnr, opts, opts.namespace)
end
--- Return the position of the next diagnostic in the current buffer.
---
----@param opts table See |vim.diagnostic.goto_next()|
----@return table Next diagnostic position as a (row, col) tuple.
+---@param opts table|nil See |vim.diagnostic.goto_next()|
+---@return table|false Next diagnostic position as a (row, col) tuple or false if no next
+--- diagnostic.
function M.get_next_pos(opts)
local next = M.get_next(opts)
if not next then
@@ -903,7 +908,7 @@ M.handlers.signs = {
end,
hide = function(namespace, bufnr)
local ns = M.get_namespace(namespace)
- if ns.user_data.sign_group then
+ if ns.user_data.sign_group and api.nvim_buf_is_valid(bufnr) then
vim.fn.sign_unplace(ns.user_data.sign_group, { buffer = bufnr })
end
end,
@@ -931,7 +936,7 @@ M.handlers.underline = {
local ns = M.get_namespace(namespace)
if not ns.user_data.underline_ns then
- ns.user_data.underline_ns = vim.api.nvim_create_namespace('')
+ ns.user_data.underline_ns = api.nvim_create_namespace('')
end
local underline_ns = ns.user_data.underline_ns
@@ -958,7 +963,9 @@ M.handlers.underline = {
local ns = M.get_namespace(namespace)
if ns.user_data.underline_ns then
diagnostic_cache_extmarks[bufnr][ns.user_data.underline_ns] = {}
- vim.api.nvim_buf_clear_namespace(bufnr, ns.user_data.underline_ns, 0, -1)
+ if api.nvim_buf_is_valid(bufnr) then
+ api.nvim_buf_clear_namespace(bufnr, ns.user_data.underline_ns, 0, -1)
+ end
end
end,
}
@@ -997,7 +1004,7 @@ M.handlers.virtual_text = {
local ns = M.get_namespace(namespace)
if not ns.user_data.virt_text_ns then
- ns.user_data.virt_text_ns = vim.api.nvim_create_namespace('')
+ ns.user_data.virt_text_ns = api.nvim_create_namespace('')
end
local virt_text_ns = ns.user_data.virt_text_ns
@@ -1009,7 +1016,7 @@ M.handlers.virtual_text = {
local virt_texts = M._get_virt_text_chunks(line_diagnostics, opts.virtual_text)
if virt_texts then
- vim.api.nvim_buf_set_extmark(bufnr, virt_text_ns, line, 0, {
+ api.nvim_buf_set_extmark(bufnr, virt_text_ns, line, 0, {
hl_mode = 'combine',
virt_text = virt_texts,
})
@@ -1021,7 +1028,9 @@ M.handlers.virtual_text = {
local ns = M.get_namespace(namespace)
if ns.user_data.virt_text_ns then
diagnostic_cache_extmarks[bufnr][ns.user_data.virt_text_ns] = {}
- vim.api.nvim_buf_clear_namespace(bufnr, ns.user_data.virt_text_ns, 0, -1)
+ if api.nvim_buf_is_valid(bufnr) then
+ api.nvim_buf_clear_namespace(bufnr, ns.user_data.virt_text_ns, 0, -1)
+ end
end
end,
}
@@ -1039,6 +1048,7 @@ function M._get_virt_text_chunks(line_diags, opts)
opts = opts or {}
local prefix = opts.prefix or 'â– '
+ local suffix = opts.suffix or ''
local spacing = opts.spacing or 4
-- Create a little more space between virtual text and contents
@@ -1052,8 +1062,11 @@ function M._get_virt_text_chunks(line_diags, opts)
-- TODO(tjdevries): Allow different servers to be shown first somehow?
-- TODO(tjdevries): Display server name associated with these?
if last.message then
+ if type(suffix) == 'function' then
+ suffix = suffix(last) or ''
+ end
table.insert(virt_texts, {
- string.format('%s %s', prefix, last.message:gsub('\r', ''):gsub('\n', ' ')),
+ string.format('%s %s%s', prefix, last.message:gsub('\r', ''):gsub('\n', ' '), suffix),
virtual_text_highlight_map[last.severity],
})
@@ -1093,6 +1106,27 @@ function M.hide(namespace, bufnr)
end
end
+--- Check whether diagnostics are disabled in a given buffer.
+---
+---@param bufnr number|nil Buffer number, or 0 for current buffer.
+---@param namespace number|nil Diagnostic namespace. When omitted, checks if
+--- all diagnostics are disabled in {bufnr}.
+--- Otherwise, only checks if diagnostics from
+--- {namespace} are disabled.
+---@return boolean
+function M.is_disabled(bufnr, namespace)
+ bufnr = get_bufnr(bufnr)
+ if namespace and M.get_namespace(namespace).disabled then
+ return true
+ end
+
+ if type(diagnostic_disabled[bufnr]) == 'table' then
+ return diagnostic_disabled[bufnr][namespace]
+ end
+
+ return diagnostic_disabled[bufnr] ~= nil
+end
+
--- Display diagnostics for the given namespace and buffer.
---
---@param namespace number|nil Diagnostic namespace. When omitted, show
@@ -1136,7 +1170,7 @@ function M.show(namespace, bufnr, diagnostics, opts)
return
end
- if is_disabled(namespace, bufnr) then
+ if M.is_disabled(bufnr, namespace) then
return
end
@@ -1153,7 +1187,7 @@ function M.show(namespace, bufnr, diagnostics, opts)
if opts.update_in_insert then
clear_scheduled_display(namespace, bufnr)
else
- local mode = vim.api.nvim_get_mode()
+ local mode = api.nvim_get_mode()
if string.sub(mode.mode, 1, 1) == 'i' then
schedule_display(namespace, bufnr, opts)
return
@@ -1220,7 +1254,9 @@ end
--- string, it is prepended to each diagnostic in the window with no
--- highlight.
--- Overrides the setting from |vim.diagnostic.config()|.
----@return tuple ({float_bufnr}, {win_id})
+--- - suffix: Same as {prefix}, but appends the text to the diagnostic instead of
+--- prepending it. Overrides the setting from |vim.diagnostic.config()|.
+---@return number|nil, number|nil: ({float_bufnr}, {win_id})
function M.open_float(opts, ...)
-- Support old (bufnr, opts) signature
local bufnr
@@ -1251,7 +1287,7 @@ function M.open_float(opts, ...)
local lnum, col
if scope == 'line' or scope == 'cursor' then
if not opts.pos then
- local pos = vim.api.nvim_win_get_cursor(0)
+ local pos = api.nvim_win_get_cursor(0)
lnum = pos[1] - 1
col = pos[2]
elseif type(opts.pos) == 'number' then
@@ -1273,7 +1309,7 @@ function M.open_float(opts, ...)
end, diagnostics)
elseif scope == 'cursor' then
-- LSP servers can send diagnostics with `end_col` past the length of the line
- local line_length = #vim.api.nvim_buf_get_lines(bufnr, lnum, lnum + 1, true)[1]
+ local line_length = #api.nvim_buf_get_lines(bufnr, lnum, lnum + 1, true)[1]
diagnostics = vim.tbl_filter(function(d)
return d.lnum == lnum
and math.min(d.col, line_length - 1) <= col
@@ -1305,9 +1341,7 @@ function M.open_float(opts, ...)
vim.validate({
header = {
header,
- function(v)
- return type(v) == 'string' or type(v) == 'table'
- end,
+ { 'string', 'table' },
"'string' or 'table'",
},
})
@@ -1315,11 +1349,11 @@ function M.open_float(opts, ...)
-- Don't insert any lines for an empty string
if string.len(if_nil(header[1], '')) > 0 then
table.insert(lines, header[1])
- table.insert(highlights, { 0, header[2] or 'Bold' })
+ table.insert(highlights, { hlname = header[2] or 'Bold' })
end
elseif #header > 0 then
table.insert(lines, header)
- table.insert(highlights, { 0, 'Bold' })
+ table.insert(highlights, { hlname = 'Bold' })
end
end
@@ -1341,9 +1375,7 @@ function M.open_float(opts, ...)
vim.validate({
prefix = {
prefix_opt,
- function(v)
- return type(v) == 'string' or type(v) == 'table' or type(v) == 'function'
- end,
+ { 'string', 'table', 'function' },
"'string' or 'table' or 'function'",
},
})
@@ -1354,18 +1386,52 @@ function M.open_float(opts, ...)
end
end
+ local suffix_opt = if_nil(opts.suffix, function(diagnostic)
+ return diagnostic.code and string.format(' [%s]', diagnostic.code) or ''
+ end)
+
+ local suffix, suffix_hl_group
+ if suffix_opt then
+ vim.validate({
+ suffix = {
+ suffix_opt,
+ { 'string', 'table', 'function' },
+ "'string' or 'table' or 'function'",
+ },
+ })
+ if type(suffix_opt) == 'string' then
+ suffix, suffix_hl_group = suffix_opt, 'NormalFloat'
+ elseif type(suffix_opt) == 'table' then
+ suffix, suffix_hl_group = suffix_opt[1] or '', suffix_opt[2] or 'NormalFloat'
+ end
+ end
+
for i, diagnostic in ipairs(diagnostics) do
if prefix_opt and type(prefix_opt) == 'function' then
prefix, prefix_hl_group = prefix_opt(diagnostic, i, #diagnostics)
prefix, prefix_hl_group = prefix or '', prefix_hl_group or 'NormalFloat'
end
+ if suffix_opt and type(suffix_opt) == 'function' then
+ suffix, suffix_hl_group = suffix_opt(diagnostic, i, #diagnostics)
+ suffix, suffix_hl_group = suffix or '', suffix_hl_group or 'NormalFloat'
+ end
local hiname = floating_highlight_map[diagnostic.severity]
local message_lines = vim.split(diagnostic.message, '\n')
- table.insert(lines, prefix .. message_lines[1])
- table.insert(highlights, { #prefix, hiname, prefix_hl_group })
- for j = 2, #message_lines do
- table.insert(lines, string.rep(' ', #prefix) .. message_lines[j])
- table.insert(highlights, { 0, hiname })
+ for j = 1, #message_lines do
+ local pre = j == 1 and prefix or string.rep(' ', #prefix)
+ local suf = j == #message_lines and suffix or ''
+ table.insert(lines, pre .. message_lines[j] .. suf)
+ table.insert(highlights, {
+ hlname = hiname,
+ prefix = {
+ length = j == 1 and #prefix or 0,
+ hlname = prefix_hl_group,
+ },
+ suffix = {
+ length = j == #message_lines and #suffix or 0,
+ hlname = suffix_hl_group,
+ },
+ })
end
end
@@ -1374,12 +1440,17 @@ function M.open_float(opts, ...)
opts.focus_id = scope
end
local float_bufnr, winnr = require('vim.lsp.util').open_floating_preview(lines, 'plaintext', opts)
- for i, hi in ipairs(highlights) do
- local prefixlen, hiname, prefix_hiname = unpack(hi)
- if prefix_hiname then
- vim.api.nvim_buf_add_highlight(float_bufnr, -1, prefix_hiname, i - 1, 0, prefixlen)
+ for i, hl in ipairs(highlights) do
+ local line = lines[i]
+ local prefix_len = hl.prefix and hl.prefix.length or 0
+ local suffix_len = hl.suffix and hl.suffix.length or 0
+ if prefix_len > 0 then
+ api.nvim_buf_add_highlight(float_bufnr, -1, hl.prefix.hlname, i - 1, 0, prefix_len)
+ end
+ api.nvim_buf_add_highlight(float_bufnr, -1, hl.hlname, i - 1, prefix_len, #line - suffix_len)
+ if suffix_len > 0 then
+ api.nvim_buf_add_highlight(float_bufnr, -1, hl.suffix.hlname, i - 1, #line - suffix_len, -1)
end
- vim.api.nvim_buf_add_highlight(float_bufnr, -1, hiname, i - 1, prefixlen, -1)
end
return float_bufnr, winnr
@@ -1410,11 +1481,15 @@ function M.reset(namespace, bufnr)
M.hide(iter_namespace, iter_bufnr)
end
- vim.api.nvim_exec_autocmds('DiagnosticChanged', {
- modeline = false,
- buffer = iter_bufnr,
- data = { diagnostics = {} },
- })
+ if api.nvim_buf_is_valid(iter_bufnr) then
+ api.nvim_exec_autocmds('DiagnosticChanged', {
+ modeline = false,
+ buffer = iter_bufnr,
+ data = { diagnostics = {} },
+ })
+ else
+ diagnostic_cache[iter_bufnr] = nil
+ end
end
end
@@ -1517,11 +1592,11 @@ end
---
--- This can be parsed into a diagnostic |diagnostic-structure|
--- with:
---- <pre>
---- local s = "WARNING filename:27:3: Variable 'foo' does not exist"
---- local pattern = "^(%w+) %w+:(%d+):(%d+): (.+)$"
---- local groups = { "severity", "lnum", "col", "message" }
---- vim.diagnostic.match(s, pattern, groups, { WARNING = vim.diagnostic.WARN })
+--- <pre>lua
+--- local s = "WARNING filename:27:3: Variable 'foo' does not exist"
+--- local pattern = "^(%w+) %w+:(%d+):(%d+): (.+)$"
+--- local groups = { "severity", "lnum", "col", "message" }
+--- vim.diagnostic.match(s, pattern, groups, { WARNING = vim.diagnostic.WARN })
--- </pre>
---
---@param str string String to parse diagnostics from.
@@ -1533,7 +1608,7 @@ end
---@param defaults table|nil Table of default values for any fields not listed in {groups}.
--- When omitted, numeric values default to 0 and "severity" defaults to
--- ERROR.
----@return diagnostic |diagnostic-structure| or `nil` if {pat} fails to match {str}.
+---@return Diagnostic|nil: |diagnostic-structure| or `nil` if {pat} fails to match {str}.
function M.match(str, pat, groups, severity_map, defaults)
vim.validate({
str = { str, 's' },
@@ -1580,7 +1655,7 @@ local errlist_type_map = {
--- passed to |setqflist()| or |setloclist()|.
---
---@param diagnostics table List of diagnostics |diagnostic-structure|.
----@return array of quickfix list items |setqflist-what|
+---@return table[] of quickfix list items |setqflist-what|
function M.toqflist(diagnostics)
vim.validate({
diagnostics = {
@@ -1605,7 +1680,11 @@ function M.toqflist(diagnostics)
end
table.sort(list, function(a, b)
if a.bufnr == b.bufnr then
- return a.lnum < b.lnum
+ if a.lnum == b.lnum then
+ return a.col < b.col
+ else
+ return a.lnum < b.lnum
+ end
else
return a.bufnr < b.bufnr
end
@@ -1617,7 +1696,7 @@ end
---
---@param list table A list of quickfix items from |getqflist()| or
--- |getloclist()|.
----@return array of diagnostics |diagnostic-structure|
+---@return Diagnostic[] array of |diagnostic-structure|
function M.fromqflist(list)
vim.validate({
list = {
diff --git a/runtime/lua/vim/filetype.lua b/runtime/lua/vim/filetype.lua
index e204fe8ae2..9293c828b8 100644
--- a/runtime/lua/vim/filetype.lua
+++ b/runtime/lua/vim/filetype.lua
@@ -170,7 +170,7 @@ local extension = {
return require('vim.filetype.detect').bindzone(bufnr, 'dcl')
end,
db = function(path, bufnr)
- return require('vim.filetype.detect').bindzone(bufnr, '')
+ return require('vim.filetype.detect').bindzone(bufnr)
end,
bicep = 'bicep',
bb = 'bitbake',
@@ -190,6 +190,7 @@ local extension = {
BUILD = 'bzl',
qc = 'c',
cabal = 'cabal',
+ capnp = 'capnp',
cdl = 'cdl',
toc = 'cdrtoc',
cfc = 'cf',
@@ -257,6 +258,7 @@ local extension = {
cc = function(path, bufnr)
return vim.g.cynlib_syntax_for_cc and 'cynlib' or 'cpp'
end,
+ cql = 'cqlang',
crm = 'crm',
csx = 'cs',
cs = 'cs',
@@ -326,11 +328,7 @@ local extension = {
end,
eex = 'eelixir',
leex = 'eelixir',
- am = function(path, bufnr)
- if not path:lower():find('makefile%.am$') then
- return 'elf'
- end
- end,
+ am = 'elf',
exs = 'elixir',
elm = 'elm',
elv = 'elvish',
@@ -418,6 +416,7 @@ local extension = {
fs = function(path, bufnr)
return require('vim.filetype.detect').fs(bufnr)
end,
+ fsh = 'fsh',
fsi = 'fsharp',
fsx = 'fsharp',
fusion = 'fusion',
@@ -555,6 +554,7 @@ local extension = {
jov = 'jovial',
jovial = 'jovial',
properties = 'jproperties',
+ jq = 'jq',
slnf = 'json',
json = 'json',
jsonp = 'json',
@@ -564,7 +564,7 @@ local extension = {
json5 = 'json5',
jsonc = 'jsonc',
jsonnet = 'jsonnet',
- libjsonnet = 'jsonnet',
+ libsonnet = 'jsonnet',
jsp = 'jsp',
jl = 'julia',
kv = 'kivy',
@@ -611,7 +611,9 @@ local extension = {
c = function(path, bufnr)
return require('vim.filetype.detect').lpc(bufnr)
end,
- lsl = 'lsl',
+ lsl = function(path, bufnr)
+ return require('vim.filetype.detect').lsl(bufnr)
+ end,
lss = 'lss',
nse = 'lua',
rockspec = 'lua',
@@ -656,6 +658,9 @@ local extension = {
dmt = 'maxima',
wxm = 'maxima',
mel = 'mel',
+ mmd = 'mermaid',
+ mmdc = 'mermaid',
+ mermaid = 'mermaid',
mf = 'mf',
mgl = 'mgl',
mgp = 'mgp',
@@ -674,6 +679,7 @@ local extension = {
DEF = 'modula2',
['m2'] = 'modula2',
mi = 'modula2',
+ lm3 = 'modula3',
ssc = 'monk',
monk = 'monk',
tsc = 'monk',
@@ -722,6 +728,10 @@ local extension = {
nsi = 'nsis',
nsh = 'nsis',
obj = 'obj',
+ obl = 'obse',
+ obse = 'obse',
+ oblivion = 'obse',
+ obscript = 'obse',
mlt = 'ocaml',
mly = 'ocaml',
mll = 'ocaml',
@@ -735,6 +745,7 @@ local extension = {
opam = 'opam',
['or'] = 'openroad',
scad = 'openscad',
+ ovpn = 'openvpn',
ora = 'ora',
org = 'org',
org_archive = 'org',
@@ -935,12 +946,14 @@ local extension = {
ice = 'slice',
score = 'slrnsc',
sol = 'solidity',
+ smali = 'smali',
tpl = 'smarty',
ihlp = 'smcl',
smcl = 'smcl',
hlp = 'smcl',
smith = 'smith',
smt = 'smith',
+ smithy = 'smithy',
sml = 'sml',
spt = 'snobol4',
sno = 'snobol4',
@@ -1009,6 +1022,7 @@ local extension = {
texinfo = 'texinfo',
text = 'text',
tfvars = 'terraform-vars',
+ thrift = 'thrift',
tla = 'tla',
tli = 'tli',
toml = 'toml',
@@ -1053,6 +1067,7 @@ local extension = {
hdl = 'vhdl',
vho = 'vhdl',
vbe = 'vhdl',
+ tape = 'vhs',
vim = 'vim',
vba = 'vim',
mar = 'vmasm',
@@ -1062,6 +1077,7 @@ local extension = {
vue = 'vue',
wat = 'wast',
wast = 'wast',
+ wdl = 'wdl',
wm = 'webmacro',
wbt = 'winbatch',
wml = 'wml',
@@ -1108,6 +1124,7 @@ local extension = {
yang = 'yang',
['z8a'] = 'z8a',
zig = 'zig',
+ zir = 'zir',
zu = 'zimbu',
zut = 'zimbutempl',
zsh = 'zsh',
@@ -1306,6 +1323,7 @@ local filename = {
['GNUmakefile.am'] = 'automake',
['named.root'] = 'bindzone',
WORKSPACE = 'bzl',
+ ['WORKSPACE.bzlmod'] = 'bzl',
BUILD = 'bzl',
['cabal.project'] = 'cabalproject',
['cabal.config'] = 'cabalconfig',
@@ -1361,13 +1379,13 @@ local filename = {
npmrc = 'dosini',
['/etc/yum.conf'] = 'dosini',
['.npmrc'] = 'dosini',
- ['.editorconfig'] = 'dosini',
['/etc/pacman.conf'] = 'confini',
['mpv.conf'] = 'confini',
dune = 'dune',
jbuild = 'dune',
['dune-workspace'] = 'dune',
['dune-project'] = 'dune',
+ ['.editorconfig'] = 'editorconfig',
['elinks.conf'] = 'elinks',
['mix.lock'] = 'elixir',
['filter-rules'] = 'elmfilt',
@@ -1410,6 +1428,7 @@ local filename = {
gnashpluginrc = 'gnash',
gnashrc = 'gnash',
['.gnuplot'] = 'gnuplot',
+ ['go.sum'] = 'gosum',
['go.work'] = 'gowork',
['.gprc'] = 'gp',
['/.gnupg/gpg.conf'] = 'gpg',
@@ -1439,11 +1458,15 @@ local filename = {
['ipf.conf'] = 'ipfilter',
['ipf6.conf'] = 'ipfilter',
['ipf.rules'] = 'ipfilter',
- ['.eslintrc'] = 'json',
- ['.babelrc'] = 'json',
['Pipfile.lock'] = 'json',
['.firebaserc'] = 'json',
['.prettierrc'] = 'json',
+ ['.babelrc'] = 'jsonc',
+ ['.eslintrc'] = 'jsonc',
+ ['.hintrc'] = 'jsonc',
+ ['.jsfmtrc'] = 'jsonc',
+ ['.jshintrc'] = 'jsonc',
+ ['.swrc'] = 'jsonc',
Kconfig = 'kconfig',
['Kconfig.debug'] = 'kconfig',
['lftp.conf'] = 'lftp',
@@ -1458,6 +1481,9 @@ local filename = {
['.sawfishrc'] = 'lisp',
['/etc/login.access'] = 'loginaccess',
['/etc/login.defs'] = 'logindefs',
+ ['.lsl'] = function(path, bufnr)
+ return require('vim.filetype.detect').lsl(bufnr)
+ end,
['.luacheckrc'] = 'lua',
['lynx.cfg'] = 'lynx',
['m3overrides'] = 'm3build',
@@ -1508,6 +1534,7 @@ local filename = {
['.latexmkrc'] = 'perl',
['pf.conf'] = 'pf',
['main.cf'] = 'pfmain',
+ ['main.cf.proto'] = 'pfmain',
pinerc = 'pine',
['.pinercex'] = 'pine',
['.pinerc'] = 'pine',
@@ -1540,6 +1567,9 @@ local filename = {
['.pythonstartup'] = 'python',
['.pythonrc'] = 'python',
SConstruct = 'python',
+ ['.Rprofile'] = 'r',
+ ['Rprofile'] = 'r',
+ ['Rprofile.site'] = 'r',
ratpoisonrc = 'ratpoison',
['.ratpoisonrc'] = 'ratpoison',
inputrc = 'readline',
@@ -1666,6 +1696,8 @@ local filename = {
fglrxrc = 'xml',
['/etc/blkid.tab'] = 'xml',
['/etc/blkid.tab.old'] = 'xml',
+ ['.clang-format'] = 'yaml',
+ ['.clang-tidy'] = 'yaml',
['/etc/zprofile'] = 'zsh',
['.zlogin'] = 'zsh',
['.zlogout'] = 'zsh',
@@ -1779,6 +1811,8 @@ local pattern = {
['.*/etc/DIR_COLORS'] = 'dircolors',
['.*/etc/dnsmasq%.conf'] = 'dnsmasq',
['php%.ini%-.*'] = 'dosini',
+ ['.*/%.aws/config'] = 'confini',
+ ['.*/%.aws/credentials'] = 'confini',
['.*/etc/pacman%.conf'] = 'confini',
['.*/etc/yum%.conf'] = 'dosini',
['.*lvs'] = 'dracula',
@@ -1885,7 +1919,9 @@ local pattern = {
['Prl.*%..*'] = starsetf('jam'),
['.*%.properties_..'] = 'jproperties',
['.*%.properties_.._..'] = 'jproperties',
+ ['org%.eclipse%..*%.prefs'] = 'jproperties',
['.*%.properties_.._.._.*'] = starsetf('jproperties'),
+ ['[jt]sconfig.*%.json'] = 'jsonc',
['Kconfig%..*'] = starsetf('kconfig'),
['.*%.[Ss][Uu][Bb]'] = 'krl',
['lilo%.conf.*'] = starsetf('lilo'),
@@ -2039,6 +2075,7 @@ local pattern = {
['.*%.ml%.cppo'] = 'ocaml',
['.*%.mli%.cppo'] = 'ocaml',
['.*%.opam%.template'] = 'opam',
+ ['.*/openvpn/.*/.*%.conf'] = 'openvpn',
['.*%.[Oo][Pp][Ll]'] = 'opl',
['.*/etc/pam%.conf'] = 'pamconf',
['.*/etc/pam%.d/.*'] = starsetf('pamconf'),
@@ -2281,10 +2318,8 @@ end
---
--- See $VIMRUNTIME/lua/vim/filetype.lua for more examples.
---
---- Note that Lua filetype detection is disabled when |g:do_legacy_filetype| is set.
----
--- Example:
---- <pre>
+--- <pre>lua
--- vim.filetype.add({
--- extension = {
--- foo = 'fooscript',
@@ -2319,8 +2354,8 @@ end
--- })
--- </pre>
---
---- To add a fallback match on contents (see |new-filetype-scripts|), use
---- <pre>
+--- To add a fallback match on contents, use
+--- <pre>lua
--- vim.filetype.add {
--- pattern = {
--- ['.*'] = {
@@ -2432,7 +2467,7 @@ end
--- Each of the three options is specified using a key to the single argument of this function.
--- Example:
---
---- <pre>
+--- <pre>lua
--- -- Using a buffer number
--- vim.filetype.match({ buf = 42 })
---
diff --git a/runtime/lua/vim/filetype/detect.lua b/runtime/lua/vim/filetype/detect.lua
index 7fc7f1b7ca..edffdde9c7 100644
--- a/runtime/lua/vim/filetype/detect.lua
+++ b/runtime/lua/vim/filetype/detect.lua
@@ -181,7 +181,7 @@ function M.cls(bufnr)
return vim.g.filetype_cls
end
local line = getlines(bufnr, 1)
- if line:find('^%%') then
+ if line:find('^[%%\\]') then
return 'tex'
elseif line:find('^#') and line:lower():find('rexx') then
return 'rexx'
@@ -634,6 +634,19 @@ function M.lpc(bufnr)
return 'c'
end
+function M.lsl(bufnr)
+ if vim.g.filetype_lsl then
+ return vim.g.filetype_lsl
+ end
+
+ local line = nextnonblank(bufnr, 1)
+ if findany(line, { '^%s*%%', ':%s*trait%s*$' }) then
+ return 'larch'
+ else
+ return 'lsl'
+ end
+end
+
function M.m(bufnr)
if vim.g.filetype_m then
return vim.g.filetype_m
@@ -1084,11 +1097,10 @@ function M.sc(bufnr)
for _, line in ipairs(getlines(bufnr, 1, 25)) do
if
findany(line, {
- '[A-Za-z0-9]*%s:%s[A-Za-z0-9]',
'var%s<',
'classvar%s<',
'%^this.*',
- '|%w*|',
+ '|%w+|',
'%+%s%w*%s{',
'%*ar%s',
})
@@ -1153,13 +1165,14 @@ function M.sh(path, contents, name)
vim.b[b].is_bash = nil
vim.b[b].is_sh = nil
end
- elseif vim.g.bash_is_sh or matchregex(name, [[\<bash\>]]) or matchregex(name, [[\<bash2\>]]) then
+ elseif vim.g.bash_is_sh or matchregex(name, [[\<\(bash\|bash2\)\>]]) then
on_detect = function(b)
vim.b[b].is_bash = 1
vim.b[b].is_kornshell = nil
vim.b[b].is_sh = nil
end
- elseif matchregex(name, [[\<sh\>]]) then
+ -- Ubuntu links sh to dash
+ elseif matchregex(name, [[\<\(sh\|dash\)\>]]) then
on_detect = function(b)
vim.b[b].is_sh = 1
vim.b[b].is_kornshell = nil
@@ -1243,17 +1256,15 @@ end
-- 2. Check the first 1000 non-comment lines for LaTeX or ConTeXt keywords.
-- 3. Default to "plain" or to g:tex_flavor, can be set in user's vimrc.
function M.tex(path, bufnr)
- local format = getlines(bufnr, 1):find('^%%&%s*(%a+)')
- if format then
+ local matched, _, format = getlines(bufnr, 1):find('^%%&%s*(%a+)')
+ if matched then
format = format:lower():gsub('pdf', '', 1)
- if format == 'tex' then
- return 'tex'
- elseif format == 'plaintex' then
- return 'plaintex'
- end
elseif path:lower():find('tex/context/.*/.*%.tex') then
return 'context'
else
+ -- Default value, may be changed later:
+ format = vim.g.tex_flavor or 'plaintex'
+
local lpat = [[documentclass\>\|usepackage\>\|begin{\|newcommand\>\|renewcommand\>]]
local cpat =
[[start\a\+\|setup\a\+\|usemodule\|enablemode\|enableregime\|setvariables\|useencoding\|usesymbols\|stelle\a\+\|verwende\a\+\|stel\a\+\|gebruik\a\+\|usa\a\+\|imposta\a\+\|regle\a\+\|utilisemodule\>]]
@@ -1262,26 +1273,25 @@ function M.tex(path, bufnr)
-- Find first non-comment line
if not l:find('^%s*%%%S') then
-- Check the next thousand lines for a LaTeX or ConTeXt keyword.
- for _, line in ipairs(getlines(bufnr, i + 1, i + 1000)) do
- local lpat_match, cpat_match =
- matchregex(line, [[\c^\s*\\\%(]] .. lpat .. [[\)\|^\s*\\\(]] .. cpat .. [[\)]])
- if lpat_match then
+ for _, line in ipairs(getlines(bufnr, i, i + 1000)) do
+ if matchregex(line, [[\c^\s*\\\%(]] .. lpat .. [[\)]]) then
return 'tex'
- elseif cpat_match then
+ elseif matchregex(line, [[\c^\s*\\\%(]] .. cpat .. [[\)]]) then
return 'context'
end
end
end
end
- -- TODO: add AMSTeX, RevTex, others?
- if not vim.g.tex_flavor or vim.g.tex_flavor == 'plain' then
- return 'plaintex'
- elseif vim.g.tex_flavor == 'context' then
- return 'context'
- else
- -- Probably LaTeX
- return 'tex'
- end
+ end -- if matched
+
+ -- Translation from formats to file types. TODO: add AMSTeX, RevTex, others?
+ if format == 'plain' then
+ return 'plaintex'
+ elseif format == 'plaintex' or format == 'context' then
+ return format
+ else
+ -- Probably LaTeX
+ return 'tex'
end
end
@@ -1447,8 +1457,8 @@ local function match_from_hashbang(contents, path)
name = 'wish'
end
- if matchregex(name, [[^\(bash\d*\|\|ksh\d*\|sh\)\>]]) then
- -- Bourne-like shell scripts: bash bash2 ksh ksh93 sh
+ if matchregex(name, [[^\(bash\d*\|dash\|ksh\d*\|sh\)\>]]) then
+ -- Bourne-like shell scripts: bash bash2 dash ksh ksh93 sh
return require('vim.filetype.detect').sh(path, contents, first_line)
elseif matchregex(name, [[^csh\>]]) then
return require('vim.filetype.detect').shell(path, contents, vim.g.filetype_csh or 'csh')
diff --git a/runtime/lua/vim/fs.lua b/runtime/lua/vim/fs.lua
index 7bd635d8b6..a0d2c4c339 100644
--- a/runtime/lua/vim/fs.lua
+++ b/runtime/lua/vim/fs.lua
@@ -1,9 +1,11 @@
local M = {}
+local iswin = vim.loop.os_uname().sysname == 'Windows_NT'
+
--- Iterate over all the parents of the given file or directory.
---
--- Example:
---- <pre>
+--- <pre>lua
--- local root_dir
--- for dir in vim.fs.parents(vim.api.nvim_buf_get_name(0)) do
--- if vim.fn.isdirectory(dir .. "/.git") == 1 then
@@ -40,7 +42,19 @@ function M.dirname(file)
if file == nil then
return nil
end
- return vim.fn.fnamemodify(file, ':h')
+ vim.validate({ file = { file, 's' } })
+ if iswin and file:match('^%w:[\\/]?$') then
+ return (file:gsub('\\', '/'))
+ elseif not file:match('[\\/]') then
+ return '.'
+ elseif file == '/' or file:match('^/[^/]+$') then
+ return '/'
+ end
+ local dir = file:match('[/\\]$') and file:sub(1, #file - 1) or file:match('^([/\\]?.+)[/\\]')
+ if iswin and dir:match('^%w:$') then
+ return dir .. '/'
+ end
+ return (dir:gsub('\\', '/'))
end
--- Return the basename of the given file or directory
@@ -48,21 +62,75 @@ end
---@param file (string) File or directory
---@return (string) Basename of {file}
function M.basename(file)
- return vim.fn.fnamemodify(file, ':t')
+ if file == nil then
+ return nil
+ end
+ vim.validate({ file = { file, 's' } })
+ if iswin and file:match('^%w:[\\/]?$') then
+ return ''
+ end
+ return file:match('[/\\]$') and '' or (file:match('[^\\/]*$'):gsub('\\', '/'))
+end
+
+---@private
+local function join_paths(...)
+ return (table.concat({ ... }, '/'):gsub('//+', '/'))
end
--- Return an iterator over the files and directories located in {path}
---
---@param path (string) An absolute or relative path to the directory to iterate
--- over. The path is first normalized |vim.fs.normalize()|.
+--- @param opts table|nil Optional keyword arguments:
+--- - depth: integer|nil How deep the traverse (default 1)
+--- - skip: (fun(dir_name: string): boolean)|nil Predicate
+--- to control traversal. Return false to stop searching the current directory.
+--- Only useful when depth > 1
+---
---@return Iterator over files and directories in {path}. Each iteration yields
--- two values: name and type. Each "name" is the basename of the file or
--- directory relative to {path}. Type is one of "file" or "directory".
-function M.dir(path)
- return function(fs)
- return vim.loop.fs_scandir_next(fs)
- end,
- vim.loop.fs_scandir(M.normalize(path))
+function M.dir(path, opts)
+ opts = opts or {}
+
+ vim.validate({
+ path = { path, { 'string' } },
+ depth = { opts.depth, { 'number' }, true },
+ skip = { opts.skip, { 'function' }, true },
+ })
+
+ if not opts.depth or opts.depth == 1 then
+ return function(fs)
+ return vim.loop.fs_scandir_next(fs)
+ end,
+ vim.loop.fs_scandir(M.normalize(path))
+ end
+
+ --- @async
+ return coroutine.wrap(function()
+ local dirs = { { path, 1 } }
+ while #dirs > 0 do
+ local dir0, level = unpack(table.remove(dirs, 1))
+ local dir = level == 1 and dir0 or join_paths(path, dir0)
+ local fs = vim.loop.fs_scandir(M.normalize(dir))
+ while fs do
+ local name, t = vim.loop.fs_scandir_next(fs)
+ if not name then
+ break
+ end
+ local f = level == 1 and name or join_paths(dir0, name)
+ coroutine.yield(f, t)
+ if
+ opts.depth
+ and level < opts.depth
+ and t == 'directory'
+ and (not opts.skip or opts.skip(f) ~= false)
+ then
+ dirs[#dirs + 1] = { f, level + 1 }
+ end
+ end
+ end
+ end)
end
--- Find files or directories in the given path.
@@ -73,17 +141,18 @@ end
--- searches are recursive and may search through many directories! If {stop}
--- is non-nil, then the search stops when the directory given in {stop} is
--- reached. The search terminates when {limit} (default 1) matches are found.
---- The search can be narrowed to find only files or or only directories by
+--- The search can be narrowed to find only files or only directories by
--- specifying {type} to be "file" or "directory", respectively.
---
---@param names (string|table|fun(name: string): boolean) Names of the files
--- and directories to find.
--- Must be base names, paths and globs are not supported.
---- If a function it is called per file and dir within the
---- traversed directories to test if they match.
+--- The function is called per file and directory within the
+--- traversed directories to test if they match {names}.
+---
---@param opts (table) Optional keyword arguments:
--- - path (string): Path to begin searching from. If
---- omitted, the current working directory is used.
+--- omitted, the |current-directory| is used.
--- - upward (boolean, default false): If true, search
--- upward through parent directories. Otherwise,
--- search through child directories
@@ -92,12 +161,12 @@ end
--- reached. The directory itself is not searched.
--- - type (string): Find only files ("file") or
--- directories ("directory"). If omitted, both
---- files and directories that match {name} are
+--- files and directories that match {names} are
--- included.
--- - limit (number, default 1): Stop the search after
--- finding this many matches. Use `math.huge` to
--- place no limit on the number of matches.
----@return (table) The paths of all matching files or directories
+---@return (table) Normalized paths |vim.fs.normalize()| of all matching files or directories
function M.find(names, opts)
opts = opts or {}
vim.validate({
@@ -133,7 +202,7 @@ function M.find(names, opts)
local t = {}
for name, type in M.dir(p) do
if names(name) and (not opts.type or opts.type == type) then
- table.insert(t, p .. '/' .. name)
+ table.insert(t, join_paths(p, name))
end
end
return t
@@ -142,7 +211,7 @@ function M.find(names, opts)
test = function(p)
local t = {}
for _, name in ipairs(names) do
- local f = p .. '/' .. name
+ local f = join_paths(p, name)
local stat = vim.loop.fs_stat(f)
if stat and (not opts.type or opts.type == stat.type) then
t[#t + 1] = f
@@ -179,7 +248,7 @@ function M.find(names, opts)
end
for other, type_ in M.dir(dir) do
- local f = dir .. '/' .. other
+ local f = join_paths(dir, other)
if type(names) == 'function' then
if names(other) and (not opts.type or opts.type == type_) then
if add(f) then
@@ -211,23 +280,29 @@ end
--- backslash (\\) characters are converted to forward slashes (/). Environment
--- variables are also expanded.
---
---- Example:
---- <pre>
---- vim.fs.normalize('C:\\Users\\jdoe')
---- => 'C:/Users/jdoe'
+--- Examples:
+--- <pre>lua
+--- vim.fs.normalize('C:\\\\Users\\\\jdoe')
+--- --> 'C:/Users/jdoe'
---
---- vim.fs.normalize('~/src/neovim')
---- => '/home/jdoe/src/neovim'
+--- vim.fs.normalize('~/src/neovim')
+--- --> '/home/jdoe/src/neovim'
---
---- vim.fs.normalize('$XDG_CONFIG_HOME/nvim/init.vim')
---- => '/Users/jdoe/.config/nvim/init.vim'
+--- vim.fs.normalize('$XDG_CONFIG_HOME/nvim/init.vim')
+--- --> '/Users/jdoe/.config/nvim/init.vim'
--- </pre>
---
---@param path (string) Path to normalize
---@return (string) Normalized path
function M.normalize(path)
vim.validate({ path = { path, 's' } })
- return (path:gsub('^~/', vim.env.HOME .. '/'):gsub('%$([%w_]+)', vim.env):gsub('\\', '/'))
+ return (
+ path
+ :gsub('^~$', vim.loop.os_homedir())
+ :gsub('^~/', vim.loop.os_homedir() .. '/')
+ :gsub('%$([%w_]+)', vim.loop.os_getenv)
+ :gsub('\\', '/')
+ )
end
return M
diff --git a/runtime/lua/vim/health.lua b/runtime/lua/vim/health.lua
index b875da0abc..044880e076 100644
--- a/runtime/lua/vim/health.lua
+++ b/runtime/lua/vim/health.lua
@@ -23,7 +23,20 @@ end
local path2name = function(path)
if path:match('%.lua$') then
-- Lua: transform "../lua/vim/lsp/health.lua" into "vim.lsp"
- return path:gsub('.-lua[%\\%/]', '', 1):gsub('[%\\%/]', '.'):gsub('%.health.-$', '')
+
+ -- Get full path, make sure all slashes are '/'
+ path = vim.fs.normalize(path)
+
+ -- Remove everything up to the last /lua/ folder
+ path = path:gsub('^.*/lua/', '')
+
+ -- Remove the filename (health.lua)
+ path = vim.fn.fnamemodify(path, ':h')
+
+ -- Change slashes to dots
+ path = path:gsub('/', '.')
+
+ return path
else
-- Vim: transform "../autoload/health/provider.vim" into "provider"
return vim.fn.fnamemodify(path, ':t:r')
diff --git a/runtime/lua/vim/highlight.lua b/runtime/lua/vim/highlight.lua
index 0fde515bd9..20ad48dd27 100644
--- a/runtime/lua/vim/highlight.lua
+++ b/runtime/lua/vim/highlight.lua
@@ -5,6 +5,7 @@ local M = {}
M.priorities = {
syntax = 50,
treesitter = 100,
+ semantic_tokens = 125,
diagnostics = 150,
user = 200,
}
diff --git a/runtime/lua/vim/keymap.lua b/runtime/lua/vim/keymap.lua
index af41794c53..ef1c66ea20 100644
--- a/runtime/lua/vim/keymap.lua
+++ b/runtime/lua/vim/keymap.lua
@@ -2,7 +2,7 @@ local keymap = {}
--- Add a new |mapping|.
--- Examples:
---- <pre>
+--- <pre>lua
--- -- Can add mapping to Lua functions
--- vim.keymap.set('n', 'lhs', function() print("real lua function") end)
---
@@ -21,13 +21,13 @@ local keymap = {}
--- </pre>
---
--- Note that in a mapping like:
---- <pre>
+--- <pre>lua
--- vim.keymap.set('n', 'asdf', require('jkl').my_fun)
--- </pre>
---
--- the ``require('jkl')`` gets evaluated during this call in order to access the function.
--- If you want to avoid this cost at startup you can wrap it in a function, for example:
---- <pre>
+--- <pre>lua
--- vim.keymap.set('n', 'asdf', function() return require('jkl').my_fun() end)
--- </pre>
---
@@ -93,7 +93,7 @@ end
--- Remove an existing mapping.
--- Examples:
---- <pre>
+--- <pre>lua
--- vim.keymap.del('n', 'lhs')
---
--- vim.keymap.del({'n', 'i', 'v'}, '<leader>w', { buffer = 5 })
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua
index 199964e24e..c5392ac154 100644
--- a/runtime/lua/vim/lsp.lua
+++ b/runtime/lua/vim/lsp.lua
@@ -4,6 +4,7 @@ local lsp_rpc = require('vim.lsp.rpc')
local protocol = require('vim.lsp.protocol')
local util = require('vim.lsp.util')
local sync = require('vim.lsp.sync')
+local semantic_tokens = require('vim.lsp.semantic_tokens')
local api = vim.api
local nvim_err_writeln, nvim_buf_get_lines, nvim_command, nvim_buf_get_option, nvim_exec_autocmds =
@@ -25,6 +26,7 @@ local lsp = {
buf = require('vim.lsp.buf'),
diagnostic = require('vim.lsp.diagnostic'),
codelens = require('vim.lsp.codelens'),
+ semantic_tokens = semantic_tokens,
util = util,
-- Allow raw RPC access.
@@ -56,6 +58,8 @@ lsp._request_name_to_capability = {
['textDocument/formatting'] = { 'documentFormattingProvider' },
['textDocument/completion'] = { 'completionProvider' },
['textDocument/documentHighlight'] = { 'documentHighlightProvider' },
+ ['textDocument/semanticTokens/full'] = { 'semanticTokensProvider' },
+ ['textDocument/semanticTokens/full/delta'] = { 'semanticTokensProvider' },
}
-- TODO improve handling of scratch buffers with LSP attached.
@@ -63,7 +67,7 @@ lsp._request_name_to_capability = {
---@private
--- Concatenates and writes a list of strings to the Vim error buffer.
---
----@param {...} (List of strings) List to write to the buffer
+---@param {...} table[] List to write to the buffer
local function err_message(...)
nvim_err_writeln(table.concat(vim.tbl_flatten({ ... })))
nvim_command('redraw')
@@ -72,7 +76,7 @@ end
---@private
--- Returns the buffer number for the given {bufnr}.
---
----@param bufnr (number) Buffer number to resolve. Defaults to the current
+---@param bufnr (number|nil) Buffer number to resolve. Defaults to the current
---buffer if not given.
---@returns bufnr (number) Number of requested buffer
local function resolve_bufnr(bufnr)
@@ -240,9 +244,9 @@ end
---@private
--- Augments a validator function with support for optional (nil) values.
---
----@param fn (function(v)) The original validator function; should return a
+---@param fn (fun(v)) The original validator function; should return a
---bool.
----@returns (function(v)) The augmented function. Also returns true if {v} is
+---@returns (fun(v)) The augmented function. Also returns true if {v} is
---`nil`.
local function optional_validator(fn)
return function(v)
@@ -813,8 +817,7 @@ end
--- Attaches the current buffer to the client.
---
--- Example:
----
---- <pre>
+--- <pre>lua
--- vim.lsp.start({
--- name = 'my-server-name',
--- cmd = {'name-of-language-server-executable'},
@@ -824,23 +827,16 @@ end
---
--- See |vim.lsp.start_client()| for all available options. The most important are:
---
---- `name` is an arbitrary name for the LSP client. It should be unique per
---- language server.
----
---- `cmd` the command as list - used to start the language server.
---- The command must be present in the `$PATH` environment variable or an
---- absolute path to the executable. Shell constructs like `~` are *NOT* expanded.
----
---- `root_dir` path to the project root.
---- By default this is used to decide if an existing client should be re-used.
---- The example above uses |vim.fs.find()| and |vim.fs.dirname()| to detect the
---- root by traversing the file system upwards starting
---- from the current directory until either a `pyproject.toml` or `setup.py`
---- file is found.
----
---- `workspace_folders` a list of { uri:string, name: string } tables.
---- The project root folders used by the language server.
---- If `nil` the property is derived from the `root_dir` for convenience.
+--- - `name` arbitrary name for the LSP client. Should be unique per language server.
+--- - `cmd` command (in list form) used to start the language server. Must be absolute, or found on
+--- `$PATH`. Shell constructs like `~` are not expanded.
+--- - `root_dir` path to the project root. By default this is used to decide if an existing client
+--- should be re-used. The example above uses |vim.fs.find()| and |vim.fs.dirname()| to detect the
+--- root by traversing the file system upwards starting from the current directory until either
+--- a `pyproject.toml` or `setup.py` file is found.
+--- - `workspace_folders` list of `{ uri:string, name: string }` tables specifying the project root
+--- folders used by the language server. If `nil` the property is derived from `root_dir` for
+--- convenience.
---
--- Language servers use this information to discover metadata like the
--- dependencies of your project and they tend to index the contents within the
@@ -901,115 +897,114 @@ end
--
--- Starts and initializes a client with the given configuration.
---
---- Parameter `cmd` is required.
----
---- The following parameters describe fields in the {config} table.
----
----
----@param cmd: (table|string|fun(dispatchers: table):table) command string or
---- list treated like |jobstart()|. The command must launch the language server
---- process. `cmd` can also be a function that creates an RPC client.
---- The function receives a dispatchers table and must return a table with the
---- functions `request`, `notify`, `is_closing` and `terminate`
---- See |vim.lsp.rpc.request()| and |vim.lsp.rpc.notify()|
---- For TCP there is a built-in rpc client factory: |vim.lsp.rpc.connect()|
----
----@param cmd_cwd: (string, default=|getcwd()|) Directory to launch
---- the `cmd` process. Not related to `root_dir`.
----
----@param cmd_env: (table) Environment flags to pass to the LSP on
---- spawn. Can be specified using keys like a map or as a list with `k=v`
---- pairs or both. Non-string values are coerced to string.
---- Example:
---- <pre>
---- { "PRODUCTION=true"; "TEST=123"; PORT = 8080; HOST = "0.0.0.0"; }
---- </pre>
----
----@param detached: (boolean, default true) Daemonize the server process so that it runs in a
---- separate process group from Nvim. Nvim will shutdown the process on exit, but if Nvim fails to
---- exit cleanly this could leave behind orphaned server processes.
----
----@param workspace_folders (table) List of workspace folders passed to the
---- language server. For backwards compatibility rootUri and rootPath will be
---- derived from the first workspace folder in this list. See `workspaceFolders` in
---- the LSP spec.
+--- Field `cmd` in {config} is required.
+---
+---@param config (table) Configuration for the server:
+--- - cmd: (table|string|fun(dispatchers: table):table) command string or
+--- list treated like |jobstart()|. The command must launch the language server
+--- process. `cmd` can also be a function that creates an RPC client.
+--- The function receives a dispatchers table and must return a table with the
+--- functions `request`, `notify`, `is_closing` and `terminate`
+--- See |vim.lsp.rpc.request()| and |vim.lsp.rpc.notify()|
+--- For TCP there is a built-in rpc client factory: |vim.lsp.rpc.connect()|
+---
+--- - cmd_cwd: (string, default=|getcwd()|) Directory to launch
+--- the `cmd` process. Not related to `root_dir`.
+---
+--- - cmd_env: (table) Environment flags to pass to the LSP on
+--- spawn. Can be specified using keys like a map or as a list with `k=v`
+--- pairs or both. Non-string values are coerced to string.
+--- Example:
+--- <pre>
+--- { "PRODUCTION=true"; "TEST=123"; PORT = 8080; HOST = "0.0.0.0"; }
+--- </pre>
+---
+--- - detached: (boolean, default true) Daemonize the server process so that it runs in a
+--- separate process group from Nvim. Nvim will shutdown the process on exit, but if Nvim fails to
+--- exit cleanly this could leave behind orphaned server processes.
+---
+--- - workspace_folders: (table) List of workspace folders passed to the
+--- language server. For backwards compatibility rootUri and rootPath will be
+--- derived from the first workspace folder in this list. See `workspaceFolders` in
+--- the LSP spec.
+---
+--- - capabilities: Map overriding the default capabilities defined by
+--- |vim.lsp.protocol.make_client_capabilities()|, passed to the language
+--- server on initialization. Hint: use make_client_capabilities() and modify
+--- its result.
+--- - Note: To send an empty dictionary use
+--- `{[vim.type_idx]=vim.types.dictionary}`, else it will be encoded as an
+--- array.
+---
+--- - handlers: Map of language server method names to |lsp-handler|
+---
+--- - settings: Map with language server specific settings. These are
+--- returned to the language server if requested via `workspace/configuration`.
+--- Keys are case-sensitive.
+---
+--- - commands: table Table that maps string of clientside commands to user-defined functions.
+--- Commands passed to start_client take precedence over the global command registry. Each key
+--- must be a unique command name, and the value is a function which is called if any LSP action
+--- (code action, code lenses, ...) triggers the command.
+---
+--- - init_options Values to pass in the initialization request
+--- as `initializationOptions`. See `initialize` in the LSP spec.
+---
+--- - name: (string, default=client-id) Name in log messages.
+---
+--- - get_language_id: function(bufnr, filetype) -> language ID as string.
+--- Defaults to the filetype.
+---
+--- - offset_encoding: (default="utf-16") One of "utf-8", "utf-16",
+--- or "utf-32" which is the encoding that the LSP server expects. Client does
+--- not verify this is correct.
+---
+--- - on_error: Callback with parameters (code, ...), invoked
+--- when the client operation throws an error. `code` is a number describing
+--- the error. Other arguments may be passed depending on the error kind. See
+--- `vim.lsp.rpc.client_errors` for possible errors.
+--- Use `vim.lsp.rpc.client_errors[code]` to get human-friendly name.
+---
+--- - before_init: Callback with parameters (initialize_params, config)
+--- invoked before the LSP "initialize" phase, where `params` contains the
+--- parameters being sent to the server and `config` is the config that was
+--- passed to |vim.lsp.start_client()|. You can use this to modify parameters before
+--- they are sent.
+---
+--- - on_init: Callback (client, initialize_result) invoked after LSP
+--- "initialize", where `result` is a table of `capabilities` and anything else
+--- the server may send. For example, clangd sends
+--- `initialize_result.offsetEncoding` if `capabilities.offsetEncoding` was
+--- sent to it. You can only modify the `client.offset_encoding` here before
+--- any notifications are sent. Most language servers expect to be sent client specified settings after
+--- initialization. Neovim does not make this assumption. A
+--- `workspace/didChangeConfiguration` notification should be sent
+--- to the server during on_init.
+---
+--- - on_exit Callback (code, signal, client_id) invoked on client
+--- exit.
+--- - code: exit code of the process
+--- - signal: number describing the signal used to terminate (if any)
+--- - client_id: client handle
---
----@param capabilities Map overriding the default capabilities defined by
---- |vim.lsp.protocol.make_client_capabilities()|, passed to the language
---- server on initialization. Hint: use make_client_capabilities() and modify
---- its result.
---- - Note: To send an empty dictionary use
---- `{[vim.type_idx]=vim.types.dictionary}`, else it will be encoded as an
---- array.
+--- - on_attach: Callback (client, bufnr) invoked when client
+--- attaches to a buffer.
---
----@param handlers Map of language server method names to |lsp-handler|
+--- - trace: ("off" | "messages" | "verbose" | nil) passed directly to the language
+--- server in the initialize request. Invalid/empty values will default to "off"
---
----@param settings Map with language server specific settings. These are
---- returned to the language server if requested via `workspace/configuration`.
---- Keys are case-sensitive.
+--- - flags: A table with flags for the client. The current (experimental) flags are:
+--- - allow_incremental_sync (bool, default true): Allow using incremental sync for buffer edits
+--- - debounce_text_changes (number, default 150): Debounce didChange
+--- notifications to the server by the given number in milliseconds. No debounce
+--- occurs if nil
+--- - exit_timeout (number|boolean, default false): Milliseconds to wait for server to
+--- exit cleanly after sending the "shutdown" request before sending kill -15.
+--- If set to false, nvim exits immediately after sending the "shutdown" request to the server.
---
----@param commands table Table that maps string of clientside commands to user-defined functions.
---- Commands passed to start_client take precedence over the global command registry. Each key
---- must be a unique command name, and the value is a function which is called if any LSP action
---- (code action, code lenses, ...) triggers the command.
----
----@param init_options Values to pass in the initialization request
---- as `initializationOptions`. See `initialize` in the LSP spec.
----
----@param name (string, default=client-id) Name in log messages.
----
----@param get_language_id function(bufnr, filetype) -> language ID as string.
---- Defaults to the filetype.
----
----@param offset_encoding (default="utf-16") One of "utf-8", "utf-16",
---- or "utf-32" which is the encoding that the LSP server expects. Client does
---- not verify this is correct.
----
----@param on_error Callback with parameters (code, ...), invoked
---- when the client operation throws an error. `code` is a number describing
---- the error. Other arguments may be passed depending on the error kind. See
---- `vim.lsp.rpc.client_errors` for possible errors.
---- Use `vim.lsp.rpc.client_errors[code]` to get human-friendly name.
----
----@param before_init Callback with parameters (initialize_params, config)
---- invoked before the LSP "initialize" phase, where `params` contains the
---- parameters being sent to the server and `config` is the config that was
---- passed to |vim.lsp.start_client()|. You can use this to modify parameters before
---- they are sent.
----
----@param on_init Callback (client, initialize_result) invoked after LSP
---- "initialize", where `result` is a table of `capabilities` and anything else
---- the server may send. For example, clangd sends
---- `initialize_result.offsetEncoding` if `capabilities.offsetEncoding` was
---- sent to it. You can only modify the `client.offset_encoding` here before
---- any notifications are sent. Most language servers expect to be sent client specified settings after
---- initialization. Neovim does not make this assumption. A
---- `workspace/didChangeConfiguration` notification should be sent
---- to the server during on_init.
----
----@param on_exit Callback (code, signal, client_id) invoked on client
---- exit.
---- - code: exit code of the process
---- - signal: number describing the signal used to terminate (if any)
---- - client_id: client handle
----
----@param on_attach Callback (client, bufnr) invoked when client
---- attaches to a buffer.
----
----@param trace: "off" | "messages" | "verbose" | nil passed directly to the language
---- server in the initialize request. Invalid/empty values will default to "off"
----@param flags: A table with flags for the client. The current (experimental) flags are:
---- - allow_incremental_sync (bool, default true): Allow using incremental sync for buffer edits
---- - debounce_text_changes (number, default 150): Debounce didChange
---- notifications to the server by the given number in milliseconds. No debounce
---- occurs if nil
---- - exit_timeout (number|boolean, default false): Milliseconds to wait for server to
---- exit cleanly after sending the "shutdown" request before sending kill -15.
---- If set to false, nvim exits immediately after sending the "shutdown" request to the server.
----
----@param root_dir string Directory where the LSP
---- server will base its workspaceFolders, rootUri, and rootPath
---- on initialization.
+--- - root_dir: (string) Directory where the LSP
+--- server will base its workspaceFolders, rootUri, and rootPath
+--- on initialization.
---
---@returns Client id. |vim.lsp.get_client_by_id()| Note: client may not be
--- fully initialized. Use `on_init` to do any actions once
@@ -1293,8 +1288,6 @@ function lsp.start_client(config)
client.initialized = true
uninitialized_clients[client_id] = nil
client.workspace_folders = workspace_folders
- -- TODO(mjlbach): Backwards compatibility, to be removed in 0.7
- client.workspaceFolders = client.workspace_folders
-- These are the cleaned up capabilities we use for dynamically deciding
-- when to send certain events to clients.
@@ -1372,7 +1365,7 @@ function lsp.start_client(config)
---
---@param method (string) LSP method name.
---@param params (table) LSP request params.
- ---@param handler (function, optional) Response |lsp-handler| for this method.
+ ---@param handler (function|nil) Response |lsp-handler| for this method.
---@param bufnr (number) Buffer handle (0 for current).
---@returns ({status}, [request_id]): {status} is a bool indicating
---whether the request was successful. If it is `false`, then it will
@@ -1383,8 +1376,10 @@ function lsp.start_client(config)
---@see |vim.lsp.buf_request()|
function client.request(method, params, handler, bufnr)
if not handler then
- handler = resolve_handler(method)
- or error(string.format('not found: %q request handler for client %q.', method, client.name))
+ handler = assert(
+ resolve_handler(method),
+ string.format('not found: %q request handler for client %q.', method, client.name)
+ )
end
-- Ensure pending didChange notifications are sent so that the server doesn't operate on a stale state
changetracking.flush(client, bufnr)
@@ -1402,7 +1397,7 @@ function lsp.start_client(config)
nvim_exec_autocmds('User', { pattern = 'LspRequest', modeline = false })
end)
- if success then
+ if success and request_id then
client.requests[request_id] = { type = 'pending', bufnr = bufnr, method = method }
nvim_exec_autocmds('User', { pattern = 'LspRequest', modeline = false })
end
@@ -1417,8 +1412,8 @@ function lsp.start_client(config)
---
---@param method (string) LSP method name.
---@param params (table) LSP request params.
- ---@param timeout_ms (number, optional, default=1000) Maximum time in
- ---milliseconds to wait for a result.
+ ---@param timeout_ms (number|nil) Maximum time in milliseconds to wait for
+ --- a result. Defaults to 1000
---@param bufnr (number) Buffer handle (0 for current).
---@returns { err=err, result=result }, a dictionary, where `err` and `result` come from the |lsp-handler|.
---On timeout, cancel or error, returns `(nil, err)` where `err` is a
@@ -1441,7 +1436,9 @@ function lsp.start_client(config)
end, 10)
if not wait_result then
- client.cancel_request(request_id)
+ if request_id then
+ client.cancel_request(request_id)
+ end
return nil, wait_result_reason[reason]
end
return request_result
@@ -1536,6 +1533,16 @@ function lsp.start_client(config)
-- TODO(ashkan) handle errors.
pcall(config.on_attach, client, bufnr)
end
+
+ -- schedule the initialization of semantic tokens to give the above
+ -- on_attach and LspAttach callbacks the ability to schedule wrap the
+ -- opt-out (deleting the semanticTokensProvider from capabilities)
+ vim.schedule(function()
+ if vim.tbl_get(client.server_capabilities, 'semanticTokensProvider', 'full') then
+ semantic_tokens.start(bufnr, client.id)
+ end
+ end)
+
client.attached_buffers[bufnr] = true
end
@@ -1549,15 +1556,21 @@ end
--- Notify all attached clients that a buffer has changed.
local text_document_did_change_handler
do
- text_document_did_change_handler =
- function(_, bufnr, changedtick, firstline, lastline, new_lastline)
- -- Detach (nvim_buf_attach) via returning True to on_lines if no clients are attached
- if tbl_isempty(all_buffer_active_clients[bufnr] or {}) then
- return true
- end
- util.buf_versions[bufnr] = changedtick
- changetracking.send_changes(bufnr, firstline, lastline, new_lastline)
+ text_document_did_change_handler = function(
+ _,
+ bufnr,
+ changedtick,
+ firstline,
+ lastline,
+ new_lastline
+ )
+ -- Detach (nvim_buf_attach) via returning True to on_lines if no clients are attached
+ if tbl_isempty(all_buffer_active_clients[bufnr] or {}) then
+ return true
end
+ util.buf_versions[bufnr] = changedtick
+ changetracking.send_changes(bufnr, firstline, lastline, new_lastline)
+ end
end
---@private
@@ -1621,9 +1634,37 @@ function lsp.buf_attach_client(bufnr, client_id)
all_buffer_active_clients[bufnr] = buffer_client_ids
local uri = vim.uri_from_bufnr(bufnr)
- local augroup = ('lsp_c_%d_b_%d_did_save'):format(client_id, bufnr)
+ local augroup = ('lsp_c_%d_b_%d_save'):format(client_id, bufnr)
+ local group = api.nvim_create_augroup(augroup, { clear = true })
+ api.nvim_create_autocmd('BufWritePre', {
+ group = group,
+ buffer = bufnr,
+ desc = 'vim.lsp: textDocument/willSave',
+ callback = function(ctx)
+ for_each_buffer_client(ctx.buf, function(client)
+ local params = {
+ textDocument = {
+ uri = uri,
+ },
+ reason = protocol.TextDocumentSaveReason.Manual,
+ }
+ if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'willSave') then
+ client.notify('textDocument/willSave', params)
+ end
+ if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'willSaveWaitUntil') then
+ local result, err =
+ client.request_sync('textDocument/willSaveWaitUntil', params, 1000, ctx.buf)
+ if result and result.result then
+ util.apply_text_edits(result.result, ctx.buf, client.offset_encoding)
+ elseif err then
+ log.error(vim.inspect(err))
+ end
+ end
+ end)
+ end,
+ })
api.nvim_create_autocmd('BufWritePost', {
- group = api.nvim_create_augroup(augroup, { clear = true }),
+ group = group,
buffer = bufnr,
desc = 'vim.lsp: textDocument/didSave handler',
callback = function(ctx)
@@ -1761,16 +1802,15 @@ end
---
--- You can also use the `stop()` function on a |vim.lsp.client| object.
--- To stop all clients:
----
---- <pre>
+--- <pre>lua
--- vim.lsp.stop_client(vim.lsp.get_active_clients())
--- </pre>
---
--- By default asks the server to shutdown, unless stop was requested
--- already for this client, then force-shutdown is attempted.
---
----@param client_id client id or |vim.lsp.client| object, or list thereof
----@param force boolean (optional) shutdown forcefully
+---@param client_id number|table id or |vim.lsp.client| object, or list thereof
+---@param force boolean|nil shutdown forcefully
function lsp.stop_client(client_id, force)
local ids = type(client_id) == 'table' and client_id or { client_id }
for _, id in ipairs(ids) do
@@ -1784,10 +1824,16 @@ function lsp.stop_client(client_id, force)
end
end
+---@class vim.lsp.get_active_clients.filter
+---@field id number|nil Match clients by id
+---@field bufnr number|nil match clients attached to the given buffer
+---@field name string|nil match clients by name
+
--- Get active clients.
---
----@param filter (table|nil) A table with key-value pairs used to filter the
---- returned clients. The available keys are:
+---@param filter vim.lsp.get_active_clients.filter|nil (table|nil) A table with
+--- key-value pairs used to filter the returned clients.
+--- The available keys are:
--- - id (number): Only return clients with the given id
--- - bufnr (number): Only return clients attached to this buffer
--- - name (string): Only return clients with the given name
@@ -1804,7 +1850,8 @@ function lsp.get_active_clients(filter)
for client_id in pairs(t) do
local client = active_clients[client_id]
if
- (filter.id == nil or client.id == filter.id)
+ client
+ and (filter.id == nil or client.id == filter.id)
and (filter.name == nil or client.name == filter.name)
then
clients[#clients + 1] = client
@@ -1935,7 +1982,7 @@ end
---
---@param bufnr (number) Buffer handle, or 0 for current.
---@param method (string) LSP method name
----@param params (optional, table) Parameters to send to the server
+---@param params (table|nil) Parameters to send to the server
---@param callback (function) The callback to call when all requests are finished.
-- Unlike `buf_request`, this will collect all the responses from each server instead of handling them.
-- A map of client_id:request_result will be provided to the callback
@@ -1977,9 +2024,9 @@ end
---
---@param bufnr (number) Buffer handle, or 0 for current.
---@param method (string) LSP method name
----@param params (optional, table) Parameters to send to the server
----@param timeout_ms (optional, number, default=1000) Maximum time in
---- milliseconds to wait for a result.
+---@param params (table|nil) Parameters to send to the server
+---@param timeout_ms (number|nil) Maximum time in milliseconds to wait for a
+--- result. Defaults to 1000
---
---@returns Map of client_id:request_result. On timeout, cancel or error,
--- returns `(nil, err)` where `err` is a string describing the failure
@@ -2004,9 +2051,9 @@ function lsp.buf_request_sync(bufnr, method, params, timeout_ms)
end
--- Send a notification to a server
----@param bufnr [number] (optional): The number of the buffer
----@param method [string]: Name of the request method
----@param params [string]: Arguments to send to the server
+---@param bufnr (number|nil) The number of the buffer
+---@param method (string) Name of the request method
+---@param params (any) Arguments to send to the server
---
---@returns true if any client returns true; false otherwise
function lsp.buf_notify(bufnr, method, params)
@@ -2047,8 +2094,8 @@ end
---@see |complete-items|
---@see |CompleteDone|
---
----@param findstart 0 or 1, decides behavior
----@param base If findstart=0, text to match against
+---@param findstart number 0 or 1, decides behavior
+---@param base number findstart=0, text to match against
---
---@returns (number) Decided by {findstart}:
--- - findstart=0: column where the completion starts, or -2 or -3
@@ -2177,8 +2224,8 @@ end
--- Otherwise, uses "workspace/symbol". If no results are returned from
--- any LSP servers, falls back to using built-in tags.
---
----@param pattern Pattern used to find a workspace symbol
----@param flags See |tag-function|
+---@param pattern string Pattern used to find a workspace symbol
+---@param flags string See |tag-function|
---
---@returns A list of matching tags
function lsp.tagfunc(...)
@@ -2187,7 +2234,7 @@ end
---Checks whether a client is stopped.
---
----@param client_id (Number)
+---@param client_id (number)
---@returns true if client is stopped, false otherwise.
function lsp.client_is_stopped(client_id)
return active_clients[client_id] == nil
@@ -2196,7 +2243,7 @@ end
--- Gets a map of client_id:client pairs for the given buffer, where each value
--- is a |vim.lsp.client| object.
---
----@param bufnr (optional, number): Buffer handle, or 0 for current
+---@param bufnr (number|nil): Buffer handle, or 0 for current
---@returns (table) Table of (client_id, client) pairs
---@deprecated Use |vim.lsp.get_active_clients()| instead.
function lsp.buf_get_clients(bufnr)
@@ -2225,7 +2272,7 @@ lsp.log_levels = log.levels
---
---@see |vim.lsp.log_levels|
---
----@param level [number|string] the case insensitive level name or number
+---@param level (number|string) the case insensitive level name or number
function lsp.set_log_level(level)
if type(level) == 'string' or type(level) == 'number' then
log.set_level(level)
@@ -2246,7 +2293,7 @@ end
---@param fn function Function to run on each client attached to buffer
--- {bufnr}. The function takes the client, client ID, and
--- buffer number as arguments. Example:
---- <pre>
+--- <pre>lua
--- vim.lsp.for_each_buffer_client(0, function(client, client_id, bufnr)
--- print(vim.inspect(client))
--- end)
diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua
index c593e72d62..6ac885c78f 100644
--- a/runtime/lua/vim/lsp/buf.lua
+++ b/runtime/lua/vim/lsp/buf.lua
@@ -162,11 +162,11 @@ end
--- Predicate used to filter clients. Receives a client as argument and must return a
--- boolean. Clients matching the predicate are included. Example:
---
---- <pre>
---- -- Never request typescript-language-server for formatting
---- vim.lsp.buf.format {
---- filter = function(client) return client.name ~= "tsserver" end
---- }
+--- <pre>lua
+--- -- Never request typescript-language-server for formatting
+--- vim.lsp.buf.format {
+--- filter = function(client) return client.name ~= "tsserver" end
+--- }
--- </pre>
---
--- - async boolean|nil
@@ -197,20 +197,21 @@ function M.format(options)
clients = vim.tbl_filter(options.filter, clients)
end
+ local mode = api.nvim_get_mode().mode
+ local range = options.range
+ if not range and mode == 'v' or mode == 'V' then
+ range = range_from_selection()
+ end
+ local method = range and 'textDocument/rangeFormatting' or 'textDocument/formatting'
+
clients = vim.tbl_filter(function(client)
- return client.supports_method('textDocument/formatting')
+ return client.supports_method(method)
end, clients)
if #clients == 0 then
vim.notify('[LSP] Format request failed, no matching language servers.')
end
- local mode = api.nvim_get_mode().mode
- local range = options.range
- if not range and mode == 'v' or mode == 'V' then
- range = range_from_selection()
- end
-
---@private
local function set_range(client, params)
if range then
@@ -221,7 +222,6 @@ function M.format(options)
return params
end
- local method = range and 'textDocument/rangeFormatting' or 'textDocument/formatting'
if options.async then
local do_format
do_format = function(idx, client)
@@ -383,7 +383,7 @@ end
--- Lists all the references to the symbol under the cursor in the quickfix window.
---
----@param context (table) Context for the request
+---@param context (table|nil) Context for the request
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references
---@param options table|nil additional options
--- - on_list: (function) handler for list results. See |lsp-on-list-handler|
@@ -464,7 +464,7 @@ end
---
function M.list_workspace_folders()
local workspace_folders = {}
- for _, client in pairs(vim.lsp.buf_get_clients()) do
+ for _, client in pairs(vim.lsp.get_active_clients({ bufnr = 0 })) do
for _, folder in pairs(client.workspace_folders or {}) do
table.insert(workspace_folders, folder.name)
end
@@ -487,9 +487,9 @@ function M.add_workspace_folder(workspace_folder)
end
local params = util.make_workspace_params(
{ { uri = vim.uri_from_fname(workspace_folder), name = workspace_folder } },
- { {} }
+ {}
)
- for _, client in pairs(vim.lsp.buf_get_clients()) do
+ for _, client in pairs(vim.lsp.get_active_clients({ bufnr = 0 })) do
local found = false
for _, folder in pairs(client.workspace_folders or {}) do
if folder.name == workspace_folder then
@@ -522,7 +522,7 @@ function M.remove_workspace_folder(workspace_folder)
{ {} },
{ { uri = vim.uri_from_fname(workspace_folder), name = workspace_folder } }
)
- for _, client in pairs(vim.lsp.buf_get_clients()) do
+ for _, client in pairs(vim.lsp.get_active_clients({ bufnr = 0 })) do
for idx, folder in pairs(client.workspace_folders) do
if folder.name == workspace_folder then
vim.lsp.buf_notify(0, 'workspace/didChangeWorkspaceFolders', params)
@@ -555,11 +555,10 @@ end
--- Send request to the server to resolve document highlights for the current
--- text document position. This request can be triggered by a key mapping or
--- by events such as `CursorHold`, e.g.:
----
---- <pre>
---- autocmd CursorHold <buffer> lua vim.lsp.buf.document_highlight()
---- autocmd CursorHoldI <buffer> lua vim.lsp.buf.document_highlight()
---- autocmd CursorMoved <buffer> lua vim.lsp.buf.clear_references()
+--- <pre>vim
+--- autocmd CursorHold <buffer> lua vim.lsp.buf.document_highlight()
+--- autocmd CursorHoldI <buffer> lua vim.lsp.buf.document_highlight()
+--- autocmd CursorMoved <buffer> lua vim.lsp.buf.clear_references()
--- </pre>
---
--- Note: Usage of |vim.lsp.buf.document_highlight()| requires the following highlight groups
@@ -734,6 +733,7 @@ end
--- List of LSP `CodeActionKind`s used to filter the code actions.
--- Most language servers support values like `refactor`
--- or `quickfix`.
+--- - triggerKind (number|nil): The reason why code actions were requested.
--- - filter: (function|nil)
--- Predicate taking an `CodeAction` and returning a boolean.
--- - apply: (boolean|nil)
@@ -747,6 +747,7 @@ end
--- using mark-like indexing. See |api-indexing|
---
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction
+---@see vim.lsp.protocol.constants.CodeActionTriggerKind
function M.code_action(options)
validate({ options = { options, 't', true } })
options = options or {}
@@ -756,6 +757,9 @@ function M.code_action(options)
options = { options = options }
end
local context = options.context or {}
+ if not context.triggerKind then
+ context.triggerKind = vim.lsp.protocol.CodeActionTriggerKind.Invoked
+ end
if not context.diagnostics then
local bufnr = api.nvim_get_current_buf()
context.diagnostics = vim.lsp.diagnostic.get_line_diagnostics(bufnr)
diff --git a/runtime/lua/vim/lsp/codelens.lua b/runtime/lua/vim/lsp/codelens.lua
index 4fa02c8db2..17489ed84d 100644
--- a/runtime/lua/vim/lsp/codelens.lua
+++ b/runtime/lua/vim/lsp/codelens.lua
@@ -108,13 +108,36 @@ function M.run()
end
end
+---@private
+local function resolve_bufnr(bufnr)
+ return bufnr == 0 and api.nvim_get_current_buf() or bufnr
+end
+
+--- Clear the lenses
+---
+---@param client_id number|nil filter by client_id. All clients if nil
+---@param bufnr number|nil filter by buffer. All buffers if nil
+function M.clear(client_id, bufnr)
+ local buffers = bufnr and { resolve_bufnr(bufnr) } or vim.tbl_keys(lens_cache_by_buf)
+ for _, iter_bufnr in pairs(buffers) do
+ local client_ids = client_id and { client_id } or vim.tbl_keys(namespaces)
+ for _, iter_client_id in pairs(client_ids) do
+ local ns = namespaces[iter_client_id]
+ lens_cache_by_buf[iter_bufnr][iter_client_id] = {}
+ api.nvim_buf_clear_namespace(iter_bufnr, ns, 0, -1)
+ end
+ end
+end
+
--- Display the lenses using virtual text
---
---@param lenses table of lenses to display (`CodeLens[] | null`)
---@param bufnr number
---@param client_id number
function M.display(lenses, bufnr, client_id)
+ local ns = namespaces[client_id]
if not lenses or not next(lenses) then
+ api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
return
end
local lenses_by_lnum = {}
@@ -126,7 +149,6 @@ function M.display(lenses, bufnr, client_id)
end
table.insert(line_lenses, lens)
end
- local ns = namespaces[client_id]
local num_lines = api.nvim_buf_line_count(bufnr)
for i = 0, num_lines do
local line_lenses = lenses_by_lnum[i] or {}
@@ -241,7 +263,8 @@ end
---
--- It is recommended to trigger this using an autocmd or via keymap.
---
---- <pre>
+--- Example:
+--- <pre>vim
--- autocmd BufEnter,CursorHold,InsertLeave <buffer> lua vim.lsp.codelens.refresh()
--- </pre>
---
diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua
index 1f9d084e2b..5e2bf75f1b 100644
--- a/runtime/lua/vim/lsp/diagnostic.lua
+++ b/runtime/lua/vim/lsp/diagnostic.lua
@@ -150,7 +150,7 @@ end
---
--- See |vim.diagnostic.config()| for configuration options. Handler-specific
--- configuration can be set using |vim.lsp.with()|:
---- <pre>
+--- <pre>lua
--- vim.lsp.handlers["textDocument/publishDiagnostics"] = vim.lsp.with(
--- vim.lsp.diagnostic.on_publish_diagnostics, {
--- -- Enable underline, use default values
diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua
index 93fd621161..5096100a60 100644
--- a/runtime/lua/vim/lsp/handlers.lua
+++ b/runtime/lua/vim/lsp/handlers.lua
@@ -81,22 +81,38 @@ M['window/workDoneProgress/create'] = function(_, result, ctx)
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_showMessageRequest
+---@param result lsp.ShowMessageRequestParams
M['window/showMessageRequest'] = function(_, result)
- local actions = result.actions
- print(result.message)
- local option_strings = { result.message, '\nRequest Actions:' }
- for i, action in ipairs(actions) do
- local title = action.title:gsub('\r\n', '\\r\\n')
- title = title:gsub('\n', '\\n')
- table.insert(option_strings, string.format('%d. %s', i, title))
- end
-
- -- window/showMessageRequest can return either MessageActionItem[] or null.
- local choice = vim.fn.inputlist(option_strings)
- if choice < 1 or choice > #actions then
- return vim.NIL
+ local actions = result.actions or {}
+ local co, is_main = coroutine.running()
+ if co and not is_main then
+ local opts = {
+ prompt = result.message .. ': ',
+ format_item = function(action)
+ return (action.title:gsub('\r\n', '\\r\\n')):gsub('\n', '\\n')
+ end,
+ }
+ vim.ui.select(actions, opts, function(choice)
+ -- schedule to ensure resume doesn't happen _before_ yield with
+ -- default synchronous vim.ui.select
+ vim.schedule(function()
+ coroutine.resume(co, choice or vim.NIL)
+ end)
+ end)
+ return coroutine.yield()
else
- return actions[choice]
+ local option_strings = { result.message, '\nRequest Actions:' }
+ for i, action in ipairs(actions) do
+ local title = action.title:gsub('\r\n', '\\r\\n')
+ title = title:gsub('\n', '\\n')
+ table.insert(option_strings, string.format('%d. %s', i, title))
+ end
+ local choice = vim.fn.inputlist(option_strings)
+ if choice < 1 or choice > #actions then
+ return vim.NIL
+ else
+ return actions[choice]
+ end
end
end
@@ -115,9 +131,10 @@ end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit
M['workspace/applyEdit'] = function(_, workspace_edit, ctx)
- if not workspace_edit then
- return
- end
+ assert(
+ workspace_edit,
+ 'workspace/applyEdit must be called with `ApplyWorkspaceEditParams`. Server is violating the specification'
+ )
-- TODO(ashkan) Do something more with label?
local client_id = ctx.client_id
local client = vim.lsp.get_client_by_id(client_id)
@@ -297,13 +314,15 @@ M['textDocument/completion'] = function(_, result, _, _)
end
--- |lsp-handler| for the method "textDocument/hover"
---- <pre>
---- vim.lsp.handlers["textDocument/hover"] = vim.lsp.with(
---- vim.lsp.handlers.hover, {
---- -- Use a sharp border with `FloatBorder` highlights
---- border = "single"
---- }
---- )
+--- <pre>lua
+--- vim.lsp.handlers["textDocument/hover"] = vim.lsp.with(
+--- vim.lsp.handlers.hover, {
+--- -- Use a sharp border with `FloatBorder` highlights
+--- border = "single",
+--- -- add the title in hover float window
+--- title = "hover"
+--- }
+--- )
--- </pre>
---@param config table Configuration table.
--- - border: (default=nil)
@@ -312,8 +331,14 @@ end
function M.hover(_, result, ctx, config)
config = config or {}
config.focus_id = ctx.method
+ if api.nvim_get_current_buf() ~= ctx.bufnr then
+ -- Ignore result since buffer changed. This happens for slow language servers.
+ return
+ end
if not (result and result.contents) then
- vim.notify('No information available')
+ if config.silent ~= true then
+ vim.notify('No information available')
+ end
return
end
local markdown_lines = util.convert_input_to_markdown_lines(result.contents)
@@ -377,13 +402,13 @@ M['textDocument/implementation'] = location_handler
--- |lsp-handler| for the method "textDocument/signatureHelp".
--- The active parameter is highlighted with |hl-LspSignatureActiveParameter|.
---- <pre>
---- vim.lsp.handlers["textDocument/signatureHelp"] = vim.lsp.with(
---- vim.lsp.handlers.signature_help, {
---- -- Use a sharp border with `FloatBorder` highlights
---- border = "single"
---- }
---- )
+--- <pre>lua
+--- vim.lsp.handlers["textDocument/signatureHelp"] = vim.lsp.with(
+--- vim.lsp.handlers.signature_help, {
+--- -- Use a sharp border with `FloatBorder` highlights
+--- border = "single"
+--- }
+--- )
--- </pre>
---@param config table Configuration table.
--- - border: (default=nil)
@@ -392,6 +417,10 @@ M['textDocument/implementation'] = location_handler
function M.signature_help(_, result, ctx, config)
config = config or {}
config.focus_id = ctx.method
+ if api.nvim_get_current_buf() ~= ctx.bufnr then
+ -- Ignore result since buffer changed. This happens for slow language servers.
+ return
+ end
-- When use `autocmd CompleteDone <silent><buffer> lua vim.lsp.buf.signature_help()` to call signatureHelp handler
-- If the completion item doesn't have signatures It will make noise. Change to use `print` that can use `<silent>` to ignore
if not (result and result.signatures and result.signatures[1]) then
@@ -519,15 +548,15 @@ M['window/showDocument'] = function(_, result, ctx, _)
-- TODO(lvimuser): ask the user for confirmation
local cmd
if vim.fn.has('win32') == 1 then
- cmd = { 'cmd.exe', '/c', 'start', '""', vim.fn.shellescape(uri) }
+ cmd = { 'cmd.exe', '/c', 'start', '""', uri }
elseif vim.fn.has('macunix') == 1 then
- cmd = { 'open', vim.fn.shellescape(uri) }
+ cmd = { 'open', uri }
else
- cmd = { 'xdg-open', vim.fn.shellescape(uri) }
+ cmd = { 'xdg-open', uri }
end
local ret = vim.fn.system(cmd)
- if vim.v.shellerror ~= 0 then
+ if vim.v.shell_error ~= 0 then
return {
success = false,
error = {
@@ -553,7 +582,10 @@ M['window/showDocument'] = function(_, result, ctx, _)
range = result.selection,
}
- local success = util.show_document(location, client.offset_encoding, true, result.takeFocus)
+ local success = util.show_document(location, client.offset_encoding, {
+ reuse_win = true,
+ focus = result.takeFocus,
+ })
return { success = success or false }
end
diff --git a/runtime/lua/vim/lsp/health.lua b/runtime/lua/vim/lsp/health.lua
index ba730e3d6d..987707e661 100644
--- a/runtime/lua/vim/lsp/health.lua
+++ b/runtime/lua/vim/lsp/health.lua
@@ -2,8 +2,8 @@ local M = {}
--- Performs a healthcheck for LSP
function M.check()
- local report_info = vim.fn['health#report_info']
- local report_warn = vim.fn['health#report_warn']
+ local report_info = vim.health.report_info
+ local report_warn = vim.health.report_warn
local log = require('vim.lsp.log')
local current_log_level = log.get_level()
@@ -27,6 +27,18 @@ function M.check()
local report_fn = (log_size / 1000000 > 100 and report_warn or report_info)
report_fn(string.format('Log size: %d KB', log_size / 1000))
+
+ local clients = vim.lsp.get_active_clients()
+ vim.health.report_start('vim.lsp: Active Clients')
+ if next(clients) then
+ for _, client in pairs(clients) do
+ report_info(
+ string.format('%s (id=%s, root_dir=%s)', client.name, client.id, client.config.root_dir)
+ )
+ end
+ else
+ report_info('No active clients')
+ end
end
return M
diff --git a/runtime/lua/vim/lsp/log.lua b/runtime/lua/vim/lsp/log.lua
index 6c6ba0f206..d1a78572aa 100644
--- a/runtime/lua/vim/lsp/log.lua
+++ b/runtime/lua/vim/lsp/log.lua
@@ -20,6 +20,17 @@ local format_func = function(arg)
end
do
+ ---@private
+ local function notify(msg, level)
+ if vim.in_fast_event() then
+ vim.schedule(function()
+ vim.notify(msg, level)
+ end)
+ else
+ vim.notify(msg, level)
+ end
+ end
+
local path_sep = vim.loop.os_uname().version:match('Windows') and '\\' or '/'
---@private
local function path_join(...)
@@ -53,7 +64,7 @@ do
logfile, openerr = io.open(logfilename, 'a+')
if not logfile then
local err_msg = string.format('Failed to open LSP client log file: %s', openerr)
- vim.notify(err_msg, vim.log.levels.ERROR)
+ notify(err_msg, vim.log.levels.ERROR)
return false
end
@@ -64,7 +75,7 @@ do
log_info.size / (1000 * 1000),
logfilename
)
- vim.notify(warn_msg)
+ notify(warn_msg)
end
-- Start message for logging
@@ -130,7 +141,7 @@ end
vim.tbl_add_reverse_lookup(log.levels)
--- Sets the current log level.
----@param level (string or number) One of `vim.lsp.log.levels`
+---@param level (string|number) One of `vim.lsp.log.levels`
function log.set_level(level)
if type(level) == 'string' then
current_log_level =
diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua
index 4034753322..12345b6c8c 100644
--- a/runtime/lua/vim/lsp/protocol.lua
+++ b/runtime/lua/vim/lsp/protocol.lua
@@ -20,6 +20,14 @@ function transform_schema_to_table()
end
--]=]
+---@class lsp.ShowMessageRequestParams
+---@field type lsp.MessageType
+---@field message string
+---@field actions nil|lsp.MessageActionItem[]
+
+---@class lsp.MessageActionItem
+---@field title string
+
local constants = {
DiagnosticSeverity = {
-- Reports an error.
@@ -39,6 +47,7 @@ local constants = {
Deprecated = 2,
},
+ ---@enum lsp.MessageType
MessageType = {
-- An error message.
Error = 1,
@@ -142,6 +151,7 @@ local constants = {
},
-- Represents reasons why a text document is saved.
+ ---@enum lsp.TextDocumentSaveReason
TextDocumentSaveReason = {
-- Manually triggered, e.g. by the user pressing save, by starting debugging,
-- or by an API call.
@@ -294,6 +304,17 @@ local constants = {
-- Base kind for an organize imports source action
SourceOrganizeImports = 'source.organizeImports',
},
+ -- The reason why code actions were requested.
+ ---@enum lsp.CodeActionTriggerKind
+ CodeActionTriggerKind = {
+ -- Code actions were explicitly requested by the user or by an extension.
+ Invoked = 1,
+ -- Code actions were requested automatically.
+ --
+ -- This typically happens when current selection in a file changes, but can
+ -- also be triggered when file content changes.
+ Automatic = 2,
+ },
}
for k, v in pairs(constants) do
@@ -619,14 +640,63 @@ export interface WorkspaceClientCapabilities {
function protocol.make_client_capabilities()
return {
textDocument = {
- synchronization = {
+ semanticTokens = {
dynamicRegistration = false,
+ tokenTypes = {
+ 'namespace',
+ 'type',
+ 'class',
+ 'enum',
+ 'interface',
+ 'struct',
+ 'typeParameter',
+ 'parameter',
+ 'variable',
+ 'property',
+ 'enumMember',
+ 'event',
+ 'function',
+ 'method',
+ 'macro',
+ 'keyword',
+ 'modifier',
+ 'comment',
+ 'string',
+ 'number',
+ 'regexp',
+ 'operator',
+ 'decorator',
+ },
+ tokenModifiers = {
+ 'declaration',
+ 'definition',
+ 'readonly',
+ 'static',
+ 'deprecated',
+ 'abstract',
+ 'async',
+ 'modification',
+ 'documentation',
+ 'defaultLibrary',
+ },
+ formats = { 'relative' },
+ requests = {
+ -- TODO(jdrouhard): Add support for this
+ range = false,
+ full = { delta = true },
+ },
- -- TODO(ashkan) Send textDocument/willSave before saving (BufWritePre)
- willSave = false,
+ overlappingTokenSupport = true,
+ -- TODO(jdrouhard): Add support for this
+ multilineTokenSupport = false,
+ serverCancelSupport = false,
+ augmentsSyntaxTokens = true,
+ },
+ synchronization = {
+ dynamicRegistration = false,
- -- TODO(ashkan) Implement textDocument/willSaveWaitUntil
- willSaveWaitUntil = false,
+ willSave = true,
+ willSaveWaitUntil = true,
-- Send textDocument/didSave after saving (BufWritePost)
didSave = true,
@@ -637,7 +707,7 @@ function protocol.make_client_capabilities()
codeActionLiteralSupport = {
codeActionKind = {
valueSet = (function()
- local res = vim.tbl_values(protocol.CodeActionKind)
+ local res = vim.tbl_values(constants.CodeActionKind)
table.sort(res)
return res
end)(),
@@ -742,6 +812,9 @@ function protocol.make_client_capabilities()
end)(),
},
},
+ callHierarchy = {
+ dynamicRegistration = false,
+ },
},
workspace = {
symbol = {
@@ -765,9 +838,9 @@ function protocol.make_client_capabilities()
workspaceEdit = {
resourceOperations = { 'rename', 'create', 'delete' },
},
- },
- callHierarchy = {
- dynamicRegistration = false,
+ semanticTokens = {
+ refreshSupport = true,
+ },
},
experimental = nil,
window = {
@@ -861,8 +934,8 @@ function protocol._resolve_capabilities_compat(server_capabilities)
text_document_sync_properties = {
text_document_open_close = if_nil(textDocumentSync.openClose, false),
text_document_did_change = if_nil(textDocumentSync.change, TextDocumentSyncKind.None),
- text_document_will_save = if_nil(textDocumentSync.willSave, false),
- text_document_will_save_wait_until = if_nil(textDocumentSync.willSaveWaitUntil, false),
+ text_document_will_save = if_nil(textDocumentSync.willSave, true),
+ text_document_will_save_wait_until = if_nil(textDocumentSync.willSaveWaitUntil, true),
text_document_save = if_nil(textDocumentSync.save, false),
text_document_save_include_text = if_nil(
type(textDocumentSync.save) == 'table' and textDocumentSync.save.includeText,
diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua
index ff62623544..f1492601ff 100644
--- a/runtime/lua/vim/lsp/rpc.lua
+++ b/runtime/lua/vim/lsp/rpc.lua
@@ -293,7 +293,7 @@ end
---@private
--- Sends a notification to the LSP server.
---@param method (string) The invoked LSP method
----@param params (table|nil): Parameters for the invoked LSP method
+---@param params (any): Parameters for the invoked LSP method
---@returns (bool) `true` if notification could be sent, `false` if not
function Client:notify(method, params)
return self:encode_and_send({
@@ -391,44 +391,46 @@ function Client:handle_body(body)
-- Schedule here so that the users functions don't trigger an error and
-- we can still use the result.
schedule(function()
- local status, result
- status, result, err = self:try_call(
- client_errors.SERVER_REQUEST_HANDLER_ERROR,
- self.dispatchers.server_request,
- decoded.method,
- decoded.params
- )
- local _ = log.debug()
- and log.debug(
- 'server_request: callback result',
- { status = status, result = result, err = err }
+ coroutine.wrap(function()
+ local status, result
+ status, result, err = self:try_call(
+ client_errors.SERVER_REQUEST_HANDLER_ERROR,
+ self.dispatchers.server_request,
+ decoded.method,
+ decoded.params
)
- if status then
- if result == nil and err == nil then
- error(
- string.format(
- 'method %q: either a result or an error must be sent to the server in response',
- decoded.method
- )
- )
- end
- if err then
- assert(
- type(err) == 'table',
- 'err must be a table. Use rpc_response_error to help format errors.'
- )
- local code_name = assert(
- protocol.ErrorCodes[err.code],
- 'Errors must use protocol.ErrorCodes. Use rpc_response_error to help format errors.'
+ local _ = log.debug()
+ and log.debug(
+ 'server_request: callback result',
+ { status = status, result = result, err = err }
)
- err.message = err.message or code_name
+ if status then
+ if result == nil and err == nil then
+ error(
+ string.format(
+ 'method %q: either a result or an error must be sent to the server in response',
+ decoded.method
+ )
+ )
+ end
+ if err then
+ assert(
+ type(err) == 'table',
+ 'err must be a table. Use rpc_response_error to help format errors.'
+ )
+ local code_name = assert(
+ protocol.ErrorCodes[err.code],
+ 'Errors must use protocol.ErrorCodes. Use rpc_response_error to help format errors.'
+ )
+ err.message = err.message or code_name
+ end
+ else
+ -- On an exception, result will contain the error message.
+ err = rpc_response_error(protocol.ErrorCodes.InternalError, result)
+ result = nil
end
- else
- -- On an exception, result will contain the error message.
- err = rpc_response_error(protocol.ErrorCodes.InternalError, result)
- result = nil
- end
- self:send_response(decoded.id, err, result)
+ self:send_response(decoded.id, err, result)
+ end)()
end)
-- This works because we are expecting vim.NIL here
elseif decoded.id and (decoded.result ~= vim.NIL or decoded.error ~= vim.NIL) then
diff --git a/runtime/lua/vim/lsp/semantic_tokens.lua b/runtime/lua/vim/lsp/semantic_tokens.lua
new file mode 100644
index 0000000000..b1bc48dac6
--- /dev/null
+++ b/runtime/lua/vim/lsp/semantic_tokens.lua
@@ -0,0 +1,702 @@
+local api = vim.api
+local handlers = require('vim.lsp.handlers')
+local util = require('vim.lsp.util')
+
+--- @class STTokenRange
+--- @field line number line number 0-based
+--- @field start_col number start column 0-based
+--- @field end_col number end column 0-based
+--- @field type string token type as string
+--- @field modifiers string[] token modifiers as strings
+--- @field extmark_added boolean whether this extmark has been added to the buffer yet
+---
+--- @class STCurrentResult
+--- @field version number document version associated with this result
+--- @field result_id string resultId from the server; used with delta requests
+--- @field highlights STTokenRange[] cache of highlight ranges for this document version
+--- @field tokens number[] raw token array as received by the server. used for calculating delta responses
+--- @field namespace_cleared boolean whether the namespace was cleared for this result yet
+---
+--- @class STActiveRequest
+--- @field request_id number the LSP request ID of the most recent request sent to the server
+--- @field version number the document version associated with the most recent request
+---
+--- @class STClientState
+--- @field namespace number
+--- @field active_request STActiveRequest
+--- @field current_result STCurrentResult
+
+---@class STHighlighter
+---@field active table<number, STHighlighter>
+---@field bufnr number
+---@field augroup number augroup for buffer events
+---@field debounce number milliseconds to debounce requests for new tokens
+---@field timer table uv_timer for debouncing requests for new tokens
+---@field client_state table<number, STClientState>
+local STHighlighter = { active = {} }
+
+---@private
+local function binary_search(tokens, line)
+ local lo = 1
+ local hi = #tokens
+ while lo < hi do
+ local mid = math.floor((lo + hi) / 2)
+ if tokens[mid].line < line then
+ lo = mid + 1
+ else
+ hi = mid
+ end
+ end
+ return lo
+end
+
+--- Extracts modifier strings from the encoded number in the token array
+---
+---@private
+---@return string[]
+local function modifiers_from_number(x, modifiers_table)
+ local modifiers = {}
+ local idx = 1
+ while x > 0 do
+ if _G.bit then
+ if _G.bit.band(x, 1) == 1 then
+ modifiers[#modifiers + 1] = modifiers_table[idx]
+ end
+ x = _G.bit.rshift(x, 1)
+ else
+ --TODO(jdrouhard): remove this branch once `bit` module is available for non-LuaJIT (#21222)
+ if x % 2 == 1 then
+ modifiers[#modifiers + 1] = modifiers_table[idx]
+ end
+ x = math.floor(x / 2)
+ end
+ idx = idx + 1
+ end
+
+ return modifiers
+end
+
+--- Converts a raw token list to a list of highlight ranges used by the on_win callback
+---
+---@private
+---@return STTokenRange[]
+local function tokens_to_ranges(data, bufnr, client)
+ local legend = client.server_capabilities.semanticTokensProvider.legend
+ local token_types = legend.tokenTypes
+ local token_modifiers = legend.tokenModifiers
+ local ranges = {}
+
+ local line
+ local start_char = 0
+ for i = 1, #data, 5 do
+ local delta_line = data[i]
+ line = line and line + delta_line or delta_line
+ local delta_start = data[i + 1]
+ start_char = delta_line == 0 and start_char + delta_start or delta_start
+
+ -- data[i+3] +1 because Lua tables are 1-indexed
+ local token_type = token_types[data[i + 3] + 1]
+ local modifiers = modifiers_from_number(data[i + 4], token_modifiers)
+
+ ---@private
+ local function _get_byte_pos(char_pos)
+ return util._get_line_byte_from_position(bufnr, {
+ line = line,
+ character = char_pos,
+ }, client.offset_encoding)
+ end
+
+ local start_col = _get_byte_pos(start_char)
+ local end_col = _get_byte_pos(start_char + data[i + 2])
+
+ if token_type then
+ ranges[#ranges + 1] = {
+ line = line,
+ start_col = start_col,
+ end_col = end_col,
+ type = token_type,
+ modifiers = modifiers,
+ extmark_added = false,
+ }
+ end
+ end
+
+ return ranges
+end
+
+--- Construct a new STHighlighter for the buffer
+---
+---@private
+---@param bufnr number
+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.client_state = {}
+
+ STHighlighter.active[bufnr] = self
+
+ api.nvim_buf_attach(bufnr, false, {
+ on_lines = function(_, buf)
+ local highlighter = STHighlighter.active[buf]
+ if not highlighter then
+ return true
+ end
+ highlighter:on_change()
+ end,
+ on_reload = function(_, buf)
+ local highlighter = STHighlighter.active[buf]
+ if highlighter then
+ highlighter:reset()
+ highlighter:send_request()
+ end
+ end,
+ on_detach = function(_, buf)
+ local highlighter = STHighlighter.active[buf]
+ if highlighter then
+ highlighter:destroy()
+ end
+ end,
+ })
+
+ api.nvim_create_autocmd({ 'BufWinEnter', 'InsertLeave' }, {
+ buffer = self.bufnr,
+ group = self.augroup,
+ callback = function()
+ self:send_request()
+ end,
+ })
+
+ api.nvim_create_autocmd('LspDetach', {
+ buffer = self.bufnr,
+ group = self.augroup,
+ callback = function(args)
+ self:detach(args.data.client_id)
+ if vim.tbl_isempty(self.client_state) then
+ self:destroy()
+ end
+ end,
+ })
+
+ return self
+end
+
+---@private
+function STHighlighter:destroy()
+ for client_id, _ in pairs(self.client_state) do
+ self:detach(client_id)
+ end
+
+ api.nvim_del_augroup_by_id(self.augroup)
+ STHighlighter.active[self.bufnr] = nil
+end
+
+---@private
+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),
+ active_request = {},
+ current_result = {},
+ }
+ self.client_state[client_id] = state
+ end
+end
+
+---@private
+function STHighlighter:detach(client_id)
+ local state = self.client_state[client_id]
+ if state then
+ --TODO: delete namespace if/when that becomes possible
+ api.nvim_buf_clear_namespace(self.bufnr, state.namespace, 0, -1)
+ self.client_state[client_id] = nil
+ end
+end
+
+--- This is the entry point for getting all the tokens in a buffer.
+---
+--- For the given clients (or all attached, if not provided), this sends a request
+--- to ask for semantic tokens. If the server supports delta requests, that will
+--- be prioritized if we have a previous requestId and token array.
+---
+--- This function will skip servers where there is an already an active request in
+--- flight for the same version. If there is a stale request in flight, that is
+--- cancelled prior to sending a new one.
+---
+--- Finally, if the request was successful, the requestId and document version
+--- are saved to facilitate document synchronization in the response.
+---
+---@private
+function STHighlighter:send_request()
+ local version = util.buf_versions[self.bufnr]
+
+ self:reset_timer()
+
+ for client_id, state in pairs(self.client_state) do
+ local client = vim.lsp.get_client_by_id(client_id)
+
+ local current_result = state.current_result
+ local active_request = state.active_request
+
+ -- Only send a request for this client if the current result is out of date and
+ -- there isn't a current a request in flight for this version
+ if client and current_result.version ~= version and active_request.version ~= version then
+ -- cancel stale in-flight request
+ if active_request.request_id then
+ client.cancel_request(active_request.request_id)
+ active_request = {}
+ state.active_request = active_request
+ end
+
+ local spec = client.server_capabilities.semanticTokensProvider.full
+ local hasEditProvider = type(spec) == 'table' and spec.delta
+
+ local params = { textDocument = util.make_text_document_params(self.bufnr) }
+ local method = 'textDocument/semanticTokens/full'
+
+ if hasEditProvider and current_result.result_id then
+ method = method .. '/delta'
+ params.previousResultId = current_result.result_id
+ end
+ local success, request_id = client.request(method, params, function(err, response, ctx)
+ -- look client up again using ctx.client_id instead of using a captured
+ -- client object
+ local c = vim.lsp.get_client_by_id(ctx.client_id)
+ local highlighter = STHighlighter.active[ctx.bufnr]
+ if not err and c and highlighter then
+ highlighter:process_response(response, c, version)
+ end
+ end, self.bufnr)
+
+ if success then
+ active_request.request_id = request_id
+ active_request.version = version
+ end
+ end
+ end
+end
+
+--- This function will parse the semantic token responses and set up the cache
+--- (current_result). It also performs document synchronization by checking the
+--- version of the document associated with the resulting request_id and only
+--- performing work if the response is not out-of-date.
+---
+--- Delta edits are applied if necessary, and new highlight ranges are calculated
+--- and stored in the buffer state.
+---
+--- Finally, a redraw command is issued to force nvim to redraw the screen to
+--- pick up changed highlight tokens.
+---
+---@private
+function STHighlighter:process_response(response, client, version)
+ local state = self.client_state[client.id]
+ if not state then
+ return
+ end
+
+ -- ignore stale responses
+ if state.active_request.version and version ~= state.active_request.version then
+ return
+ end
+
+ -- reset active request
+ state.active_request = {}
+
+ -- skip nil responses
+ if response == nil then
+ return
+ end
+
+ -- if we have a response to a delta request, update the state of our tokens
+ -- appropriately. if it's a full response, just use that
+ local tokens
+ local token_edits = response.edits
+ if token_edits then
+ table.sort(token_edits, function(a, b)
+ return a.start < b.start
+ end)
+
+ tokens = {}
+ local old_tokens = state.current_result.tokens
+ local idx = 1
+ for _, token_edit in ipairs(token_edits) do
+ vim.list_extend(tokens, old_tokens, idx, token_edit.start)
+ if token_edit.data then
+ vim.list_extend(tokens, token_edit.data)
+ end
+ idx = token_edit.start + token_edit.deleteCount + 1
+ end
+ vim.list_extend(tokens, old_tokens, idx)
+ else
+ tokens = response.data
+ end
+
+ -- Update the state with the new results
+ local current_result = state.current_result
+ current_result.version = version
+ current_result.result_id = response.resultId
+ current_result.tokens = tokens
+ current_result.highlights = tokens_to_ranges(tokens, self.bufnr, client)
+ current_result.namespace_cleared = false
+
+ api.nvim_command('redraw!')
+end
+
+--- on_win handler for the decoration provider (see |nvim_set_decoration_provider|)
+---
+--- If there is a current result for the buffer and the version matches the
+--- current document version, then the tokens are valid and can be applied. As
+--- the buffer is drawn, this function will add extmark highlights for every
+--- token in the range of visible lines. Once a highlight has been added, it
+--- sticks around until the document changes and there's a new set of matching
+--- highlight tokens available.
+---
+--- If this is the first time a buffer is being drawn with a new set of
+--- highlights for the current document version, the namespace is cleared to
+--- remove extmarks from the last version. It's done here instead of the response
+--- handler to avoid the "blink" that occurs due to the timing between the
+--- response handler and the actual redraw.
+---
+---@private
+function STHighlighter:on_win(topline, botline)
+ for _, state in pairs(self.client_state) do
+ local current_result = state.current_result
+ if current_result.version and current_result.version == util.buf_versions[self.bufnr] then
+ if not current_result.namespace_cleared then
+ api.nvim_buf_clear_namespace(self.bufnr, state.namespace, 0, -1)
+ current_result.namespace_cleared = true
+ end
+
+ -- We can't use ephemeral extmarks because the buffer updates are not in
+ -- sync with the list of semantic tokens. There's a delay between the
+ -- buffer changing and when the LSP server can respond with updated
+ -- tokens, and we don't want to "blink" the token highlights while
+ -- updates are in flight, and we don't want to use stale tokens because
+ -- they likely won't line up right with the actual buffer.
+ --
+ -- Instead, we have to use normal extmarks that can attach to locations
+ -- in the buffer and are persisted between redraws.
+ local highlights = current_result.highlights
+ local idx = binary_search(highlights, topline)
+
+ for i = idx, #highlights do
+ local token = highlights[i]
+
+ if token.line > botline then
+ break
+ end
+
+ if not token.extmark_added then
+ -- `strict = false` is necessary here for the 1% of cases where the
+ -- current result doesn't actually match the buffer contents. Some
+ -- LSP servers can respond with stale tokens on requests if they are
+ -- still processing changes from a didChange notification.
+ --
+ -- LSP servers that do this _should_ follow up known stale responses
+ -- with a refresh notification once they've finished processing the
+ -- didChange notification, which would re-synchronize the tokens from
+ -- our end.
+ --
+ -- The server I know of that does this is clangd when the preamble of
+ -- a file changes and the token request is processed with a stale
+ -- preamble while the new one is still being built. Once the preamble
+ -- finishes, clangd sends a refresh request which lets the client
+ -- re-synchronize the tokens.
+ api.nvim_buf_set_extmark(self.bufnr, state.namespace, token.line, token.start_col, {
+ hl_group = '@' .. token.type,
+ end_col = token.end_col,
+ priority = vim.highlight.priorities.semantic_tokens,
+ strict = false,
+ })
+
+ -- TODO(bfredl) use single extmark when hl_group supports table
+ if #token.modifiers > 0 then
+ for _, modifier in pairs(token.modifiers) do
+ api.nvim_buf_set_extmark(self.bufnr, state.namespace, token.line, token.start_col, {
+ hl_group = '@' .. modifier,
+ end_col = token.end_col,
+ priority = vim.highlight.priorities.semantic_tokens + 1,
+ strict = false,
+ })
+ end
+ end
+
+ token.extmark_added = true
+ end
+ end
+ end
+ end
+end
+
+--- Reset the buffer's highlighting state and clears the extmark highlights.
+---
+---@private
+function STHighlighter:reset()
+ for client_id, state in pairs(self.client_state) do
+ api.nvim_buf_clear_namespace(self.bufnr, state.namespace, 0, -1)
+ state.current_result = {}
+ if state.active_request.request_id then
+ local client = vim.lsp.get_client_by_id(client_id)
+ assert(client)
+ client.cancel_request(state.active_request.request_id)
+ state.active_request = {}
+ end
+ end
+end
+
+--- Mark a client's results as dirty. This method will cancel any active
+--- requests to the server and pause new highlights from being added
+--- in the on_win callback. The rest of the current results are saved
+--- in case the server supports delta requests.
+---
+---@private
+---@param client_id number
+function STHighlighter:mark_dirty(client_id)
+ local state = self.client_state[client_id]
+ assert(state)
+
+ -- if we clear the version from current_result, it'll cause the
+ -- next request to be sent and will also pause new highlights
+ -- from being added in on_win until a new result comes from
+ -- the server
+ if state.current_result then
+ state.current_result.version = nil
+ end
+
+ if state.active_request.request_id then
+ local client = vim.lsp.get_client_by_id(client_id)
+ assert(client)
+ client.cancel_request(state.active_request.request_id)
+ state.active_request = {}
+ end
+end
+
+---@private
+function STHighlighter:on_change()
+ self:reset_timer()
+ if self.debounce > 0 then
+ self.timer = vim.defer_fn(function()
+ self:send_request()
+ end, self.debounce)
+ else
+ self:send_request()
+ end
+end
+
+---@private
+function STHighlighter:reset_timer()
+ local timer = self.timer
+ if timer then
+ self.timer = nil
+ if not timer:is_closing() then
+ timer:stop()
+ timer:close()
+ end
+ end
+end
+
+local M = {}
+
+--- Start the semantic token highlighting engine for the given buffer with the
+--- given client. The client must already be attached to the buffer.
+---
+--- NOTE: This is currently called automatically by |vim.lsp.buf_attach_client()|. To
+--- opt-out of semantic highlighting with a server that supports it, you can
+--- delete the semanticTokensProvider table from the {server_capabilities} of
+--- your client in your |LspAttach| callback or your configuration's
+--- `on_attach` callback:
+--- <pre>lua
+--- client.server_capabilities.semanticTokensProvider = nil
+--- </pre>
+---
+---@param bufnr number
+---@param client_id number
+---@param opts (nil|table) Optional keyword arguments
+--- - debounce (number, default: 200): Debounce token requests
+--- to the server by the given number in milliseconds
+function M.start(bufnr, client_id, opts)
+ vim.validate({
+ bufnr = { bufnr, 'n', false },
+ client_id = { client_id, 'n', false },
+ })
+
+ opts = opts or {}
+ assert(
+ (not opts.debounce or type(opts.debounce) == 'number'),
+ 'opts.debounce must be a number with the debounce time in milliseconds'
+ )
+
+ local client = vim.lsp.get_client_by_id(client_id)
+ if not client then
+ vim.notify('[LSP] No client with id ' .. client_id, vim.log.levels.ERROR)
+ return
+ end
+
+ if not vim.lsp.buf_is_attached(bufnr, client_id) then
+ vim.notify(
+ '[LSP] Client with id ' .. client_id .. ' not attached to buffer ' .. bufnr,
+ vim.log.levels.WARN
+ )
+ return
+ end
+
+ if not vim.tbl_get(client.server_capabilities, 'semanticTokensProvider', 'full') then
+ vim.notify('[LSP] Server does not support semantic tokens', vim.log.levels.WARN)
+ return
+ end
+
+ local highlighter = STHighlighter.active[bufnr]
+
+ if not highlighter then
+ highlighter = STHighlighter.new(bufnr)
+ highlighter.debounce = opts.debounce or 200
+ else
+ highlighter.debounce = math.max(highlighter.debounce, opts.debounce or 200)
+ end
+
+ highlighter:attach(client_id)
+ highlighter:send_request()
+end
+
+--- Stop the semantic token highlighting engine for the given buffer with the
+--- given client.
+---
+--- NOTE: This is automatically called by a |LspDetach| autocmd that is set up as part
+--- of `start()`, so you should only need this function to manually disengage the semantic
+--- token engine without fully detaching the LSP client from the buffer.
+---
+---@param bufnr number
+---@param client_id number
+function M.stop(bufnr, client_id)
+ vim.validate({
+ bufnr = { bufnr, 'n', false },
+ client_id = { client_id, 'n', false },
+ })
+
+ local highlighter = STHighlighter.active[bufnr]
+ if not highlighter then
+ return
+ end
+
+ highlighter:detach(client_id)
+
+ if vim.tbl_isempty(highlighter.client_state) then
+ highlighter:destroy()
+ end
+end
+
+--- Return the semantic token(s) at the given position.
+--- If called without arguments, returns the token under the cursor.
+---
+---@param bufnr number|nil Buffer number (0 for current buffer, default)
+---@param row number|nil Position row (default cursor position)
+---@param col number|nil Position column (default cursor position)
+---
+---@return table|nil (table|nil) List of tokens at position
+function M.get_at_pos(bufnr, row, col)
+ if bufnr == nil or bufnr == 0 then
+ bufnr = api.nvim_get_current_buf()
+ end
+
+ local highlighter = STHighlighter.active[bufnr]
+ if not highlighter then
+ return
+ end
+
+ if row == nil or col == nil then
+ local cursor = api.nvim_win_get_cursor(0)
+ row, col = cursor[1] - 1, cursor[2]
+ end
+
+ local tokens = {}
+ for client_id, client in pairs(highlighter.client_state) do
+ local highlights = client.current_result.highlights
+ if highlights then
+ local idx = binary_search(highlights, row)
+ for i = idx, #highlights do
+ local token = highlights[i]
+
+ if token.line > row then
+ break
+ end
+
+ if token.start_col <= col and token.end_col > col then
+ token.client_id = client_id
+ tokens[#tokens + 1] = token
+ end
+ end
+ end
+ end
+ return tokens
+end
+
+--- Force a refresh of all semantic tokens
+---
+--- Only has an effect if the buffer is currently active for semantic token
+--- highlighting (|vim.lsp.semantic_tokens.start()| has been called for it)
+---
+---@param bufnr (nil|number) default: current buffer
+function M.force_refresh(bufnr)
+ vim.validate({
+ bufnr = { bufnr, 'n', true },
+ })
+
+ if bufnr == nil or bufnr == 0 then
+ bufnr = api.nvim_get_current_buf()
+ end
+
+ local highlighter = STHighlighter.active[bufnr]
+ if not highlighter then
+ return
+ end
+
+ highlighter:reset()
+ highlighter:send_request()
+end
+
+--- |lsp-handler| for the method `workspace/semanticTokens/refresh`
+---
+--- Refresh requests are sent by the server to indicate a project-wide change
+--- that requires all tokens to be re-requested by the client. This handler will
+--- invalidate the current results of all buffers and automatically kick off a
+--- new request for buffers that are displayed in a window. For those that aren't, a
+--- the BufWinEnter event should take care of it next time it's displayed.
+---
+---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#semanticTokens_refreshRequest
+handlers['workspace/semanticTokens/refresh'] = function(err, _, ctx)
+ if err then
+ return vim.NIL
+ end
+
+ for _, bufnr in ipairs(vim.lsp.get_buffers_by_client_id(ctx.client_id)) do
+ local highlighter = STHighlighter.active[bufnr]
+ if highlighter and highlighter.client_state[ctx.client_id] then
+ highlighter:mark_dirty(ctx.client_id)
+
+ if not vim.tbl_isempty(vim.fn.win_findbuf(bufnr)) then
+ highlighter:send_request()
+ end
+ end
+ end
+
+ return vim.NIL
+end
+
+local namespace = api.nvim_create_namespace('vim_lsp_semantic_tokens')
+api.nvim_set_decoration_provider(namespace, {
+ on_win = function(_, _, bufnr, topline, botline)
+ local highlighter = STHighlighter.active[bufnr]
+ if highlighter then
+ highlighter:on_win(topline, botline)
+ end
+ end,
+})
+
+--- for testing only! there is no guarantee of API stability with this!
+---
+---@private
+M.__STHighlighter = STHighlighter
+
+return M
diff --git a/runtime/lua/vim/lsp/sync.lua b/runtime/lua/vim/lsp/sync.lua
index 0d65e86b55..826352f036 100644
--- a/runtime/lua/vim/lsp/sync.lua
+++ b/runtime/lua/vim/lsp/sync.lua
@@ -392,7 +392,7 @@ end
---@param lastline number line to begin search in old_lines for last difference
---@param new_lastline number line to begin search in new_lines for last difference
---@param offset_encoding string encoding requested by language server
----@returns table TextDocumentContentChangeEvent see https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocumentContentChangeEvent
+---@returns table TextDocumentContentChangeEvent see https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentContentChangeEvent
function M.compute_diff(
prev_lines,
curr_lines,
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index b0f9c1660e..38051e6410 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -856,7 +856,7 @@ end
--- `textDocument/signatureHelp`, and potentially others.
---
---@param input (`MarkedString` | `MarkedString[]` | `MarkupContent`)
----@param contents (table, optional, default `{}`) List of strings to extend with converted lines
+---@param contents (table|nil) List of strings to extend with converted lines. Defaults to {}.
---@returns {contents}, extended with lines of converted markdown.
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover
function M.convert_input_to_markdown_lines(input, contents)
@@ -1015,6 +1015,7 @@ end
--- - border (string or table) override `border`
--- - focusable (string or table) override `focusable`
--- - zindex (string or table) override `zindex`, defaults to 50
+--- - relative ("mouse"|"cursor") defaults to "cursor"
---@returns (table) Options
function M.make_floating_popup_options(width, height, opts)
validate({
@@ -1029,7 +1030,8 @@ function M.make_floating_popup_options(width, height, opts)
local anchor = ''
local row, col
- local lines_above = vim.fn.winline() - 1
+ local lines_above = opts.relative == 'mouse' and vim.fn.getmousepos().line - 1
+ or vim.fn.winline() - 1
local lines_below = vim.fn.winheight(0) - lines_above
if lines_above < lines_below then
@@ -1042,7 +1044,9 @@ function M.make_floating_popup_options(width, height, opts)
row = 0
end
- if vim.fn.wincol() + width + (opts.offset_x or 0) <= api.nvim_get_option('columns') then
+ local wincol = opts.relative == 'mouse' and vim.fn.getmousepos().column or vim.fn.wincol()
+
+ if wincol + width + (opts.offset_x or 0) <= api.nvim_get_option('columns') then
anchor = anchor .. 'W'
col = 0
else
@@ -1050,17 +1054,26 @@ function M.make_floating_popup_options(width, height, opts)
col = 1
end
+ local title = (opts.border and opts.title) and opts.title or nil
+ local title_pos
+
+ if title then
+ title_pos = opts.title_pos or 'center'
+ end
+
return {
anchor = anchor,
col = col + (opts.offset_x or 0),
height = height,
focusable = opts.focusable,
- relative = 'cursor',
+ relative = opts.relative == 'mouse' and 'mouse' or 'cursor',
row = row + (opts.offset_y or 0),
style = 'minimal',
width = width,
border = opts.border or default_border,
zindex = opts.zindex or 50,
+ title = title,
+ title_pos = title_pos,
}
end
@@ -1068,7 +1081,7 @@ end
---
---@param location table (`Location`|`LocationLink`)
---@param offset_encoding "utf-8" | "utf-16" | "utf-32"
----@param opts table options
+---@param opts table|nil options
--- - reuse_win (boolean) Jump to existing window if buffer is already open.
--- - focus (boolean) Whether to focus/jump to location if possible. Defaults to true.
---@return boolean `true` if succeeded
@@ -1125,7 +1138,7 @@ end
---
---@param location table (`Location`|`LocationLink`)
---@param offset_encoding "utf-8" | "utf-16" | "utf-32"
----@param reuse_win boolean Jump to existing window if buffer is already open.
+---@param reuse_win boolean|nil Jump to existing window if buffer is already open.
---@return boolean `true` if the jump succeeded
function M.jump_to_location(location, offset_encoding, reuse_win)
if offset_encoding == nil then
@@ -1252,7 +1265,7 @@ function M.stylize_markdown(bufnr, contents, opts)
-- when ft is nil, we get the ft from the regex match
local matchers = {
block = { nil, '```+([a-zA-Z0-9_]*)', '```+' },
- pre = { '', '<pre>', '</pre>' },
+ pre = { nil, '<pre>([a-z0-9]*)', '</pre>' },
code = { '', '<code>', '</code>' },
text = { 'text', '<text>', '</text>' },
}
@@ -1277,8 +1290,6 @@ function M.stylize_markdown(bufnr, contents, opts)
-- Clean up
contents = M._trim(contents, opts)
- -- Insert blank line separator after code block?
- local add_sep = opts.separator == nil and true or opts.separator
local stripped = {}
local highlights = {}
-- keep track of lnums that contain markdown
@@ -1306,7 +1317,7 @@ function M.stylize_markdown(bufnr, contents, opts)
finish = #stripped,
})
-- add a separator, but not on the last line
- if add_sep and i < #contents then
+ if opts.separator and i < #contents then
table.insert(stripped, '---')
markdown_lines[#stripped] = true
end
@@ -1670,7 +1681,7 @@ do --[[ References ]]
---@param bufnr number Buffer id
---@param references table List of `DocumentHighlight` objects to highlight
---@param offset_encoding string One of "utf-8", "utf-16", "utf-32".
- ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#documentHighlight
+ ---@see https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentContentChangeEvent
function M.buf_highlight_references(bufnr, references, offset_encoding)
validate({
bufnr = { bufnr, 'n', true },
@@ -1901,7 +1912,7 @@ end
--- Creates a `TextDocumentPositionParams` object for the current buffer and cursor position.
---
---@param window number|nil: window handle or 0 for current, defaults to current
----@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of buffer of `window`
+---@param offset_encoding string|nil utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of buffer of `window`
---@returns `TextDocumentPositionParams` object
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentPositionParams
function M.make_position_params(window, offset_encoding)
@@ -1924,7 +1935,7 @@ function M._get_offset_encoding(bufnr)
local offset_encoding
- for _, client in pairs(vim.lsp.buf_get_clients(bufnr)) do
+ for _, client in pairs(vim.lsp.get_active_clients({ bufnr = bufnr })) do
if client.offset_encoding == nil then
vim.notify_once(
string.format(
diff --git a/runtime/lua/vim/secure.lua b/runtime/lua/vim/secure.lua
new file mode 100644
index 0000000000..443b152273
--- /dev/null
+++ b/runtime/lua/vim/secure.lua
@@ -0,0 +1,188 @@
+local M = {}
+
+---@private
+--- Reads trust database from $XDG_STATE_HOME/nvim/trust.
+---
+---@return (table) Contents of trust database, if it exists. Empty table otherwise.
+local function read_trust()
+ local trust = {}
+ local f = io.open(vim.fn.stdpath('state') .. '/trust', 'r')
+ if f then
+ local contents = f:read('*a')
+ if contents then
+ for line in vim.gsplit(contents, '\n') do
+ local hash, file = string.match(line, '^(%S+) (.+)$')
+ if hash and file then
+ trust[file] = hash
+ end
+ end
+ end
+ f:close()
+ end
+ return trust
+end
+
+---@private
+--- Writes provided {trust} table to trust database at
+--- $XDG_STATE_HOME/nvim/trust.
+---
+---@param trust (table) Trust table to write
+local function write_trust(trust)
+ vim.validate({ trust = { trust, 't' } })
+ local f = assert(io.open(vim.fn.stdpath('state') .. '/trust', 'w'))
+
+ local t = {}
+ for p, h in pairs(trust) do
+ t[#t + 1] = string.format('%s %s\n', h, p)
+ end
+ f:write(table.concat(t))
+ f:close()
+end
+
+--- Attempt to read the file at {path} prompting the user if the file should be
+--- trusted. The user's choice is persisted in a trust database at
+--- $XDG_STATE_HOME/nvim/trust.
+---
+---@see |:trust|
+---
+---@param path (string) Path to a file to read.
+---
+---@return (string|nil) The contents of the given file if it exists and is
+--- trusted, or nil otherwise.
+function M.read(path)
+ vim.validate({ path = { path, 's' } })
+ local fullpath = vim.loop.fs_realpath(vim.fs.normalize(path))
+ if not fullpath then
+ return nil
+ end
+
+ local trust = read_trust()
+
+ if trust[fullpath] == '!' then
+ -- File is denied
+ return nil
+ end
+
+ local contents
+ do
+ local f = io.open(fullpath, 'r')
+ if not f then
+ return nil
+ end
+ contents = f:read('*a')
+ f:close()
+ end
+
+ local hash = vim.fn.sha256(contents)
+ if trust[fullpath] == hash then
+ -- File already exists in trust database
+ return contents
+ end
+
+ -- File either does not exist in trust database or the hash does not match
+ local ok, result = pcall(
+ vim.fn.confirm,
+ string.format('%s is not trusted.', fullpath),
+ '&ignore\n&view\n&deny\n&allow',
+ 1
+ )
+
+ if not ok and result ~= 'Keyboard interrupt' then
+ error(result)
+ elseif not ok or result == 0 or result == 1 then
+ -- Cancelled or ignored
+ return nil
+ elseif result == 2 then
+ -- View
+ vim.cmd('sview ' .. fullpath)
+ return nil
+ elseif result == 3 then
+ -- Deny
+ trust[fullpath] = '!'
+ contents = nil
+ elseif result == 4 then
+ -- Allow
+ trust[fullpath] = hash
+ end
+
+ write_trust(trust)
+
+ return contents
+end
+
+--- Manage the trust database.
+---
+--- The trust database is located at |$XDG_STATE_HOME|/nvim/trust.
+---
+---@param opts (table):
+--- - action (string): "allow" to add a file to the trust database and trust it,
+--- "deny" to add a file to the trust database and deny it,
+--- "remove" to remove file from the trust database
+--- - path (string|nil): Path to a file to update. Mutually exclusive with {bufnr}.
+--- Cannot be used when {action} is "allow".
+--- - bufnr (number|nil): Buffer number to update. Mutually exclusive with {path}.
+---@return (boolean, string) success, msg:
+--- - true and full path of target file if operation was successful
+--- - false and error message on failure
+function M.trust(opts)
+ vim.validate({
+ path = { opts.path, 's', true },
+ bufnr = { opts.bufnr, 'n', true },
+ action = {
+ opts.action,
+ function(m)
+ return m == 'allow' or m == 'deny' or m == 'remove'
+ end,
+ [["allow" or "deny" or "remove"]],
+ },
+ })
+
+ local path = opts.path
+ local bufnr = opts.bufnr
+ local action = opts.action
+
+ assert(not path or not bufnr, '"path" and "bufnr" are mutually exclusive')
+
+ if action == 'allow' then
+ assert(not path, '"path" is not valid when action is "allow"')
+ end
+
+ local fullpath
+ if path then
+ fullpath = vim.loop.fs_realpath(vim.fs.normalize(path))
+ elseif bufnr then
+ local bufname = vim.api.nvim_buf_get_name(bufnr)
+ if bufname == '' then
+ return false, 'buffer is not associated with a file'
+ end
+ fullpath = vim.loop.fs_realpath(vim.fs.normalize(bufname))
+ else
+ error('one of "path" or "bufnr" is required')
+ end
+
+ if not fullpath then
+ return false, string.format('invalid path: %s', path)
+ end
+
+ local trust = read_trust()
+
+ if action == 'allow' then
+ local newline = vim.bo[bufnr].fileformat == 'unix' and '\n' or '\r\n'
+ local contents = table.concat(vim.api.nvim_buf_get_lines(bufnr, 0, -1, false), newline)
+ if vim.bo[bufnr].endofline then
+ contents = contents .. newline
+ end
+ local hash = vim.fn.sha256(contents)
+
+ trust[fullpath] = hash
+ elseif action == 'deny' then
+ trust[fullpath] = '!'
+ elseif action == 'remove' then
+ trust[fullpath] = nil
+ end
+
+ write_trust(trust)
+ return true, fullpath
+end
+
+return M
diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua
index f03d608e56..cc48e3f193 100644
--- a/runtime/lua/vim/shared.lua
+++ b/runtime/lua/vim/shared.lua
@@ -49,6 +49,9 @@ vim.deepcopy = (function()
if f then
return f(orig, cache or {})
else
+ if type(orig) == 'userdata' and orig == vim.NIL then
+ return vim.NIL
+ end
error('Cannot deepcopy object of type ' .. type(orig))
end
end
@@ -57,12 +60,13 @@ end)()
--- Splits a string at each instance of a separator.
---
---@see |vim.split()|
+---@see |luaref-patterns|
---@see https://www.lua.org/pil/20.2.html
---@see http://lua-users.org/wiki/StringLibraryTutorial
---
---@param s string String to split
---@param sep string Separator or pattern
----@param plain boolean If `true` use `sep` literally (passed to string.find)
+---@param plain (boolean|nil) If `true` use `sep` literally (passed to string.find)
---@return fun():string (function) Iterator over the split components
function vim.gsplit(s, sep, plain)
vim.validate({ s = { s, 's' }, sep = { sep, 's' }, plain = { plain, 'b', true } })
@@ -99,20 +103,18 @@ end
--- Splits a string at each instance of a separator.
---
--- Examples:
---- <pre>
---- split(":aa::b:", ":") => {'','aa','','b',''}
---- split("axaby", "ab?") => {'','x','y'}
---- split("x*yz*o", "*", {plain=true}) => {'x','yz','o'}
---- split("|x|y|z|", "|", {trimempty=true}) => {'x', 'y', 'z'}
+--- <pre>lua
+--- split(":aa::b:", ":") --> {'','aa','','b',''}
+--- split("axaby", "ab?") --> {'','x','y'}
+--- split("x*yz*o", "*", {plain=true}) --> {'x','yz','o'}
+--- split("|x|y|z|", "|", {trimempty=true}) --> {'x', 'y', 'z'}
--- </pre>
---
---@see |vim.gsplit()|
---
----@alias split_kwargs {plain: boolean, trimempty: boolean} | boolean | nil
----
---@param s string String to split
---@param sep string Separator or pattern
----@param kwargs? {plain: boolean, trimempty: boolean} (table|nil) Keyword arguments:
+---@param kwargs (table|nil) Keyword arguments:
--- - plain: (boolean) If `true` use `sep` literally (passed to string.find)
--- - trimempty: (boolean) If `true` remove empty items from the front
--- and back of the list
@@ -159,8 +161,8 @@ end
---
---@see From https://github.com/premake/premake-core/blob/master/src/base/table.lua
---
----@param t table<T, any> (table) Table
---@generic T: table
+---@param t table<T, any> (table) Table
---@return T[] (list) List of keys
function vim.tbl_keys(t)
assert(type(t) == 'table', string.format('Expected table, got %s', type(t)))
@@ -382,7 +384,7 @@ end
--- Return `nil` if the key does not exist.
---
--- Examples:
---- <pre>
+--- <pre>lua
--- vim.tbl_get({ key = { nested_key = true }}, 'key', 'nested_key') == true
--- vim.tbl_get({ key = {}}, 'key', 'nested_key') == nil
--- </pre>
@@ -394,15 +396,14 @@ end
function vim.tbl_get(o, ...)
local keys = { ... }
if #keys == 0 then
- return
+ return nil
end
for i, k in ipairs(keys) do
- if type(o[k]) ~= 'table' and next(keys, i) then
- return nil
- end
o = o[k]
if o == nil then
- return
+ return nil
+ elseif type(o) ~= 'table' and next(keys, i) then
+ return nil
end
end
return o
@@ -417,8 +418,8 @@ end
---@generic T: table
---@param dst T List which will be modified and appended to
---@param src table List from which values will be inserted
----@param start? number Start index on src. Defaults to 1
----@param finish? number Final index on src. Defaults to `#src`
+---@param start (number|nil) Start index on src. Defaults to 1
+---@param finish (number|nil) Final index on src. Defaults to `#src`
---@return T dst
function vim.list_extend(dst, src, start, finish)
vim.validate({
@@ -457,6 +458,33 @@ function vim.tbl_flatten(t)
return result
end
+--- Enumerate a table sorted by its keys.
+---
+---@see Based on https://github.com/premake/premake-core/blob/master/src/base/table.lua
+---
+---@param t table List-like table
+---@return iterator over sorted keys and their values
+function vim.spairs(t)
+ assert(type(t) == 'table', string.format('Expected table, got %s', type(t)))
+
+ -- collect the keys
+ local keys = {}
+ for k in pairs(t) do
+ table.insert(keys, k)
+ end
+ table.sort(keys)
+
+ -- Return the iterator function.
+ -- TODO(justinmk): Return "iterator function, table {t}, and nil", like pairs()?
+ local i = 0
+ return function()
+ i = i + 1
+ if keys[i] then
+ return keys[i], t[keys[i]]
+ end
+ end
+end
+
--- Tests if a Lua table can be treated as an array.
---
--- Empty table `{}` is assumed to be an array, unless it was created by
@@ -486,7 +514,7 @@ function vim.tbl_islist(t)
-- TODO(bfredl): in the future, we will always be inside nvim
-- then this check can be deleted.
if vim._empty_dict_mt == nil then
- return nil
+ return false
end
return getmetatable(t) ~= vim._empty_dict_mt
end
@@ -494,9 +522,9 @@ end
--- Counts the number of non-nil values in table `t`.
---
---- <pre>
---- vim.tbl_count({ a=1, b=2 }) => 2
---- vim.tbl_count({ 1, 2 }) => 2
+--- <pre>lua
+--- vim.tbl_count({ a=1, b=2 }) --> 2
+--- vim.tbl_count({ 1, 2 }) --> 2
--- </pre>
---
---@see https://github.com/Tieske/Penlight/blob/master/lua/pl/tablex.lua
@@ -516,8 +544,8 @@ end
---
---@generic T
---@param list T[] (list) Table
----@param start number Start range of slice
----@param finish number End range of slice
+---@param start number|nil Start range of slice
+---@param finish number|nil End range of slice
---@return T[] (list) Copy of table sliced from start to finish (inclusive)
function vim.list_slice(list, start, finish)
local new_list = {}
@@ -529,6 +557,7 @@ end
--- Trim whitespace (Lua pattern "%s") from both sides of a string.
---
+---@see |luaref-patterns|
---@see https://www.lua.org/pil/20.2.html
---@param s string String to trim
---@return string String with whitespace removed from its beginning and end
@@ -544,7 +573,7 @@ end
---@return string %-escaped pattern string
function vim.pesc(s)
vim.validate({ s = { s, 's' } })
- return s:gsub('[%(%)%.%%%+%-%*%?%[%]%^%$]', '%%%1')
+ return (s:gsub('[%(%)%.%%%+%-%*%?%[%]%^%$]', '%%%1'))
end
--- Tests if `s` starts with `prefix`.
@@ -570,7 +599,7 @@ end
--- Validates a parameter specification (types and values).
---
--- Usage example:
---- <pre>
+--- <pre>lua
--- function user.new(name, age, hobbies)
--- vim.validate{
--- name={name, 'string'},
@@ -582,24 +611,24 @@ end
--- </pre>
---
--- Examples with explicit argument values (can be run directly):
---- <pre>
+--- <pre>lua
--- vim.validate{arg1={{'foo'}, 'table'}, arg2={'foo', 'string'}}
---- => NOP (success)
+--- --> NOP (success)
---
--- vim.validate{arg1={1, 'table'}}
---- => error('arg1: expected table, got number')
+--- --> error('arg1: expected table, got number')
---
--- vim.validate{arg1={3, function(a) return (a % 2) == 0 end, 'even number'}}
---- => error('arg1: expected even number, got 3')
+--- --> error('arg1: expected even number, got 3')
--- </pre>
---
--- If multiple types are valid they can be given as a list.
---- <pre>
+--- <pre>lua
--- vim.validate{arg1={{'foo'}, {'table', 'string'}}, arg2={'foo', {'table', 'string'}}}
---- => NOP (success)
+--- --> NOP (success)
---
--- vim.validate{arg1={1, {'string', table'}}}
---- => error('arg1: expected string|table, got number')
+--- --> error('arg1: expected string|table, got number')
---
--- </pre>
---
@@ -734,7 +763,7 @@ end
--- If {create} is `nil`, this will create a defaulttable whose constructor function is
--- this function, effectively allowing to create nested tables on the fly:
---
---- <pre>
+--- <pre>lua
--- local a = vim.defaulttable()
--- a.b.c = 1
--- </pre>
diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua
index 6cd00516bf..582922ecb6 100644
--- a/runtime/lua/vim/treesitter.lua
+++ b/runtime/lua/vim/treesitter.lua
@@ -240,7 +240,7 @@ function M.get_captures_at_pos(bufnr, row, col)
if M.is_in_node_range(node, row, col) then
local c = q._query.captures[capture] -- name of the capture in the query
if c ~= nil then
- table.insert(matches, { capture = c, metadata = metadata })
+ table.insert(matches, { capture = c, metadata = metadata, lang = tree:lang() })
end
end
end
@@ -275,16 +275,17 @@ end
---@param row number Position row
---@param col number Position column
---@param opts table Optional keyword arguments:
+--- - lang string|nil Parser language
--- - ignore_injections boolean Ignore injected languages (default true)
---
----@return userdata |tsnode| under the cursor
+---@return userdata|nil |tsnode| under the cursor
function M.get_node_at_pos(bufnr, row, col, opts)
if bufnr == 0 then
bufnr = a.nvim_get_current_buf()
end
local ts_range = { row, col, row, col }
- local root_lang_tree = M.get_parser(bufnr)
+ local root_lang_tree = M.get_parser(bufnr, opts.lang)
if not root_lang_tree then
return
end
@@ -313,7 +314,7 @@ end
--- In this case, add ``vim.bo.syntax = 'on'`` after the call to `start`.
---
--- Example:
---- <pre>
+--- <pre>lua
--- vim.api.nvim_create_autocmd( 'FileType', { pattern = 'tex',
--- callback = function(args)
--- vim.treesitter.start(args.buf, 'latex')
@@ -326,12 +327,8 @@ end
---@param lang (string|nil) Language of the parser (default: buffer filetype)
function M.start(bufnr, lang)
bufnr = bufnr or a.nvim_get_current_buf()
-
local parser = M.get_parser(bufnr, lang)
-
M.highlighter.new(parser)
-
- vim.b[bufnr].ts_highlight = true
end
--- Stops treesitter highlighting for a buffer
@@ -343,8 +340,203 @@ function M.stop(bufnr)
if M.highlighter.active[bufnr] then
M.highlighter.active[bufnr]:destroy()
end
+end
+
+--- Open a window that displays a textual representation of the nodes in the language tree.
+---
+--- While in the window, press "a" to toggle display of anonymous nodes, "I" to toggle the
+--- display of the source language of each node, and press <Enter> to jump to the node under the
+--- cursor in the source buffer.
+---
+---@param opts table|nil Optional options table with the following possible keys:
+--- - lang (string|nil): The language of the source buffer. If omitted, the
+--- filetype of the source buffer is used.
+--- - bufnr (number|nil): Buffer to draw the tree into. If omitted, a new
+--- buffer is created.
+--- - winid (number|nil): Window id to display the tree buffer in. If omitted,
+--- a new window is created with {command}.
+--- - command (string|nil): Vimscript command to create the window. Default
+--- value is "topleft 60vnew". Only used when {winid} is nil.
+--- - title (string|fun(bufnr:number):string|nil): Title of the window. If a
+--- function, it accepts the buffer number of the source buffer as its only
+--- argument and should return a string.
+function M.show_tree(opts)
+ vim.validate({
+ opts = { opts, 't', true },
+ })
+
+ opts = opts or {}
+
+ local Playground = require('vim.treesitter.playground')
+ local buf = a.nvim_get_current_buf()
+ local win = a.nvim_get_current_win()
+ local pg = assert(Playground:new(buf, opts.lang))
+
+ -- Close any existing playground window
+ if vim.b[buf].playground then
+ local w = vim.b[buf].playground
+ if a.nvim_win_is_valid(w) then
+ a.nvim_win_close(w, true)
+ end
+ end
+
+ local w = opts.winid
+ if not w then
+ vim.cmd(opts.command or 'topleft 60vnew')
+ w = a.nvim_get_current_win()
+ end
+
+ local b = opts.bufnr
+ if b then
+ a.nvim_win_set_buf(w, b)
+ else
+ b = a.nvim_win_get_buf(w)
+ end
+
+ vim.b[buf].playground = w
+
+ vim.wo[w].scrolloff = 5
+ vim.wo[w].wrap = false
+ vim.bo[b].buflisted = false
+ vim.bo[b].buftype = 'nofile'
+ vim.bo[b].bufhidden = 'wipe'
+
+ local title = opts.title
+ if not title then
+ local bufname = a.nvim_buf_get_name(buf)
+ title = string.format('Syntax tree for %s', vim.fn.fnamemodify(bufname, ':.'))
+ elseif type(title) == 'function' then
+ title = title(buf)
+ end
+
+ assert(type(title) == 'string', 'Window title must be a string')
+ a.nvim_buf_set_name(b, title)
+
+ pg:draw(b)
+
+ vim.fn.matchadd('Comment', '\\[[0-9:-]\\+\\]')
+ vim.fn.matchadd('String', '".*"')
- vim.bo[bufnr].syntax = 'on'
+ a.nvim_buf_clear_namespace(buf, pg.ns, 0, -1)
+ a.nvim_buf_set_keymap(b, 'n', '<CR>', '', {
+ desc = 'Jump to the node under the cursor in the source buffer',
+ callback = function()
+ local row = a.nvim_win_get_cursor(w)[1]
+ local pos = pg:get(row)
+ a.nvim_set_current_win(win)
+ a.nvim_win_set_cursor(win, { pos.lnum + 1, pos.col })
+ end,
+ })
+ a.nvim_buf_set_keymap(b, 'n', 'a', '', {
+ desc = 'Toggle anonymous nodes',
+ callback = function()
+ pg.opts.anon = not pg.opts.anon
+ pg:draw(b)
+ end,
+ })
+ a.nvim_buf_set_keymap(b, 'n', 'I', '', {
+ desc = 'Toggle language display',
+ callback = function()
+ pg.opts.lang = not pg.opts.lang
+ pg:draw(b)
+ end,
+ })
+
+ local group = a.nvim_create_augroup('treesitter/playground', {})
+
+ a.nvim_create_autocmd('CursorMoved', {
+ group = group,
+ buffer = b,
+ callback = function()
+ a.nvim_buf_clear_namespace(buf, pg.ns, 0, -1)
+ local row = a.nvim_win_get_cursor(w)[1]
+ local pos = pg:get(row)
+ a.nvim_buf_set_extmark(buf, pg.ns, pos.lnum, pos.col, {
+ end_row = pos.end_lnum,
+ end_col = math.max(0, pos.end_col),
+ hl_group = 'Visual',
+ })
+ end,
+ })
+
+ a.nvim_create_autocmd('CursorMoved', {
+ group = group,
+ buffer = buf,
+ callback = function()
+ if not a.nvim_buf_is_loaded(b) then
+ return true
+ end
+
+ a.nvim_buf_clear_namespace(b, pg.ns, 0, -1)
+
+ local cursor = a.nvim_win_get_cursor(win)
+ local cursor_node = M.get_node_at_pos(buf, cursor[1] - 1, cursor[2], {
+ lang = opts.lang,
+ ignore_injections = false,
+ })
+ if not cursor_node then
+ return
+ end
+
+ local cursor_node_id = cursor_node:id()
+ for i, v in pg:iter() do
+ if v.id == cursor_node_id then
+ local start = v.depth
+ local end_col = start + #v.text
+ a.nvim_buf_set_extmark(b, pg.ns, i - 1, start, {
+ end_col = end_col,
+ hl_group = 'Visual',
+ })
+ a.nvim_win_set_cursor(w, { i, 0 })
+ break
+ end
+ end
+ end,
+ })
+
+ a.nvim_create_autocmd({ 'TextChanged', 'InsertLeave' }, {
+ group = group,
+ buffer = buf,
+ callback = function()
+ if not a.nvim_buf_is_loaded(b) then
+ return true
+ end
+
+ pg = assert(Playground:new(buf, opts.lang))
+ pg:draw(b)
+ end,
+ })
+
+ a.nvim_create_autocmd('BufLeave', {
+ group = group,
+ buffer = b,
+ callback = function()
+ a.nvim_buf_clear_namespace(buf, pg.ns, 0, -1)
+ end,
+ })
+
+ a.nvim_create_autocmd('BufLeave', {
+ group = group,
+ buffer = buf,
+ callback = function()
+ if not a.nvim_buf_is_loaded(b) then
+ return true
+ end
+
+ a.nvim_buf_clear_namespace(b, pg.ns, 0, -1)
+ end,
+ })
+
+ a.nvim_create_autocmd('BufHidden', {
+ group = group,
+ buffer = buf,
+ once = true,
+ callback = function()
+ if a.nvim_win_is_valid(w) then
+ a.nvim_win_close(w, true)
+ end
+ end,
+ })
end
return M
diff --git a/runtime/lua/vim/treesitter/health.lua b/runtime/lua/vim/treesitter/health.lua
index 4995c80a02..c0a1eca0ce 100644
--- a/runtime/lua/vim/treesitter/health.lua
+++ b/runtime/lua/vim/treesitter/health.lua
@@ -1,5 +1,6 @@
local M = {}
local ts = vim.treesitter
+local health = require('vim.health')
--- Lists the parsers currently installed
---
@@ -10,27 +11,23 @@ end
--- Performs a healthcheck for treesitter integration
function M.check()
- local report_info = vim.fn['health#report_info']
- local report_ok = vim.fn['health#report_ok']
- local report_error = vim.fn['health#report_error']
local parsers = M.list_parsers()
- report_info(string.format('Runtime ABI version : %d', ts.language_version))
+ health.report_info(string.format('Nvim runtime ABI version: %d', ts.language_version))
for _, parser in pairs(parsers) do
local parsername = vim.fn.fnamemodify(parser, ':t:r')
-
local is_loadable, ret = pcall(ts.language.require_language, parsername)
- if not is_loadable then
- report_error(string.format('Impossible to load parser for %s: %s', parsername, ret))
+ if not is_loadable or not ret then
+ health.report_error(
+ string.format('Parser "%s" failed to load (path: %s): %s', parsername, parser, ret or '?')
+ )
elseif ret then
local lang = ts.language.inspect_language(parsername)
- report_ok(
- string.format('Loaded parser for %s: ABI version %d', parsername, lang._abi_version)
+ health.report_ok(
+ string.format('Parser: %-10s ABI: %d, path: %s', parsername, lang._abi_version, parser)
)
- else
- report_error(string.format('Unable to load parser for %s', parsername))
end
end
end
diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua
index 83a26aff13..d77a0d0d03 100644
--- a/runtime/lua/vim/treesitter/highlighter.lua
+++ b/runtime/lua/vim/treesitter/highlighter.lua
@@ -88,7 +88,10 @@ function TSHighlighter.new(tree, opts)
end
end
+ self.orig_spelloptions = vim.bo[self.bufnr].spelloptions
+
vim.bo[self.bufnr].syntax = ''
+ vim.b[self.bufnr].ts_highlight = true
TSHighlighter.active[self.bufnr] = self
@@ -114,6 +117,14 @@ function TSHighlighter:destroy()
if TSHighlighter.active[self.bufnr] then
TSHighlighter.active[self.bufnr] = nil
end
+
+ if vim.api.nvim_buf_is_loaded(self.bufnr) then
+ vim.bo[self.bufnr].spelloptions = self.orig_spelloptions
+ vim.b[self.bufnr].ts_highlight = nil
+ if vim.g.syntax_on == 1 then
+ a.nvim_exec_autocmds('FileType', { group = 'syntaxset', buffer = self.bufnr })
+ end
+ end
end
---@private
@@ -164,7 +175,7 @@ function TSHighlighter:get_query(lang)
end
---@private
-local function on_line_impl(self, buf, line, spell)
+local function on_line_impl(self, buf, line, is_spell_nav)
self.tree:for_each_tree(function(tstree, tree)
if not tstree then
return
@@ -201,17 +212,26 @@ local function on_line_impl(self, buf, line, spell)
local start_row, start_col, end_row, end_col = node:range()
local hl = highlighter_query.hl_cache[capture]
- local is_spell = highlighter_query:query().captures[capture] == 'spell'
+ local capture_name = highlighter_query:query().captures[capture]
+ local spell = nil
+ if capture_name == 'spell' then
+ spell = true
+ elseif capture_name == 'nospell' then
+ spell = false
+ end
+
+ -- Give nospell a higher priority so it always overrides spell captures.
+ local spell_pri_offset = capture_name == 'nospell' and 1 or 0
- if hl and end_row >= line and (not spell or is_spell) then
+ if hl and end_row >= line and (not is_spell_nav or spell ~= nil) then
a.nvim_buf_set_extmark(buf, ns, start_row, start_col, {
end_line = end_row,
end_col = end_col,
hl_group = hl,
ephemeral = true,
- priority = tonumber(metadata.priority) or 100, -- Low but leaves room below
+ priority = (tonumber(metadata.priority) or 100) + spell_pri_offset, -- Low but leaves room below
conceal = metadata.conceal,
- spell = is_spell,
+ spell = spell,
})
end
if start_row > line then
diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua
index e9d70c4204..a1e96f8ef2 100644
--- a/runtime/lua/vim/treesitter/languagetree.lua
+++ b/runtime/lua/vim/treesitter/languagetree.lua
@@ -608,7 +608,9 @@ end
---@return userdata|nil Found |tsnode|
function LanguageTree:named_node_for_range(range, opts)
local tree = self:tree_for_range(range, opts)
- return tree:root():named_descendant_for_range(unpack(range))
+ if tree then
+ return tree:root():named_descendant_for_range(unpack(range))
+ end
end
--- Gets the appropriate language that contains {range}.
diff --git a/runtime/lua/vim/treesitter/playground.lua b/runtime/lua/vim/treesitter/playground.lua
new file mode 100644
index 0000000000..bb073290c6
--- /dev/null
+++ b/runtime/lua/vim/treesitter/playground.lua
@@ -0,0 +1,186 @@
+local api = vim.api
+
+local M = {}
+
+---@class Playground
+---@field ns number API namespace
+---@field opts table Options table with the following keys:
+--- - anon (boolean): If true, display anonymous nodes
+--- - lang (boolean): If true, display the language alongside each node
+---
+---@class Node
+---@field id number Node id
+---@field text string Node text
+---@field named boolean True if this is a named (non-anonymous) node
+---@field depth number Depth of the node within the tree
+---@field lnum number Beginning line number of this node in the source buffer
+---@field col number Beginning column number of this node in the source buffer
+---@field end_lnum number Final line number of this node in the source buffer
+---@field end_col number Final column number of this node in the source buffer
+---@field lang string Source language of this node
+
+--- Traverse all child nodes starting at {node}.
+---
+--- This is a recursive function. The {depth} parameter indicates the current recursion level.
+--- {lang} is a string indicating the language of the tree currently being traversed. Each traversed
+--- node is added to {tree}. When recursion completes, {tree} is an array of all nodes in the order
+--- they were visited.
+---
+--- {injections} is a table mapping node ids from the primary tree to language tree injections. Each
+--- injected language has a series of trees nested within the primary language's tree, and the root
+--- node of each of these trees is contained within a node in the primary tree. The {injections}
+--- table maps nodes in the primary tree to root nodes of injected trees.
+---
+---@param node userdata Starting node to begin traversal |tsnode|
+---@param depth number Current recursion depth
+---@param lang string Language of the tree currently being traversed
+---@param injections table Mapping of node ids to root nodes of injected language trees (see
+--- explanation above)
+---@param tree Node[] Output table containing a list of tables each representing a node in the tree
+---@private
+local function traverse(node, depth, lang, injections, tree)
+ local injection = injections[node:id()]
+ if injection then
+ traverse(injection.root, depth, injection.lang, injections, tree)
+ end
+
+ for child, field in node:iter_children() do
+ local type = child:type()
+ local lnum, col, end_lnum, end_col = child:range()
+ local named = child:named()
+ local text
+ if named then
+ if field then
+ text = string.format('%s: (%s)', field, type)
+ else
+ text = string.format('(%s)', type)
+ end
+ else
+ text = string.format('"%s"', type:gsub('\n', '\\n'))
+ end
+
+ table.insert(tree, {
+ id = child:id(),
+ text = text,
+ named = named,
+ depth = depth,
+ lnum = lnum,
+ col = col,
+ end_lnum = end_lnum,
+ end_col = end_col,
+ lang = lang,
+ })
+
+ traverse(child, depth + 1, lang, injections, tree)
+ end
+
+ return tree
+end
+
+--- Create a new Playground object.
+---
+---@param bufnr number Source buffer number
+---@param lang string|nil Language of source buffer
+---
+---@return Playground|nil
+---@return string|nil Error message, if any
+---
+---@private
+function M.new(self, bufnr, lang)
+ local ok, parser = pcall(vim.treesitter.get_parser, bufnr or 0, lang)
+ if not ok then
+ return nil, 'No parser available for the given buffer'
+ end
+
+ -- For each child tree (injected language), find the root of the tree and locate the node within
+ -- the primary tree that contains that root. Add a mapping from the node in the primary tree to
+ -- the root in the child tree to the {injections} table.
+ local root = parser:parse()[1]:root()
+ local injections = {}
+ parser:for_each_child(function(child, lang_)
+ child:for_each_tree(function(tree)
+ local r = tree:root()
+ local node = root:named_descendant_for_range(r:range())
+ if node then
+ injections[node:id()] = {
+ lang = lang_,
+ root = r,
+ }
+ end
+ end)
+ end)
+
+ local nodes = traverse(root, 0, parser:lang(), injections, {})
+
+ local named = {}
+ for _, v in ipairs(nodes) do
+ if v.named then
+ named[#named + 1] = v
+ end
+ end
+
+ local t = {
+ ns = api.nvim_create_namespace(''),
+ nodes = nodes,
+ named = named,
+ opts = {
+ anon = false,
+ lang = false,
+ },
+ }
+
+ setmetatable(t, self)
+ self.__index = self
+ return t
+end
+
+--- Write the contents of this Playground into {bufnr}.
+---
+---@param bufnr number Buffer number to write into.
+---@private
+function M.draw(self, bufnr)
+ vim.bo[bufnr].modifiable = true
+ local lines = {}
+ for _, item in self:iter() do
+ lines[#lines + 1] = table.concat({
+ string.rep(' ', item.depth),
+ item.text,
+ item.lnum == item.end_lnum
+ and string.format(' [%d:%d-%d]', item.lnum + 1, item.col + 1, item.end_col)
+ or string.format(
+ ' [%d:%d-%d:%d]',
+ item.lnum + 1,
+ item.col + 1,
+ item.end_lnum + 1,
+ item.end_col
+ ),
+ self.opts.lang and string.format(' %s', item.lang) or '',
+ })
+ end
+ api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
+ vim.bo[bufnr].modifiable = false
+end
+
+--- Get node {i} from this Playground object.
+---
+--- The node number is dependent on whether or not anonymous nodes are displayed.
+---
+---@param i number Node number to get
+---@return Node
+---@private
+function M.get(self, i)
+ local t = self.opts.anon and self.nodes or self.named
+ return t[i]
+end
+
+--- Iterate over all of the nodes in this Playground object.
+---
+---@return function Iterator over all nodes in this Playground
+---@return table
+---@return number
+---@private
+function M.iter(self)
+ return ipairs(self.opts.anon and self.nodes or self.named)
+end
+
+return M
diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua
index 7ca7384a88..dbf134573d 100644
--- a/runtime/lua/vim/treesitter/query.lua
+++ b/runtime/lua/vim/treesitter/query.lua
@@ -419,7 +419,8 @@ local directive_handlers = {
--- Adds a new predicate to be used in queries
---
---@param name string Name of the predicate, without leading #
----@param handler function(match:string, pattern:string, bufnr:number, predicate:function)
+---@param handler function(match:table, pattern:string, bufnr:number, predicate:string[])
+--- - see |vim.treesitter.query.add_directive()| for argument meanings
function M.add_predicate(name, handler, force)
if predicate_handlers[name] and not force then
error(string.format('Overriding %s', name))
@@ -436,7 +437,12 @@ end
--- metadata table `metadata[capture_id].key = value`
---
---@param name string Name of the directive, without leading #
----@param handler function(match:string, pattern:string, bufnr:number, predicate:function, metadata:table)
+---@param handler function(match:table, pattern:string, bufnr:number, predicate:string[], metadata:table)
+--- - match: see |treesitter-query|
+--- - node-level data are accessible via `match[capture_id]`
+--- - pattern: see |treesitter-query|
+--- - predicate: list of strings containing the full directive being called, e.g.
+--- `(node (#set! conceal "-"))` would get the predicate `{ "#set!", "conceal", "-" }`
function M.add_directive(name, handler, force)
if directive_handlers[name] and not force then
error(string.format('Overriding %s', name))
@@ -549,7 +555,7 @@ end
--- The iterator returns three values: a numeric id identifying the capture,
--- the captured node, and metadata from any directives processing the match.
--- The following example shows how to get captures by name:
---- <pre>
+--- <pre>lua
--- for id, node, metadata in query:iter_captures(tree:root(), bufnr, first, last) do
--- local name = query.captures[id] -- name of the capture in the query
--- -- typically useful info about the node:
@@ -603,7 +609,7 @@ end
--- If the query has more than one pattern, the capture table might be sparse
--- and e.g. `pairs()` method should be used over `ipairs`.
--- Here is an example iterating over all captures in every match:
---- <pre>
+--- <pre>lua
--- for pattern, match, metadata in cquery:iter_matches(tree:root(), bufnr, first, last) do
--- for id, node in pairs(match) do
--- local name = query.captures[id]
diff --git a/runtime/lua/vim/ui.lua b/runtime/lua/vim/ui.lua
index 6f1ce3089d..8f5be15221 100644
--- a/runtime/lua/vim/ui.lua
+++ b/runtime/lua/vim/ui.lua
@@ -21,7 +21,7 @@ local M = {}
---
---
--- Example:
---- <pre>
+--- <pre>lua
--- vim.ui.select({ 'tabs', 'spaces' }, {
--- prompt = 'Select tabs or spaces:',
--- format_item = function(item)
@@ -73,11 +73,12 @@ end
--- user inputs.
---@param on_confirm function ((input|nil) -> ())
--- Called once the user confirms or abort the input.
---- `input` is what the user typed.
+--- `input` is what the user typed (it might be
+--- an empty string if nothing was entered), or
--- `nil` if the user aborted the dialog.
---
--- Example:
---- <pre>
+--- <pre>lua
--- vim.ui.input({ prompt = 'Enter value for shiftwidth: ' }, function(input)
--- vim.o.shiftwidth = tonumber(input)
--- end)
@@ -88,11 +89,17 @@ function M.input(opts, on_confirm)
})
opts = (opts and not vim.tbl_isempty(opts)) and opts or vim.empty_dict()
- local input = vim.fn.input(opts)
- if #input > 0 then
- on_confirm(input)
- else
+
+ -- Note that vim.fn.input({}) returns an empty string when cancelled.
+ -- vim.ui.input() should distinguish aborting from entering an empty string.
+ local _canceled = vim.NIL
+ opts = vim.tbl_extend('keep', opts, { cancelreturn = _canceled })
+
+ local ok, input = pcall(vim.fn.input, opts)
+ if not ok or input == _canceled then
on_confirm(nil)
+ else
+ on_confirm(input)
end
end