aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua/vim')
-rw-r--r--runtime/lua/vim/F.lua2
-rw-r--r--runtime/lua/vim/_editor.lua740
-rw-r--r--runtime/lua/vim/_init_packages.lua83
-rw-r--r--runtime/lua/vim/_meta.lua123
-rw-r--r--runtime/lua/vim/diagnostic.lua499
-rw-r--r--runtime/lua/vim/filetype.lua1641
-rw-r--r--runtime/lua/vim/highlight.lua114
-rw-r--r--runtime/lua/vim/keymap.lua145
-rw-r--r--runtime/lua/vim/lsp.lua468
-rw-r--r--runtime/lua/vim/lsp/buf.lua128
-rw-r--r--runtime/lua/vim/lsp/diagnostic.lua89
-rw-r--r--runtime/lua/vim/lsp/handlers.lua111
-rw-r--r--runtime/lua/vim/lsp/log.lua5
-rw-r--r--runtime/lua/vim/lsp/protocol.lua144
-rw-r--r--runtime/lua/vim/lsp/rpc.lua18
-rw-r--r--runtime/lua/vim/lsp/sync.lua43
-rw-r--r--runtime/lua/vim/lsp/util.lua607
-rw-r--r--runtime/lua/vim/shared.lua111
-rw-r--r--runtime/lua/vim/treesitter.lua3
-rw-r--r--runtime/lua/vim/treesitter/highlighter.lua31
-rw-r--r--runtime/lua/vim/treesitter/language.lua4
-rw-r--r--runtime/lua/vim/treesitter/languagetree.lua30
-rw-r--r--runtime/lua/vim/treesitter/query.lua77
-rw-r--r--runtime/lua/vim/ui.lua25
-rw-r--r--runtime/lua/vim/uri.lua4
25 files changed, 4136 insertions, 1109 deletions
diff --git a/runtime/lua/vim/F.lua b/runtime/lua/vim/F.lua
index 1a258546a5..9327c652db 100644
--- a/runtime/lua/vim/F.lua
+++ b/runtime/lua/vim/F.lua
@@ -27,7 +27,7 @@ function F.nil_wrap(fn)
end
end
---- like {...} except preserve the lenght explicitly
+--- like {...} except preserve the length explicitly
function F.pack_len(...)
return {n=select('#', ...), ...}
end
diff --git a/runtime/lua/vim/_editor.lua b/runtime/lua/vim/_editor.lua
new file mode 100644
index 0000000000..d4db4850bd
--- /dev/null
+++ b/runtime/lua/vim/_editor.lua
@@ -0,0 +1,740 @@
+-- Nvim-Lua stdlib: the `vim` module (:help lua-stdlib)
+--
+-- Lua code lives in one of three places:
+-- 1. runtime/lua/vim/ (the runtime): For "nice to have" features, e.g. the
+-- `inspect` and `lpeg` modules.
+-- 2. runtime/lua/vim/shared.lua: pure lua functions which always
+-- are available. Used in the test runner, as well as worker threads
+-- and processes launched from Nvim.
+-- 3. runtime/lua/vim/_editor.lua: Code which directly interacts with
+-- the Nvim editor state. Only available in the main thread.
+--
+-- Guideline: "If in doubt, put it in the runtime".
+--
+-- Most functions should live directly in `vim.`, not in submodules.
+-- The only "forbidden" names are those claimed by legacy `if_lua`:
+-- $ vim
+-- :lua for k,v in pairs(vim) do print(k) end
+-- buffer
+-- open
+-- window
+-- lastline
+-- firstline
+-- type
+-- line
+-- eval
+-- dict
+-- beep
+-- list
+-- command
+--
+-- Reference (#6580):
+-- - https://github.com/luafun/luafun
+-- - https://github.com/rxi/lume
+-- - http://leafo.net/lapis/reference/utilities.html
+-- - https://github.com/torch/paths
+-- - https://github.com/bakpakin/Fennel (pretty print, repl)
+-- - https://github.com/howl-editor/howl/tree/master/lib/howl/util
+
+local vim = assert(vim)
+
+-- These are for loading runtime modules lazily since they aren't available in
+-- the nvim binary as specified in executor.c
+for k,v in pairs {
+ treesitter=true;
+ filetype = true;
+ F=true;
+ lsp=true;
+ highlight=true;
+ diagnostic=true;
+ keymap=true;
+ ui=true;
+} do vim._submodules[k] = v end
+
+vim.log = {
+ levels = {
+ TRACE = 0;
+ DEBUG = 1;
+ INFO = 2;
+ WARN = 3;
+ ERROR = 4;
+ }
+}
+
+-- Internal-only until comments in #8107 are addressed.
+-- Returns:
+-- {errcode}, {output}
+function vim._system(cmd)
+ local out = vim.fn.system(cmd)
+ local err = vim.v.shell_error
+ return err, out
+end
+
+-- Gets process info from the `ps` command.
+-- Used by nvim_get_proc() as a fallback.
+function vim._os_proc_info(pid)
+ if pid == nil or pid <= 0 or type(pid) ~= 'number' then
+ error('invalid pid')
+ end
+ local cmd = { 'ps', '-p', pid, '-o', 'comm=', }
+ local err, name = vim._system(cmd)
+ if 1 == err and vim.trim(name) == '' then
+ return {} -- Process not found.
+ elseif 0 ~= err then
+ error('command failed: '..vim.fn.string(cmd))
+ end
+ local _, ppid = vim._system({ 'ps', '-p', pid, '-o', 'ppid=', })
+ -- Remove trailing whitespace.
+ name = vim.trim(name):gsub('^.*/', '')
+ ppid = tonumber(ppid) or -1
+ return {
+ name = name,
+ pid = pid,
+ ppid = ppid,
+ }
+end
+
+-- Gets process children from the `pgrep` command.
+-- Used by nvim_get_proc_children() as a fallback.
+function vim._os_proc_children(ppid)
+ if ppid == nil or ppid <= 0 or type(ppid) ~= 'number' then
+ error('invalid ppid')
+ end
+ local cmd = { 'pgrep', '-P', ppid, }
+ local err, rv = vim._system(cmd)
+ if 1 == err and vim.trim(rv) == '' then
+ return {} -- Process not found.
+ elseif 0 ~= err then
+ error('command failed: '..vim.fn.string(cmd))
+ end
+ local children = {}
+ for s in rv:gmatch('%S+') do
+ local i = tonumber(s)
+ if i ~= nil then
+ table.insert(children, i)
+ end
+ end
+ return children
+end
+
+-- TODO(ZyX-I): Create compatibility layer.
+
+--- Return a human-readable representation of the given object.
+---
+---@see https://github.com/kikito/inspect.lua
+---@see https://github.com/mpeterv/vinspect
+local function inspect(object, options) -- luacheck: no unused
+ error(object, options) -- Stub for gen_vimdoc.py
+end
+
+do
+ local tdots, tick, got_line1, undo_started, trailing_nl = 0, 0, false, false, false
+
+ --- Paste handler, invoked by |nvim_paste()| when a conforming UI
+ --- (such as the |TUI|) pastes text into the editor.
+ ---
+ --- Example: To remove ANSI color codes when pasting:
+ --- <pre>
+ --- vim.paste = (function(overridden)
+ --- return function(lines, phase)
+ --- for i,line in ipairs(lines) do
+ --- -- Scrub ANSI color codes from paste input.
+ --- lines[i] = line:gsub('\27%[[0-9;mK]+', '')
+ --- end
+ --- overridden(lines, phase)
+ --- end
+ --- end)(vim.paste)
+ --- </pre>
+ ---
+ ---@see |paste|
+ ---
+ ---@param lines |readfile()|-style list of lines to paste. |channel-lines|
+ ---@param phase -1: "non-streaming" paste: the call contains all lines.
+ --- If paste is "streamed", `phase` indicates the stream state:
+ --- - 1: starts the paste (exactly once)
+ --- - 2: continues the paste (zero or more times)
+ --- - 3: ends the paste (exactly once)
+ ---@returns false if client should cancel the paste.
+ function vim.paste(lines, phase)
+ local now = vim.loop.now()
+ local is_first_chunk = phase < 2
+ local is_last_chunk = phase == -1 or phase == 3
+ if is_first_chunk then -- Reset flags.
+ tdots, tick, got_line1, undo_started, trailing_nl = now, 0, false, false, false
+ end
+ if #lines == 0 then
+ lines = {''}
+ end
+ if #lines == 1 and lines[1] == '' and not is_last_chunk then
+ -- An empty chunk can cause some edge cases in streamed pasting,
+ -- so don't do anything unless it is the last chunk.
+ return true
+ end
+ -- Note: mode doesn't always start with "c" in cmdline mode, so use getcmdtype() instead.
+ if vim.fn.getcmdtype() ~= '' then -- cmdline-mode: paste only 1 line.
+ if not got_line1 then
+ got_line1 = (#lines > 1)
+ vim.api.nvim_set_option('paste', true) -- For nvim_input().
+ -- Escape "<" and control characters
+ local line1 = lines[1]:gsub('<', '<lt>'):gsub('(%c)', '\022%1')
+ vim.api.nvim_input(line1)
+ vim.api.nvim_set_option('paste', false)
+ end
+ return true
+ end
+ local mode = vim.api.nvim_get_mode().mode
+ if undo_started then
+ vim.api.nvim_command('undojoin')
+ end
+ if mode:find('^i') or mode:find('^n?t') then -- Insert mode or Terminal buffer
+ vim.api.nvim_put(lines, 'c', false, true)
+ elseif phase < 2 and mode:find('^R') and not mode:find('^Rv') then -- Replace mode
+ -- TODO: implement Replace mode streamed pasting
+ -- TODO: support Virtual Replace mode
+ local nchars = 0
+ for _, line in ipairs(lines) do
+ nchars = nchars + line:len()
+ end
+ local row, col = unpack(vim.api.nvim_win_get_cursor(0))
+ local bufline = vim.api.nvim_buf_get_lines(0, row-1, row, true)[1]
+ local firstline = lines[1]
+ firstline = bufline:sub(1, col)..firstline
+ lines[1] = firstline
+ lines[#lines] = lines[#lines]..bufline:sub(col + nchars + 1, bufline:len())
+ vim.api.nvim_buf_set_lines(0, row-1, row, false, lines)
+ elseif mode:find('^[nvV\22sS\19]') then -- Normal or Visual or Select mode
+ if mode:find('^n') then -- Normal mode
+ -- When there was a trailing new line in the previous chunk,
+ -- the cursor is on the first character of the next line,
+ -- so paste before the cursor instead of after it.
+ vim.api.nvim_put(lines, 'c', not trailing_nl, false)
+ else -- Visual or Select mode
+ vim.api.nvim_command([[exe "silent normal! \<Del>"]])
+ local del_start = vim.fn.getpos("'[")
+ local cursor_pos = vim.fn.getpos('.')
+ if mode:find('^[VS]') then -- linewise
+ if cursor_pos[2] < del_start[2] then -- replacing lines at eof
+ -- create a new line
+ vim.api.nvim_put({''}, 'l', true, true)
+ end
+ vim.api.nvim_put(lines, 'c', false, false)
+ else
+ -- paste after cursor when replacing text at eol, otherwise paste before cursor
+ vim.api.nvim_put(lines, 'c', cursor_pos[3] < del_start[3], false)
+ end
+ end
+ -- put cursor at the end of the text instead of one character after it
+ vim.fn.setpos('.', vim.fn.getpos("']"))
+ trailing_nl = lines[#lines] == ''
+ else -- Don't know what to do in other modes
+ return false
+ end
+ undo_started = true
+ if phase ~= -1 and (now - tdots >= 100) then
+ local dots = ('.'):rep(tick % 4)
+ tdots = now
+ tick = tick + 1
+ -- Use :echo because Lua print('') is a no-op, and we want to clear the
+ -- message when there are zero dots.
+ vim.api.nvim_command(('echo "%s"'):format(dots))
+ end
+ if is_last_chunk then
+ vim.api.nvim_command('redraw'..(tick > 1 and '|echo ""' or ''))
+ end
+ return true -- Paste will not continue if not returning `true`.
+ end
+end
+
+--- Defers callback `cb` until the Nvim API is safe to call.
+---
+---@see |lua-loop-callbacks|
+---@see |vim.schedule()|
+---@see |vim.in_fast_event()|
+function vim.schedule_wrap(cb)
+ return (function (...)
+ local args = vim.F.pack_len(...)
+ vim.schedule(function() cb(vim.F.unpack_len(args)) end)
+ end)
+end
+
+-- vim.fn.{func}(...)
+vim.fn = setmetatable({}, {
+ __index = function(t, key)
+ local _fn
+ if vim.api[key] ~= nil then
+ _fn = function()
+ error(string.format("Tried to call API function with vim.fn: use vim.api.%s instead", key))
+ end
+ else
+ _fn = function(...)
+ return vim.call(key, ...)
+ end
+ end
+ t[key] = _fn
+ return _fn
+ end
+})
+
+vim.funcref = function(viml_func_name)
+ return vim.fn[viml_func_name]
+end
+
+-- An easier alias for commands.
+vim.cmd = function(command)
+ return vim.api.nvim_exec(command, false)
+end
+
+-- These are the vim.env/v/g/o/bo/wo variable magic accessors.
+do
+ local validate = vim.validate
+
+ --@private
+ local function make_dict_accessor(scope, handle)
+ validate {
+ scope = {scope, 's'};
+ }
+ local mt = {}
+ function mt:__newindex(k, v)
+ return vim._setvar(scope, handle or 0, k, v)
+ end
+ function mt:__index(k)
+ if handle == nil and type(k) == 'number' then
+ return make_dict_accessor(scope, k)
+ end
+ return vim._getvar(scope, handle or 0, k)
+ end
+ return setmetatable({}, mt)
+ end
+
+ vim.g = make_dict_accessor('g', false)
+ vim.v = make_dict_accessor('v', false)
+ vim.b = make_dict_accessor('b')
+ vim.w = make_dict_accessor('w')
+ vim.t = make_dict_accessor('t')
+end
+
+--- Get a table of lines with start, end columns for a region marked by two points
+---
+---@param bufnr number of buffer
+---@param pos1 (line, column) tuple marking beginning of region
+---@param pos2 (line, column) tuple marking end of region
+---@param regtype type of selection (:help setreg)
+---@param inclusive boolean indicating whether the selection is end-inclusive
+---@return region lua 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)
+ end
+
+ -- check that region falls within current buffer
+ local buf_line_count = vim.api.nvim_buf_line_count(bufnr)
+ pos1[1] = math.min(pos1[1], buf_line_count - 1)
+ pos2[1] = math.min(pos2[1], buf_line_count - 1)
+
+ -- in case of block selection, columns need to be adjusted for non-ASCII characters
+ -- TODO: handle double-width characters
+ local bufline
+ if regtype:byte() == 22 then
+ bufline = vim.api.nvim_buf_get_lines(bufnr, pos1[1], pos1[1] + 1, true)[1]
+ pos1[2] = vim.str_utfindex(bufline, pos1[2])
+ end
+
+ local region = {}
+ for l = pos1[1], pos2[1] do
+ local c1, c2
+ if regtype:byte() == 22 then -- block selection: take width from regtype
+ c1 = pos1[2]
+ 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
+ c1 = vim.str_byteindex(bufline, c1)
+ end
+ if c2 < #bufline then
+ c2 = vim.str_byteindex(bufline, c2)
+ end
+ else
+ c1 = (l == pos1[1]) and (pos1[2]) or 0
+ c2 = (l == pos2[1]) and (pos2[2] + (inclusive and 1 or 0)) or -1
+ end
+ table.insert(region, l, {c1, c2})
+ end
+ return region
+end
+
+--- Defers calling `fn` until `timeout` ms passes.
+---
+--- Use to do a one-shot timer that calls `fn`
+--- Note: The {fn} is |schedule_wrap|ped automatically, so API functions are
+--- safe to call.
+---@param fn Callback to call once `timeout` expires
+---@param timeout Number of milliseconds to wait before calling `fn`
+---@return timer luv timer object
+function vim.defer_fn(fn, timeout)
+ vim.validate { fn = { fn, 'c', true}; }
+ local timer = vim.loop.new_timer()
+ timer:start(timeout, 0, vim.schedule_wrap(function()
+ timer:stop()
+ timer:close()
+
+ fn()
+ end))
+
+ return timer
+end
+
+
+--- Display a notification to the user.
+---
+--- This function can be overridden by plugins to display notifications using a
+--- custom provider (such as the system notification provider). By default,
+--- writes to |:messages|.
+---
+---@param msg string Content of the notification to show to the user.
+---@param level number|nil One of the values from |vim.log.levels|.
+---@param opts table|nil Optional parameters. Unused by default.
+function vim.notify(msg, level, opts) -- luacheck: no unused args
+ if level == vim.log.levels.ERROR then
+ vim.api.nvim_err_writeln(msg)
+ elseif level == vim.log.levels.WARN then
+ vim.api.nvim_echo({{msg, 'WarningMsg'}}, true, {})
+ else
+ vim.api.nvim_echo({{msg}}, true, {})
+ end
+end
+
+do
+ local notified = {}
+
+ --- Display a notification only one time.
+ ---
+ --- Like |vim.notify()|, but subsequent calls with the same message will not
+ --- display a notification.
+ ---
+ ---@param msg string Content of the notification to show to the user.
+ ---@param level number|nil One of the values from |vim.log.levels|.
+ ---@param opts table|nil Optional parameters. Unused by default.
+ function vim.notify_once(msg, level, opts) -- luacheck: no unused args
+ if not notified[msg] then
+ vim.notify(msg, level, opts)
+ notified[msg] = true
+ end
+ end
+end
+
+---@private
+function vim.register_keystroke_callback()
+ error('vim.register_keystroke_callback is deprecated, instead use: vim.on_key')
+end
+
+local on_key_cbs = {}
+
+--- Adds Lua function {fn} with namespace id {ns_id} as a listener to every,
+--- yes every, input key.
+---
+--- The Nvim command-line option |-w| is related but does not support callbacks
+--- and cannot be toggled dynamically.
+---
+---@param fn function: Callback function. It should take one string argument.
+--- On each key press, Nvim passes the key char to fn(). |i_CTRL-V|
+--- If {fn} is nil, it removes the callback for the associated {ns_id}
+---@param ns_id number? Namespace ID. If nil or 0, generates and returns a new
+--- |nvim_create_namespace()| id.
+---
+---@return number Namespace id associated with {fn}. Or count of all callbacks
+---if on_key() is called without arguments.
+---
+---@note {fn} will be removed if an error occurs while calling.
+---@note {fn} will not be cleared by |nvim_buf_clear_namespace()|
+---@note {fn} will receive the keys after mappings have been evaluated
+function vim.on_key(fn, ns_id)
+ if fn == nil and ns_id == nil then
+ return #on_key_cbs
+ end
+
+ vim.validate {
+ fn = { fn, 'c', true},
+ ns_id = { ns_id, 'n', true }
+ }
+
+ if ns_id == nil or ns_id == 0 then
+ ns_id = vim.api.nvim_create_namespace('')
+ end
+
+ on_key_cbs[ns_id] = fn
+ return ns_id
+end
+
+--- Executes the on_key callbacks.
+---@private
+function vim._on_key(char)
+ local failed_ns_ids = {}
+ local failed_messages = {}
+ for k, v in pairs(on_key_cbs) do
+ local ok, err_msg = pcall(v, char)
+ if not ok then
+ vim.on_key(nil, k)
+ table.insert(failed_ns_ids, k)
+ table.insert(failed_messages, err_msg)
+ end
+ end
+
+ if failed_ns_ids[1] then
+ error(string.format(
+ "Error executing 'on_key' with ns_ids '%s'\n Messages: %s",
+ table.concat(failed_ns_ids, ", "),
+ table.concat(failed_messages, "\n")))
+ end
+end
+
+--- Generate a list of possible completions for the string.
+--- String starts with ^ and then has the pattern.
+---
+--- 1. Can we get it to just return things in the global namespace with that name prefix
+--- 2. Can we get it to return things from global namespace even with `print(` in front.
+function vim._expand_pat(pat, env)
+ env = env or _G
+
+ pat = string.sub(pat, 2, #pat)
+
+ if pat == '' then
+ local result = vim.tbl_keys(env)
+ table.sort(result)
+ return result, 0
+ end
+
+ -- TODO: We can handle spaces in [] ONLY.
+ -- We should probably do that at some point, just for cooler completion.
+ -- TODO: We can suggest the variable names to go in []
+ -- This would be difficult as well.
+ -- Probably just need to do a smarter match than just `:match`
+
+ -- Get the last part of the pattern
+ local last_part = pat:match("[%w.:_%[%]'\"]+$")
+ if not last_part then return {}, 0 end
+
+ local parts, search_index = vim._expand_pat_get_parts(last_part)
+
+ local match_part = string.sub(last_part, search_index, #last_part)
+ local prefix_match_pat = string.sub(pat, 1, #pat - #match_part) or ''
+
+ local final_env = env
+
+ for _, part in ipairs(parts) do
+ if type(final_env) ~= 'table' then
+ return {}, 0
+ end
+ local key
+
+ -- Normally, we just have a string
+ -- Just attempt to get the string directly from the environment
+ if type(part) == "string" then
+ key = part
+ else
+ -- However, sometimes you want to use a variable, and complete on it
+ -- With this, you have the power.
+
+ -- MY_VAR = "api"
+ -- vim[MY_VAR]
+ -- -> _G[MY_VAR] -> "api"
+ local result_key = part[1]
+ if not result_key then
+ return {}, 0
+ end
+
+ local result = rawget(env, result_key)
+
+ if result == nil then
+ return {}, 0
+ end
+
+ key = result
+ end
+ local field = rawget(final_env, key)
+ if field == nil then
+ local mt = getmetatable(final_env)
+ if mt and type(mt.__index) == "table" then
+ field = rawget(mt.__index, key)
+ elseif final_env == vim and vim._submodules[key] then
+ field = vim[key]
+ end
+ end
+ final_env = field
+
+ if not final_env then
+ return {}, 0
+ end
+ end
+
+ local keys = {}
+ ---@private
+ local function insert_keys(obj)
+ for k,_ in pairs(obj) do
+ if type(k) == "string" and string.sub(k,1,string.len(match_part)) == match_part then
+ table.insert(keys,k)
+ end
+ end
+ end
+
+ if type(final_env) == "table" then
+ insert_keys(final_env)
+ end
+ local mt = getmetatable(final_env)
+ if mt and type(mt.__index) == "table" then
+ insert_keys(mt.__index)
+ end
+ if final_env == vim then
+ insert_keys(vim._submodules)
+ end
+
+ table.sort(keys)
+
+ return keys, #prefix_match_pat
+end
+
+vim._expand_pat_get_parts = function(lua_string)
+ local parts = {}
+
+ local accumulator, search_index = '', 1
+ local in_brackets, bracket_end = false, -1
+ local string_char = nil
+ for idx = 1, #lua_string do
+ local s = lua_string:sub(idx, idx)
+
+ if not in_brackets and (s == "." or s == ":") then
+ table.insert(parts, accumulator)
+ accumulator = ''
+
+ search_index = idx + 1
+ elseif s == "[" then
+ in_brackets = true
+
+ table.insert(parts, accumulator)
+ accumulator = ''
+
+ search_index = idx + 1
+ elseif in_brackets then
+ if idx == bracket_end then
+ in_brackets = false
+ search_index = idx + 1
+
+ if string_char == "VAR" then
+ table.insert(parts, { accumulator })
+ accumulator = ''
+
+ string_char = nil
+ end
+ elseif not string_char then
+ bracket_end = string.find(lua_string, ']', idx, true)
+
+ if s == '"' or s == "'" then
+ string_char = s
+ elseif s ~= ' ' then
+ string_char = "VAR"
+ accumulator = s
+ end
+ elseif string_char then
+ if string_char ~= s then
+ accumulator = accumulator .. s
+ else
+ table.insert(parts, accumulator)
+ accumulator = ''
+
+ string_char = nil
+ end
+ end
+ else
+ accumulator = accumulator .. s
+ end
+ end
+
+ parts = vim.tbl_filter(function(val) return #val > 0 end, parts)
+
+ return parts, search_index
+end
+
+---Prints given arguments in human-readable format.
+---Example:
+---<pre>
+--- -- 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>
+---@see |vim.inspect()|
+---@return given arguments.
+function vim.pretty_print(...)
+ local objects = {}
+ for i = 1, select('#', ...) do
+ local v = select(i, ...)
+ table.insert(objects, vim.inspect(v))
+ end
+
+ print(table.concat(objects, ' '))
+ return ...
+end
+
+function vim._cs_remote(rcid, server_addr, connect_error, args)
+ local function connection_failure_errmsg(consequence)
+ local explanation
+ if server_addr == '' then
+ explanation = "No server specified with --server"
+ else
+ explanation = "Failed to connect to '" .. server_addr .. "'"
+ if connect_error ~= "" then
+ explanation = explanation .. ": " .. connect_error
+ end
+ end
+ return "E247: " .. explanation .. ". " .. consequence
+ end
+
+ local f_silent = false
+ local f_tab = false
+
+ local subcmd = string.sub(args[1],10)
+ if subcmd == 'tab' then
+ f_tab = true
+ elseif subcmd == 'silent' then
+ f_silent = true
+ elseif subcmd == 'wait' or subcmd == 'wait-silent' or subcmd == 'tab-wait' or subcmd == 'tab-wait-silent' then
+ return { errmsg = 'E5600: Wait commands not yet implemented in nvim' }
+ elseif subcmd == 'tab-silent' then
+ f_tab = true
+ f_silent = true
+ elseif subcmd == 'send' then
+ if rcid == 0 then
+ return { errmsg = connection_failure_errmsg('Send failed.') }
+ end
+ vim.fn.rpcrequest(rcid, 'nvim_input', args[2])
+ return { should_exit = true, tabbed = false }
+ elseif subcmd == 'expr' then
+ if rcid == 0 then
+ return { errmsg = connection_failure_errmsg('Send expression failed.') }
+ end
+ print(vim.fn.rpcrequest(rcid, 'nvim_eval', args[2]))
+ return { should_exit = true, tabbed = false }
+ elseif subcmd ~= '' then
+ return { errmsg='Unknown option argument: ' .. args[1] }
+ end
+
+ if rcid == 0 then
+ if not f_silent then
+ vim.notify(connection_failure_errmsg("Editing locally"), vim.log.levels.WARN)
+ end
+ else
+ local command = {}
+ if f_tab then table.insert(command, 'tab') end
+ table.insert(command, 'drop')
+ for i = 2, #args do
+ table.insert(command, vim.fn.fnameescape(args[i]))
+ end
+ vim.fn.rpcrequest(rcid, 'nvim_command', table.concat(command, ' '))
+ end
+
+ return {
+ should_exit = rcid ~= 0,
+ tabbed = f_tab,
+ }
+end
+
+require('vim._meta')
+
+return vim
diff --git a/runtime/lua/vim/_init_packages.lua b/runtime/lua/vim/_init_packages.lua
new file mode 100644
index 0000000000..7d27741f1b
--- /dev/null
+++ b/runtime/lua/vim/_init_packages.lua
@@ -0,0 +1,83 @@
+-- prevents luacheck from making lints for setting things on vim
+local vim = assert(vim)
+
+local pathtrails = {}
+vim._so_trails = {}
+for s in (package.cpath..';'):gmatch('[^;]*;') do
+ s = s:sub(1, -2) -- Strip trailing semicolon
+ -- Find out path patterns. pathtrail should contain something like
+ -- /?.so, \?.dll. This allows not to bother determining what correct
+ -- suffixes are.
+ local pathtrail = s:match('[/\\][^/\\]*%?.*$')
+ if pathtrail and not pathtrails[pathtrail] then
+ pathtrails[pathtrail] = true
+ table.insert(vim._so_trails, pathtrail)
+ end
+end
+
+function vim._load_package(name)
+ local basename = name:gsub('%.', '/')
+ local paths = {"lua/"..basename..".lua", "lua/"..basename.."/init.lua"}
+ local found = vim.api.nvim__get_runtime(paths, false, {is_lua=true})
+ if #found > 0 then
+ local f, err = loadfile(found[1])
+ return f or error(err)
+ end
+
+ local so_paths = {}
+ for _,trail in ipairs(vim._so_trails) do
+ local path = "lua"..trail:gsub('?', basename) -- so_trails contains a leading slash
+ table.insert(so_paths, path)
+ end
+
+ found = vim.api.nvim__get_runtime(so_paths, false, {is_lua=true})
+ if #found > 0 then
+ -- Making function name in Lua 5.1 (see src/loadlib.c:mkfuncname) is
+ -- a) strip prefix up to and including the first dash, if any
+ -- b) replace all dots by underscores
+ -- c) prepend "luaopen_"
+ -- So "foo-bar.baz" should result in "luaopen_bar_baz"
+ local dash = name:find("-", 1, true)
+ local modname = dash and name:sub(dash + 1) or name
+ local f, err = package.loadlib(found[1], "luaopen_"..modname:gsub("%.", "_"))
+ return f or error(err)
+ end
+ return nil
+end
+
+-- Insert vim._load_package after the preloader at position 2
+table.insert(package.loaders, 2, vim._load_package)
+
+-- builtin functions which always should be available
+require'vim.shared'
+
+vim._submodules = {inspect=true}
+
+-- These are for loading runtime modules in the vim namespace lazily.
+setmetatable(vim, {
+ __index = function(t, key)
+ if vim._submodules[key] then
+ t[key] = require('vim.'..key)
+ return t[key]
+ elseif vim.startswith(key, 'uri_') then
+ local val = require('vim.uri')[key]
+ if val ~= nil then
+ -- Expose all `vim.uri` functions on the `vim` module.
+ t[key] = val
+ return t[key]
+ end
+ end
+ end
+})
+
+--- <Docs described in |vim.empty_dict()| >
+---@private
+--- TODO: should be in vim.shared when vim.shared always uses nvim-lua
+function vim.empty_dict()
+ return setmetatable({}, vim._empty_dict_mt)
+end
+
+-- only on main thread: functions for interacting with editor state
+if not vim.is_thread() then
+ require'vim._editor'
+end
diff --git a/runtime/lua/vim/_meta.lua b/runtime/lua/vim/_meta.lua
index f7d47c1030..522e26caa7 100644
--- a/runtime/lua/vim/_meta.lua
+++ b/runtime/lua/vim/_meta.lua
@@ -16,10 +16,6 @@ for _, v in pairs(a.nvim_get_all_options_info()) do
if v.shortname ~= "" then options_info[v.shortname] = v end
end
-local is_global_option = function(info) return info.scope == "global" end
-local is_buffer_option = function(info) return info.scope == "buf" end
-local is_window_option = function(info) return info.scope == "win" end
-
local get_scoped_options = function(scope)
local result = {}
for name, option_info in pairs(options_info) do
@@ -133,107 +129,18 @@ do -- window option accessor
vim.wo = new_win_opt_accessor(nil)
end
---[[
-Local window setter
-
-buffer options: does not get copied when split
- nvim_set_option(buf_opt, value) -> sets the default for NEW buffers
- this sets the hidden global default for buffer options
-
- nvim_buf_set_option(...) -> sets the local value for the buffer
-
- set opt=value, does BOTH global default AND buffer local value
- setlocal opt=value, does ONLY buffer local value
-
-window options: gets copied
- does not need to call nvim_set_option because nobody knows what the heck this does⸮
- We call it anyway for more readable code.
-
-
- Command global value local value
- :set option=value set set
- :setlocal option=value - set
-:setglobal option=value set -
---]]
-local function set_scoped_option(k, v, set_type)
- local info = options_info[k]
-
- -- Don't let people do setlocal with global options.
- -- That is a feature that doesn't make sense.
- if set_type == SET_TYPES.LOCAL and is_global_option(info) then
- error(string.format("Unable to setlocal option: '%s', which is a global option.", k))
- end
-
- -- Only `setlocal` skips setting the default/global value
- -- This will more-or-less noop for window options, but that's OK
- if set_type ~= SET_TYPES.LOCAL then
- a.nvim_set_option(k, v)
- end
-
- if is_window_option(info) then
- if set_type ~= SET_TYPES.GLOBAL then
- a.nvim_win_set_option(0, k, v)
- end
- elseif is_buffer_option(info) then
- if set_type == SET_TYPES.LOCAL
- or (set_type == SET_TYPES.SET and not info.global_local) then
- a.nvim_buf_set_option(0, k, v)
- end
- end
-end
-
---[[
-Local window getter
-
- Command global value local value
- :set option? - display
- :setlocal option? - display
-:setglobal option? display -
---]]
-local function get_scoped_option(k, set_type)
- local info = assert(options_info[k], "Must be a valid option: " .. tostring(k))
-
- if set_type == SET_TYPES.GLOBAL or is_global_option(info) then
- return a.nvim_get_option(k)
- end
-
- if is_buffer_option(info) then
- local was_set, value = pcall(a.nvim_buf_get_option, 0, k)
- if was_set then return value end
-
- if info.global_local then
- return a.nvim_get_option(k)
- end
-
- error("buf_get: This should not be able to happen, given my understanding of options // " .. k)
- end
-
- if is_window_option(info) then
- local ok, value = pcall(a.nvim_win_get_option, 0, k)
- if ok then
- return value
- end
-
- local global_ok, global_val = pcall(a.nvim_get_option, k)
- if global_ok then
- return global_val
- end
-
- error("win_get: This should never happen. File an issue and tag @tjdevries")
- end
-
- error("This fallback case should not be possible. " .. k)
-end
-
-- vim global option
-- this ONLY sets the global option. like `setglobal`
-vim.go = make_meta_accessor(a.nvim_get_option, a.nvim_set_option)
+vim.go = make_meta_accessor(
+ function(k) return a.nvim_get_option_value(k, {scope = "global"}) end,
+ function(k, v) return a.nvim_set_option_value(k, v, {scope = "global"}) end
+)
-- vim `set` style options.
-- it has no additional metamethod magic.
vim.o = make_meta_accessor(
- function(k) return get_scoped_option(k, SET_TYPES.SET) end,
- function(k, v) return set_scoped_option(k, v, SET_TYPES.SET) end
+ function(k) return a.nvim_get_option_value(k, {}) end,
+ function(k, v) return a.nvim_set_option_value(k, v, {}) end
)
---@brief [[
@@ -389,6 +296,10 @@ local convert_value_to_vim = (function()
}
return function(name, info, value)
+ if value == nil then
+ return vim.NIL
+ end
+
local option_type = get_option_type(name, info)
assert_valid_value(name, value, valid_types[option_type])
@@ -398,7 +309,7 @@ end)()
--- Converts a vimoption_T style value to a Lua value
local convert_value_to_lua = (function()
- -- Map of OptionType to functions that take vimoption_T values and conver to lua values.
+ -- Map of OptionType to functions that take vimoption_T values and convert to lua values.
-- Each function takes (info, vim_value) -> lua_value
local to_lua_value = {
[OptionTypes.BOOLEAN] = function(_, value) return value end,
@@ -671,15 +582,19 @@ local create_option_metatable = function(set_type)
}, option_mt)
end
- -- TODO(tjdevries): consider supporting `nil` for set to remove the local option.
- -- vim.cmd [[set option<]]
+ local scope
+ if set_type == SET_TYPES.GLOBAL then
+ scope = "global"
+ elseif set_type == SET_TYPES.LOCAL then
+ scope = "local"
+ end
option_mt = {
-- To set a value, instead use:
-- opt[my_option] = value
_set = function(self)
local value = convert_value_to_vim(self._name, self._info, self._value)
- set_scoped_option(self._name, value, set_type)
+ a.nvim_set_option_value(self._name, value, {scope = scope})
return self
end,
@@ -716,7 +631,7 @@ local create_option_metatable = function(set_type)
set_mt = {
__index = function(_, k)
- return make_option(k, get_scoped_option(k, set_type))
+ return make_option(k, a.nvim_get_option_value(k, {scope = scope}))
end,
__newindex = function(_, k, v)
diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua
index 0bc7a305f0..1ec66d7c55 100644
--- a/runtime/lua/vim/diagnostic.lua
+++ b/runtime/lua/vim/diagnostic.lua
@@ -30,13 +30,40 @@ M.handlers = setmetatable({}, {
__newindex = function(t, name, handler)
vim.validate { handler = {handler, "t" } }
rawset(t, name, handler)
- if not global_diagnostic_options[name] then
+ if global_diagnostic_options[name] == nil then
global_diagnostic_options[name] = true
end
end,
})
--- Local functions {{{
+-- Metatable that automatically creates an empty table when assigning to a missing key
+local bufnr_and_namespace_cacher_mt = {
+ __index = function(t, bufnr)
+ assert(bufnr > 0, "Invalid buffer number")
+ t[bufnr] = {}
+ return t[bufnr]
+ end,
+}
+
+local diagnostic_cache = setmetatable({}, {
+ __index = function(t, bufnr)
+ assert(bufnr > 0, "Invalid buffer number")
+ vim.api.nvim_buf_attach(bufnr, false, {
+ on_detach = function()
+ rawset(t, bufnr, nil) -- clear cache
+ end
+ })
+ t[bufnr] = {}
+ return t[bufnr]
+ end,
+})
+
+local diagnostic_cache_extmarks = setmetatable({}, bufnr_and_namespace_cacher_mt)
+local diagnostic_attached_buffers = {}
+local diagnostic_disabled = {}
+local bufs_waiting_to_update = setmetatable({}, bufnr_and_namespace_cacher_mt)
+
+local all_namespaces = {}
---@private
local function to_severity(severity)
@@ -64,23 +91,22 @@ local function filter_by_severity(severity, diagnostics)
end
---@private
-local function prefix_source(source, diagnostics)
- vim.validate { source = {source, function(v)
- return v == "always" or v == "if_many"
- end, "'always' or 'if_many'" } }
-
- if source == "if_many" then
- local sources = {}
- for _, d in pairs(diagnostics) do
- if d.source then
- sources[d.source] = true
+local function count_sources(bufnr)
+ local seen = {}
+ local count = 0
+ for _, namespace_diagnostics in pairs(diagnostic_cache[bufnr]) do
+ for _, diagnostic in ipairs(namespace_diagnostics) do
+ if diagnostic.source and not seen[diagnostic.source] then
+ seen[diagnostic.source] = true
+ count = count + 1
end
end
- if #vim.tbl_keys(sources) <= 1 then
- return diagnostics
- end
end
+ return count
+end
+---@private
+local function prefix_source(diagnostics)
return vim.tbl_map(function(d)
if not d.source then
return d
@@ -106,8 +132,6 @@ local function reformat_diagnostics(format, diagnostics)
return formatted
end
-local all_namespaces = {}
-
---@private
local function enabled_value(option, namespace)
local ns = namespace and M.get_namespace(namespace) or {}
@@ -213,36 +237,6 @@ local function get_bufnr(bufnr)
return bufnr
end
--- Metatable that automatically creates an empty table when assigning to a missing key
-local bufnr_and_namespace_cacher_mt = {
- __index = function(t, bufnr)
- if not bufnr or bufnr == 0 then
- bufnr = vim.api.nvim_get_current_buf()
- end
-
- if rawget(t, bufnr) == nil then
- rawset(t, bufnr, {})
- end
-
- return rawget(t, bufnr)
- end,
-
- __newindex = function(t, bufnr, v)
- if not bufnr or bufnr == 0 then
- bufnr = vim.api.nvim_get_current_buf()
- end
-
- rawset(t, bufnr, v)
- end,
-}
-
-local diagnostic_cleanup = setmetatable({}, bufnr_and_namespace_cacher_mt)
-local diagnostic_cache = setmetatable({}, bufnr_and_namespace_cacher_mt)
-local diagnostic_cache_extmarks = setmetatable({}, bufnr_and_namespace_cacher_mt)
-local diagnostic_attached_buffers = {}
-local diagnostic_disabled = {}
-local bufs_waiting_to_update = setmetatable({}, bufnr_and_namespace_cacher_mt)
-
---@private
local function is_disabled(namespace, bufnr)
local ns = M.get_namespace(namespace)
@@ -277,6 +271,8 @@ end
---@private
local function set_diagnostic_cache(namespace, bufnr, diagnostics)
for _, diagnostic in ipairs(diagnostics) do
+ assert(diagnostic.lnum, "Diagnostic line number is required")
+ assert(diagnostic.col, "Diagnostic column is required")
diagnostic.severity = diagnostic.severity and to_severity(diagnostic.severity) or M.severity.ERROR
diagnostic.end_lnum = diagnostic.end_lnum or diagnostic.lnum
diagnostic.end_col = diagnostic.end_col or diagnostic.col
@@ -287,11 +283,6 @@ local function set_diagnostic_cache(namespace, bufnr, diagnostics)
end
---@private
-local function clear_diagnostic_cache(namespace, bufnr)
- diagnostic_cache[bufnr][namespace] = nil
-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})
@@ -307,11 +298,6 @@ local function restore_extmarks(bufnr, last)
if not found[extmark[1]] then
local opts = extmark[4]
opts.id = extmark[1]
- -- HACK: end_row should be end_line
- if opts.end_row then
- opts.end_line = opts.end_row
- opts.end_row = nil
- end
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, extmark[2], extmark[3], opts)
end
end
@@ -377,6 +363,71 @@ local function clear_scheduled_display(namespace, bufnr)
end
---@private
+local function get_diagnostics(bufnr, opts, clamp)
+ opts = opts or {}
+
+ local namespace = opts.namespace
+ local diagnostics = {}
+
+ -- 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)
+ return rawget(t, k)
+ end,
+ })
+
+ ---@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
+ local line_count = buf_line_count[b] - 1
+ if (d.lnum > line_count or d.end_lnum > line_count or d.lnum < 0 or d.end_lnum < 0) then
+ d = vim.deepcopy(d)
+ d.lnum = math.max(math.min(d.lnum, line_count), 0)
+ d.end_lnum = math.max(math.min(d.end_lnum, line_count), 0)
+ end
+ end
+ table.insert(diagnostics, d)
+ 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
+ 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
+ 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
+ end
+ else
+ bufnr = get_bufnr(bufnr)
+ for _, diagnostic in pairs(diagnostic_cache[bufnr][namespace] or {}) do
+ add(bufnr, diagnostic)
+ end
+ end
+
+ if opts.severity then
+ diagnostics = filter_by_severity(opts.severity, diagnostics)
+ end
+
+ return diagnostics
+end
+
+---@private
local function set_list(loclist, opts)
opts = opts or {}
local open = vim.F.if_nil(opts.open, true)
@@ -386,7 +437,9 @@ local function set_list(loclist, opts)
if loclist then
bufnr = vim.api.nvim_win_get_buf(winnr)
end
- local diagnostics = M.get(bufnr, opts)
+ -- Don't clamp line numbers since the quickfix list can already handle line
+ -- numbers beyond the end of the buffer
+ local diagnostics = get_diagnostics(bufnr, opts, false)
local items = M.toqflist(diagnostics)
if loclist then
vim.fn.setloclist(winnr, {}, ' ', { title = title, items = items })
@@ -394,21 +447,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 "copen")
- end
-end
-
----@private
---- To (slightly) improve performance, modifies diagnostics in place.
-local function clamp_line_numbers(bufnr, diagnostics)
- local buf_line_count = vim.api.nvim_buf_line_count(bufnr)
- if buf_line_count == 0 then
- return
- end
-
- for _, diagnostic in ipairs(diagnostics) do
- diagnostic.lnum = math.max(math.min(diagnostic.lnum, buf_line_count - 1), 0)
- diagnostic.end_lnum = math.max(math.min(diagnostic.end_lnum, buf_line_count - 1), 0)
+ vim.api.nvim_command(loclist and "lopen" or "botright copen")
end
end
@@ -418,8 +457,7 @@ local function next_diagnostic(position, search_forward, bufnr, opts, namespace)
bufnr = get_bufnr(bufnr)
local wrap = vim.F.if_nil(opts.wrap, true)
local line_count = vim.api.nvim_buf_line_count(bufnr)
- local diagnostics = M.get(bufnr, vim.tbl_extend("keep", opts, {namespace = namespace}))
- clamp_line_numbers(bufnr, diagnostics)
+ local diagnostics = get_diagnostics(bufnr, vim.tbl_extend("keep", opts, {namespace = namespace}), true)
local line_diagnostics = diagnostic_lines(diagnostics)
for i = 0, line_count do
local offset = i * (search_forward and 1 or -1)
@@ -431,13 +469,14 @@ 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 sort_diagnostics, is_next
if search_forward then
sort_diagnostics = function(a, b) return a.col < b.col end
- is_next = function(diagnostic) return diagnostic.col > position[2] end
+ is_next = function(d) return math.min(d.col, line_length - 1) > position[2] end
else
sort_diagnostics = function(a, b) return a.col > b.col end
- is_next = function(diagnostic) return diagnostic.col < position[2] end
+ is_next = function(d) return math.min(d.col, line_length - 1) < position[2] end
end
table.sort(line_diagnostics[lnum], sort_diagnostics)
if i == 0 then
@@ -465,26 +504,28 @@ local function diagnostic_move_pos(opts, pos)
return
end
- -- Save position in the window's jumplist
- vim.api.nvim_win_call(win_id, function() vim.cmd("normal! m'") end)
-
- vim.api.nvim_win_set_cursor(win_id, {pos[1] + 1, pos[2]})
+ vim.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]})
+ -- Open folds under the cursor
+ vim.cmd("normal! zv")
+ end)
if float then
local float_opts = type(float) == "table" and float or {}
vim.schedule(function()
M.open_float(
- vim.api.nvim_win_get_buf(win_id),
- vim.tbl_extend("keep", float_opts, {scope="cursor"})
+ vim.tbl_extend("keep", float_opts, {
+ bufnr = vim.api.nvim_win_get_buf(win_id),
+ scope = "cursor",
+ focus = false,
+ })
)
end)
end
end
--- }}}
-
--- Public API {{{
-
--- Configure diagnostic options globally or for a specific diagnostic
--- namespace.
---
@@ -511,15 +552,24 @@ end
--- - `table`: Enable this feature with overrides. Use an empty table to use default values.
--- - `function`: Function with signature (namespace, bufnr) that returns any of the above.
---
----@param opts table Configuration table with the following keys:
+---@param opts table|nil When omitted or "nil", retrieve the current configuration. Otherwise, a
+--- configuration table with the following keys:
--- - underline: (default true) Use underline for diagnostics. Options:
--- * severity: Only underline diagnostics matching the given severity
--- |diagnostic-severity|
---- - virtual_text: (default true) Use virtual text for diagnostics. Options:
+--- - virtual_text: (default true) Use virtual text for diagnostics. If multiple diagnostics
+--- are set for a namespace, one prefix per diagnostic + the last diagnostic
+--- message are shown.
+--- Options:
--- * severity: Only show virtual text for diagnostics matching the given
--- severity |diagnostic-severity|
---- * source: (string) Include the diagnostic source in virtual
---- text. One of "always" or "if_many".
+--- * source: (boolean or string) Include the diagnostic source in virtual
+--- text. Use "if_many" to only show sources if there is more than
+--- one diagnostic source in the buffer. Otherwise, any truthy value
+--- means to always show the diagnostic source.
+--- * spacing: (number) Amount of empty spaces inserted at the beginning
+--- of the virtual text.
+--- * prefix: (string) Prepend diagnostic message with prefix.
--- * 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:
@@ -537,26 +587,7 @@ end
--- * priority: (number, default 10) Base priority to use for signs. When
--- {severity_sort} is used, the priority of a sign is adjusted based on
--- its severity. Otherwise, all signs use the same priority.
---- - float: Options for floating windows:
---- * severity: See |diagnostic-severity|.
---- * header: (string or table) String to use as the header for the floating
---- window. If a table, it is interpreted as a [text, hl_group] tuple.
---- Defaults to "Diagnostics:".
---- * source: (string) Include the diagnostic source in
---- the message. One of "always" or "if_many".
---- * 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.
---- * prefix: (function, string, or table) Prefix each diagnostic in the floating
---- window. If a function, it must have the signature (diagnostic, i,
---- total) -> (string, string), where {i} is the index of the diagnostic
---- being evaluated and {total} is the total number of diagnostics
---- displayed in the window. The function should return a string which
---- is prepended to each diagnostic in the window as well as an
---- (optional) highlight group which will be used to highlight the
---- prefix. If {prefix} is a table, it is interpreted as a [text,
---- hl_group] tuple as in |nvim_echo()|; otherwise, if {prefix} is a
---- string, it is prepended to each diagnostic in the window with no
---- highlight.
+--- - float: Options for floating windows. See |vim.diagnostic.open_float()|.
--- - update_in_insert: (default false) Update diagnostics in Insert mode (if false,
--- diagnostics are updated on InsertLeave)
--- - severity_sort: (default false) Sort diagnostics by severity. This affects the order in
@@ -564,11 +595,12 @@ end
--- are displayed before lower severities (e.g. ERROR is displayed before WARN).
--- Options:
--- * reverse: (boolean) Reverse sort order
+---
---@param namespace number|nil Update the options for the given namespace. When omitted, update the
--- global diagnostic options.
function M.config(opts, namespace)
vim.validate {
- opts = { opts, 't' },
+ opts = { opts, 't', true },
namespace = { namespace, 'n', true },
}
@@ -580,10 +612,13 @@ function M.config(opts, namespace)
t = global_diagnostic_options
end
- for opt in pairs(global_diagnostic_options) do
- if opts[opt] ~= nil then
- t[opt] = opts[opt]
- end
+ if not opts then
+ -- Return current config
+ return vim.deepcopy(t)
+ end
+
+ for k, v in pairs(opts) do
+ t[k] = v
end
if namespace then
@@ -613,37 +648,39 @@ function M.set(namespace, bufnr, diagnostics, opts)
vim.validate {
namespace = {namespace, 'n'},
bufnr = {bufnr, 'n'},
- diagnostics = {diagnostics, 't'},
+ diagnostics = {
+ diagnostics,
+ vim.tbl_islist,
+ "a list of diagnostics",
+ },
opts = {opts, 't', true},
}
+ bufnr = get_bufnr(bufnr)
+
if vim.tbl_isempty(diagnostics) then
- clear_diagnostic_cache(namespace, bufnr)
+ diagnostic_cache[bufnr][namespace] = nil
else
- if not diagnostic_cleanup[bufnr][namespace] then
- diagnostic_cleanup[bufnr][namespace] = true
-
- -- Clean up our data when the buffer unloads.
- vim.api.nvim_buf_attach(bufnr, false, {
- on_detach = function(_, b)
- clear_diagnostic_cache(namespace, b)
- diagnostic_cleanup[b][namespace] = nil
- end
- })
- end
set_diagnostic_cache(namespace, bufnr, diagnostics)
end
if vim.api.nvim_buf_is_loaded(bufnr) then
- M.show(namespace, bufnr, diagnostics, opts)
+ M.show(namespace, bufnr, nil, opts)
end
- vim.api.nvim_command("doautocmd <nomodeline> User DiagnosticsChanged")
+ vim.api.nvim_buf_call(bufnr, function()
+ vim.api.nvim_command(
+ string.format(
+ "doautocmd <nomodeline> DiagnosticChanged %s",
+ vim.fn.fnameescape(vim.api.nvim_buf_get_name(bufnr))
+ )
+ )
+ end)
end
--- Get namespace metadata.
---
----@param ns number Diagnostic namespace
+---@param namespace number Diagnostic namespace
---@return table Namespace metadata
function M.get_namespace(namespace)
vim.validate { namespace = { namespace, 'n' } }
@@ -689,49 +726,7 @@ function M.get(bufnr, opts)
opts = { opts, 't', true },
}
- opts = opts or {}
-
- local namespace = opts.namespace
- local diagnostics = {}
-
- ---@private
- local function add(d)
- if not opts.lnum or d.lnum == opts.lnum then
- table.insert(diagnostics, d)
- end
- end
-
- if namespace == nil and bufnr == nil then
- for _, t in pairs(diagnostic_cache) do
- for _, v in pairs(t) do
- for _, diagnostic in pairs(v) do
- add(diagnostic)
- end
- end
- end
- elseif namespace == nil then
- for iter_namespace in pairs(diagnostic_cache[bufnr]) do
- for _, diagnostic in pairs(diagnostic_cache[bufnr][iter_namespace]) do
- add(diagnostic)
- end
- end
- elseif bufnr == nil then
- for _, t in pairs(diagnostic_cache) do
- for _, diagnostic in pairs(t[namespace] or {}) do
- add(diagnostic)
- end
- end
- else
- for _, diagnostic in pairs(diagnostic_cache[bufnr][namespace] or {}) do
- add(diagnostic)
- end
- end
-
- if opts.severity then
- diagnostics = filter_by_severity(opts.severity, diagnostics)
- end
-
- return diagnostics
+ return get_diagnostics(bufnr, opts, false)
end
--- Get the previous diagnostic closest to the cursor position.
@@ -807,7 +802,9 @@ end
--- - severity: See |diagnostic-severity|.
--- - float: (boolean or table, default true) If "true", call |vim.diagnostic.open_float()|
--- after moving. If a table, pass the table as the {opts} parameter to
---- |vim.diagnostic.open_float()|.
+--- |vim.diagnostic.open_float()|. Unless overridden, the float will show
+--- diagnostics at the new cursor position (as if "cursor" were passed to
+--- the "scope" option).
--- - win_id: (number, default 0) Window ID
function M.goto_next(opts)
return diagnostic_move_pos(
@@ -821,11 +818,16 @@ M.handlers.signs = {
vim.validate {
namespace = {namespace, 'n'},
bufnr = {bufnr, 'n'},
- diagnostics = {diagnostics, 't'},
+ diagnostics = {
+ diagnostics,
+ vim.tbl_islist,
+ "a list of diagnostics",
+ },
opts = {opts, 't', true},
}
bufnr = get_bufnr(bufnr)
+ opts = opts or {}
if opts.signs and opts.signs.severity then
diagnostics = filter_by_severity(opts.signs.severity, diagnostics)
@@ -884,11 +886,16 @@ M.handlers.underline = {
vim.validate {
namespace = {namespace, 'n'},
bufnr = {bufnr, 'n'},
- diagnostics = {diagnostics, 't'},
+ diagnostics = {
+ diagnostics,
+ vim.tbl_islist,
+ "a list of diagnostics",
+ },
opts = {opts, 't', true},
}
bufnr = get_bufnr(bufnr)
+ opts = opts or {}
if opts.underline and opts.underline.severity then
diagnostics = filter_by_severity(opts.underline.severity, diagnostics)
@@ -913,7 +920,8 @@ M.handlers.underline = {
underline_ns,
higroup,
{ diagnostic.lnum, diagnostic.col },
- { diagnostic.end_lnum, diagnostic.end_col }
+ { diagnostic.end_lnum, diagnostic.end_col },
+ { priority = vim.highlight.priorities.diagnostics }
)
end
save_extmarks(underline_ns, bufnr)
@@ -932,19 +940,27 @@ M.handlers.virtual_text = {
vim.validate {
namespace = {namespace, 'n'},
bufnr = {bufnr, 'n'},
- diagnostics = {diagnostics, 't'},
+ diagnostics = {
+ diagnostics,
+ vim.tbl_islist,
+ "a list of diagnostics",
+ },
opts = {opts, 't', true},
}
bufnr = get_bufnr(bufnr)
+ opts = opts or {}
local severity
if opts.virtual_text then
if opts.virtual_text.format then
diagnostics = reformat_diagnostics(opts.virtual_text.format, diagnostics)
end
- if opts.virtual_text.source then
- diagnostics = prefix_source(opts.virtual_text.source, diagnostics)
+ if
+ opts.virtual_text.source
+ and (opts.virtual_text.source ~= "if_many" or count_sources(bufnr) > 1)
+ then
+ diagnostics = prefix_source(diagnostics)
end
if opts.virtual_text.severity then
severity = opts.virtual_text.severity
@@ -1089,7 +1105,11 @@ function M.show(namespace, bufnr, diagnostics, opts)
vim.validate {
namespace = { namespace, 'n', true },
bufnr = { bufnr, 'n', true },
- diagnostics = { diagnostics, 't', true },
+ diagnostics = {
+ diagnostics,
+ function(v) return v == nil or vim.tbl_islist(v) end,
+ "a list of diagnostics",
+ },
opts = { opts, 't', true },
}
@@ -1115,7 +1135,7 @@ function M.show(namespace, bufnr, diagnostics, opts)
M.hide(namespace, bufnr)
- diagnostics = diagnostics or M.get(bufnr, {namespace=namespace})
+ diagnostics = diagnostics or get_diagnostics(bufnr, {namespace=namespace}, true)
if not diagnostics or vim.tbl_isempty(diagnostics) then
return
@@ -1141,8 +1161,6 @@ function M.show(namespace, bufnr, diagnostics, opts)
end
end
- clamp_line_numbers(bufnr, diagnostics)
-
for handler_name, handler in pairs(M.handlers) do
if handler.show and opts[handler_name] then
handler.show(namespace, bufnr, diagnostics, opts)
@@ -1152,12 +1170,15 @@ end
--- Show diagnostics in a floating window.
---
----@param bufnr number|nil Buffer number. Defaults to the current buffer.
---@param opts table|nil Configuration table with the same keys as
--- |vim.lsp.util.open_floating_preview()| in addition to the following:
+--- - bufnr: (number) Buffer number to show diagnostics from.
+--- Defaults to the current buffer.
--- - namespace: (number) Limit diagnostics to the given namespace
---- - scope: (string, default "buffer") Show diagnostics from the whole buffer ("buffer"),
+--- - scope: (string, default "line") Show diagnostics from the whole buffer ("buffer"),
--- the current cursor line ("line"), or the current cursor position ("cursor").
+--- Shorthand versions are also accepted ("c" for "cursor", "l" for "line", "b"
+--- for "buffer").
--- - pos: (number or table) If {scope} is "line" or "cursor", use this position rather
--- than the cursor position. If a number, interpreted as a line number;
--- otherwise, a (row, col) tuple.
@@ -1168,23 +1189,54 @@ end
--- - header: (string or table) String to use as the header for the floating window. If a
--- table, it is interpreted as a [text, hl_group] tuple. Overrides the setting
--- from |vim.diagnostic.config()|.
---- - source: (string) Include the diagnostic source in the message. One of "always" or
---- "if_many". Overrides the setting from |vim.diagnostic.config()|.
+--- - source: (boolean or string) Include the diagnostic source in the message.
+--- Use "if_many" to only show sources if there is more than one source of
+--- diagnostics in the buffer. Otherwise, any truthy value means to always show
+--- the diagnostic source. Overrides the setting from
+--- |vim.diagnostic.config()|.
--- - 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.
--- Overrides the setting from |vim.diagnostic.config()|.
---- - prefix: (function, string, or table) Prefix each diagnostic in the floating window.
+--- - prefix: (function, string, or table) Prefix each diagnostic in the floating
+--- window. If a function, it must have the signature (diagnostic, i,
+--- total) -> (string, string), where {i} is the index of the diagnostic
+--- being evaluated and {total} is the total number of diagnostics
+--- displayed in the window. The function should return a string which
+--- is prepended to each diagnostic in the window as well as an
+--- (optional) highlight group which will be used to highlight the
+--- prefix. If {prefix} is a table, it is interpreted as a [text,
+--- hl_group] tuple as in |nvim_echo()|; otherwise, if {prefix} is a
+--- 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})
-function M.open_float(bufnr, opts)
- vim.validate {
- bufnr = { bufnr, 'n', true },
- opts = { opts, 't', true },
- }
+function M.open_float(opts, ...)
+ -- Support old (bufnr, opts) signature
+ local bufnr
+ if opts == nil or type(opts) == "number" then
+ bufnr = opts
+ opts = ...
+ else
+ vim.validate {
+ opts = { opts, 't', true },
+ }
+ end
opts = opts or {}
- bufnr = get_bufnr(bufnr)
- local scope = opts.scope or "buffer"
+ bufnr = get_bufnr(bufnr or opts.bufnr)
+
+ do
+ -- Resolve options with user settings from vim.diagnostic.config
+ -- Unlike the other decoration functions (e.g. set_virtual_text, set_signs, etc.) `open_float`
+ -- does not have a dedicated table for configuration options; instead, the options are mixed in
+ -- with its `opts` table which also includes "keyword" parameters. So we create a dedicated
+ -- options table that inherits missing keys from the global configuration before resolving.
+ local t = global_diagnostic_options.float
+ local float_opts = vim.tbl_extend("keep", opts, type(t) == "table" and t or {})
+ opts = get_resolved_options({ float = float_opts }, nil, bufnr).float
+ end
+
+ local scope = ({l = "line", c = "cursor", b = "buffer"})[opts.scope] or opts.scope or "line"
local lnum, col
if scope == "line" or scope == "cursor" then
if not opts.pos then
@@ -1202,19 +1254,7 @@ function M.open_float(bufnr, opts)
error("Invalid value for option 'scope'")
end
- do
- -- Resolve options with user settings from vim.diagnostic.config
- -- Unlike the other decoration functions (e.g. set_virtual_text, set_signs, etc.) `open_float`
- -- does not have a dedicated table for configuration options; instead, the options are mixed in
- -- with its `opts` table which also includes "keyword" parameters. So we create a dedicated
- -- options table that inherits missing keys from the global configuration before resolving.
- local t = global_diagnostic_options.float
- local float_opts = vim.tbl_extend("keep", opts, type(t) == "table" and t or {})
- opts = get_resolved_options({ float = float_opts }, nil, bufnr).float
- end
-
- local diagnostics = M.get(bufnr, opts)
- clamp_line_numbers(bufnr, diagnostics)
+ local diagnostics = get_diagnostics(bufnr, opts, true)
if scope == "line" then
diagnostics = vim.tbl_filter(function(d)
@@ -1266,8 +1306,8 @@ function M.open_float(bufnr, opts)
diagnostics = reformat_diagnostics(opts.format, diagnostics)
end
- if opts.source then
- diagnostics = prefix_source(opts.source, diagnostics)
+ if opts.source and (opts.source ~= "if_many" or count_sources(bufnr) > 1) then
+ diagnostics = prefix_source(diagnostics)
end
local prefix_opt = if_nil(opts.prefix, (scope == "cursor" and #diagnostics <= 1) and "" or function(_, i)
@@ -1334,16 +1374,23 @@ function M.reset(namespace, bufnr)
bufnr = {bufnr, 'n', true},
}
- local buffers = bufnr and {bufnr} or vim.tbl_keys(diagnostic_cache)
+ local buffers = bufnr and {get_bufnr(bufnr)} or vim.tbl_keys(diagnostic_cache)
for _, iter_bufnr in ipairs(buffers) do
local namespaces = namespace and {namespace} or vim.tbl_keys(diagnostic_cache[iter_bufnr])
for _, iter_namespace in ipairs(namespaces) do
- clear_diagnostic_cache(iter_namespace, iter_bufnr)
+ diagnostic_cache[iter_bufnr][iter_namespace] = nil
M.hide(iter_namespace, iter_bufnr)
end
- end
- vim.api.nvim_command("doautocmd <nomodeline> User DiagnosticsChanged")
+ vim.api.nvim_buf_call(iter_bufnr, function()
+ vim.api.nvim_command(
+ string.format(
+ "doautocmd <nomodeline> DiagnosticChanged %s",
+ vim.fn.fnameescape(vim.api.nvim_buf_get_name(iter_bufnr))
+ )
+ )
+ end)
+ end
end
--- Add all diagnostics to the quickfix list.
@@ -1508,7 +1555,13 @@ local errlist_type_map = {
---@param diagnostics table List of diagnostics |diagnostic-structure|.
---@return array of quickfix list items |setqflist-what|
function M.toqflist(diagnostics)
- vim.validate { diagnostics = {diagnostics, 't'} }
+ vim.validate {
+ diagnostics = {
+ diagnostics,
+ vim.tbl_islist,
+ "a list of diagnostics",
+ },
+ }
local list = {}
for _, v in ipairs(diagnostics) do
@@ -1539,7 +1592,13 @@ end
--- |getloclist()|.
---@return array of diagnostics |diagnostic-structure|
function M.fromqflist(list)
- vim.validate { list = {list, 't'} }
+ vim.validate {
+ list = {
+ list,
+ vim.tbl_islist,
+ "a list of quickfix items",
+ },
+ }
local diagnostics = {}
for _, item in ipairs(list) do
@@ -1563,6 +1622,4 @@ function M.fromqflist(list)
return diagnostics
end
--- }}}
-
return M
diff --git a/runtime/lua/vim/filetype.lua b/runtime/lua/vim/filetype.lua
new file mode 100644
index 0000000000..603f9f854a
--- /dev/null
+++ b/runtime/lua/vim/filetype.lua
@@ -0,0 +1,1641 @@
+local api = vim.api
+
+local M = {}
+
+---@private
+local function starsetf(ft)
+ return {function(path)
+ if not vim.g.fg_ignore_pat then
+ return ft
+ end
+
+ local re = vim.regex(vim.g.fg_ignore_pat)
+ if re:match_str(path) then
+ return ft
+ end
+ end, {
+ -- Starset matches should always have lowest priority
+ priority = -math.huge,
+ }}
+end
+
+---@private
+local function getline(bufnr, lnum)
+ return api.nvim_buf_get_lines(bufnr, lnum-1, lnum, false)[1] or ""
+end
+
+-- Filetypes based on file extension
+-- luacheck: push no unused args
+local extension = {
+ -- BEGIN EXTENSION
+ ["8th"] = "8th",
+ ["a65"] = "a65",
+ aap = "aap",
+ abap = "abap",
+ abc = "abc",
+ abl = "abel",
+ wrm = "acedb",
+ ads = "ada",
+ ada = "ada",
+ gpr = "ada",
+ adb = "ada",
+ tdf = "ahdl",
+ aidl = "aidl",
+ aml = "aml",
+ run = "ampl",
+ scpt = "applescript",
+ ino = "arduino",
+ pde = "arduino",
+ art = "art",
+ asciidoc = "asciidoc",
+ adoc = "asciidoc",
+ ["asn1"] = "asn",
+ asn = "asn",
+ atl = "atlas",
+ as = "atlas",
+ ahk = "autohotkey",
+ ["au3"] = "autoit",
+ ave = "ave",
+ gawk = "awk",
+ awk = "awk",
+ ref = "b",
+ imp = "b",
+ mch = "b",
+ bc = "bc",
+ bdf = "bdf",
+ beancount = "beancount",
+ bib = "bib",
+ bicep = "bicep",
+ bl = "blank",
+ bsdl = "bsdl",
+ bst = "bst",
+ bzl = "bzl",
+ bazel = "bzl",
+ BUILD = "bzl",
+ qc = "c",
+ cabal = "cabal",
+ cdl = "cdl",
+ toc = "cdrtoc",
+ cfc = "cf",
+ cfm = "cf",
+ cfi = "cf",
+ hgrc = "cfg",
+ chf = "ch",
+ chai = "chaiscript",
+ chs = "chaskell",
+ chopro = "chordpro",
+ crd = "chordpro",
+ crdpro = "chordpro",
+ cho = "chordpro",
+ chordpro = "chordpro",
+ eni = "cl",
+ dcl = "clean",
+ icl = "clean",
+ cljx = "clojure",
+ clj = "clojure",
+ cljc = "clojure",
+ cljs = "clojure",
+ cook = "cook",
+ cmake = "cmake",
+ cmod = "cmod",
+ lib = "cobol",
+ cob = "cobol",
+ cbl = "cobol",
+ atg = "coco",
+ recipe = "conaryrecipe",
+ mklx = "context",
+ mkiv = "context",
+ mkii = "context",
+ mkxl = "context",
+ mkvi = "context",
+ moc = "cpp",
+ hh = "cpp",
+ tlh = "cpp",
+ inl = "cpp",
+ ipp = "cpp",
+ ["c++"] = "cpp",
+ C = "cpp",
+ cxx = "cpp",
+ H = "cpp",
+ tcc = "cpp",
+ hxx = "cpp",
+ hpp = "cpp",
+ cpp = function(path, bufnr)
+ if vim.g.cynlib_syntax_for_cc then
+ return "cynlib"
+ end
+ return "cpp"
+ end,
+ cc = function(path, bufnr)
+ if vim.g.cynlib_syntax_for_cc then
+ return "cynlib"
+ end
+ return "cpp"
+ end,
+ crm = "crm",
+ csx = "cs",
+ cs = "cs",
+ csc = "csc",
+ csdl = "csdl",
+ fdr = "csp",
+ csp = "csp",
+ css = "css",
+ con = "cterm",
+ feature = "cucumber",
+ cuh = "cuda",
+ cu = "cuda",
+ pld = "cupl",
+ si = "cuplsim",
+ cyn = "cynpp",
+ dart = "dart",
+ drt = "dart",
+ ds = "datascript",
+ dcd = "dcd",
+ def = "def",
+ desc = "desc",
+ directory = "desktop",
+ desktop = "desktop",
+ diff = "diff",
+ rej = "diff",
+ Dockerfile = "dockerfile",
+ bat = "dosbatch",
+ wrap = "dosini",
+ ini = "dosini",
+ dot = "dot",
+ gv = "dot",
+ drac = "dracula",
+ drc = "dracula",
+ dtd = "dtd",
+ dts = "dts",
+ dtsi = "dts",
+ dylan = "dylan",
+ intr = "dylanintr",
+ lid = "dylanlid",
+ ecd = "ecd",
+ eex = "eelixir",
+ leex = "eelixir",
+ exs = "elixir",
+ elm = "elm",
+ elv = "elvish",
+ epp = "epuppet",
+ erl = "erlang",
+ hrl = "erlang",
+ yaws = "erlang",
+ erb = "eruby",
+ rhtml = "eruby",
+ ec = "esqlc",
+ EC = "esqlc",
+ strl = "esterel",
+ exp = "expect",
+ factor = "factor",
+ fal = "falcon",
+ fan = "fan",
+ fwt = "fan",
+ fnl = "fennel",
+ ["m4gl"] = "fgl",
+ ["4gl"] = "fgl",
+ ["4gh"] = "fgl",
+ fish = "fish",
+ focexec = "focexec",
+ fex = "focexec",
+ fth = "forth",
+ ft = "forth",
+ FOR = "fortran",
+ ["f77"] = "fortran",
+ ["f03"] = "fortran",
+ fortran = "fortran",
+ ["F95"] = "fortran",
+ ["f90"] = "fortran",
+ ["F03"] = "fortran",
+ fpp = "fortran",
+ FTN = "fortran",
+ ftn = "fortran",
+ ["for"] = "fortran",
+ ["F90"] = "fortran",
+ ["F77"] = "fortran",
+ ["f95"] = "fortran",
+ FPP = "fortran",
+ f = "fortran",
+ F = "fortran",
+ ["F08"] = "fortran",
+ ["f08"] = "fortran",
+ fpc = "fpcmake",
+ fsl = "framescript",
+ fb = "freebasic",
+ fsi = "fsharp",
+ fsx = "fsharp",
+ fusion = "fusion",
+ gdb = "gdb",
+ gdmo = "gdmo",
+ mo = "gdmo",
+ tres = "gdresource",
+ tscn = "gdresource",
+ gd = "gdscript",
+ ged = "gedcom",
+ gmi = "gemtext",
+ gemini = "gemtext",
+ gift = "gift",
+ glsl = "glsl",
+ gpi = "gnuplot",
+ gnuplot = "gnuplot",
+ go = "go",
+ gp = "gp",
+ gs = "grads",
+ gql = "graphql",
+ graphql = "graphql",
+ graphqls = "graphql",
+ gretl = "gretl",
+ gradle = "groovy",
+ groovy = "groovy",
+ gsp = "gsp",
+ gjs = "javascript.glimmer",
+ gts = "typescript.glimmer",
+ hack = "hack",
+ hackpartial = "hack",
+ haml = "haml",
+ hsm = "hamster",
+ hbs = "handlebars",
+ ["hs-boot"] = "haskell",
+ hsig = "haskell",
+ hsc = "haskell",
+ hs = "haskell",
+ ht = "haste",
+ htpp = "hastepreproc",
+ hb = "hb",
+ sum = "hercules",
+ errsum = "hercules",
+ ev = "hercules",
+ vc = "hercules",
+ hcl = "hcl",
+ heex = "heex",
+ hex = "hex",
+ ["h32"] = "hex",
+ hjson = "hjson",
+ hog = "hog",
+ hws = "hollywood",
+ htt = "httest",
+ htb = "httest",
+ iba = "ibasic",
+ ibi = "ibasic",
+ icn = "icon",
+ inf = "inform",
+ INF = "inform",
+ ii = "initng",
+ iss = "iss",
+ mst = "ist",
+ ist = "ist",
+ ijs = "j",
+ JAL = "jal",
+ jal = "jal",
+ jpr = "jam",
+ jpl = "jam",
+ jav = "java",
+ java = "java",
+ jj = "javacc",
+ jjt = "javacc",
+ es = "javascript",
+ mjs = "javascript",
+ javascript = "javascript",
+ js = "javascript",
+ cjs = "javascript",
+ jsx = "javascriptreact",
+ clp = "jess",
+ jgr = "jgraph",
+ ["j73"] = "jovial",
+ jov = "jovial",
+ jovial = "jovial",
+ properties = "jproperties",
+ slnf = "json",
+ json = "json",
+ jsonp = "json",
+ webmanifest = "json",
+ ipynb = "json",
+ ["json-patch"] = "json",
+ json5 = "json5",
+ jsonc = "jsonc",
+ jsp = "jsp",
+ jl = "julia",
+ kv = "kivy",
+ kix = "kix",
+ kts = "kotlin",
+ kt = "kotlin",
+ ktm = "kotlin",
+ ks = "kscript",
+ k = "kwt",
+ ACE = "lace",
+ ace = "lace",
+ latte = "latte",
+ lte = "latte",
+ ld = "ld",
+ ldif = "ldif",
+ journal = "ledger",
+ ldg = "ledger",
+ ledger = "ledger",
+ less = "less",
+ lex = "lex",
+ lxx = "lex",
+ ["l++"] = "lex",
+ l = "lex",
+ lhs = "lhaskell",
+ ll = "lifelines",
+ liquid = "liquid",
+ cl = "lisp",
+ L = "lisp",
+ lisp = "lisp",
+ el = "lisp",
+ lsp = "lisp",
+ asd = "lisp",
+ lt = "lite",
+ lite = "lite",
+ lgt = "logtalk",
+ lotos = "lotos",
+ lot = "lotos",
+ lout = "lout",
+ lou = "lout",
+ ulpc = "lpc",
+ lpc = "lpc",
+ sig = "lprolog",
+ lsl = "lsl",
+ lss = "lss",
+ nse = "lua",
+ rockspec = "lua",
+ lua = "lua",
+ quake = "m3quake",
+ at = "m4",
+ eml = "mail",
+ mk = "make",
+ mak = "make",
+ dsp = "make",
+ page = "mallard",
+ map = "map",
+ mws = "maple",
+ mpl = "maple",
+ mv = "maple",
+ mkdn = "markdown",
+ md = "markdown",
+ mdwn = "markdown",
+ mkd = "markdown",
+ markdown = "markdown",
+ mdown = "markdown",
+ mhtml = "mason",
+ comp = "mason",
+ mason = "mason",
+ master = "master",
+ mas = "master",
+ mel = "mel",
+ mf = "mf",
+ mgl = "mgl",
+ mgp = "mgp",
+ my = "mib",
+ mib = "mib",
+ mix = "mix",
+ mixal = "mix",
+ nb = "mma",
+ mmp = "mmp",
+ DEF = "modula2",
+ ["m2"] = "modula2",
+ mi = "modula2",
+ ssc = "monk",
+ monk = "monk",
+ tsc = "monk",
+ isc = "monk",
+ moo = "moo",
+ mp = "mp",
+ mof = "msidl",
+ odl = "msidl",
+ msql = "msql",
+ mu = "mupad",
+ mush = "mush",
+ mysql = "mysql",
+ ["n1ql"] = "n1ql",
+ nql = "n1ql",
+ nanorc = "nanorc",
+ ncf = "ncf",
+ nginx = "nginx",
+ ninja = "ninja",
+ nix = "nix",
+ nqc = "nqc",
+ roff = "nroff",
+ tmac = "nroff",
+ man = "nroff",
+ mom = "nroff",
+ nr = "nroff",
+ tr = "nroff",
+ nsi = "nsis",
+ nsh = "nsis",
+ obj = "obj",
+ mlt = "ocaml",
+ mly = "ocaml",
+ mll = "ocaml",
+ mlp = "ocaml",
+ mlip = "ocaml",
+ mli = "ocaml",
+ ml = "ocaml",
+ occ = "occam",
+ xom = "omnimark",
+ xin = "omnimark",
+ opam = "opam",
+ ["or"] = "openroad",
+ ora = "ora",
+ org = "org",
+ org_archive = "org",
+ pxsl = "papp",
+ papp = "papp",
+ pxml = "papp",
+ pas = "pascal",
+ lpr = "pascal",
+ dpr = "pascal",
+ pbtxt = "pbtxt",
+ g = "pccts",
+ pcmk = "pcmk",
+ pdf = "pdf",
+ plx = "perl",
+ prisma = "prisma",
+ psgi = "perl",
+ al = "perl",
+ ctp = "php",
+ php = "php",
+ phpt = "php",
+ phtml = "php",
+ pike = "pike",
+ pmod = "pike",
+ rcp = "pilrc",
+ pli = "pli",
+ ["pl1"] = "pli",
+ ["p36"] = "plm",
+ plm = "plm",
+ pac = "plm",
+ plp = "plp",
+ pls = "plsql",
+ plsql = "plsql",
+ po = "po",
+ pot = "po",
+ pod = "pod",
+ pk = "poke",
+ ps = "postscr",
+ epsi = "postscr",
+ afm = "postscr",
+ epsf = "postscr",
+ eps = "postscr",
+ pfa = "postscr",
+ ai = "postscr",
+ pov = "pov",
+ ppd = "ppd",
+ it = "ppwiz",
+ ih = "ppwiz",
+ action = "privoxy",
+ pc = "proc",
+ pdb = "prolog",
+ pml = "promela",
+ proto = "proto",
+ ["psd1"] = "ps1",
+ ["psm1"] = "ps1",
+ ["ps1"] = "ps1",
+ pssc = "ps1",
+ ["ps1xml"] = "ps1xml",
+ psf = "psf",
+ psl = "psl",
+ pug = "pug",
+ arr = "pyret",
+ pxd = "pyrex",
+ pyx = "pyrex",
+ pyw = "python",
+ py = "python",
+ pyi = "python",
+ ptl = "python",
+ ql = "ql",
+ qll = "ql",
+ rad = "radiance",
+ mat = "radiance",
+ ["pod6"] = "raku",
+ rakudoc = "raku",
+ rakutest = "raku",
+ rakumod = "raku",
+ ["pm6"] = "raku",
+ raku = "raku",
+ ["t6"] = "raku",
+ ["p6"] = "raku",
+ raml = "raml",
+ rbs = "rbs",
+ rego = "rego",
+ rem = "remind",
+ remind = "remind",
+ res = "rescript",
+ resi = "rescript",
+ frt = "reva",
+ testUnit = "rexx",
+ rex = "rexx",
+ orx = "rexx",
+ rexx = "rexx",
+ jrexx = "rexx",
+ rxj = "rexx",
+ rexxj = "rexx",
+ testGroup = "rexx",
+ rxo = "rexx",
+ Rd = "rhelp",
+ rd = "rhelp",
+ rib = "rib",
+ Rmd = "rmd",
+ rmd = "rmd",
+ smd = "rmd",
+ Smd = "rmd",
+ rnc = "rnc",
+ rng = "rng",
+ rnw = "rnoweb",
+ snw = "rnoweb",
+ Rnw = "rnoweb",
+ Snw = "rnoweb",
+ rsc = "routeros",
+ x = "rpcgen",
+ rpl = "rpl",
+ Srst = "rrst",
+ srst = "rrst",
+ Rrst = "rrst",
+ rrst = "rrst",
+ rst = "rst",
+ rtf = "rtf",
+ rjs = "ruby",
+ rxml = "ruby",
+ rb = "ruby",
+ rant = "ruby",
+ ru = "ruby",
+ rbw = "ruby",
+ gemspec = "ruby",
+ builder = "ruby",
+ rake = "ruby",
+ rs = "rust",
+ sas = "sas",
+ sass = "sass",
+ sa = "sather",
+ sbt = "sbt",
+ scala = "scala",
+ ss = "scheme",
+ scm = "scheme",
+ sld = "scheme",
+ rkt = "scheme",
+ rktd = "scheme",
+ rktl = "scheme",
+ sce = "scilab",
+ sci = "scilab",
+ scss = "scss",
+ sd = "sd",
+ sdc = "sdc",
+ pr = "sdl",
+ sdl = "sdl",
+ sed = "sed",
+ sexp = "sexplib",
+ sieve = "sieve",
+ siv = "sieve",
+ sil = "sil",
+ sim = "simula",
+ ["s85"] = "sinda",
+ sin = "sinda",
+ ssm = "sisu",
+ sst = "sisu",
+ ssi = "sisu",
+ ["_sst"] = "sisu",
+ ["-sst"] = "sisu",
+ il = "skill",
+ ils = "skill",
+ cdf = "skill",
+ sl = "slang",
+ ice = "slice",
+ score = "slrnsc",
+ sol = "solidity",
+ tpl = "smarty",
+ ihlp = "smcl",
+ smcl = "smcl",
+ hlp = "smcl",
+ smith = "smith",
+ smt = "smith",
+ sml = "sml",
+ spt = "snobol4",
+ sno = "snobol4",
+ sln = "solution",
+ sparql = "sparql",
+ rq = "sparql",
+ spec = "spec",
+ spice = "spice",
+ sp = "spice",
+ spd = "spup",
+ spdata = "spup",
+ speedup = "spup",
+ spi = "spyce",
+ spy = "spyce",
+ tyc = "sql",
+ typ = "sql",
+ pkb = "sql",
+ tyb = "sql",
+ pks = "sql",
+ sqlj = "sqlj",
+ sqi = "sqr",
+ sqr = "sqr",
+ nut = "squirrel",
+ ["s28"] = "srec",
+ ["s37"] = "srec",
+ srec = "srec",
+ mot = "srec",
+ ["s19"] = "srec",
+ st = "st",
+ imata = "stata",
+ ["do"] = "stata",
+ mata = "stata",
+ ado = "stata",
+ stp = "stp",
+ quark = "supercollider",
+ sface = "surface",
+ svelte = "svelte",
+ svg = "svg",
+ swift = "swift",
+ svh = "systemverilog",
+ sv = "systemverilog",
+ tak = "tak",
+ task = "taskedit",
+ tm = "tcl",
+ tcl = "tcl",
+ itk = "tcl",
+ itcl = "tcl",
+ tk = "tcl",
+ jacl = "tcl",
+ tl = "teal",
+ tmpl = "template",
+ ti = "terminfo",
+ dtx = "tex",
+ ltx = "tex",
+ bbl = "tex",
+ latex = "tex",
+ sty = "tex",
+ texi = "texinfo",
+ txi = "texinfo",
+ texinfo = "texinfo",
+ text = "text",
+ tfvars = "terraform",
+ tla = "tla",
+ tli = "tli",
+ toml = "toml",
+ tpp = "tpp",
+ treetop = "treetop",
+ slt = "tsalt",
+ tsscl = "tsscl",
+ tssgm = "tssgm",
+ tssop = "tssop",
+ tutor = "tutor",
+ twig = "twig",
+ ts = function(path, bufnr)
+ if getline(bufnr, 1):find("<%?xml") then
+ return "xml"
+ else
+ return "typescript"
+ end
+ end,
+ tsx = "typescriptreact",
+ uc = "uc",
+ uit = "uil",
+ uil = "uil",
+ sba = "vb",
+ vb = "vb",
+ dsm = "vb",
+ ctl = "vb",
+ vbs = "vb",
+ vr = "vera",
+ vri = "vera",
+ vrh = "vera",
+ v = "verilog",
+ va = "verilogams",
+ vams = "verilogams",
+ vhdl = "vhdl",
+ vst = "vhdl",
+ vhd = "vhdl",
+ hdl = "vhdl",
+ vho = "vhdl",
+ vbe = "vhdl",
+ vim = "vim",
+ vba = "vim",
+ mar = "vmasm",
+ cm = "voscm",
+ wrl = "vrml",
+ vroom = "vroom",
+ vue = "vue",
+ wat = "wast",
+ wast = "wast",
+ wm = "webmacro",
+ wbt = "winbatch",
+ wml = "wml",
+ wsml = "wsml",
+ ad = "xdefaults",
+ xhtml = "xhtml",
+ xht = "xhtml",
+ msc = "xmath",
+ msf = "xmath",
+ ["psc1"] = "xml",
+ tpm = "xml",
+ xliff = "xml",
+ atom = "xml",
+ xul = "xml",
+ cdxml = "xml",
+ mpd = "xml",
+ rss = "xml",
+ fsproj = "xml",
+ ui = "xml",
+ vbproj = "xml",
+ xlf = "xml",
+ wsdl = "xml",
+ csproj = "xml",
+ wpl = "xml",
+ xmi = "xml",
+ ["xpm2"] = "xpm2",
+ xqy = "xquery",
+ xqm = "xquery",
+ xquery = "xquery",
+ xq = "xquery",
+ xql = "xquery",
+ xs = "xs",
+ xsd = "xsd",
+ xsl = "xslt",
+ xslt = "xslt",
+ yy = "yacc",
+ ["y++"] = "yacc",
+ yxx = "yacc",
+ yml = "yaml",
+ yaml = "yaml",
+ yang = "yang",
+ ["z8a"] = "z8a",
+ zig = "zig",
+ zu = "zimbu",
+ zut = "zimbutempl",
+ zsh = "zsh",
+ vala = "vala",
+ E = function() vim.fn["dist#ft#FTe"]() end,
+ EU = function() vim.fn["dist#ft#EuphoriaCheck"]() end,
+ EW = function() vim.fn["dist#ft#EuphoriaCheck"]() end,
+ EX = function() vim.fn["dist#ft#EuphoriaCheck"]() end,
+ EXU = function() vim.fn["dist#ft#EuphoriaCheck"]() end,
+ EXW = function() vim.fn["dist#ft#EuphoriaCheck"]() end,
+ PL = function() vim.fn["dist#ft#FTpl"]() end,
+ R = function() vim.fn["dist#ft#FTr"]() end,
+ asm = function() vim.fn["dist#ft#FTasm"]() end,
+ bas = function() vim.fn["dist#ft#FTbas"]() end,
+ bi = function() vim.fn["dist#ft#FTbas"]() end,
+ bm = function() vim.fn["dist#ft#FTbas"]() end,
+ bash = function() vim.fn["dist#ft#SetFileTypeSH"]("bash") end,
+ btm = function() vim.fn["dist#ft#FTbtm"]() end,
+ c = function() vim.fn["dist#ft#FTlpc"]() end,
+ ch = function() vim.fn["dist#ft#FTchange"]() end,
+ com = function() vim.fn["dist#ft#BindzoneCheck"]('dcl') end,
+ cpt = function() vim.fn["dist#ft#FThtml"]() end,
+ csh = function() vim.fn["dist#ft#CSH"]() end,
+ d = function() vim.fn["dist#ft#DtraceCheck"]() end,
+ db = function() vim.fn["dist#ft#BindzoneCheck"]('') end,
+ dtml = function() vim.fn["dist#ft#FThtml"]() end,
+ e = function() vim.fn["dist#ft#FTe"]() end,
+ ebuild = function() vim.fn["dist#ft#SetFileTypeSH"]("bash") end,
+ eclass = function() vim.fn["dist#ft#SetFileTypeSH"]("bash") end,
+ ent = function() vim.fn["dist#ft#FTent"]() end,
+ env = function() vim.fn["dist#ft#SetFileTypeSH"](vim.fn.getline(1)) end,
+ eu = function() vim.fn["dist#ft#EuphoriaCheck"]() end,
+ ew = function() vim.fn["dist#ft#EuphoriaCheck"]() end,
+ ex = function() vim.fn["dist#ft#ExCheck"]() end,
+ exu = function() vim.fn["dist#ft#EuphoriaCheck"]() end,
+ exw = function() vim.fn["dist#ft#EuphoriaCheck"]() end,
+ frm = function() vim.fn["dist#ft#FTfrm"]() end,
+ fs = function() vim.fn["dist#ft#FTfs"]() end,
+ h = function() vim.fn["dist#ft#FTheader"]() end,
+ htm = function() vim.fn["dist#ft#FThtml"]() end,
+ html = function() vim.fn["dist#ft#FThtml"]() end,
+ i = function() vim.fn["dist#ft#FTprogress_asm"]() end,
+ idl = function() vim.fn["dist#ft#FTidl"]() end,
+ inc = function() vim.fn["dist#ft#FTinc"]() end,
+ inp = function() vim.fn["dist#ft#Check_inp"]() end,
+ ksh = function() vim.fn["dist#ft#SetFileTypeSH"]("ksh") end,
+ lst = function() vim.fn["dist#ft#FTasm"]() end,
+ m = function() vim.fn["dist#ft#FTm"]() end,
+ mac = function() vim.fn["dist#ft#FTasm"]() end,
+ mc = function() vim.fn["dist#ft#McSetf"]() end,
+ mm = function() vim.fn["dist#ft#FTmm"]() end,
+ mms = function() vim.fn["dist#ft#FTmms"]() end,
+ p = function() vim.fn["dist#ft#FTprogress_pascal"]() end,
+ patch = function(path, bufnr)
+ local firstline = getline(bufnr, 1)
+ if string.find(firstline, "^From " .. string.rep("%x", 40) .. "+ Mon Sep 17 00:00:00 2001$") then
+ return "gitsendemail"
+ else
+ return "diff"
+ end
+ end,
+ pl = function() vim.fn["dist#ft#FTpl"]() end,
+ pp = function() vim.fn["dist#ft#FTpp"]() end,
+ pro = function() vim.fn["dist#ft#ProtoCheck"]('idlang') end,
+ pt = function() vim.fn["dist#ft#FThtml"]() end,
+ r = function() vim.fn["dist#ft#FTr"]() end,
+ rdf = function() vim.fn["dist#ft#Redif"]() end,
+ rules = function() vim.fn["dist#ft#FTRules"]() end,
+ sc = function() vim.fn["dist#ft#FTsc"]() end,
+ scd = function() vim.fn["dist#ft#FTscd"]() end,
+ sh = function() vim.fn["dist#ft#SetFileTypeSH"](vim.fn.getline(1)) end,
+ shtml = function() vim.fn["dist#ft#FThtml"]() end,
+ sql = function() vim.fn["dist#ft#SQL"]() end,
+ stm = function() vim.fn["dist#ft#FThtml"]() end,
+ tcsh = function() vim.fn["dist#ft#SetFileTypeShell"]("tcsh") end,
+ tex = function() vim.fn["dist#ft#FTtex"]() end,
+ tf = function() vim.fn["dist#ft#FTtf"]() end,
+ w = function() vim.fn["dist#ft#FTprogress_cweb"]() end,
+ xml = function() vim.fn["dist#ft#FTxml"]() end,
+ y = function() vim.fn["dist#ft#FTy"]() end,
+ zsql = function() vim.fn["dist#ft#SQL"]() end,
+ txt = function(path, bufnr)
+ --helpfiles match *.txt, but should have a modeline as last line
+ if not getline(bufnr, -1):match("vim:.*ft=help") then
+ return "text"
+ end
+ end,
+ -- END EXTENSION
+}
+
+local filename = {
+ -- BEGIN FILENAME
+ ["a2psrc"] = "a2ps",
+ ["/etc/a2ps.cfg"] = "a2ps",
+ [".a2psrc"] = "a2ps",
+ [".asoundrc"] = "alsaconf",
+ ["/usr/share/alsa/alsa.conf"] = "alsaconf",
+ ["/etc/asound.conf"] = "alsaconf",
+ ["build.xml"] = "ant",
+ [".htaccess"] = "apache",
+ ["apt.conf"] = "aptconf",
+ ["/.aptitude/config"] = "aptconf",
+ ["=tagging-method"] = "arch",
+ [".arch-inventory"] = "arch",
+ ["GNUmakefile.am"] = "automake",
+ ["named.root"] = "bindzone",
+ WORKSPACE = "bzl",
+ BUILD = "bzl",
+ ["cabal.config"] = "cabalconfig",
+ ["cabal.project"] = "cabalproject",
+ calendar = "calendar",
+ catalog = "catalog",
+ ["/etc/cdrdao.conf"] = "cdrdaoconf",
+ [".cdrdao"] = "cdrdaoconf",
+ ["/etc/default/cdrdao"] = "cdrdaoconf",
+ ["/etc/defaults/cdrdao"] = "cdrdaoconf",
+ ["cfengine.conf"] = "cfengine",
+ ["CMakeLists.txt"] = "cmake",
+ ["auto.master"] = "conf",
+ ["configure.in"] = "config",
+ ["configure.ac"] = "config",
+ [".cvsrc"] = "cvsrc",
+ ["/debian/changelog"] = "debchangelog",
+ ["changelog.dch"] = "debchangelog",
+ ["changelog.Debian"] = "debchangelog",
+ ["NEWS.dch"] = "debchangelog",
+ ["NEWS.Debian"] = "debchangelog",
+ ["/debian/control"] = "debcontrol",
+ ["/debian/copyright"] = "debcopyright",
+ ["/etc/apt/sources.list"] = "debsources",
+ ["denyhosts.conf"] = "denyhosts",
+ ["dict.conf"] = "dictconf",
+ [".dictrc"] = "dictconf",
+ ["/etc/DIR_COLORS"] = "dircolors",
+ [".dir_colors"] = "dircolors",
+ [".dircolors"] = "dircolors",
+ ["/etc/dnsmasq.conf"] = "dnsmasq",
+ Containerfile = "dockerfile",
+ Dockerfile = "dockerfile",
+ npmrc = "dosini",
+ ["/etc/yum.conf"] = "dosini",
+ ["/etc/pacman.conf"] = "dosini",
+ [".npmrc"] = "dosini",
+ [".editorconfig"] = "dosini",
+ dune = "dune",
+ jbuild = "dune",
+ ["dune-workspace"] = "dune",
+ ["dune-project"] = "dune",
+ ["elinks.conf"] = "elinks",
+ ["mix.lock"] = "elixir",
+ ["filter-rules"] = "elmfilt",
+ ["exim.conf"] = "exim",
+ exports = "exports",
+ [".fetchmailrc"] = "fetchmail",
+ fvSchemes = function() vim.fn["dist#ft#FTfoam"]() end,
+ fvSolution = function() vim.fn["dist#ft#FTfoam"]() end,
+ fvConstraints = function() vim.fn["dist#ft#FTfoam"]() end,
+ fvModels = function() vim.fn["dist#ft#FTfoam"]() end,
+ fstab = "fstab",
+ mtab = "fstab",
+ [".gdbinit"] = "gdb",
+ gdbinit = "gdb",
+ [".gdbearlyinit"] = "gdb",
+ gdbearlyinit = "gdb",
+ ["lltxxxxx.txt"] = "gedcom",
+ ["TAG_EDITMSG"] = "gitcommit",
+ ["MERGE_MSG"] = "gitcommit",
+ ["COMMIT_EDITMSG"] = "gitcommit",
+ ["NOTES_EDITMSG"] = "gitcommit",
+ ["EDIT_DESCRIPTION"] = "gitcommit",
+ [".gitconfig"] = "gitconfig",
+ [".gitmodules"] = "gitconfig",
+ ["gitolite.conf"] = "gitolite",
+ ["git-rebase-todo"] = "gitrebase",
+ gkrellmrc = "gkrellmrc",
+ [".gnashrc"] = "gnash",
+ [".gnashpluginrc"] = "gnash",
+ gnashpluginrc = "gnash",
+ gnashrc = "gnash",
+ ["go.work"] = "gowork",
+ [".gprc"] = "gp",
+ ["/.gnupg/gpg.conf"] = "gpg",
+ ["/.gnupg/options"] = "gpg",
+ ["/var/backups/gshadow.bak"] = "group",
+ ["/etc/gshadow"] = "group",
+ ["/etc/group-"] = "group",
+ ["/etc/gshadow.edit"] = "group",
+ ["/etc/gshadow-"] = "group",
+ ["/etc/group"] = "group",
+ ["/var/backups/group.bak"] = "group",
+ ["/etc/group.edit"] = "group",
+ ["/boot/grub/menu.lst"] = "grub",
+ ["/etc/grub.conf"] = "grub",
+ ["/boot/grub/grub.conf"] = "grub",
+ [".gtkrc"] = "gtkrc",
+ gtkrc = "gtkrc",
+ ["snort.conf"] = "hog",
+ ["vision.conf"] = "hog",
+ ["/etc/host.conf"] = "hostconf",
+ ["/etc/hosts.allow"] = "hostsaccess",
+ ["/etc/hosts.deny"] = "hostsaccess",
+ ["/i3/config"] = "i3config",
+ ["/sway/config"] = "i3config",
+ ["/.sway/config"] = "i3config",
+ ["/.i3/config"] = "i3config",
+ ["/.icewm/menu"] = "icemenu",
+ [".indent.pro"] = "indent",
+ indentrc = "indent",
+ inittab = "inittab",
+ ["ipf.conf"] = "ipfilter",
+ ["ipf6.conf"] = "ipfilter",
+ ["ipf.rules"] = "ipfilter",
+ [".eslintrc"] = "json",
+ [".babelrc"] = "json",
+ ["Pipfile.lock"] = "json",
+ [".firebaserc"] = "json",
+ [".prettierrc"] = "json",
+ Kconfig = "kconfig",
+ ["Kconfig.debug"] = "kconfig",
+ ["lftp.conf"] = "lftp",
+ [".lftprc"] = "lftp",
+ ["/.libao"] = "libao",
+ ["/etc/libao.conf"] = "libao",
+ ["lilo.conf"] = "lilo",
+ ["/etc/limits"] = "limits",
+ [".emacs"] = "lisp",
+ sbclrc = "lisp",
+ [".sbclrc"] = "lisp",
+ [".sawfishrc"] = "lisp",
+ ["/etc/login.access"] = "loginaccess",
+ ["/etc/login.defs"] = "logindefs",
+ ["lynx.cfg"] = "lynx",
+ ["m3overrides"] = "m3build",
+ ["m3makefile"] = "m3build",
+ ["cm3.cfg"] = "m3quake",
+ [".followup"] = "mail",
+ [".article"] = "mail",
+ [".letter"] = "mail",
+ ["/etc/aliases"] = "mailaliases",
+ ["/etc/mail/aliases"] = "mailaliases",
+ mailcap = "mailcap",
+ [".mailcap"] = "mailcap",
+ ["/etc/man.conf"] = "manconf",
+ ["man.config"] = "manconf",
+ ["meson.build"] = "meson",
+ ["meson_options.txt"] = "meson",
+ ["/etc/conf.modules"] = "modconf",
+ ["/etc/modules"] = "modconf",
+ ["/etc/modules.conf"] = "modconf",
+ ["/.mplayer/config"] = "mplayerconf",
+ ["mplayer.conf"] = "mplayerconf",
+ mrxvtrc = "mrxvtrc",
+ [".mrxvtrc"] = "mrxvtrc",
+ ["/etc/nanorc"] = "nanorc",
+ Neomuttrc = "neomuttrc",
+ [".netrc"] = "netrc",
+ [".ocamlinit"] = "ocaml",
+ [".octaverc"] = "octave",
+ octaverc = "octave",
+ ["octave.conf"] = "octave",
+ opam = "opam",
+ ["/etc/pam.conf"] = "pamconf",
+ ["pam_env.conf"] = "pamenv",
+ [".pam_environment"] = "pamenv",
+ ["/var/backups/passwd.bak"] = "passwd",
+ ["/var/backups/shadow.bak"] = "passwd",
+ ["/etc/passwd"] = "passwd",
+ ["/etc/passwd-"] = "passwd",
+ ["/etc/shadow.edit"] = "passwd",
+ ["/etc/shadow-"] = "passwd",
+ ["/etc/shadow"] = "passwd",
+ ["/etc/passwd.edit"] = "passwd",
+ ["pf.conf"] = "pf",
+ ["main.cf"] = "pfmain",
+ pinerc = "pine",
+ [".pinercex"] = "pine",
+ [".pinerc"] = "pine",
+ pinercex = "pine",
+ ["/etc/pinforc"] = "pinfo",
+ ["/.pinforc"] = "pinfo",
+ [".povrayrc"] = "povini",
+ [".procmailrc"] = "procmail",
+ [".procmail"] = "procmail",
+ ["/etc/protocols"] = "protocols",
+ [".pythonstartup"] = "python",
+ [".pythonrc"] = "python",
+ SConstruct = "python",
+ ratpoisonrc = "ratpoison",
+ [".ratpoisonrc"] = "ratpoison",
+ v = "rcs",
+ inputrc = "readline",
+ [".inputrc"] = "readline",
+ [".reminders"] = "remind",
+ ["resolv.conf"] = "resolv",
+ ["robots.txt"] = "robots",
+ Gemfile = "ruby",
+ Puppetfile = "ruby",
+ [".irbrc"] = "ruby",
+ irbrc = "ruby",
+ Vagrantfile = "ruby",
+ ["smb.conf"] = "samba",
+ screenrc = "screen",
+ [".screenrc"] = "screen",
+ ["/etc/sensors3.conf"] = "sensors",
+ ["/etc/sensors.conf"] = "sensors",
+ ["/etc/services"] = "services",
+ ["/etc/serial.conf"] = "setserial",
+ ["/etc/udev/cdsymlinks.conf"] = "sh",
+ ["/etc/slp.conf"] = "slpconf",
+ ["/etc/slp.reg"] = "slpreg",
+ ["/etc/slp.spi"] = "slpspi",
+ [".slrnrc"] = "slrnrc",
+ ["sendmail.cf"] = "sm",
+ ["squid.conf"] = "squid",
+ ["/.ssh/config"] = "sshconfig",
+ ["ssh_config"] = "sshconfig",
+ ["sshd_config"] = "sshdconfig",
+ ["/etc/sudoers"] = "sudoers",
+ ["sudoers.tmp"] = "sudoers",
+ ["/etc/sysctl.conf"] = "sysctl",
+ tags = "tags",
+ [".tclshrc"] = "tcl",
+ [".wishrc"] = "tcl",
+ ["tclsh.rc"] = "tcl",
+ ["texmf.cnf"] = "texmf",
+ COPYING = "text",
+ README = "text",
+ LICENSE = "text",
+ AUTHORS = "text",
+ tfrc = "tf",
+ [".tfrc"] = "tf",
+ ["tidy.conf"] = "tidy",
+ tidyrc = "tidy",
+ [".tidyrc"] = "tidy",
+ [".tmux.conf"] = "tmux",
+ ["/.cargo/config"] = "toml",
+ Pipfile = "toml",
+ ["Gopkg.lock"] = "toml",
+ ["/.cargo/credentials"] = "toml",
+ ["Cargo.lock"] = "toml",
+ ["trustees.conf"] = "trustees",
+ ["/etc/udev/udev.conf"] = "udevconf",
+ ["/etc/updatedb.conf"] = "updatedb",
+ ["fdrupstream.log"] = "upstreamlog",
+ vgrindefs = "vgrindefs",
+ [".exrc"] = "vim",
+ ["_exrc"] = "vim",
+ ["_viminfo"] = "viminfo",
+ [".viminfo"] = "viminfo",
+ [".wgetrc"] = "wget",
+ wgetrc = "wget",
+ [".wvdialrc"] = "wvdial",
+ ["wvdial.conf"] = "wvdial",
+ [".Xresources"] = "xdefaults",
+ [".Xpdefaults"] = "xdefaults",
+ ["xdm-config"] = "xdefaults",
+ [".Xdefaults"] = "xdefaults",
+ ["/etc/xinetd.conf"] = "xinetd",
+ fglrxrc = "xml",
+ ["/etc/blkid.tab"] = "xml",
+ ["/etc/blkid.tab.old"] = "xml",
+ ["/etc/zprofile"] = "zsh",
+ [".zlogin"] = "zsh",
+ [".zlogout"] = "zsh",
+ [".zshrc"] = "zsh",
+ [".zprofile"] = "zsh",
+ [".zcompdump"] = "zsh",
+ [".zshenv"] = "zsh",
+ [".zfbfmarks"] = "zsh",
+ [".alias"] = function() vim.fn["dist#ft#CSH"]() end,
+ [".bashrc"] = function() vim.fn["dist#ft#SetFileTypeSH"]("bash") end,
+ [".cshrc"] = function() vim.fn["dist#ft#CSH"]() end,
+ [".env"] = function() vim.fn["dist#ft#SetFileTypeSH"](vim.fn.getline(1)) end,
+ [".kshrc"] = function() vim.fn["dist#ft#SetFileTypeSH"]("ksh") end,
+ [".login"] = function() vim.fn["dist#ft#CSH"]() end,
+ [".profile"] = function() vim.fn["dist#ft#SetFileTypeSH"](vim.fn.getline(1)) end,
+ [".tcshrc"] = function() vim.fn["dist#ft#SetFileTypeShell"]("tcsh") end,
+ ["/etc/profile"] = function() vim.fn["dist#ft#SetFileTypeSH"](vim.fn.getline(1)) end,
+ APKBUILD = function() vim.fn["dist#ft#SetFileTypeSH"]("bash") end,
+ PKGBUILD = function() vim.fn["dist#ft#SetFileTypeSH"]("bash") end,
+ ["bash.bashrc"] = function() vim.fn["dist#ft#SetFileTypeSH"]("bash") end,
+ bashrc = function() vim.fn["dist#ft#SetFileTypeSH"]("bash") end,
+ crontab = starsetf('crontab'),
+ ["csh.cshrc"] = function() vim.fn["dist#ft#CSH"]() end,
+ ["csh.login"] = function() vim.fn["dist#ft#CSH"]() end,
+ ["csh.logout"] = function() vim.fn["dist#ft#CSH"]() end,
+ ["indent.pro"] = function() vim.fn["dist#ft#ProtoCheck"]('indent') end,
+ ["tcsh.login"] = function() vim.fn["dist#ft#SetFileTypeShell"]("tcsh") end,
+ ["tcsh.tcshrc"] = function() vim.fn["dist#ft#SetFileTypeShell"]("tcsh") end,
+ -- END FILENAME
+}
+
+local pattern = {
+ -- BEGIN PATTERN
+ [".*/etc/a2ps/.*%.cfg"] = "a2ps",
+ [".*/etc/a2ps%.cfg"] = "a2ps",
+ [".*/usr/share/alsa/alsa%.conf"] = "alsaconf",
+ [".*/etc/asound%.conf"] = "alsaconf",
+ [".*/etc/apache2/sites%-.*/.*%.com"] = "apache",
+ [".*/etc/httpd/.*%.conf"] = "apache",
+ [".*/%.aptitude/config"] = "aptconf",
+ ["[mM]akefile%.am"] = "automake",
+ [".*bsd"] = "bsdl",
+ ["bzr_log%..*"] = "bzr",
+ [".*enlightenment/.*%.cfg"] = "c",
+ [".*/etc/defaults/cdrdao"] = "cdrdaoconf",
+ [".*/etc/cdrdao%.conf"] = "cdrdaoconf",
+ [".*/etc/default/cdrdao"] = "cdrdaoconf",
+ [".*hgrc"] = "cfg",
+ [".*%.%.ch"] = "chill",
+ [".*%.cmake%.in"] = "cmake",
+ [".*/debian/changelog"] = "debchangelog",
+ [".*/debian/control"] = "debcontrol",
+ [".*/debian/copyright"] = "debcopyright",
+ [".*/etc/apt/sources%.list%.d/.*%.list"] = "debsources",
+ [".*/etc/apt/sources%.list"] = "debsources",
+ ["dictd.*%.conf"] = "dictdconf",
+ [".*/etc/DIR_COLORS"] = "dircolors",
+ [".*/etc/dnsmasq%.conf"] = "dnsmasq",
+ ["php%.ini%-.*"] = "dosini",
+ [".*/etc/pacman%.conf"] = "dosini",
+ [".*/etc/yum%.conf"] = "dosini",
+ [".*lvs"] = "dracula",
+ [".*lpe"] = "dracula",
+ [".*/dtrace/.*%.d"] = "dtrace",
+ [".*esmtprc"] = "esmtprc",
+ [".*Eterm/.*%.cfg"] = "eterm",
+ [".*%.git/modules/.*/config"] = "gitconfig",
+ [".*%.git/config"] = "gitconfig",
+ [".*/etc/gitconfig"] = "gitconfig",
+ [".*/%.config/git/config"] = "gitconfig",
+ [".*%.git/config%.worktree"] = "gitconfig",
+ [".*%.git/worktrees/.*/config%.worktree"] = "gitconfig",
+ ["%.gitsendemail%.msg%......."] = "gitsendemail",
+ ["gkrellmrc_."] = "gkrellmrc",
+ [".*/usr/.*/gnupg/options%.skel"] = "gpg",
+ [".*/%.gnupg/options"] = "gpg",
+ [".*/%.gnupg/gpg%.conf"] = "gpg",
+ [".*/etc/group"] = "group",
+ [".*/etc/gshadow"] = "group",
+ [".*/etc/group%.edit"] = "group",
+ [".*/var/backups/gshadow%.bak"] = "group",
+ [".*/etc/group-"] = "group",
+ [".*/etc/gshadow-"] = "group",
+ [".*/var/backups/group%.bak"] = "group",
+ [".*/etc/gshadow%.edit"] = "group",
+ [".*/boot/grub/grub%.conf"] = "grub",
+ [".*/boot/grub/menu%.lst"] = "grub",
+ [".*/etc/grub%.conf"] = "grub",
+ ["hg%-editor%-.*%.txt"] = "hgcommit",
+ [".*/etc/host%.conf"] = "hostconf",
+ [".*/etc/hosts%.deny"] = "hostsaccess",
+ [".*/etc/hosts%.allow"] = "hostsaccess",
+ [".*%.html%.m4"] = "htmlm4",
+ [".*/%.i3/config"] = "i3config",
+ [".*/sway/config"] = "i3config",
+ [".*/i3/config"] = "i3config",
+ [".*/%.sway/config"] = "i3config",
+ [".*/%.icewm/menu"] = "icemenu",
+ [".*/etc/initng/.*/.*%.i"] = "initng",
+ [".*%.properties_.."] = "jproperties",
+ [".*%.properties_.._.."] = "jproperties",
+ [".*lftp/rc"] = "lftp",
+ [".*/%.libao"] = "libao",
+ [".*/etc/libao%.conf"] = "libao",
+ [".*/etc/.*limits%.conf"] = "limits",
+ [".*/etc/limits"] = "limits",
+ [".*/etc/.*limits%.d/.*%.conf"] = "limits",
+ [".*/LiteStep/.*/.*%.rc"] = "litestep",
+ [".*/etc/login%.access"] = "loginaccess",
+ [".*/etc/login%.defs"] = "logindefs",
+ [".*/etc/mail/aliases"] = "mailaliases",
+ [".*/etc/aliases"] = "mailaliases",
+ [".*[mM]akefile"] = "make",
+ [".*/etc/man%.conf"] = "manconf",
+ [".*/etc/modules%.conf"] = "modconf",
+ [".*/etc/conf%.modules"] = "modconf",
+ [".*/etc/modules"] = "modconf",
+ [".*%.[mi][3g]"] = "modula3",
+ [".*/%.mplayer/config"] = "mplayerconf",
+ ["rndc.*%.conf"] = "named",
+ ["rndc.*%.key"] = "named",
+ ["named.*%.conf"] = "named",
+ [".*/etc/nanorc"] = "nanorc",
+ [".*%.NS[ACGLMNPS]"] = "natural",
+ ["nginx.*%.conf"] = "nginx",
+ [".*/etc/nginx/.*"] = "nginx",
+ [".*nginx%.conf"] = "nginx",
+ [".*/nginx/.*%.conf"] = "nginx",
+ [".*/usr/local/nginx/conf/.*"] = "nginx",
+ [".*%.ml%.cppo"] = "ocaml",
+ [".*%.mli%.cppo"] = "ocaml",
+ [".*%.opam%.template"] = "opam",
+ [".*%.[Oo][Pp][Ll]"] = "opl",
+ [".*/etc/pam%.conf"] = "pamconf",
+ [".*/etc/passwd-"] = "passwd",
+ [".*/etc/shadow"] = "passwd",
+ [".*/etc/shadow%.edit"] = "passwd",
+ [".*/var/backups/shadow%.bak"] = "passwd",
+ [".*/var/backups/passwd%.bak"] = "passwd",
+ [".*/etc/passwd"] = "passwd",
+ [".*/etc/passwd%.edit"] = "passwd",
+ [".*/etc/shadow-"] = "passwd",
+ [".*/%.pinforc"] = "pinfo",
+ [".*/etc/pinforc"] = "pinfo",
+ [".*/etc/protocols"] = "protocols",
+ [".*baseq[2-3]/.*%.cfg"] = "quake",
+ [".*quake[1-3]/.*%.cfg"] = "quake",
+ [".*id1/.*%.cfg"] = "quake",
+ ["[rR]antfile"] = "ruby",
+ ["[rR]akefile"] = "ruby",
+ [".*/etc/sensors%.conf"] = "sensors",
+ [".*/etc/sensors3%.conf"] = "sensors",
+ [".*/etc/services"] = "services",
+ [".*/etc/serial%.conf"] = "setserial",
+ [".*/etc/udev/cdsymlinks%.conf"] = "sh",
+ [".*%._sst%.meta"] = "sisu",
+ [".*%.%-sst%.meta"] = "sisu",
+ [".*%.sst%.meta"] = "sisu",
+ [".*/etc/slp%.conf"] = "slpconf",
+ [".*/etc/slp%.reg"] = "slpreg",
+ [".*/etc/slp%.spi"] = "slpspi",
+ [".*/etc/ssh/ssh_config%.d/.*%.conf"] = "sshconfig",
+ [".*/%.ssh/config"] = "sshconfig",
+ [".*/etc/ssh/sshd_config%.d/.*%.conf"] = "sshdconfig",
+ [".*/etc/sudoers"] = "sudoers",
+ ["svn%-commit.*%.tmp"] = "svn",
+ [".*%.swift%.gyb"] = "swiftgyb",
+ [".*/etc/sysctl%.conf"] = "sysctl",
+ [".*/etc/sysctl%.d/.*%.conf"] = "sysctl",
+ [".*/etc/systemd/.*%.conf%.d/.*%.conf"] = "systemd",
+ [".*/%.config/systemd/user/.*%.d/.*%.conf"] = "systemd",
+ [".*/etc/systemd/system/.*%.d/.*%.conf"] = "systemd",
+ [".*%.t%.html"] = "tilde",
+ ["%.?tmux.*%.conf"] = "tmux",
+ ["%.?tmux.*%.conf.*"] = { "tmux", { priority = -1 } },
+ [".*/%.cargo/config"] = "toml",
+ [".*/%.cargo/credentials"] = "toml",
+ [".*/etc/udev/udev%.conf"] = "udevconf",
+ [".*/etc/udev/permissions%.d/.*%.permissions"] = "udevperm",
+ [".*/etc/updatedb%.conf"] = "updatedb",
+ [".*/%.init/.*%.override"] = "upstart",
+ [".*/usr/share/upstart/.*%.conf"] = "upstart",
+ [".*/%.config/upstart/.*%.override"] = "upstart",
+ [".*/etc/init/.*%.conf"] = "upstart",
+ [".*/etc/init/.*%.override"] = "upstart",
+ [".*/%.config/upstart/.*%.conf"] = "upstart",
+ [".*/%.init/.*%.conf"] = "upstart",
+ [".*/usr/share/upstart/.*%.override"] = "upstart",
+ [".*%.ws[fc]"] = "wsh",
+ [".*/etc/xinetd%.conf"] = "xinetd",
+ [".*/etc/blkid%.tab"] = "xml",
+ [".*/etc/blkid%.tab%.old"] = "xml",
+ [".*%.vbproj%.user"] = "xml",
+ [".*%.fsproj%.user"] = "xml",
+ [".*%.csproj%.user"] = "xml",
+ [".*/etc/xdg/menus/.*%.menu"] = "xml",
+ [".*Xmodmap"] = "xmodmap",
+ [".*/etc/zprofile"] = "zsh",
+ ["%.bash[_-]aliases"] = function() vim.fn["dist#ft#SetFileTypeSH"]("bash") end,
+ ["%.bash[_-]logout"] = function() vim.fn["dist#ft#SetFileTypeSH"]("bash") end,
+ ["%.bash[_-]profile"] = function() vim.fn["dist#ft#SetFileTypeSH"]("bash") end,
+ ["%.cshrc.*"] = function() vim.fn["dist#ft#CSH"]() end,
+ ["%.gtkrc.*"] = starsetf('gtkrc'),
+ ["%.kshrc.*"] = function() vim.fn["dist#ft#SetFileTypeSH"]("ksh") end,
+ ["%.login.*"] = function() vim.fn["dist#ft#CSH"]() end,
+ ["%.neomuttrc.*"] = starsetf('neomuttrc'),
+ ["%.profile.*"] = function() vim.fn["dist#ft#SetFileTypeSH"](vim.fn.getline(1)) end,
+ ["%.reminders.*"] = starsetf('remind'),
+ ["%.tcshrc.*"] = function() vim.fn["dist#ft#SetFileTypeShell"]("tcsh") end,
+ ["%.zcompdump.*"] = starsetf('zsh'),
+ ["%.zlog.*"] = starsetf('zsh'),
+ ["%.zsh.*"] = starsetf('zsh'),
+ [".*%.[1-9]"] = function() vim.fn["dist#ft#FTnroff"]() end,
+ [".*%.[aA]"] = function() vim.fn["dist#ft#FTasm"]() end,
+ [".*%.[sS]"] = function() vim.fn["dist#ft#FTasm"]() end,
+ [".*%.properties_.._.._.*"] = starsetf('jproperties'),
+ [".*%.vhdl_[0-9].*"] = starsetf('vhdl'),
+ [".*/%.fvwm/.*"] = starsetf('fvwm'),
+ [".*/%.gitconfig%.d/.*"] = starsetf('gitconfig'),
+ [".*/%.neomutt/neomuttrc.*"] = starsetf('neomuttrc'),
+ [".*/Xresources/.*"] = starsetf('xdefaults'),
+ [".*/app%-defaults/.*"] = starsetf('xdefaults'),
+ [".*/bind/db%..*"] = starsetf('bindzone'),
+ [".*/debian/patches/.*"] = function() vim.fn["dist#ft#Dep3patch"]() end,
+ [".*/etc/Muttrc%.d/.*"] = starsetf('muttrc'),
+ [".*/etc/apache2/.*%.conf.*"] = starsetf('apache'),
+ [".*/etc/apache2/conf%..*/.*"] = starsetf('apache'),
+ [".*/etc/apache2/mods%-.*/.*"] = starsetf('apache'),
+ [".*/etc/apache2/sites%-.*/.*"] = starsetf('apache'),
+ [".*/etc/cron%.d/.*"] = starsetf('crontab'),
+ [".*/etc/dnsmasq%.d/.*"] = starsetf('dnsmasq'),
+ [".*/etc/httpd/conf%..*/.*"] = starsetf('apache'),
+ [".*/etc/httpd/conf%.d/.*%.conf.*"] = starsetf('apache'),
+ [".*/etc/httpd/mods%-.*/.*"] = starsetf('apache'),
+ [".*/etc/httpd/sites%-.*/.*"] = starsetf('apache'),
+ [".*/etc/logcheck/.*%.d.*/.*"] = starsetf('logcheck'),
+ [".*/etc/modprobe%..*"] = starsetf('modconf'),
+ [".*/etc/pam%.d/.*"] = starsetf('pamconf'),
+ [".*/etc/profile"] = function() vim.fn["dist#ft#SetFileTypeSH"](vim.fn.getline(1)) end,
+ [".*/etc/proftpd/.*%.conf.*"] = starsetf('apachestyle'),
+ [".*/etc/proftpd/conf%..*/.*"] = starsetf('apachestyle'),
+ [".*/etc/sudoers%.d/.*"] = starsetf('sudoers'),
+ [".*/etc/xinetd%.d/.*"] = starsetf('xinetd'),
+ [".*/etc/yum%.repos%.d/.*"] = starsetf('dosini'),
+ [".*/gitolite%-admin/conf/.*"] = starsetf('gitolite'),
+ [".*/named/db%..*"] = starsetf('bindzone'),
+ [".*/tmp/lltmp.*"] = starsetf('gedcom'),
+ [".*asterisk.*/.*voicemail%.conf.*"] = starsetf('asteriskvm'),
+ [".*asterisk/.*%.conf.*"] = starsetf('asterisk'),
+ [".*vimrc.*"] = starsetf('vim'),
+ [".*xmodmap.*"] = starsetf('xmodmap'),
+ ["/etc/gitconfig%.d/.*"] = starsetf('gitconfig'),
+ ["/etc/hostname%..*"] = starsetf('config'),
+ ["Containerfile%..*"] = starsetf('dockerfile'),
+ ["Dockerfile%..*"] = starsetf('dockerfile'),
+ ["JAM.*%..*"] = starsetf('jam'),
+ ["Kconfig%..*"] = starsetf('kconfig'),
+ ["Neomuttrc.*"] = starsetf('neomuttrc'),
+ ["Prl.*%..*"] = starsetf('jam'),
+ ["Xresources.*"] = starsetf('xdefaults'),
+ ["[mM]akefile.*"] = starsetf('make'),
+ ["[rR]akefile.*"] = starsetf('ruby'),
+ ["access%.conf.*"] = starsetf('apache'),
+ ["apache%.conf.*"] = starsetf('apache'),
+ ["apache2%.conf.*"] = starsetf('apache'),
+ ["bash%-fc[-%.]"] = function() vim.fn["dist#ft#SetFileTypeSH"]("bash") end,
+ ["cabal%.project%..*"] = starsetf('cabalproject'),
+ ["crontab%..*"] = starsetf('crontab'),
+ ["drac%..*"] = starsetf('dracula'),
+ ["gtkrc.*"] = starsetf('gtkrc'),
+ ["httpd%.conf.*"] = starsetf('apache'),
+ ["lilo%.conf.*"] = starsetf('lilo'),
+ ["neomuttrc.*"] = starsetf('neomuttrc'),
+ ["proftpd%.conf.*"] = starsetf('apachestyle'),
+ ["reportbug%-.*"] = starsetf('mail'),
+ ["sgml%.catalog.*"] = starsetf('catalog'),
+ ["srm%.conf.*"] = starsetf('apache'),
+ ["tmac%..*"] = starsetf('nroff'),
+ ["zlog.*"] = starsetf('zsh'),
+ ["zsh.*"] = starsetf('zsh'),
+ ["ae%d+%.txt"] = 'mail',
+ ["snd%.%d+"] = "mail",
+ ["%.letter%.%d+"] = "mail",
+ ["%.article%.%d+"] = "mail",
+ ["pico%.%d+"] = "mail",
+ ["mutt%-.*%-%w+"] = "mail",
+ ["neomutt%-.*%-%w+"] = "mail",
+ ["muttng%-.*%-%w+"] = "mail",
+ ["mutt" .. string.rep("[%w_-]", 6)] = "mail",
+ ["neomutt" .. string.rep("[%w_-]", 6)] = "mail",
+ ["/tmp/SLRN[0-9A-Z.]+"] = "mail",
+ ["[a-zA-Z0-9].*Dict"] = function() vim.fn["dist#ft#FTfoam"]() end,
+ ["[a-zA-Z0-9].*Dict%..*"] = function() vim.fn["dist#ft#FTfoam"]() end,
+ ["[a-zA-Z].*Properties"] = function() vim.fn["dist#ft#FTfoam"]() end,
+ ["[a-zA-Z].*Properties%..*"] = function() vim.fn["dist#ft#FTfoam"]() end,
+ [".*Transport%..*"] = function() vim.fn["dist#ft#FTfoam"]() end,
+ [".*/constant/g"] = function() vim.fn["dist#ft#FTfoam"]() end,
+ [".*/0/.*"] = function() vim.fn["dist#ft#FTfoam"]() end,
+ [".*/0%.orig/.*"] = function() vim.fn["dist#ft#FTfoam"]() end,
+ [".*/etc/sensors%.d/[^.].*"] = starsetf('sensors'),
+ [".*%.git/.*"] = function(path, bufnr)
+ local firstline = getline(bufnr, 1)
+ if firstline:find("^" .. string.rep("%x", 40) .. "+ ") or firstline:sub(1, 5) == "ref: " then
+ return "git"
+ end
+ end,
+ [".*%.[Cc][Ff][Gg]"] = function() vim.fn["dist#ft#FTcfg"]() end,
+ [".*%.[Dd][Aa][Tt]"] = function() vim.fn["dist#ft#FTdat"]() end,
+ [".*%.[Mm][Oo][Dd]"] = function() vim.fn["dist#ft#FTmod"]() end,
+ [".*%.[Ss][Rr][Cc]"] = function() vim.fn["dist#ft#FTsrc"]() end,
+ [".*%.[Ss][Uu][Bb]"] = "krl",
+ [".*%.[Pp][Rr][Gg]"] = function() vim.fn["dist#ft#FTprg"]() end,
+ [".*%.[Ss][Yy][Ss]"] = function() vim.fn["dist#ft#FTsys"]() end,
+ -- Neovim only
+ [".*/queries/.*%.scm"] = "query", -- tree-sitter queries
+ -- END PATTERN
+}
+-- luacheck: pop
+
+---@private
+local function sort_by_priority(t)
+ local sorted = {}
+ for k, v in pairs(t) do
+ local ft = type(v) == "table" and v[1] or v
+ assert(type(ft) == "string" or type(ft) == "function", "Expected string or function for filetype")
+
+ local opts = (type(v) == "table" and type(v[2]) == "table") and v[2] or {}
+ if not opts.priority then
+ opts.priority = 0
+ end
+ table.insert(sorted, { [k] = { ft, opts } })
+ end
+ table.sort(sorted, function(a, b)
+ return a[next(a)][2].priority > b[next(b)][2].priority
+ end)
+ return sorted
+end
+
+local pattern_sorted = sort_by_priority(pattern)
+
+---@private
+local function normalize_path(path)
+ return (path:gsub("\\", "/"):gsub("^~", vim.env.HOME))
+end
+
+--- Add new filetype mappings.
+---
+--- Filetype mappings can be added either by extension or by filename (either
+--- the "tail" or the full file path). The full file path is checked first,
+--- followed by the file name. If a match is not found using the filename, then
+--- the filename is matched against the list of patterns (sorted by priority)
+--- until a match is found. Lastly, if pattern matching does not find a
+--- filetype, then the file extension is used.
+---
+--- The filetype can be either a string (in which case it is used as the
+--- filetype directly) or a function. If a function, it takes the full path and
+--- buffer number of the file as arguments (along with captures from the matched
+--- pattern, if any) and should return a string that will be used as the
+--- buffer's filetype.
+---
+--- Filename patterns can specify an optional priority to resolve cases when a
+--- file path matches multiple patterns. Higher priorities are matched first.
+--- When omitted, the priority defaults to 0.
+---
+--- See $VIMRUNTIME/lua/vim/filetype.lua for more examples.
+---
+--- Note that Lua filetype detection is only enabled when |g:do_filetype_lua| is
+--- set to 1.
+---
+--- Example:
+--- <pre>
+--- vim.filetype.add({
+--- extension = {
+--- foo = "fooscript",
+--- bar = function(path, bufnr)
+--- if some_condition() then
+--- return "barscript"
+--- end
+--- return "bar"
+--- end,
+--- },
+--- filename = {
+--- [".foorc"] = "toml",
+--- ["/etc/foo/config"] = "toml",
+--- },
+--- pattern = {
+--- [".*/etc/foo/.*"] = "fooscript",
+--- -- Using an optional priority
+--- [".*/etc/foo/.*%.conf"] = { "dosini", { priority = 10 } },
+--- ["README.(%a+)$"] = function(path, bufnr, ext)
+--- if ext == "md" then
+--- return "markdown"
+--- elseif ext == "rst" then
+--- return "rst"
+--- end
+--- end,
+--- },
+--- })
+--- </pre>
+---
+---@param filetypes table A table containing new filetype maps (see example).
+function M.add(filetypes)
+ for k, v in pairs(filetypes.extension or {}) do
+ extension[k] = v
+ end
+
+ for k, v in pairs(filetypes.filename or {}) do
+ filename[normalize_path(k)] = v
+ end
+
+ for k, v in pairs(filetypes.pattern or {}) do
+ pattern[normalize_path(k)] = v
+ end
+
+ if filetypes.pattern then
+ pattern_sorted = sort_by_priority(pattern)
+ end
+end
+
+---@private
+local function dispatch(ft, path, bufnr, ...)
+ if type(ft) == "function" then
+ ft = ft(path, bufnr, ...)
+ end
+
+ if type(ft) == "string" then
+ api.nvim_buf_set_option(bufnr, "filetype", ft)
+ return true
+ end
+
+ -- Any non-falsey value (that is, anything other than 'nil' or 'false') will
+ -- end filetype matching. This is useful for e.g. the dist#ft functions that
+ -- return 0, but set the buffer's filetype themselves
+ return ft
+end
+
+---@private
+local function match_pattern(name, path, tail, pat)
+ -- If the pattern contains a / match against the full path, otherwise just the tail
+ local fullpat = "^" .. pat .. "$"
+ local matches
+ if pat:find("/") then
+ -- Similar to |autocmd-pattern|, if the pattern contains a '/' then check for a match against
+ -- both the short file name (as typed) and the full file name (after expanding to full path
+ -- and resolving symlinks)
+ matches = name:match(fullpat) or path:match(fullpat)
+ else
+ matches = tail:match(fullpat)
+ end
+ return matches
+end
+
+--- Set the filetype for the given buffer from a file name.
+---
+---@param name string File name (can be an absolute or relative path)
+---@param bufnr number|nil The buffer to set the filetype for. Defaults to the current buffer.
+function M.match(name, bufnr)
+ -- When fired from the main filetypedetect autocommand the {bufnr} argument is omitted, so we use
+ -- the current buffer. The {bufnr} argument is provided to allow extensibility in case callers
+ -- wish to perform filetype detection on buffers other than the current one.
+ bufnr = bufnr or api.nvim_get_current_buf()
+
+ name = normalize_path(name)
+
+ -- First check for the simple case where the full path exists as a key
+ local path = vim.fn.resolve(vim.fn.fnamemodify(name, ":p"))
+ if dispatch(filename[path], path, bufnr) then
+ return
+ end
+
+ -- Next check against just the file name
+ local tail = vim.fn.fnamemodify(name, ":t")
+ if dispatch(filename[tail], path, bufnr) then
+ return
+ end
+
+ -- Next, check the file path against available patterns with non-negative priority
+ local j = 1
+ for i, v in ipairs(pattern_sorted) do
+ local k = next(v)
+ local opts = v[k][2]
+ if opts.priority < 0 then
+ j = i
+ break
+ end
+
+ local ft = v[k][1]
+ local matches = match_pattern(name, path, tail, k)
+ if matches then
+ if dispatch(ft, path, bufnr, matches) then
+ return
+ end
+ end
+ end
+
+ -- Next, check file extension
+ local ext = vim.fn.fnamemodify(name, ":e")
+ if dispatch(extension[ext], path, bufnr) then
+ return
+ end
+
+ -- Finally, check patterns with negative priority
+ for i = j, #pattern_sorted do
+ local v = pattern_sorted[i]
+ local k = next(v)
+
+ local ft = v[k][1]
+ local matches = match_pattern(name, path, tail, k)
+ if matches then
+ if dispatch(ft, path, bufnr, matches) then
+ return
+ end
+ end
+ end
+end
+
+return M
diff --git a/runtime/lua/vim/highlight.lua b/runtime/lua/vim/highlight.lua
index 236f3165f2..4105ef0675 100644
--- a/runtime/lua/vim/highlight.lua
+++ b/runtime/lua/vim/highlight.lua
@@ -1,9 +1,16 @@
local api = vim.api
-local highlight = {}
+local M = {}
+
+M.priorities = {
+ syntax = 50,
+ treesitter = 100,
+ diagnostics = 150,
+ user = 200,
+}
---@private
-function highlight.create(higroup, hi_info, default)
+function M.create(higroup, hi_info, default)
local options = {}
-- TODO: Add validation
for k, v in pairs(hi_info) do
@@ -13,33 +20,50 @@ function highlight.create(higroup, hi_info, default)
end
---@private
-function highlight.link(higroup, link_to, force)
+function M.link(higroup, link_to, force)
vim.cmd(string.format([[highlight%s link %s %s]], force and "!" or " default", higroup, link_to))
end
-
--- Highlight range between two positions
---
---@param bufnr number of buffer to apply highlighting to
---@param ns namespace to add highlight to
---@param higroup highlight group to use for highlighting
----@param rtype type of range (:help setreg, default charwise)
----@param inclusive boolean indicating whether the range is end-inclusive (default false)
-function highlight.range(bufnr, ns, higroup, start, finish, rtype, inclusive)
- rtype = rtype or 'v'
- inclusive = inclusive or false
+---@param start first position (tuple {line,col})
+---@param finish second position (tuple {line,col})
+---@param opts table with options:
+-- - regtype type of range (:help setreg, default charwise)
+-- - inclusive boolean indicating whether the range is end-inclusive (default false)
+-- - priority number indicating priority of highlight (default priorities.user)
+function M.range(bufnr, ns, higroup, start, finish, opts)
+ opts = opts or {}
+ local regtype = opts.regtype or "v"
+ local inclusive = opts.inclusive or false
+ local priority = opts.priority or M.priorities.user
-- sanity check
- if start[2] < 0 or finish[1] < start[1] then return end
+ if start[2] < 0 or finish[1] < start[1] then
+ return
+ end
- local region = vim.region(bufnr, start, finish, rtype, inclusive)
+ local region = vim.region(bufnr, start, finish, regtype, inclusive)
for linenr, cols in pairs(region) do
- api.nvim_buf_add_highlight(bufnr, ns, higroup, linenr, cols[1], cols[2])
+ local end_row
+ if cols[2] == -1 then
+ end_row = linenr + 1
+ cols[2] = 0
+ end
+ api.nvim_buf_set_extmark(bufnr, ns, linenr, cols[1], {
+ hl_group = higroup,
+ end_row = end_row,
+ end_col = cols[2],
+ priority = priority,
+ strict = false,
+ })
end
-
end
-local yank_ns = api.nvim_create_namespace('hlyank')
+local yank_ns = api.nvim_create_namespace("hlyank")
--- Highlight the yanked region
---
--- use from init.vim via
@@ -49,26 +73,40 @@ local yank_ns = api.nvim_create_namespace('hlyank')
--- customize conditions (here: do not highlight a visual selection) via
--- au TextYankPost * lua vim.highlight.on_yank {on_visual=false}
---
--- @param opts dictionary with options controlling the highlight:
+-- @param opts table with options controlling the highlight:
-- - higroup highlight group for yanked region (default "IncSearch")
-- - timeout time in ms before highlight is cleared (default 150)
-- - on_macro highlight when executing macro (default false)
-- - on_visual highlight when yanking visual selection (default true)
-- - event event structure (default vim.v.event)
-function highlight.on_yank(opts)
- vim.validate {
- opts = { opts,
- function(t) if t == nil then return true else return type(t) == 'table' end end,
- 'a table or nil to configure options (see `:h highlight.on_yank`)',
- }}
+function M.on_yank(opts)
+ vim.validate({
+ opts = {
+ opts,
+ function(t)
+ if t == nil then
+ return true
+ else
+ return type(t) == "table"
+ end
+ end,
+ "a table or nil to configure options (see `:h highlight.on_yank`)",
+ },
+ })
opts = opts or {}
local event = opts.event or vim.v.event
local on_macro = opts.on_macro or false
local on_visual = (opts.on_visual ~= false)
- if (not on_macro) and vim.fn.reg_executing() ~= '' then return end
- if event.operator ~= 'y' or event.regtype == '' then return end
- if (not on_visual) and event.visual then return end
+ if not on_macro and vim.fn.reg_executing() ~= "" then
+ return
+ end
+ if event.operator ~= "y" or event.regtype == "" then
+ return
+ end
+ if not on_visual and event.visual then
+ return
+ end
local higroup = opts.higroup or "IncSearch"
local timeout = opts.timeout or 150
@@ -79,19 +117,23 @@ function highlight.on_yank(opts)
local pos1 = vim.fn.getpos("'[")
local pos2 = vim.fn.getpos("']")
- pos1 = {pos1[2] - 1, pos1[3] - 1 + pos1[4]}
- pos2 = {pos2[2] - 1, pos2[3] - 1 + pos2[4]}
+ pos1 = { pos1[2] - 1, pos1[3] - 1 + pos1[4] }
+ pos2 = { pos2[2] - 1, pos2[3] - 1 + pos2[4] }
- highlight.range(bufnr, yank_ns, higroup, pos1, pos2, event.regtype, event.inclusive)
-
- vim.defer_fn(
- function()
- if api.nvim_buf_is_valid(bufnr) then
- api.nvim_buf_clear_namespace(bufnr, yank_ns, 0, -1)
- end
- end,
- timeout
+ M.range(
+ bufnr,
+ yank_ns,
+ higroup,
+ pos1,
+ pos2,
+ { regtype = event.regtype, inclusive = event.inclusive, priority = M.priorities.user }
)
+
+ vim.defer_fn(function()
+ if api.nvim_buf_is_valid(bufnr) then
+ api.nvim_buf_clear_namespace(bufnr, yank_ns, 0, -1)
+ end
+ end, timeout)
end
-return highlight
+return M
diff --git a/runtime/lua/vim/keymap.lua b/runtime/lua/vim/keymap.lua
new file mode 100644
index 0000000000..0986d21196
--- /dev/null
+++ b/runtime/lua/vim/keymap.lua
@@ -0,0 +1,145 @@
+local keymap = {}
+
+--- Add a new |mapping|.
+--- Examples:
+--- <pre>
+--- -- Can add mapping to Lua functions
+--- vim.keymap.set('n', 'lhs', function() print("real lua function") end)
+---
+--- -- Can use it to map multiple modes
+--- vim.keymap.set({'n', 'v'}, '<leader>lr', vim.lsp.buf.references, { buffer=true })
+---
+--- -- Can add mapping for specific buffer
+--- vim.keymap.set('n', '<leader>w', "<cmd>w<cr>", { silent = true, buffer = 5 })
+---
+--- -- Expr mappings
+--- vim.keymap.set('i', '<Tab>', function()
+--- return vim.fn.pumvisible() == 1 and "<C-n>" or "<Tab>"
+--- end, { expr = true })
+--- -- <Plug> mappings
+--- vim.keymap.set('n', '[%%', '<Plug>(MatchitNormalMultiBackward)')
+--- </pre>
+---
+--- Note that in a mapping like:
+--- <pre>
+--- 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>
+--- vim.keymap.set('n', 'asdf', function() return require('jkl').my_fun() end)
+--- </pre>
+---
+---@param mode string|table Same mode short names as |nvim_set_keymap()|.
+--- Can also be list of modes to create mapping on multiple modes.
+---@param lhs string Left-hand side |{lhs}| of the mapping.
+---@param rhs string|function Right-hand side |{rhs}| of the mapping. Can also be a Lua function.
+--- If a Lua function and `opts.expr == true`, returning `nil` is
+--- equivalent to an empty string.
+--
+---@param opts table A table of |:map-arguments| such as "silent". In addition to the options
+--- listed in |nvim_set_keymap()|, this table also accepts the following keys:
+--- - replace_keycodes: (boolean, default true) When both this and expr is "true",
+--- |nvim_replace_termcodes()| is applied to the result of Lua expr maps.
+--- - remap: (boolean) Make the mapping recursive. This is the
+--- inverse of the "noremap" option from |nvim_set_keymap()|.
+--- Default `false`.
+---@see |nvim_set_keymap()|
+function keymap.set(mode, lhs, rhs, opts)
+ vim.validate {
+ mode = {mode, {'s', 't'}},
+ lhs = {lhs, 's'},
+ rhs = {rhs, {'s', 'f'}},
+ opts = {opts, 't', true}
+ }
+
+ opts = vim.deepcopy(opts) or {}
+ local is_rhs_luaref = type(rhs) == "function"
+ mode = type(mode) == 'string' and {mode} or mode
+
+ if is_rhs_luaref and opts.expr then
+ local user_rhs = rhs
+ rhs = function ()
+ local res = user_rhs()
+ if res == nil then
+ -- TODO(lewis6991): Handle this in C?
+ return ''
+ elseif opts.replace_keycodes ~= false then
+ return vim.api.nvim_replace_termcodes(res, true, true, true)
+ else
+ return res
+ end
+ end
+ end
+ -- clear replace_keycodes from opts table
+ opts.replace_keycodes = nil
+
+ if opts.remap == nil then
+ -- default remap value is false
+ opts.noremap = true
+ else
+ -- remaps behavior is opposite of noremap option.
+ opts.noremap = not opts.remap
+ opts.remap = nil
+ end
+
+ if is_rhs_luaref then
+ opts.callback = rhs
+ rhs = ''
+ end
+
+ if opts.buffer then
+ local bufnr = opts.buffer == true and 0 or opts.buffer
+ opts.buffer = nil
+ for _, m in ipairs(mode) do
+ vim.api.nvim_buf_set_keymap(bufnr, m, lhs, rhs, opts)
+ end
+ else
+ opts.buffer = nil
+ for _, m in ipairs(mode) do
+ vim.api.nvim_set_keymap(m, lhs, rhs, opts)
+ end
+ end
+end
+
+--- Remove an existing mapping.
+--- Examples:
+--- <pre>
+--- vim.keymap.del('n', 'lhs')
+---
+--- vim.keymap.del({'n', 'i', 'v'}, '<leader>w', { buffer = 5 })
+--- </pre>
+---@param opts table A table of optional arguments:
+--- - buffer: (number or boolean) Remove a mapping from the given buffer.
+--- When "true" or 0, use the current buffer.
+---@see |vim.keymap.set()|
+---
+function keymap.del(modes, lhs, opts)
+ vim.validate {
+ mode = {modes, {'s', 't'}},
+ lhs = {lhs, 's'},
+ opts = {opts, 't', true}
+ }
+
+ opts = opts or {}
+ modes = type(modes) == 'string' and {modes} or modes
+
+ local buffer = false
+ if opts.buffer ~= nil then
+ buffer = opts.buffer == true and 0 or opts.buffer
+ opts.buffer = nil
+ end
+
+ if buffer == false then
+ for _, mode in ipairs(modes) do
+ vim.api.nvim_del_keymap(mode, lhs)
+ end
+ else
+ for _, mode in ipairs(modes) do
+ vim.api.nvim_buf_del_keymap(buffer, mode, lhs)
+ end
+ end
+end
+
+return keymap
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua
index 88dcb38dd1..7bbe3637db 100644
--- a/runtime/lua/vim/lsp.lua
+++ b/runtime/lua/vim/lsp.lua
@@ -115,6 +115,13 @@ local format_line_ending = {
["mac"] = '\r',
}
+---@private
+---@param bufnr (number)
+---@returns (string)
+local function buf_get_line_ending(bufnr)
+ return format_line_ending[nvim_buf_get_option(bufnr, 'fileformat')] or '\n'
+end
+
local client_index = 0
---@private
--- Returns a new, unused client id.
@@ -130,12 +137,6 @@ local all_buffer_active_clients = {}
local uninitialized_clients = {}
---@private
---- Invokes a function for each LSP client attached to the buffer {bufnr}.
----
----@param bufnr (Number) of buffer
----@param fn (function({client}, {client_id}, {bufnr}) Function to run on
----each client attached to that buffer.
----@param restrict_client_ids table list of client ids on which to restrict function application.
local function for_each_buffer_client(bufnr, fn, restrict_client_ids)
validate {
fn = { fn, 'f' };
@@ -236,7 +237,6 @@ local function validate_client_config(config)
config = { config, 't' };
}
validate {
- root_dir = { config.root_dir, optional_validator(is_dir), "directory" };
handlers = { config.handlers, "t", true };
capabilities = { config.capabilities, "t", true };
cmd_cwd = { config.cmd_cwd, optional_validator(is_dir), "directory" };
@@ -256,7 +256,7 @@ local function validate_client_config(config)
(not config.flags
or not config.flags.debounce_text_changes
or type(config.flags.debounce_text_changes) == 'number'),
- "flags.debounce_text_changes must be nil or a number with the debounce time in milliseconds"
+ "flags.debounce_text_changes must be a number with the debounce time in milliseconds"
)
local cmd, cmd_args = lsp._cmd_parts(config.cmd)
@@ -278,9 +278,10 @@ end
---@param bufnr (number) Buffer handle, or 0 for current.
---@returns Buffer text as string.
local function buf_get_full_text(bufnr)
- local text = table.concat(nvim_buf_get_lines(bufnr, 0, -1, true), '\n')
+ local line_ending = buf_get_line_ending(bufnr)
+ local text = table.concat(nvim_buf_get_lines(bufnr, 0, -1, true), line_ending)
if nvim_buf_get_option(bufnr, 'eol') then
- text = text .. '\n'
+ text = text .. line_ending
end
return text
end
@@ -289,7 +290,7 @@ end
--- Memoizes a function. On first run, the function return value is saved and
--- immediately returned on subsequent runs. If the function returns a multival,
--- only the first returned value will be memoized and returned. The function will only be run once,
---- even if it has side-effects.
+--- even if it has side effects.
---
---@param fn (function) Function to run
---@returns (function) Memoized function
@@ -305,67 +306,138 @@ local function once(fn)
end
end
-
local changetracking = {}
do
--@private
--- client_id → state
---
--- state
- --- pending_change?: function that the timer starts to trigger didChange
- --- pending_changes: list of tables with the pending changesets; for incremental_sync only
--- use_incremental_sync: bool
- --- buffers?: table (bufnr → lines); for incremental sync only
+ --- buffers: bufnr -> buffer_state
+ ---
+ --- buffer_state
+ --- pending_change?: function that the timer starts to trigger didChange
+ --- pending_changes: table (uri -> list of pending changeset tables));
+ --- Only set if incremental_sync is used
+ ---
--- timer?: uv_timer
+ --- lines: table
local state_by_client = {}
+ ---@private
function changetracking.init(client, bufnr)
+ local use_incremental_sync = (
+ if_nil(client.config.flags.allow_incremental_sync, true)
+ and client.resolved_capabilities.text_document_did_change == protocol.TextDocumentSyncKind.Incremental
+ )
local state = state_by_client[client.id]
if not state then
state = {
- pending_changes = {};
- use_incremental_sync = (
- if_nil(client.config.flags.allow_incremental_sync, true)
- and client.resolved_capabilities.text_document_did_change == protocol.TextDocumentSyncKind.Incremental
- );
+ buffers = {};
+ debounce = client.config.flags.debounce_text_changes or 150,
+ use_incremental_sync = use_incremental_sync;
}
state_by_client[client.id] = state
end
- if not state.use_incremental_sync then
- return
- end
- if not state.buffers then
- state.buffers = {}
+ if not state.buffers[bufnr] then
+ local buf_state = {}
+ state.buffers[bufnr] = buf_state
+ if use_incremental_sync then
+ buf_state.lines = nvim_buf_get_lines(bufnr, 0, -1, true)
+ buf_state.lines_tmp = {}
+ buf_state.pending_changes = {}
+ end
end
- state.buffers[bufnr] = nvim_buf_get_lines(bufnr, 0, -1, true)
end
+ ---@private
function changetracking.reset_buf(client, bufnr)
+ changetracking.flush(client, bufnr)
local state = state_by_client[client.id]
- if state then
- changetracking._reset_timer(state)
- if state.buffers then
- state.buffers[bufnr] = nil
+ if state and state.buffers then
+ local buf_state = state.buffers[bufnr]
+ state.buffers[bufnr] = nil
+ if buf_state and buf_state.timer then
+ buf_state.timer:stop()
+ buf_state.timer:close()
+ buf_state.timer = nil
end
end
end
+ ---@private
function changetracking.reset(client_id)
local state = state_by_client[client_id]
- if state then
- state_by_client[client_id] = nil
- changetracking._reset_timer(state)
+ if not state then
+ return
+ end
+ for _, buf_state in pairs(state.buffers) do
+ if buf_state.timer then
+ buf_state.timer:stop()
+ buf_state.timer:close()
+ buf_state.timer = nil
+ end
end
+ state.buffers = {}
end
- function changetracking.prepare(bufnr, firstline, lastline, new_lastline, changedtick)
- local incremental_changes = function(client)
- local cached_buffers = state_by_client[client.id].buffers
- local curr_lines = nvim_buf_get_lines(bufnr, 0, -1, true)
- local line_ending = format_line_ending[vim.api.nvim_buf_get_option(0, 'fileformat')]
+ ---@private
+ --
+ -- Adjust debounce time by taking time of last didChange notification into
+ -- consideration. If the last didChange happened more than `debounce` time ago,
+ -- debounce can be skipped and otherwise maybe reduced.
+ --
+ -- This turns the debounce into a kind of client rate limiting
+ local function next_debounce(debounce, buf_state)
+ if debounce == 0 then
+ return 0
+ end
+ local ns_to_ms = 0.000001
+ if not buf_state.last_flush then
+ return debounce
+ end
+ local now = uv.hrtime()
+ local ms_since_last_flush = (now - buf_state.last_flush) * ns_to_ms
+ return math.max(debounce - ms_since_last_flush, 0)
+ end
+
+ ---@private
+ function changetracking.prepare(bufnr, firstline, lastline, new_lastline)
+ local incremental_changes = function(client, buf_state)
+
+ local prev_lines = buf_state.lines
+ local curr_lines = buf_state.lines_tmp
+
+ local changed_lines = nvim_buf_get_lines(bufnr, firstline, new_lastline, true)
+ for i = 1, firstline do
+ curr_lines[i] = prev_lines[i]
+ end
+ for i = firstline + 1, new_lastline do
+ curr_lines[i] = changed_lines[i - firstline]
+ end
+ for i = lastline + 1, #prev_lines do
+ curr_lines[i - lastline + new_lastline] = prev_lines[i]
+ end
+ if tbl_isempty(curr_lines) then
+ -- Can happen when deleting the entire contents of a buffer, see https://github.com/neovim/neovim/issues/16259.
+ curr_lines[1] = ''
+ end
+
+ local line_ending = buf_get_line_ending(bufnr)
local incremental_change = sync.compute_diff(
- cached_buffers[bufnr], curr_lines, firstline, lastline, new_lastline, client.offset_encoding or 'utf-16', line_ending or '\n')
- cached_buffers[bufnr] = curr_lines
+ buf_state.lines, curr_lines, firstline, lastline, new_lastline, client.offset_encoding or 'utf-16', line_ending)
+
+ -- Double-buffering of lines tables is used to reduce the load on the garbage collector.
+ -- At this point the prev_lines table is useless, but its internal storage has already been allocated,
+ -- so let's keep it around for the next didChange event, in which it will become the next
+ -- curr_lines table. Note that setting elements to nil doesn't actually deallocate slots in the
+ -- internal storage - it merely marks them as free, for the GC to deallocate them.
+ for i in ipairs(prev_lines) do
+ prev_lines[i] = nil
+ end
+ buf_state.lines = curr_lines
+ buf_state.lines_tmp = prev_lines
+
return incremental_change
end
local full_changes = once(function()
@@ -379,65 +451,68 @@ do
return
end
local state = state_by_client[client.id]
- local debounce = client.config.flags.debounce_text_changes
- if not debounce then
- local changes = state.use_incremental_sync and incremental_changes(client) or full_changes()
- client.notify("textDocument/didChange", {
- textDocument = {
- uri = uri;
- version = changedtick;
- };
- contentChanges = { changes, }
- })
- return
- end
- changetracking._reset_timer(state)
+ local buf_state = state.buffers[bufnr]
+ changetracking._reset_timer(buf_state)
+ local debounce = next_debounce(state.debounce, buf_state)
if state.use_incremental_sync then
-- This must be done immediately and cannot be delayed
-- The contents would further change and startline/endline may no longer fit
- table.insert(state.pending_changes, incremental_changes(client))
+ table.insert(buf_state.pending_changes, incremental_changes(client, buf_state))
end
- state.pending_change = function()
- state.pending_change = nil
+ buf_state.pending_change = function()
+ buf_state.pending_change = nil
+ buf_state.last_flush = uv.hrtime()
if client.is_stopped() or not vim.api.nvim_buf_is_valid(bufnr) then
return
end
- local contentChanges
- if state.use_incremental_sync then
- contentChanges = state.pending_changes
- state.pending_changes = {}
- else
- contentChanges = { full_changes(), }
- end
+ local changes = state.use_incremental_sync and buf_state.pending_changes or { full_changes() }
client.notify("textDocument/didChange", {
textDocument = {
- uri = uri;
- version = changedtick;
- };
- contentChanges = contentChanges
+ uri = uri,
+ version = util.buf_versions[bufnr],
+ },
+ contentChanges = changes,
})
+ buf_state.pending_changes = {}
+ end
+ if debounce == 0 then
+ buf_state.pending_change()
+ else
+ local timer = vim.loop.new_timer()
+ buf_state.timer = timer
+ -- Must use schedule_wrap because `full_changes()` calls nvim_buf_get_lines
+ timer:start(debounce, 0, vim.schedule_wrap(buf_state.pending_change))
end
- state.timer = vim.loop.new_timer()
- -- Must use schedule_wrap because `full_changes()` calls nvim_buf_get_lines
- state.timer:start(debounce, 0, vim.schedule_wrap(state.pending_change))
end
end
- function changetracking._reset_timer(state)
- if state.timer then
- state.timer:stop()
- state.timer:close()
- state.timer = nil
+ function changetracking._reset_timer(buf_state)
+ if buf_state.timer then
+ buf_state.timer:stop()
+ buf_state.timer:close()
+ buf_state.timer = nil
end
end
--- Flushes any outstanding change notification.
- function changetracking.flush(client)
+ ---@private
+ function changetracking.flush(client, bufnr)
local state = state_by_client[client.id]
- if state then
- changetracking._reset_timer(state)
- if state.pending_change then
- state.pending_change()
+ if not state then
+ return
+ end
+ if bufnr then
+ local buf_state = state.buffers[bufnr] or {}
+ changetracking._reset_timer(buf_state)
+ if buf_state.pending_change then
+ buf_state.pending_change()
+ end
+ else
+ for _, buf_state in pairs(state.buffers) do
+ changetracking._reset_timer(buf_state)
+ if buf_state.pending_change then
+ buf_state.pending_change()
+ end
end
end
end
@@ -475,7 +550,8 @@ local function text_document_did_open_handler(bufnr, client)
-- Protect against a race where the buffer disappears
-- between `did_open_handler` and the scheduled function firing.
if vim.api.nvim_buf_is_valid(bufnr) then
- vim.lsp.diagnostic.redraw(bufnr, client.id)
+ local namespace = vim.lsp.diagnostic.get_namespace(client.id)
+ vim.diagnostic.show(namespace, bufnr)
end
end)
end
@@ -545,6 +621,12 @@ end
---
--- - {handlers} (table): The handlers used by the client as described in |lsp-handler|.
---
+--- - {requests} (table): The current pending requests in flight
+--- to the server. Entries are key-value pairs with the key
+--- being the request ID while the value is a table with `type`,
+--- `bufnr`, and `method` key-value pairs. `type` is either "pending"
+--- for an active request, or "cancel" for a cancel request.
+---
--- - {config} (table): copy of the table that was passed by the user
--- to |vim.lsp.start_client()|.
---
@@ -566,12 +648,10 @@ end
--
--- Starts and initializes a client with the given configuration.
---
---- Parameters `cmd` and `root_dir` are required.
+--- Parameter `cmd` is required.
---
--- The following parameters describe fields in the {config} table.
---
----@param root_dir: (string) Directory where the LSP server will base
---- its rootUri on initialization.
---
---@param cmd: (required, string or list treated like |jobstart()|) Base command
--- that initiates the LSP client.
@@ -587,6 +667,11 @@ end
--- { "PRODUCTION=true"; "TEST=123"; PORT = 8080; HOST = "0.0.0.0"; }
--- </pre>
---
+---@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.
+---
---@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
@@ -603,17 +688,13 @@ end
---
---@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 comand name, and the value is a function which is called if any LSP action
+--- 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 workspace_folders (table) List of workspace folders passed to the
---- language server. Defaults to root_dir if not set. See `workspaceFolders` in
---- the LSP spec
---
---@param get_language_id function(bufnr, filetype) -> language ID as string.
--- Defaults to the filetype.
@@ -625,8 +706,8 @@ end
---@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.client_errors| for possible errors.
---- Use `vim.lsp.client_errors[code]` to get human-friendly name.
+--- |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
@@ -661,8 +742,13 @@ end
--- notifications to the server by the given number in milliseconds. No debounce
--- occurs if nil
--- - exit_timeout (number, default 500): 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.
+--- 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.
+---
---@returns Client id. |vim.lsp.get_client_by_id()| Note: client may not be
--- fully initialized. Use `on_init` to do any actions once
--- the client has been initialized.
@@ -732,8 +818,8 @@ function lsp.start_client(config)
---
---@param code (number) Error code
---@param err (...) Other arguments may be passed depending on the error kind
- ---@see |vim.lsp.client_errors| for possible errors. Use
- ---`vim.lsp.client_errors[code]` to get a human-friendly name.
+ ---@see |vim.lsp.rpc.client_errors| for possible errors. Use
+ ---`vim.lsp.rpc.client_errors[code]` to get a human-friendly name.
function dispatch.on_error(code, err)
local _ = log.error() and log.error(log_prefix, "on_error", { code = lsp.client_errors[code], err = err })
err_message(log_prefix, ': Error ', lsp.client_errors[code], ': ', vim.inspect(err))
@@ -752,6 +838,10 @@ function lsp.start_client(config)
---@param code (number) exit code of the process
---@param signal (number) the signal used to terminate (if any)
function dispatch.on_exit(code, signal)
+ if config.on_exit then
+ pcall(config.on_exit, code, signal, client_id)
+ end
+
active_clients[client_id] = nil
uninitialized_clients[client_id] = nil
@@ -767,10 +857,6 @@ function lsp.start_client(config)
vim.notify(msg, vim.log.levels.WARN)
end)
end
-
- if config.on_exit then
- pcall(config.on_exit, code, signal, client_id)
- end
end
-- Start the RPC client.
@@ -779,6 +865,9 @@ function lsp.start_client(config)
env = config.cmd_env;
})
+ -- Return nil if client fails to start
+ if not rpc then return end
+
local client = {
id = client_id;
name = name;
@@ -805,11 +894,24 @@ function lsp.start_client(config)
}
local version = vim.version()
- if config.root_dir and not config.workspace_folders then
- config.workspace_folders = {{
- uri = vim.uri_from_fname(config.root_dir);
- name = string.format("%s", config.root_dir);
- }};
+ local workspace_folders
+ local root_uri
+ local root_path
+ if config.workspace_folders or config.root_dir then
+ if config.root_dir and not config.workspace_folders then
+ workspace_folders = {{
+ uri = vim.uri_from_fname(config.root_dir);
+ name = string.format("%s", config.root_dir);
+ }};
+ else
+ workspace_folders = config.workspace_folders
+ end
+ root_uri = workspace_folders[1].uri
+ root_path = vim.uri_to_fname(root_uri)
+ else
+ workspace_folders = nil
+ root_uri = nil
+ root_path = nil
end
local initialize_params = {
@@ -827,10 +929,15 @@ function lsp.start_client(config)
-- The rootPath of the workspace. Is null if no folder is open.
--
-- @deprecated in favour of rootUri.
- rootPath = config.root_dir;
+ rootPath = root_path or vim.NIL;
-- The rootUri of the workspace. Is null if no folder is open. If both
-- `rootPath` and `rootUri` are set `rootUri` wins.
- rootUri = config.root_dir and vim.uri_from_fname(config.root_dir);
+ rootUri = root_uri or vim.NIL;
+ -- The workspace folders configured in the client when the server starts.
+ -- This property is only available if the client supports workspace folders.
+ -- It can be `null` if the client supports workspace folders but none are
+ -- configured.
+ workspaceFolders = workspace_folders or vim.NIL;
-- User provided initialization options.
initializationOptions = config.init_options;
-- The capabilities provided by the client (editor or tool)
@@ -838,21 +945,6 @@ function lsp.start_client(config)
-- The initial trace setting. If omitted trace is disabled ("off").
-- trace = "off" | "messages" | "verbose";
trace = valid_traces[config.trace] or 'off';
- -- The workspace folders configured in the client when the server starts.
- -- This property is only available if the client supports workspace folders.
- -- It can be `null` if the client supports workspace folders but none are
- -- configured.
- --
- -- Since 3.6.0
- -- workspaceFolders?: WorkspaceFolder[] | null;
- -- export interface WorkspaceFolder {
- -- -- The associated URI for this workspace folder.
- -- uri
- -- -- The name of the workspace folder. Used to refer to this
- -- -- workspace folder in the user interface.
- -- name
- -- }
- workspaceFolders = config.workspace_folders,
}
if config.before_init then
-- TODO(ashkan) handle errors here.
@@ -865,7 +957,9 @@ function lsp.start_client(config)
rpc.notify('initialized', vim.empty_dict())
client.initialized = true
uninitialized_clients[client_id] = nil
- client.workspaceFolders = initialize_params.workspaceFolders
+ client.workspace_folders = workspace_folders
+ -- TODO(mjlbach): Backwards compatibility, to be removed in 0.7
+ client.workspaceFolders = client.workspace_folders
client.server_capabilities = assert(result.capabilities, "initialize result doesn't contain capabilities")
-- These are the cleaned up capabilities we use for dynamically deciding
-- when to send certain events to clients.
@@ -924,7 +1018,7 @@ function lsp.start_client(config)
or error(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)
+ changetracking.flush(client, bufnr)
bufnr = resolve_bufnr(bufnr)
local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, handler, bufnr)
local success, request_id = rpc.request(method, params, function(err, result)
@@ -981,14 +1075,16 @@ function lsp.start_client(config)
---@private
--- Sends a notification to an LSP server.
---
- ---@param method (string) LSP method name.
- ---@param params (optional, table) LSP request params.
- ---@param bufnr (number) Buffer handle, or 0 for current.
+ ---@param method string LSP method name.
+ ---@param params table|nil LSP request params.
---@returns {status} (bool) true if the notification was successful.
---If it is false, then it will always be false
---(the client has shutdown).
- function client.notify(...)
- return rpc.notify(...)
+ function client.notify(method, params)
+ if method ~= 'textDocument/didChange' then
+ changetracking.flush(client)
+ end
+ return rpc.notify(method, params)
end
---@private
@@ -1007,7 +1103,7 @@ function lsp.start_client(config)
return rpc.notify("$/cancelRequest", { id = id })
end
- -- Track this so that we can escalate automatically if we've alredy tried a
+ -- Track this so that we can escalate automatically if we've already tried a
-- graceful shutdown
local graceful_shutdown_failed = false
---@private
@@ -1079,12 +1175,12 @@ local text_document_did_change_handler
do
text_document_did_change_handler = function(_, bufnr, changedtick, firstline, lastline, new_lastline)
- -- Don't do anything if there are no clients attached.
+ -- 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
+ return true
end
util.buf_versions[bufnr] = changedtick
- local compute_change_and_notify = changetracking.prepare(bufnr, firstline, lastline, new_lastline, changedtick)
+ local compute_change_and_notify = changetracking.prepare(bufnr, firstline, lastline, new_lastline)
for_each_buffer_client(bufnr, compute_change_and_notify)
end
end
@@ -1098,7 +1194,7 @@ function lsp._text_document_did_save_handler(bufnr)
if client.resolved_capabilities.text_document_save then
local included_text
if client.resolved_capabilities.text_document_save_include_text then
- included_text = text()
+ included_text = text(bufnr)
end
client.notify('textDocument/didSave', {
textDocument = {
@@ -1123,6 +1219,12 @@ function lsp.buf_attach_client(bufnr, client_id)
client_id = {client_id, 'n'};
}
bufnr = resolve_bufnr(bufnr)
+ if not vim.api.nvim_buf_is_loaded(bufnr) then
+ local _ = log.warn() and log.warn(
+ string.format("buf_attach_client called on unloaded buffer (id: %d): ", bufnr)
+ )
+ return false
+ end
local buffer_client_ids = all_buffer_active_clients[bufnr]
-- This is our first time attaching to this buffer.
if not buffer_client_ids then
@@ -1143,6 +1245,7 @@ function lsp.buf_attach_client(bufnr, client_id)
on_reload = function()
local params = { textDocument = { uri = uri; } }
for_each_buffer_client(bufnr, function(client, _)
+ changetracking.reset_buf(client, bufnr)
if client.resolved_capabilities.text_document_open_close then
client.notify('textDocument/didClose', params)
end
@@ -1152,10 +1255,10 @@ function lsp.buf_attach_client(bufnr, client_id)
on_detach = function()
local params = { textDocument = { uri = uri; } }
for_each_buffer_client(bufnr, function(client, _)
+ changetracking.reset_buf(client, bufnr)
if client.resolved_capabilities.text_document_open_close then
client.notify('textDocument/didClose', params)
end
- changetracking.reset_buf(client, bufnr)
end)
util.buf_versions[bufnr] = nil
all_buffer_active_clients[bufnr] = nil
@@ -1180,6 +1283,50 @@ function lsp.buf_attach_client(bufnr, client_id)
return true
end
+--- Detaches client from the specified buffer.
+--- Note: While the server is notified that the text document (buffer)
+--- was closed, it is still able to send notifications should it ignore this notification.
+---
+---@param bufnr number Buffer handle, or 0 for current
+---@param client_id number Client id
+function lsp.buf_detach_client(bufnr, client_id)
+ validate {
+ bufnr = {bufnr, 'n', true};
+ client_id = {client_id, 'n'};
+ }
+ bufnr = resolve_bufnr(bufnr)
+
+ local client = lsp.get_client_by_id(client_id)
+ if not client or not client.attached_buffers[bufnr] then
+ vim.notify(
+ string.format('Buffer (id: %d) is not attached to client (id: %d). Cannot detach.', client_id, bufnr)
+ )
+ return
+ end
+
+ changetracking.reset_buf(client, bufnr)
+
+ if client.resolved_capabilities.text_document_open_close then
+ local uri = vim.uri_from_bufnr(bufnr)
+ local params = { textDocument = { uri = uri; } }
+ client.notify('textDocument/didClose', params)
+ end
+
+ client.attached_buffers[bufnr] = nil
+ util.buf_versions[bufnr] = nil
+
+ all_buffer_active_clients[bufnr][client_id] = nil
+ if #vim.tbl_keys(all_buffer_active_clients[bufnr]) == 0 then
+ all_buffer_active_clients[bufnr] = nil
+ end
+
+ local namespace = vim.lsp.diagnostic.get_namespace(client_id)
+ vim.diagnostic.reset(namespace, bufnr)
+
+ vim.notify(string.format('Detached buffer (id: %d) from client (id: %d)', bufnr, client_id))
+
+end
+
--- Checks if a buffer is attached for a particular client.
---
---@param bufnr (number) Buffer handle, or 0 for current
@@ -1190,7 +1337,7 @@ end
--- Gets a client by id, or nil if the id is invalid.
--- The returned client may not yet be fully initialized.
---
+---
---@param client_id number client id
---
---@returns |vim.lsp.client| object, or nil
@@ -1199,7 +1346,7 @@ function lsp.get_client_by_id(client_id)
end
--- Returns list of buffers attached to client_id.
---
+---
---@param client_id number client id
---@returns list of buffer ids
function lsp.get_buffers_by_client_id(client_id)
@@ -1269,6 +1416,7 @@ function lsp._vim_exit_handler()
local poll_time = 50
+ ---@private
local function check_clients_closed()
for client_id, timeout in pairs(timeouts) do
timeouts[client_id] = timeout - poll_time
@@ -1303,8 +1451,8 @@ nvim_command("autocmd VimLeavePre * lua vim.lsp._vim_exit_handler()")
---@param method (string) LSP method name
---@param params (optional, table) Parameters to send to the server
---@param handler (optional, function) See |lsp-handler|
--- If nil, follows resolution strategy defined in |lsp-handler-configuration|
---
+--- If nil, follows resolution strategy defined in |lsp-handler-configuration|
+---
---@returns 2-tuple:
--- - Map of client-id:request-id pairs for all successful requests.
--- - Function which can be used to cancel all the requests. You could instead
@@ -1450,7 +1598,7 @@ end
local function adjust_start_col(lnum, line, items, encoding)
local min_start_char = nil
for _, item in pairs(items) do
- if item.textEdit and item.textEdit.range.start.line == lnum - 1 then
+ if item.filterText == nil and item.textEdit and item.textEdit.range.start.line == lnum - 1 then
if min_start_char and min_start_char ~= item.textEdit.range.start.character then
return nil
end
@@ -1458,11 +1606,7 @@ local function adjust_start_col(lnum, line, items, encoding)
end
end
if min_start_char then
- if encoding == 'utf-8' then
- return min_start_char
- else
- return vim.str_byteindex(line, min_start_char, encoding == 'utf-16')
- end
+ return util._str_byteindex_enc(line, min_start_char, encoding)
else
return nil
end
@@ -1545,7 +1689,7 @@ end
---
--- Currently only supports a single client. This can be set via
--- `setlocal formatexpr=v:lua.vim.lsp.formatexpr()` but will typically or in `on_attach`
---- via `vim.api.nvim_buf_set_option(bufnr, 'formatexpr', 'v:lua.vim.lsp.formatexpr(#{timeout_ms:250})')`.
+--- via ``vim.api.nvim_buf_set_option(bufnr, 'formatexpr', 'v:lua.vim.lsp.formatexpr(#{timeout_ms:250})')``.
---
---@param opts table options for customizing the formatting expression which takes the
--- following optional keys:
@@ -1575,9 +1719,9 @@ function lsp.formatexpr(opts)
local client_results = vim.lsp.buf_request_sync(0, "textDocument/rangeFormatting", params, timeout_ms)
-- Apply the text edits from one and only one of the clients.
- for _, response in pairs(client_results) do
+ for client_id, response in pairs(client_results) do
if response.result then
- vim.lsp.util.apply_text_edits(response.result, 0)
+ vim.lsp.util.apply_text_edits(response.result, 0, vim.lsp.get_client_by_id(client_id).offset_encoding)
return 0
end
end
@@ -1627,14 +1771,14 @@ end
--
-- Can be used to lookup the number from the name or the
-- name from the number.
--- Levels by name: "trace", "debug", "info", "warn", "error"
--- Level numbers begin with "trace" at 0
+-- Levels by name: "TRACE", "DEBUG", "INFO", "WARN", "ERROR"
+-- Level numbers begin with "TRACE" at 0
lsp.log_levels = log.levels
--- Sets the global log level for LSP logging.
---
---- Levels by name: "trace", "debug", "info", "warn", "error"
---- Level numbers begin with "trace" at 0
+--- Levels by name: "TRACE", "DEBUG", "INFO", "WARN", "ERROR"
+--- Level numbers begin with "TRACE" at 0
---
--- Use `lsp.log_levels` for reverse lookup.
---
@@ -1655,7 +1799,17 @@ function lsp.get_log_path()
return log.get_filename()
end
---- Call {fn} for every client attached to {bufnr}
+--- Invokes a function for each LSP client attached to a buffer.
+---
+---@param bufnr number Buffer number
+---@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>
+--- vim.lsp.for_each_buffer_client(0, function(client, client_id, bufnr)
+--- print(vim.inspect(client))
+--- end)
+--- </pre>
function lsp.for_each_buffer_client(bufnr, fn)
return for_each_buffer_client(bufnr, fn)
end
@@ -1714,11 +1868,11 @@ end
--- using `workspace/executeCommand`.
---
--- The first argument to the function will be the `Command`:
--- Command
--- title: String
--- command: String
--- arguments?: any[]
---
+--- Command
+--- title: String
+--- command: String
+--- arguments?: any[]
+---
--- The second argument is the `ctx` of |lsp-handler|
lsp.commands = setmetatable({}, {
__newindex = function(tbl, key, value)
diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua
index 747d761730..eb7ec579f1 100644
--- a/runtime/lua/vim/lsp/buf.lua
+++ b/runtime/lua/vim/lsp/buf.lua
@@ -116,31 +116,30 @@ end
--- asks the user to select one.
--
---@returns The client that the user selected or nil
-local function select_client(method)
- local clients = vim.tbl_values(vim.lsp.buf_get_clients());
- clients = vim.tbl_filter(function (client)
+local function select_client(method, on_choice)
+ validate {
+ on_choice = { on_choice, 'function', false },
+ }
+ local clients = vim.tbl_values(vim.lsp.buf_get_clients())
+ clients = vim.tbl_filter(function(client)
return client.supports_method(method)
end, clients)
-- better UX when choices are always in the same order (between restarts)
- table.sort(clients, function (a, b) return a.name < b.name end)
+ table.sort(clients, function(a, b)
+ return a.name < b.name
+ end)
if #clients > 1 then
- local choices = {}
- for k,v in pairs(clients) do
- table.insert(choices, string.format("%d %s", k, v.name))
- end
- local user_choice = vim.fn.confirm(
- "Select a language server:",
- table.concat(choices, "\n"),
- 0,
- "Question"
- )
- if user_choice == 0 then return nil end
- return clients[user_choice]
+ vim.ui.select(clients, {
+ prompt = 'Select a language server:',
+ format_item = function(client)
+ return client.name
+ end,
+ }, on_choice)
elseif #clients < 1 then
- return nil
+ on_choice(nil)
else
- return clients[1]
+ on_choice(clients[1])
end
end
@@ -152,11 +151,15 @@ end
--
---@see https://microsoft.github.io/language-server-protocol/specification#textDocument_formatting
function M.formatting(options)
- local client = select_client("textDocument/formatting")
- if client == nil then return end
-
local params = util.make_formatting_params(options)
- return client.request("textDocument/formatting", params, nil, vim.api.nvim_get_current_buf())
+ local bufnr = vim.api.nvim_get_current_buf()
+ select_client('textDocument/formatting', function(client)
+ if client == nil then
+ return
+ end
+
+ return client.request('textDocument/formatting', params, nil, bufnr)
+ end)
end
--- Performs |vim.lsp.buf.formatting()| synchronously.
@@ -165,23 +168,27 @@ end
--- saved. {timeout_ms} is passed on to |vim.lsp.buf_request_sync()|. Example:
---
--- <pre>
---- vim.api.nvim_command[[autocmd BufWritePre <buffer> lua vim.lsp.buf.formatting_sync()]]
+--- autocmd BufWritePre <buffer> lua vim.lsp.buf.formatting_sync()
--- </pre>
---
---@param options Table with valid `FormattingOptions` entries
---@param timeout_ms (number) Request timeout
---@see |vim.lsp.buf.formatting_seq_sync|
function M.formatting_sync(options, timeout_ms)
- local client = select_client("textDocument/formatting")
- if client == nil then return end
-
local params = util.make_formatting_params(options)
- local result, err = client.request_sync("textDocument/formatting", params, timeout_ms, vim.api.nvim_get_current_buf())
- if result and result.result then
- util.apply_text_edits(result.result)
- elseif err then
- vim.notify("vim.lsp.buf.formatting_sync: " .. err, vim.log.levels.WARN)
- end
+ local bufnr = vim.api.nvim_get_current_buf()
+ select_client('textDocument/formatting', function(client)
+ if client == nil then
+ return
+ end
+
+ local result, err = client.request_sync('textDocument/formatting', params, timeout_ms, bufnr)
+ if result and result.result then
+ util.apply_text_edits(result.result, bufnr, client.offset_encoding)
+ elseif err then
+ vim.notify('vim.lsp.buf.formatting_sync: ' .. err, vim.log.levels.WARN)
+ end
+ end)
end
--- Formats the current buffer by sequentially requesting formatting from attached clients.
@@ -202,6 +209,7 @@ end
---the remaining clients in the order as they occur in the `order` list.
function M.formatting_seq_sync(options, timeout_ms, order)
local clients = vim.tbl_values(vim.lsp.buf_get_clients());
+ local bufnr = vim.api.nvim_get_current_buf()
-- sort the clients according to `order`
for _, client_name in pairs(order or {}) do
@@ -220,7 +228,7 @@ function M.formatting_seq_sync(options, timeout_ms, order)
local params = util.make_formatting_params(options)
local result, err = client.request_sync("textDocument/formatting", params, timeout_ms, vim.api.nvim_get_current_buf())
if result and result.result then
- util.apply_text_edits(result.result)
+ util.apply_text_edits(result.result, bufnr, client.offset_encoding)
elseif err then
vim.notify(string.format("vim.lsp.buf.formatting_seq_sync: (%s) %s", client.name, err), vim.log.levels.WARN)
end
@@ -236,12 +244,15 @@ end
---@param end_pos ({number, number}, optional) mark-indexed position.
---Defaults to the end of the last visual selection.
function M.range_formatting(options, start_pos, end_pos)
- local client = select_client("textDocument/rangeFormatting")
- if client == nil then return end
-
local params = util.make_given_range_params(start_pos, end_pos)
params.options = util.make_formatting_params(options).options
- return client.request("textDocument/rangeFormatting", params)
+ select_client('textDocument/rangeFormatting', function(client)
+ if client == nil then
+ return
+ end
+
+ return client.request('textDocument/rangeFormatting', params)
+ end)
end
--- Renames all references to the symbol under the cursor.
@@ -261,6 +272,7 @@ function M.rename(new_name)
request('textDocument/rename', params)
end
+ ---@private
local function prepare_rename(err, result)
if err == nil and result == nil then
vim.notify('nothing to rename', vim.log.levels.INFO)
@@ -369,7 +381,7 @@ end
function M.list_workspace_folders()
local workspace_folders = {}
for _, client in pairs(vim.lsp.buf_get_clients()) do
- for _, folder in pairs(client.workspaceFolders or {}) do
+ for _, folder in pairs(client.workspace_folders or {}) do
table.insert(workspace_folders, folder.name)
end
end
@@ -389,7 +401,7 @@ function M.add_workspace_folder(workspace_folder)
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
local found = false
- for _, folder in pairs(client.workspaceFolders or {}) do
+ for _, folder in pairs(client.workspace_folders or {}) do
if folder.name == workspace_folder then
found = true
print(workspace_folder, "is already part of this workspace")
@@ -398,10 +410,10 @@ function M.add_workspace_folder(workspace_folder)
end
if not found then
vim.lsp.buf_notify(0, 'workspace/didChangeWorkspaceFolders', params)
- if not client.workspaceFolders then
- client.workspaceFolders = {}
+ if not client.workspace_folders then
+ client.workspace_folders = {}
end
- table.insert(client.workspaceFolders, params.event.added[1])
+ table.insert(client.workspace_folders, params.event.added[1])
end
end
end
@@ -415,10 +427,10 @@ function M.remove_workspace_folder(workspace_folder)
if not (workspace_folder and #workspace_folder > 0) then return 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 idx, folder in pairs(client.workspaceFolders) do
+ for idx, folder in pairs(client.workspace_folders) do
if folder.name == workspace_folder then
vim.lsp.buf_notify(0, 'workspace/didChangeWorkspaceFolders', params)
- client.workspaceFolders[idx] = nil
+ client.workspace_folders[idx] = nil
return
end
end
@@ -435,18 +447,21 @@ end
---@param query (string, optional)
function M.workspace_symbol(query)
query = query or npcall(vfn.input, "Query: ")
+ if query == nil then
+ return
+ end
local params = {query = query}
request('workspace/symbol', params)
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`, eg:
+--- by events such as `CursorHold`, e.g.:
---
--- <pre>
---- vim.api.nvim_command [[autocmd CursorHold <buffer> lua vim.lsp.buf.document_highlight()]]
---- vim.api.nvim_command [[autocmd CursorHoldI <buffer> lua vim.lsp.buf.document_highlight()]]
---- vim.api.nvim_command [[autocmd CursorMoved <buffer> lua vim.lsp.buf.clear_references()]]
+--- 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
@@ -491,7 +506,7 @@ local function on_code_action_results(results, ctx)
---@private
local function apply_action(action, client)
if action.edit then
- util.apply_workspace_edit(action.edit)
+ util.apply_workspace_edit(action.edit, client.offset_encoding)
end
if action.command then
local command = type(action.command) == 'table' and action.command or action
@@ -615,14 +630,19 @@ end
--- Executes an LSP server command.
---
----@param command A valid `ExecuteCommandParams` object
+---@param command_params table A valid `ExecuteCommandParams` object
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand
-function M.execute_command(command)
+function M.execute_command(command_params)
validate {
- command = { command.command, 's' },
- arguments = { command.arguments, 't', true }
+ command = { command_params.command, 's' },
+ arguments = { command_params.arguments, 't', true }
+ }
+ command_params = {
+ command=command_params.command,
+ arguments=command_params.arguments,
+ workDoneToken=command_params.workDoneToken,
}
- request('workspace/executeCommand', command)
+ request('workspace/executeCommand', command_params )
end
return M
diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua
index 1e6f83c1ba..614d83f565 100644
--- a/runtime/lua/vim/lsp/diagnostic.lua
+++ b/runtime/lua/vim/lsp/diagnostic.lua
@@ -104,8 +104,10 @@ local function diagnostic_lsp_to_vim(diagnostics, bufnr, client_id)
severity = severity_lsp_to_vim(diagnostic.severity),
message = diagnostic.message,
source = diagnostic.source,
+ code = diagnostic.code,
user_data = {
lsp = {
+ -- usage of user_data.lsp.code is deprecated in favor of the top-level code field
code = diagnostic.code,
codeDescription = diagnostic.codeDescription,
tags = diagnostic.tags,
@@ -120,7 +122,8 @@ end
---@private
local function diagnostic_vim_to_lsp(diagnostics)
return vim.tbl_map(function(diagnostic)
- return vim.tbl_extend("error", {
+ return vim.tbl_extend("keep", {
+ -- "keep" the below fields over any duplicate fields in diagnostic.user_data.lsp
range = {
start = {
line = diagnostic.lnum,
@@ -134,6 +137,7 @@ local function diagnostic_vim_to_lsp(diagnostics)
severity = severity_vim_to_lsp(diagnostic.severity),
message = diagnostic.message,
source = diagnostic.source,
+ code = diagnostic.code,
}, diagnostic.user_data and (diagnostic.user_data.lsp or {}) or {})
end, diagnostics)
end
@@ -153,19 +157,6 @@ function M.get_namespace(client_id)
return _client_namespaces[client_id]
end
---- Save diagnostics to the current buffer.
----
---- Handles saving diagnostics from multiple clients in the same buffer.
----@param diagnostics Diagnostic[]
----@param bufnr number
----@param client_id number
----@private
-function M.save(diagnostics, bufnr, client_id)
- local namespace = M.get_namespace(client_id)
- vim.diagnostic.set(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id))
-end
--- }}}
-
--- |lsp-handler| for the method "textDocument/publishDiagnostics"
---
--- See |vim.diagnostic.config()| for configuration options. Handler-specific
@@ -181,8 +172,8 @@ end
--- },
--- -- Use a function to dynamically turn signs off
--- -- and on, using buffer local variables
---- signs = function(bufnr, client_id)
---- return vim.bo[bufnr].show_signs == false
+--- signs = function(namespace, bufnr)
+--- return vim.b[bufnr].show_signs == true
--- end,
--- -- Disable a feature
--- update_in_insert = false,
@@ -220,12 +211,9 @@ function M.on_publish_diagnostics(_, result, ctx, config)
end
vim.diagnostic.set(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id))
-
- -- Keep old autocmd for back compat. This should eventually be removed.
- vim.api.nvim_command("doautocmd <nomodeline> User LspDiagnosticsChanged")
end
---- Clear diagnotics and diagnostic cache.
+--- Clear diagnostics and diagnostic cache.
---
--- Diagnostic producers should prefer |vim.diagnostic.reset()|. However,
--- this method signature is still used internally in some parts of the LSP
@@ -248,6 +236,23 @@ end
-- Deprecated Functions {{{
+
+--- Save diagnostics to the current buffer.
+---
+---@deprecated Prefer |vim.diagnostic.set()|
+---
+--- Handles saving diagnostics from multiple clients in the same buffer.
+---@param diagnostics Diagnostic[]
+---@param bufnr number
+---@param client_id number
+---@private
+function M.save(diagnostics, bufnr, client_id)
+ vim.notify_once('vim.lsp.diagnostic.save is deprecated. See :h deprecated', vim.log.levels.WARN)
+ local namespace = M.get_namespace(client_id)
+ vim.diagnostic.set(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id))
+end
+-- }}}
+
--- Get all diagnostics for clients
---
---@deprecated Prefer |vim.diagnostic.get()|
@@ -256,6 +261,7 @@ end
--- If nil, diagnostics of all clients are included.
---@return table with diagnostics grouped by bufnr (bufnr: Diagnostic[])
function M.get_all(client_id)
+ vim.notify_once('vim.lsp.diagnostic.get_all is deprecated. See :h deprecated', vim.log.levels.WARN)
local result = {}
local namespace
if client_id then
@@ -277,6 +283,7 @@ end
--- Else, return just the diagnostics associated with the client_id.
---@param predicate function|nil Optional function for filtering diagnostics
function M.get(bufnr, client_id, predicate)
+ vim.notify_once('vim.lsp.diagnostic.get is deprecated. See :h deprecated', vim.log.levels.WARN)
predicate = predicate or function() return true end
if client_id == nil then
local all_diagnostics = {}
@@ -338,6 +345,7 @@ end
---@param severity DiagnosticSeverity
---@param client_id number the client id
function M.get_count(bufnr, severity, client_id)
+ vim.notify_once('vim.lsp.diagnostic.get_count is deprecated. See :h deprecated', vim.log.levels.WARN)
severity = severity_lsp_to_vim(severity)
local opts = { severity = severity }
if client_id ~= nil then
@@ -354,6 +362,7 @@ end
---@param opts table See |vim.lsp.diagnostic.goto_next()|
---@return table Previous diagnostic
function M.get_prev(opts)
+ vim.notify_once('vim.lsp.diagnostic.get_prev is deprecated. See :h deprecated', vim.log.levels.WARN)
if opts then
if opts.severity then
opts.severity = severity_lsp_to_vim(opts.severity)
@@ -371,6 +380,7 @@ end
---@param opts table See |vim.lsp.diagnostic.goto_next()|
---@return table Previous diagnostic position
function M.get_prev_pos(opts)
+ vim.notify_once('vim.lsp.diagnostic.get_prev_pos is deprecated. See :h deprecated', vim.log.levels.WARN)
if opts then
if opts.severity then
opts.severity = severity_lsp_to_vim(opts.severity)
@@ -387,6 +397,7 @@ end
---
---@param opts table See |vim.lsp.diagnostic.goto_next()|
function M.goto_prev(opts)
+ vim.notify_once('vim.lsp.diagnostic.goto_prev is deprecated. See :h deprecated', vim.log.levels.WARN)
if opts then
if opts.severity then
opts.severity = severity_lsp_to_vim(opts.severity)
@@ -404,6 +415,7 @@ end
---@param opts table See |vim.lsp.diagnostic.goto_next()|
---@return table Next diagnostic
function M.get_next(opts)
+ vim.notify_once('vim.lsp.diagnostic.get_next is deprecated. See :h deprecated', vim.log.levels.WARN)
if opts then
if opts.severity then
opts.severity = severity_lsp_to_vim(opts.severity)
@@ -421,6 +433,7 @@ end
---@param opts table See |vim.lsp.diagnostic.goto_next()|
---@return table Next diagnostic position
function M.get_next_pos(opts)
+ vim.notify_once('vim.lsp.diagnostic.get_next_pos is deprecated. See :h deprecated', vim.log.levels.WARN)
if opts then
if opts.severity then
opts.severity = severity_lsp_to_vim(opts.severity)
@@ -434,25 +447,8 @@ end
--- Move to the next diagnostic
---
---@deprecated Prefer |vim.diagnostic.goto_next()|
----
----@param opts table|nil Configuration table. Keys:
---- - {client_id}: (number)
---- - If nil, will consider all clients attached to buffer.
---- - {cursor_position}: (Position, default current position)
---- - See |nvim_win_get_cursor()|
---- - {wrap}: (boolean, default true)
---- - Whether to loop around file or not. Similar to 'wrapscan'
---- - {severity}: (DiagnosticSeverity)
---- - Exclusive severity to consider. Overrides {severity_limit}
---- - {severity_limit}: (DiagnosticSeverity)
---- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
---- - {enable_popup}: (boolean, default true)
---- - Call |vim.lsp.diagnostic.show_line_diagnostics()| on jump
---- - {popup_opts}: (table)
---- - Table to pass as {opts} parameter to |vim.lsp.diagnostic.show_line_diagnostics()|
---- - {win_id}: (number, default 0)
---- - Window ID
function M.goto_next(opts)
+ vim.notify_once('vim.lsp.diagnostic.goto_next is deprecated. See :h deprecated', vim.log.levels.WARN)
if opts then
if opts.severity then
opts.severity = severity_lsp_to_vim(opts.severity)
@@ -476,6 +472,7 @@ end
--- - severity_limit (DiagnosticSeverity):
--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
function M.set_signs(diagnostics, bufnr, client_id, _, opts)
+ vim.notify_once('vim.lsp.diagnostic.set_signs is deprecated. See :h deprecated', vim.log.levels.WARN)
local namespace = M.get_namespace(client_id)
if opts and not opts.severity and opts.severity_limit then
opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
@@ -496,6 +493,7 @@ end
--- - severity_limit (DiagnosticSeverity):
--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
function M.set_underline(diagnostics, bufnr, client_id, _, opts)
+ vim.notify_once('vim.lsp.diagnostic.set_underline is deprecated. See :h deprecated', vim.log.levels.WARN)
local namespace = M.get_namespace(client_id)
if opts and not opts.severity and opts.severity_limit then
opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
@@ -517,6 +515,7 @@ end
--- - severity_limit (DiagnosticSeverity):
--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
function M.set_virtual_text(diagnostics, bufnr, client_id, _, opts)
+ vim.notify_once('vim.lsp.diagnostic.set_virtual_text is deprecated. See :h deprecated', vim.log.levels.WARN)
local namespace = M.get_namespace(client_id)
if opts and not opts.severity and opts.severity_limit then
opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
@@ -535,6 +534,7 @@ end
---@return an array of [text, hl_group] arrays. This can be passed directly to
--- the {virt_text} option of |nvim_buf_set_extmark()|.
function M.get_virtual_text_chunks_for_line(bufnr, _, line_diags, opts)
+ vim.notify_once('vim.lsp.diagnostic.get_virtual_text_chunks_for_line is deprecated. See :h deprecated', vim.log.levels.WARN)
return vim.diagnostic._get_virt_text_chunks(diagnostic_lsp_to_vim(line_diags, bufnr), opts)
end
@@ -552,6 +552,7 @@ end
---@param position table|nil The (0,0)-indexed position
---@return table {popup_bufnr, win_id}
function M.show_position_diagnostics(opts, buf_nr, position)
+ vim.notify_once('vim.lsp.diagnostic.show_position_diagnostics is deprecated. See :h deprecated', vim.log.levels.WARN)
opts = opts or {}
opts.scope = "cursor"
opts.pos = position
@@ -565,7 +566,7 @@ end
--- Open a floating window with the diagnostics from {line_nr}
---
----@deprecated Prefer |vim.diagnostic.show_line_diagnostics()|
+---@deprecated Prefer |vim.diagnostic.open_float()|
---
---@param opts table Configuration table
--- - all opts for |vim.lsp.diagnostic.get_line_diagnostics()| and
@@ -575,6 +576,7 @@ end
---@param client_id number|nil the client id
---@return table {popup_bufnr, win_id}
function M.show_line_diagnostics(opts, buf_nr, line_nr, client_id)
+ vim.notify_once('vim.lsp.diagnostic.show_line_diagnostics is deprecated. See :h deprecated', vim.log.levels.WARN)
opts = opts or {}
opts.scope = "line"
opts.pos = line_nr
@@ -586,7 +588,7 @@ end
--- Redraw diagnostics for the given buffer and client
---
----@deprecated Prefer |vim.diagnostic.redraw()|
+---@deprecated Prefer |vim.diagnostic.show()|
---
--- This calls the "textDocument/publishDiagnostics" handler manually using
--- the cached diagnostics already received from the server. This can be useful
@@ -598,6 +600,7 @@ end
--- client. The default is to redraw diagnostics for all attached
--- clients.
function M.redraw(bufnr, client_id)
+ vim.notify_once('vim.lsp.diagnostic.redraw is deprecated. See :h deprecated', vim.log.levels.WARN)
bufnr = get_bufnr(bufnr)
if not client_id then
return vim.lsp.for_each_buffer_client(bufnr, function(client)
@@ -625,6 +628,7 @@ end
--- - {workspace}: (boolean, default true)
--- - Set the list with workspace diagnostics
function M.set_qflist(opts)
+ vim.notify_once('vim.lsp.diagnostic.set_qflist is deprecated. See :h deprecated', vim.log.levels.WARN)
opts = opts or {}
if opts.severity then
opts.severity = severity_lsp_to_vim(opts.severity)
@@ -656,6 +660,7 @@ end
--- - {workspace}: (boolean, default false)
--- - Set the list with workspace diagnostics
function M.set_loclist(opts)
+ vim.notify_once('vim.lsp.diagnostic.set_loclist is deprecated. See :h deprecated', vim.log.levels.WARN)
opts = opts or {}
if opts.severity then
opts.severity = severity_lsp_to_vim(opts.severity)
@@ -683,6 +688,7 @@ end
-- send diagnostic information and the client will still process it. The
-- diagnostics are simply not displayed to the user.
function M.disable(bufnr, client_id)
+ vim.notify_once('vim.lsp.diagnostic.disable is deprecated. See :h deprecated', vim.log.levels.WARN)
if not client_id then
return vim.lsp.for_each_buffer_client(bufnr, function(client)
M.disable(bufnr, client.id)
@@ -703,6 +709,7 @@ end
--- client. The default is to enable diagnostics for all attached
--- clients.
function M.enable(bufnr, client_id)
+ vim.notify_once('vim.lsp.diagnostic.enable is deprecated. See :h deprecated', vim.log.levels.WARN)
if not client_id then
return vim.lsp.for_each_buffer_client(bufnr, function(client)
M.enable(bufnr, client.id)
@@ -717,5 +724,3 @@ end
-- }}}
return M
-
--- vim: fdm=marker
diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua
index fa4f2b22a5..f5aefd4402 100644
--- a/runtime/lua/vim/lsp/handlers.lua
+++ b/runtime/lua/vim/lsp/handlers.lua
@@ -28,7 +28,7 @@ local function progress_handler(_, result, ctx, _)
local client_name = client and client.name or string.format("id=%d", client_id)
if not client then
err_message("LSP[", client_name, "] client has shut down after sending the message")
- return
+ return vim.NIL
end
local val = result.value -- unspecified yet
local token = result.token -- string or number
@@ -70,6 +70,7 @@ M['window/workDoneProgress/create'] = function(_, result, ctx)
local client_name = client and client.name or string.format("id=%d", client_id)
if not client then
err_message("LSP[", client_name, "] client has shut down after sending the message")
+ return vim.NIL
end
client.messages.progress[token] = {}
return vim.NIL
@@ -110,13 +111,15 @@ M['client/registerCapability'] = function(_, _, ctx)
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit
-M['workspace/applyEdit'] = function(_, workspace_edit)
+M['workspace/applyEdit'] = function(_, workspace_edit, ctx)
if not workspace_edit then return end
-- TODO(ashkan) Do something more with label?
+ local client_id = ctx.client_id
+ local client = vim.lsp.get_client_by_id(client_id)
if workspace_edit.label then
print("Workspace edit", workspace_edit.label)
end
- local status, result = pcall(util.apply_workspace_edit, workspace_edit.edit)
+ local status, result = pcall(util.apply_workspace_edit, workspace_edit.edit, client.offset_encoding)
return {
applied = status;
failureReason = result;
@@ -149,6 +152,17 @@ M['workspace/configuration'] = function(_, result, ctx)
return response
end
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_workspaceFolders
+M['workspace/workspaceFolders'] = function(_, _, ctx)
+ local client_id = ctx.client_id
+ local client = vim.lsp.get_client_by_id(client_id)
+ if not client then
+ err_message("LSP[id=", client_id, "] client has shut down after sending the message")
+ return
+ end
+ return client.workspace_folders or vim.NIL
+end
+
M['textDocument/publishDiagnostics'] = function(...)
return require('vim.lsp.diagnostic').on_publish_diagnostics(...)
end
@@ -158,6 +172,31 @@ M['textDocument/codeLens'] = function(...)
end
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references
+M['textDocument/references'] = function(_, result, ctx, config)
+ if not result or vim.tbl_isempty(result) then
+ vim.notify('No references found')
+ else
+ local client = vim.lsp.get_client_by_id(ctx.client_id)
+ config = config or {}
+ if config.loclist then
+ vim.fn.setloclist(0, {}, ' ', {
+ title = 'References';
+ items = util.locations_to_items(result, client.offset_encoding);
+ context = ctx;
+ })
+ api.nvim_command("lopen")
+ else
+ vim.fn.setqflist({}, ' ', {
+ title = 'References';
+ items = util.locations_to_items(result, client.offset_encoding);
+ context = ctx;
+ })
+ api.nvim_command("botright copen")
+ end
+ end
+end
+
---@private
--- Return a function that converts LSP responses to list items and opens the list
@@ -168,23 +207,26 @@ end
--- loclist: (boolean) use the location list (default is to use the quickfix list)
---
---@param map_result function `((resp, bufnr) -> list)` to convert the response
----@param entity name of the resource used in a `not found` error message
-local function response_to_list(map_result, entity)
- return function(_,result, ctx, config)
+---@param entity string name of the resource used in a `not found` error message
+---@param title_fn function Function to call to generate list title
+local function response_to_list(map_result, entity, title_fn)
+ return function(_, result, ctx, config)
if not result or vim.tbl_isempty(result) then
vim.notify('No ' .. entity .. ' found')
else
config = config or {}
if config.loclist then
vim.fn.setloclist(0, {}, ' ', {
- title = 'Language Server';
+ title = title_fn(ctx);
items = map_result(result, ctx.bufnr);
+ context = ctx;
})
api.nvim_command("lopen")
else
vim.fn.setqflist({}, ' ', {
- title = 'Language Server';
+ title = title_fn(ctx);
items = map_result(result, ctx.bufnr);
+ context = ctx;
})
api.nvim_command("botright copen")
end
@@ -193,31 +235,36 @@ local function response_to_list(map_result, entity)
end
---see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references
-M['textDocument/references'] = response_to_list(util.locations_to_items, 'references')
-
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol
-M['textDocument/documentSymbol'] = response_to_list(util.symbols_to_items, 'document symbols')
+M['textDocument/documentSymbol'] = response_to_list(util.symbols_to_items, 'document symbols', function(ctx)
+ local fname = vim.fn.fnamemodify(vim.uri_to_fname(ctx.params.textDocument.uri), ":.")
+ return string.format('Symbols in %s', fname)
+end)
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_symbol
-M['workspace/symbol'] = response_to_list(util.symbols_to_items, 'symbols')
+M['workspace/symbol'] = response_to_list(util.symbols_to_items, 'symbols', function(ctx)
+ return string.format("Symbols matching '%s'", ctx.params.query)
+end)
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rename
-M['textDocument/rename'] = function(_, result, _)
+M['textDocument/rename'] = function(_, result, ctx, _)
if not result then return end
- util.apply_workspace_edit(result)
+ local client = vim.lsp.get_client_by_id(ctx.client_id)
+ util.apply_workspace_edit(result, client.offset_encoding)
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rangeFormatting
M['textDocument/rangeFormatting'] = function(_, result, ctx, _)
if not result then return end
- util.apply_text_edits(result, ctx.bufnr)
+ local client = vim.lsp.get_client_by_id(ctx.client_id)
+ util.apply_text_edits(result, ctx.bufnr, client.offset_encoding)
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting
M['textDocument/formatting'] = function(_, result, ctx, _)
if not result then return end
- util.apply_text_edits(result, ctx.bufnr)
+ local client = vim.lsp.get_client_by_id(ctx.client_id)
+ util.apply_text_edits(result, ctx.bufnr, client.offset_encoding)
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
@@ -245,7 +292,7 @@ end
---@param config table Configuration table.
--- - border: (default=nil)
--- - Add borders to the floating window
---- - See |vim.api.nvim_open_win()|
+--- - See |nvim_open_win()|
function M.hover(_, result, ctx, config)
config = config or {}
config.focus_id = ctx.method
@@ -276,19 +323,23 @@ local function location_handler(_, result, ctx, _)
local _ = log.info() and log.info(ctx.method, 'No location found')
return nil
end
+ local client = vim.lsp.get_client_by_id(ctx.client_id)
-- textDocument/definition can return Location or Location[]
-- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition
if vim.tbl_islist(result) then
- util.jump_to_location(result[1])
+ util.jump_to_location(result[1], client.offset_encoding)
if #result > 1 then
- util.set_qflist(util.locations_to_items(result))
- api.nvim_command("copen")
+ vim.fn.setqflist({}, ' ', {
+ title = 'LSP locations',
+ items = util.locations_to_items(result, client.offset_encoding)
+ })
+ api.nvim_command("botright copen")
end
else
- util.jump_to_location(result)
+ util.jump_to_location(result, client.offset_encoding)
end
end
@@ -378,8 +429,8 @@ local make_call_hierarchy_handler = function(direction)
})
end
end
- util.set_qflist(items)
- api.nvim_command("copen")
+ vim.fn.setqflist({}, ' ', {title = 'LSP call hierarchy', items = items})
+ api.nvim_command("botright copen")
end
end
@@ -438,14 +489,20 @@ for k, fn in pairs(M) do
})
if err then
- local client = vim.lsp.get_client_by_id(ctx.client_id)
- local client_name = client and client.name or string.format("client_id=%d", ctx.client_id)
-- LSP spec:
-- interface ResponseError:
-- code: integer;
-- message: string;
-- data?: string | number | boolean | array | object | null;
- return err_message(client_name .. ': ' .. tostring(err.code) .. ': ' .. err.message)
+
+ -- Per LSP, don't show ContentModified error to the user.
+ if err.code ~= protocol.ErrorCodes.ContentModified then
+ local client = vim.lsp.get_client_by_id(ctx.client_id)
+ local client_name = client and client.name or string.format("client_id=%d", ctx.client_id)
+
+ err_message(client_name .. ': ' .. tostring(err.code) .. ': ' .. err.message)
+ end
+ return
end
return fn(err, result, ctx, config)
diff --git a/runtime/lua/vim/lsp/log.lua b/runtime/lua/vim/lsp/log.lua
index 4597f1919a..e0b5653587 100644
--- a/runtime/lua/vim/lsp/log.lua
+++ b/runtime/lua/vim/lsp/log.lua
@@ -8,8 +8,8 @@ local log = {}
-- Log level dictionary with reverse lookup as well.
--
-- Can be used to lookup the number from the name or the name from the number.
--- Levels by name: 'trace', 'debug', 'info', 'warn', 'error'
--- Level numbers begin with 'trace' at 0
+-- Levels by name: "TRACE", "DEBUG", "INFO", "WARN", "ERROR"
+-- Level numbers begin with "TRACE" at 0
log.levels = vim.deepcopy(vim.log.levels)
-- Default log level is warn.
@@ -101,6 +101,7 @@ function log.set_level(level)
end
--- Gets the current log level.
+---@return string current log level
function log.get_level()
return current_log_level
end
diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua
index b3aa8b934f..86c9e2fd58 100644
--- a/runtime/lua/vim/lsp/protocol.lua
+++ b/runtime/lua/vim/lsp/protocol.lua
@@ -776,149 +776,9 @@ function protocol.make_client_capabilities()
}
end
---[=[
-export interface DocumentFilter {
- --A language id, like `typescript`.
- language?: string;
- --A Uri [scheme](#Uri.scheme), like `file` or `untitled`.
- scheme?: string;
- --A glob pattern, like `*.{ts,js}`.
- --
- --Glob patterns can have the following syntax:
- --- `*` to match one or more characters in a path segment
- --- `?` to match on one character in a path segment
- --- `**` to match any number of path segments, including none
- --- `{}` to group conditions (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files)
- --- `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
- --- `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`)
- pattern?: string;
-}
---]=]
-
---[[
---Static registration options to be returned in the initialize request.
-interface StaticRegistrationOptions {
- --The id used to register the request. The id can be used to deregister
- --the request again. See also Registration#id.
- id?: string;
-}
-
-export interface DocumentFilter {
- --A language id, like `typescript`.
- language?: string;
- --A Uri [scheme](#Uri.scheme), like `file` or `untitled`.
- scheme?: string;
- --A glob pattern, like `*.{ts,js}`.
- --
- --Glob patterns can have the following syntax:
- --- `*` to match one or more characters in a path segment
- --- `?` to match on one character in a path segment
- --- `**` to match any number of path segments, including none
- --- `{}` to group conditions (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files)
- --- `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
- --- `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`)
- pattern?: string;
-}
-export type DocumentSelector = DocumentFilter[];
-export interface TextDocumentRegistrationOptions {
- --A document selector to identify the scope of the registration. If set to null
- --the document selector provided on the client side will be used.
- documentSelector: DocumentSelector | null;
-}
-
---Code Action options.
-export interface CodeActionOptions {
- --CodeActionKinds that this server may return.
- --
- --The list of kinds may be generic, such as `CodeActionKind.Refactor`, or the server
- --may list out every specific kind they provide.
- codeActionKinds?: CodeActionKind[];
-}
-
-interface ServerCapabilities {
- --Defines how text documents are synced. Is either a detailed structure defining each notification or
- --for backwards compatibility the TextDocumentSyncKind number. If omitted it defaults to `TextDocumentSyncKind.None`.
- textDocumentSync?: TextDocumentSyncOptions | number;
- --The server provides hover support.
- hoverProvider?: boolean;
- --The server provides completion support.
- completionProvider?: CompletionOptions;
- --The server provides signature help support.
- signatureHelpProvider?: SignatureHelpOptions;
- --The server provides goto definition support.
- definitionProvider?: boolean;
- --The server provides Goto Type Definition support.
- --
- --Since 3.6.0
- typeDefinitionProvider?: boolean | (TextDocumentRegistrationOptions & StaticRegistrationOptions);
- --The server provides Goto Implementation support.
- --
- --Since 3.6.0
- implementationProvider?: boolean | (TextDocumentRegistrationOptions & StaticRegistrationOptions);
- --The server provides find references support.
- referencesProvider?: boolean;
- --The server provides document highlight support.
- documentHighlightProvider?: boolean;
- --The server provides document symbol support.
- documentSymbolProvider?: boolean;
- --The server provides workspace symbol support.
- workspaceSymbolProvider?: boolean;
- --The server provides code actions. The `CodeActionOptions` return type is only
- --valid if the client signals code action literal support via the property
- --`textDocument.codeAction.codeActionLiteralSupport`.
- codeActionProvider?: boolean | CodeActionOptions;
- --The server provides code lens.
- codeLensProvider?: CodeLensOptions;
- --The server provides document formatting.
- documentFormattingProvider?: boolean;
- --The server provides document range formatting.
- documentRangeFormattingProvider?: boolean;
- --The server provides document formatting on typing.
- documentOnTypeFormattingProvider?: DocumentOnTypeFormattingOptions;
- --The server provides rename support. RenameOptions may only be
- --specified if the client states that it supports
- --`prepareSupport` in its initial `initialize` request.
- renameProvider?: boolean | RenameOptions;
- --The server provides document link support.
- documentLinkProvider?: DocumentLinkOptions;
- --The server provides color provider support.
- --
- --Since 3.6.0
- colorProvider?: boolean | ColorProviderOptions | (ColorProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions);
- --The server provides folding provider support.
- --
- --Since 3.10.0
- foldingRangeProvider?: boolean | FoldingRangeProviderOptions | (FoldingRangeProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions);
- --The server provides go to declaration support.
- --
- --Since 3.14.0
- declarationProvider?: boolean | (TextDocumentRegistrationOptions & StaticRegistrationOptions);
- --The server provides execute command support.
- executeCommandProvider?: ExecuteCommandOptions;
- --Workspace specific server capabilities
- workspace?: {
- --The server supports workspace folder.
- --
- --Since 3.6.0
- workspaceFolders?: {
- * The server has support for workspace folders
- supported?: boolean;
- * Whether the server wants to receive workspace folder
- * change notifications.
- *
- * If a strings is provided the string is treated as a ID
- * under which the notification is registered on the client
- * side. The ID can be used to unregister for these events
- * using the `client/unregisterCapability` request.
- changeNotifications?: string | boolean;
- }
- }
- --Experimental server capabilities.
- experimental?: any;
-}
---]]
-
--- Creates a normalized object describing LSP server capabilities.
+---@param server_capabilities table Table of capabilities supported by the server
+---@return table Normalized table of capabilities
function protocol.resolve_capabilities(server_capabilities)
local general_properties = {}
local text_document_sync_properties
diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua
index bce1e9f35d..1ecac50df4 100644
--- a/runtime/lua/vim/lsp/rpc.lua
+++ b/runtime/lua/vim/lsp/rpc.lua
@@ -133,7 +133,8 @@ local function request_parser_loop()
end
end
-local client_errors = vim.tbl_add_reverse_lookup {
+--- Mapping of error codes used by the client
+local client_errors = {
INVALID_SERVER_MESSAGE = 1;
INVALID_SERVER_JSON = 2;
NO_RESULT_CALLBACK_FOUND = 3;
@@ -143,6 +144,8 @@ local client_errors = vim.tbl_add_reverse_lookup {
SERVER_RESULT_CALLBACK_ERROR = 7;
}
+client_errors = vim.tbl_add_reverse_lookup(client_errors)
+
--- Constructs an error message from an LSP error object.
---
---@param err (table) The error object
@@ -230,7 +233,7 @@ function default_dispatchers.on_error(code, err)
end
--- Starts an LSP server process and create an LSP RPC client object to
---- interact with it.
+--- interact with it. Communication with the server is currently limited to stdio.
---
---@param cmd (string) Command to start the LSP server.
---@param cmd_args (table) List of additional string arguments to pass to {cmd}.
@@ -264,8 +267,6 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
if extra_spawn_params and extra_spawn_params.cwd then
assert(is_dir(extra_spawn_params.cwd), "cwd must be a directory")
- elseif not (vim.fn.executable(cmd) == 1) then
- error(string.format("The given command %q is not executable.", cmd))
end
if dispatchers then
local user_dispatchers = dispatchers
@@ -325,7 +326,14 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
end
handle, pid = uv.spawn(cmd, spawn_params, onexit)
if handle == nil then
- error(string.format("start `%s` failed: %s", cmd, pid))
+ local msg = string.format("Spawning language server with cmd: `%s` failed", cmd)
+ if string.match(pid, "ENOENT") then
+ msg = msg .. ". The language server is either not installed, missing from PATH, or not executable."
+ else
+ msg = msg .. string.format(" with error message: %s", pid)
+ end
+ vim.notify(msg, vim.log.levels.WARN)
+ return
end
end
diff --git a/runtime/lua/vim/lsp/sync.lua b/runtime/lua/vim/lsp/sync.lua
index f185e3973f..9955fff3e2 100644
--- a/runtime/lua/vim/lsp/sync.lua
+++ b/runtime/lua/vim/lsp/sync.lua
@@ -74,6 +74,7 @@ local function byte_to_utf(line, byte, offset_encoding)
return utf_idx + 1
end
+---@private
local function compute_line_length(line, offset_encoding)
local length
local _
@@ -104,15 +105,16 @@ local function align_end_position(line, byte, offset_encoding)
char = compute_line_length(line, offset_encoding) + 1
else
-- Modifying line, find the nearest utf codepoint
- local offset = str_utf_end(line, byte)
+ local offset = str_utf_start(line, byte)
-- If the byte does not fall on the start of the character, then
-- align to the start of the next character.
- if offset > 0 then
- char = byte_to_utf(line, byte, offset_encoding) + 1
- byte = byte + offset
- else
+ if offset < 0 then
+ byte = byte + str_utf_end(line, byte) + 1
+ end
+ if byte <= #line then
char = byte_to_utf(line, byte, offset_encoding)
- byte = byte + offset
+ else
+ char = compute_line_length(line, offset_encoding) + 1
end
-- Extending line, find the nearest utf codepoint for the last valid character
end
@@ -129,13 +131,30 @@ end
---@param offset_encoding string utf-8|utf-16|utf-32|nil (fallback to utf-8)
---@returns table<int, int> line_idx, byte_idx, and char_idx of first change position
local function compute_start_range(prev_lines, curr_lines, firstline, lastline, new_lastline, offset_encoding)
+ local char_idx
+ local byte_idx
-- If firstline == lastline, no existing text is changed. All edit operations
-- occur on a new line pointed to by lastline. This occurs during insertion of
-- new lines(O), the new newline is inserted at the line indicated by
-- new_lastline.
- -- If firstline == new_lastline, the first change occured on a line that was deleted.
+ if firstline == lastline then
+ local line_idx
+ local line = prev_lines[firstline - 1]
+ if line then
+ line_idx = firstline - 1
+ byte_idx = #line + 1
+ char_idx = compute_line_length(line, offset_encoding) + 1
+ else
+ line_idx = firstline
+ byte_idx = 1
+ char_idx = 1
+ end
+ return { line_idx = line_idx, byte_idx = byte_idx, char_idx = char_idx }
+ end
+
+ -- If firstline == new_lastline, the first change occurred on a line that was deleted.
-- In this case, the first byte change is also at the first byte of firstline
- if firstline == new_lastline or firstline == lastline then
+ if firstline == new_lastline then
return { line_idx = firstline, byte_idx = 1, char_idx = 1 }
end
@@ -156,8 +175,6 @@ local function compute_start_range(prev_lines, curr_lines, firstline, lastline,
end
-- Convert byte to codepoint if applicable
- local char_idx
- local byte_idx
if start_byte_idx == 1 or (#prev_line == 0 and start_byte_idx == 1)then
byte_idx = start_byte_idx
char_idx = 1
@@ -166,7 +183,7 @@ local function compute_start_range(prev_lines, curr_lines, firstline, lastline,
char_idx = compute_line_length(prev_line, offset_encoding) + 1
else
byte_idx = start_byte_idx + str_utf_start(prev_line, start_byte_idx)
- char_idx = byte_to_utf(prev_line, start_byte_idx, offset_encoding)
+ char_idx = byte_to_utf(prev_line, byte_idx, offset_encoding)
end
-- Return the start difference (shared for new and prev lines)
@@ -187,7 +204,7 @@ end
---@param offset_encoding string
---@returns (int, int) end_line_idx and end_col_idx of range
local function compute_end_range(prev_lines, curr_lines, start_range, firstline, lastline, new_lastline, offset_encoding)
- -- If firstline == new_lastline, the first change occured on a line that was deleted.
+ -- If firstline == new_lastline, the first change occurred on a line that was deleted.
-- In this case, the last_byte...
if firstline == new_lastline then
return { line_idx = (lastline - new_lastline + firstline), byte_idx = 1, char_idx = 1 }, { line_idx = firstline, byte_idx = 1, char_idx = 1 }
@@ -296,7 +313,7 @@ end
---@private
-- rangelength depends on the offset encoding
--- bytes for utf-8 (clangd with extenion)
+-- bytes for utf-8 (clangd with extension)
-- codepoints for utf-16
-- codeunits for utf-32
-- Line endings count here as 2 chars for \r\n (dos), 1 char for \n (unix), and 1 char for \r (mac)
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index ba5c20ef9f..401dac9acd 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -10,14 +10,6 @@ local uv = vim.loop
local npcall = vim.F.npcall
local split = vim.split
-local _warned = {}
-local warn_once = function(message)
- if not _warned[message] then
- vim.api.nvim_err_writeln(message)
- _warned[message] = true
- end
-end
-
local M = {}
local default_border = {
@@ -90,6 +82,49 @@ local function split_lines(value)
return split(value, '\n', true)
end
+--- Convert byte index to `encoding` index.
+--- Convenience wrapper around vim.str_utfindex
+---@param line string line to be indexed
+---@param index number byte index (utf-8), or `nil` for length
+---@param encoding string utf-8|utf-16|utf-32|nil defaults to utf-16
+---@return number `encoding` index of `index` in `line`
+function M._str_utfindex_enc(line, index, encoding)
+ if not encoding then encoding = 'utf-16' end
+ if encoding == 'utf-8' then
+ if index then return index else return #line end
+ elseif encoding == 'utf-16' then
+ local _, col16 = vim.str_utfindex(line, index)
+ return col16
+ elseif encoding == 'utf-32' then
+ local col32, _ = vim.str_utfindex(line, index)
+ return col32
+ else
+ error("Invalid encoding: " .. vim.inspect(encoding))
+ end
+end
+
+--- Convert UTF index to `encoding` index.
+--- Convenience wrapper around vim.str_byteindex
+---Alternative to vim.str_byteindex that takes an encoding.
+---@param line string line to be indexed
+---@param index number UTF index
+---@param encoding string utf-8|utf-16|utf-32|nil defaults to utf-16
+---@return number byte (utf-8) index of `encoding` index `index` in `line`
+function M._str_byteindex_enc(line, index, encoding)
+ if not encoding then encoding = 'utf-16' end
+ if encoding == 'utf-8' then
+ if index then return index else return #line end
+ elseif encoding == 'utf-16' then
+ return vim.str_byteindex(line, index, true)
+ elseif encoding == 'utf-32' then
+ return vim.str_byteindex(line, index)
+ else
+ error("Invalid encoding: " .. vim.inspect(encoding))
+ end
+end
+
+local _str_utfindex_enc = M._str_utfindex_enc
+local _str_byteindex_enc = M._str_byteindex_enc
--- Replaces text in a range with new text.
---
--- CAUTION: Changes in-place!
@@ -148,8 +183,101 @@ local function sort_by_key(fn)
end
---@private
+--- Gets the zero-indexed lines from the given buffer.
+--- Works on unloaded buffers by reading the file using libuv to bypass buf reading events.
+--- Falls back to loading the buffer and nvim_buf_get_lines for buffers with non-file URI.
+---
+---@param bufnr number bufnr to get the lines from
+---@param rows number[] zero-indexed line numbers
+---@return table<number string> a table mapping rows to lines
+local function get_lines(bufnr, rows)
+ rows = type(rows) == "table" and rows or { rows }
+
+ -- This is needed for bufload and bufloaded
+ if bufnr == 0 then
+ bufnr = vim.api.nvim_get_current_buf()
+ end
+
+ ---@private
+ local function buf_lines()
+ local lines = {}
+ for _, row in pairs(rows) do
+ lines[row] = (vim.api.nvim_buf_get_lines(bufnr, row, row + 1, false) or { "" })[1]
+ end
+ return lines
+ end
+
+ local uri = vim.uri_from_bufnr(bufnr)
+
+ -- load the buffer if this is not a file uri
+ -- Custom language server protocol extensions can result in servers sending URIs with custom schemes. Plugins are able to load these via `BufReadCmd` autocmds.
+ if uri:sub(1, 4) ~= "file" then
+ vim.fn.bufload(bufnr)
+ return buf_lines()
+ end
+
+ -- use loaded buffers if available
+ if vim.fn.bufloaded(bufnr) == 1 then
+ return buf_lines()
+ end
+
+ local filename = api.nvim_buf_get_name(bufnr)
+
+ -- get the data from the file
+ local fd = uv.fs_open(filename, "r", 438)
+ if not fd then return "" end
+ local stat = uv.fs_fstat(fd)
+ local data = uv.fs_read(fd, stat.size, 0)
+ uv.fs_close(fd)
+
+ local lines = {} -- rows we need to retrieve
+ local need = 0 -- keep track of how many unique rows we need
+ for _, row in pairs(rows) do
+ if not lines[row] then
+ need = need + 1
+ end
+ lines[row] = true
+ end
+
+ local found = 0
+ local lnum = 0
+
+ for line in string.gmatch(data, "([^\n]*)\n?") do
+ if lines[lnum] == true then
+ lines[lnum] = line
+ found = found + 1
+ if found == need then break end
+ end
+ lnum = lnum + 1
+ end
+
+ -- change any lines we didn't find to the empty string
+ for i, line in pairs(lines) do
+ if line == true then
+ lines[i] = ""
+ end
+ end
+ return lines
+end
+
+
+---@private
+--- Gets the zero-indexed line from the given buffer.
+--- Works on unloaded buffers by reading the file using libuv to bypass buf reading events.
+--- Falls back to loading the buffer and nvim_buf_get_lines for buffers with non-file URI.
+---
+---@param bufnr number
+---@param row number zero-indexed line number
+---@return string the line at row in filename
+local function get_line(bufnr, row)
+ return get_lines(bufnr, { row })[row]
+end
+
+
+---@private
--- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position
--- Returns a zero-indexed column, since set_lines() does the conversion to
+---@param offset_encoding string utf-8|utf-16|utf-32
--- 1-indexed
local function get_line_byte_from_position(bufnr, position, offset_encoding)
-- LSP's line and characters are 0-indexed
@@ -158,31 +286,19 @@ local function get_line_byte_from_position(bufnr, position, offset_encoding)
-- When on the first character, we can ignore the difference between byte and
-- character
if col > 0 then
- if not api.nvim_buf_is_loaded(bufnr) then
- vim.fn.bufload(bufnr)
- end
-
- local line = position.line
- local lines = api.nvim_buf_get_lines(bufnr, line, line + 1, false)
- if #lines > 0 then
- local ok, result
-
- if offset_encoding == "utf-16" or not offset_encoding then
- ok, result = pcall(vim.str_byteindex, lines[1], col, true)
- elseif offset_encoding == "utf-32" then
- ok, result = pcall(vim.str_byteindex, lines[1], col, false)
- end
-
- if ok then
- return result
- end
- return math.min(#lines[1], col)
+ local line = get_line(bufnr, position.line) or ''
+ local ok, result
+ ok, result = pcall(_str_byteindex_enc, line, col, offset_encoding)
+ if ok then
+ return result
end
+ return math.min(#line, col)
end
return col
end
--- Process and return progress reports from lsp server
+---@private
function M.get_progress_messages()
local new_messages = {}
@@ -244,8 +360,14 @@ end
--- Applies a list of text edits to a buffer.
---@param text_edits table list of `TextEdit` objects
---@param bufnr number Buffer id
+---@param offset_encoding string utf-8|utf-16|utf-32
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textEdit
-function M.apply_text_edits(text_edits, bufnr)
+function M.apply_text_edits(text_edits, bufnr, offset_encoding)
+ validate {
+ text_edits = { text_edits, 't', false };
+ bufnr = { bufnr, 'number', false };
+ offset_encoding = { offset_encoding, 'string', false };
+ }
if not next(text_edits) then return end
if not api.nvim_buf_is_loaded(bufnr) then
vim.fn.bufload(bufnr)
@@ -279,26 +401,6 @@ function M.apply_text_edits(text_edits, bufnr)
end
end)
- -- Some LSP servers may return +1 range of the buffer content but nvim_buf_set_text can't accept it so we should fix it here.
- local has_eol_text_edit = false
- local max = vim.api.nvim_buf_line_count(bufnr)
- -- TODO handle offset_encoding
- local _, len = vim.str_utfindex(vim.api.nvim_buf_get_lines(bufnr, -2, -1, false)[1] or '')
- text_edits = vim.tbl_map(function(text_edit)
- if max <= text_edit.range.start.line then
- text_edit.range.start.line = max - 1
- text_edit.range.start.character = len
- text_edit.newText = '\n' .. text_edit.newText
- has_eol_text_edit = true
- end
- if max <= text_edit.range['end'].line then
- text_edit.range['end'].line = max - 1
- text_edit.range['end'].character = len
- has_eol_text_edit = true
- end
- return text_edit
- end, text_edits)
-
-- Some LSP servers are depending on the VSCode behavior.
-- The VSCode will re-locate the cursor position after applying TextEdit so we also do it.
local is_current_buf = vim.api.nvim_get_current_buf() == bufnr
@@ -318,16 +420,38 @@ function M.apply_text_edits(text_edits, bufnr)
-- Apply text edits.
local is_cursor_fixed = false
+ local has_eol_text_edit = false
for _, text_edit in ipairs(text_edits) do
+ -- Normalize line ending
+ text_edit.newText, _ = string.gsub(text_edit.newText, '\r\n?', '\n')
+
+ -- Convert from LSP style ranges to Neovim style ranges.
local e = {
start_row = text_edit.range.start.line,
- start_col = get_line_byte_from_position(bufnr, text_edit.range.start),
+ start_col = get_line_byte_from_position(bufnr, text_edit.range.start, offset_encoding),
end_row = text_edit.range['end'].line,
- end_col = get_line_byte_from_position(bufnr, text_edit.range['end']),
+ end_col = get_line_byte_from_position(bufnr, text_edit.range['end'], offset_encoding),
text = vim.split(text_edit.newText, '\n', true),
}
+
+ -- Some LSP servers may return +1 range of the buffer content but nvim_buf_set_text can't accept it so we should fix it here.
+ local max = vim.api.nvim_buf_line_count(bufnr)
+ if max <= e.start_row or max <= e.end_row then
+ local len = #(get_line(bufnr, max - 1) or '')
+ if max <= e.start_row then
+ e.start_row = max - 1
+ e.start_col = len
+ table.insert(e.text, 1, '')
+ end
+ if max <= e.end_row then
+ e.end_row = max - 1
+ e.end_col = len
+ end
+ has_eol_text_edit = true
+ end
vim.api.nvim_buf_set_text(bufnr, e.start_row, e.start_col, e.end_row, e.end_col, e.text)
+ -- Fix cursor position.
local row_count = (e.end_row - e.start_row) + 1
if e.end_row < cursor.row then
cursor.row = cursor.row + (#e.text - row_count)
@@ -342,10 +466,13 @@ function M.apply_text_edits(text_edits, bufnr)
end
end
+ local max = vim.api.nvim_buf_line_count(bufnr)
+
+ -- Apply fixed cursor position.
if is_cursor_fixed then
local is_valid_cursor = true
- is_valid_cursor = is_valid_cursor and cursor.row < vim.api.nvim_buf_line_count(bufnr)
- is_valid_cursor = is_valid_cursor and cursor.col <= #(vim.api.nvim_buf_get_lines(bufnr, cursor.row, cursor.row + 1, false)[1] or '')
+ is_valid_cursor = is_valid_cursor and cursor.row < max
+ is_valid_cursor = is_valid_cursor and cursor.col <= #(get_line(bufnr, max - 1) or '')
if is_valid_cursor then
vim.api.nvim_win_set_cursor(0, { cursor.row + 1, cursor.col })
end
@@ -353,8 +480,8 @@ function M.apply_text_edits(text_edits, bufnr)
-- Remove final line if needed
local fix_eol = has_eol_text_edit
- fix_eol = fix_eol and api.nvim_buf_get_option(bufnr, 'fixeol')
- fix_eol = fix_eol and (vim.api.nvim_buf_get_lines(bufnr, -2, -1, false)[1] or '') == ''
+ fix_eol = fix_eol and (api.nvim_buf_get_option(bufnr, 'eol') or (api.nvim_buf_get_option(bufnr, 'fixeol') and not api.nvim_buf_get_option('binary')))
+ fix_eol = fix_eol and get_line(bufnr, max - 1) == ''
if fix_eol then
vim.api.nvim_buf_set_lines(bufnr, -2, -1, false, {})
end
@@ -392,9 +519,12 @@ end
---@param text_document_edit table: a `TextDocumentEdit` object
---@param index number: Optional index of the edit, if from a list of edits (or nil, if not from a list)
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentEdit
-function M.apply_text_document_edit(text_document_edit, index)
+function M.apply_text_document_edit(text_document_edit, index, offset_encoding)
local text_document = text_document_edit.textDocument
local bufnr = vim.uri_to_bufnr(text_document.uri)
+ if offset_encoding == nil then
+ vim.notify_once("apply_text_document_edit must be called with valid offset encoding", vim.log.levels.WARN)
+ end
-- For lists of text document edits,
-- do not check the version after the first edit.
@@ -413,7 +543,7 @@ function M.apply_text_document_edit(text_document_edit, index)
return
end
- M.apply_text_edits(text_document_edit.edits, bufnr)
+ M.apply_text_edits(text_document_edit.edits, bufnr, offset_encoding)
end
--- Parses snippets in a completion entry.
@@ -474,7 +604,7 @@ local function remove_unmatch_completion_items(items, prefix)
end, items)
end
---- Acording to LSP spec, if the client set `completionItemKind.valueSet`,
+--- According to LSP spec, if the client set `completionItemKind.valueSet`,
--- the client must handle it properly even if it receives a value outside the
--- specification.
---
@@ -574,7 +704,7 @@ function M.rename(old_fname, new_fname, opts)
api.nvim_buf_delete(oldbuf, { force = true })
end
-
+---@private
local function create_file(change)
local opts = change.options or {}
-- from spec: Overwrite wins over `ignoreIfExists`
@@ -586,7 +716,7 @@ local function create_file(change)
vim.fn.bufadd(fname)
end
-
+---@private
local function delete_file(change)
local opts = change.options or {}
local fname = vim.uri_to_fname(change.uri)
@@ -610,9 +740,13 @@ end
--- Applies a `WorkspaceEdit`.
---
----@param workspace_edit (table) `WorkspaceEdit`
+---@param workspace_edit table `WorkspaceEdit`
+---@param offset_encoding string utf-8|utf-16|utf-32 (required)
--see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit
-function M.apply_workspace_edit(workspace_edit)
+function M.apply_workspace_edit(workspace_edit, offset_encoding)
+ if offset_encoding == nil then
+ vim.notify_once("apply_workspace_edit must be called with valid offset encoding", vim.log.levels.WARN)
+ end
if workspace_edit.documentChanges then
for idx, change in ipairs(workspace_edit.documentChanges) do
if change.kind == "rename" then
@@ -628,7 +762,7 @@ function M.apply_workspace_edit(workspace_edit)
elseif change.kind then
error(string.format("Unsupported change: %q", vim.inspect(change)))
else
- M.apply_text_document_edit(change, idx)
+ M.apply_text_document_edit(change, idx, offset_encoding)
end
end
return
@@ -641,7 +775,7 @@ function M.apply_workspace_edit(workspace_edit)
for uri, changes in pairs(all_changes) do
local bufnr = vim.uri_to_bufnr(uri)
- M.apply_text_edits(changes, bufnr)
+ M.apply_text_edits(changes, bufnr, offset_encoding)
end
end
@@ -717,7 +851,8 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers
local active_hl
local active_signature = signature_help.activeSignature or 0
-- If the activeSignature is not inside the valid range, then clip it.
- if active_signature >= #signature_help.signatures then
+ -- In 3.15 of the protocol, activeSignature was allowed to be negative
+ if active_signature >= #signature_help.signatures or active_signature < 0 then
active_signature = 0
end
local signature = signature_help.signatures[active_signature + 1]
@@ -858,12 +993,16 @@ end
--- Jumps to a location.
---
----@param location (`Location`|`LocationLink`)
+---@param location table (`Location`|`LocationLink`)
+---@param offset_encoding string utf-8|utf-16|utf-32 (required)
---@returns `true` if the jump succeeded
-function M.jump_to_location(location)
+function M.jump_to_location(location, offset_encoding)
-- location may be Location or LocationLink
local uri = location.uri or location.targetUri
if uri == nil then return end
+ if offset_encoding == nil then
+ vim.notify_once("jump_to_location must be called with valid offset encoding", vim.log.levels.WARN)
+ end
local bufnr = vim.uri_to_bufnr(uri)
-- Save position in jumplist
vim.cmd "normal! m'"
@@ -875,11 +1014,13 @@ function M.jump_to_location(location)
--- Jump to new location (adjusting for UTF-16 encoding of characters)
api.nvim_set_current_buf(bufnr)
- api.nvim_buf_set_option(0, 'buflisted', true)
+ api.nvim_buf_set_option(bufnr, 'buflisted', true)
local range = location.range or location.targetSelectionRange
local row = range.start.line
- local col = get_line_byte_from_position(0, range.start)
+ local col = get_line_byte_from_position(bufnr, range.start, offset_encoding)
api.nvim_win_set_cursor(0, {row + 1, col})
+ -- Open folds under the cursor
+ vim.cmd("normal! zv")
return true
end
@@ -997,7 +1138,7 @@ function M.stylize_markdown(bufnr, contents, opts)
block = {nil, "```+([a-zA-Z0-9_]*)", "```+"},
pre = {"", "<pre>", "</pre>"},
code = {"", "<code>", "</code>"},
- text = {"plaintex", "<text>", "</text>"},
+ text = {"text", "<text>", "</text>"},
}
local match_begin = function(line)
@@ -1054,7 +1195,7 @@ function M.stylize_markdown(bufnr, contents, opts)
markdown_lines[#stripped] = true
end
else
- -- strip any emty lines or separators prior to this separator in actual markdown
+ -- strip any empty lines or separators prior to this separator in actual markdown
if line:match("^---+$") then
while markdown_lines[#stripped] and (stripped[#stripped]:match("^%s*$") or stripped[#stripped]:match("^---+$")) do
markdown_lines[#stripped] = false
@@ -1087,7 +1228,7 @@ function M.stylize_markdown(bufnr, contents, opts)
local idx = 1
---@private
- -- keep track of syntaxes we already inlcuded.
+ -- keep track of syntaxes we already included.
-- no need to include the same syntax more than once
local langs = {}
local fences = get_markdown_fences()
@@ -1133,17 +1274,57 @@ function M.stylize_markdown(bufnr, contents, opts)
return stripped
end
+---@private
--- Creates autocommands to close a preview window when events happen.
---
----@param events (table) list of events
----@param winnr (number) window id of preview window
+---@param events table list of events
+---@param winnr number window id of preview window
+---@param bufnrs table list of buffers where the preview window will remain visible
---@see |autocmd-events|
-function M.close_preview_autocmd(events, winnr)
+local function close_preview_autocmd(events, winnr, bufnrs)
+ local augroup = 'preview_window_'..winnr
+
+ -- close the preview window when entered a buffer that is not
+ -- the floating window buffer or the buffer that spawned it
+ vim.cmd(string.format([[
+ augroup %s
+ autocmd!
+ autocmd BufEnter * lua vim.lsp.util._close_preview_window(%d, {%s})
+ augroup end
+ ]], augroup, winnr, table.concat(bufnrs, ',')))
+
if #events > 0 then
- api.nvim_command("autocmd "..table.concat(events, ',').." <buffer> ++once lua pcall(vim.api.nvim_win_close, "..winnr..", true)")
+ vim.cmd(string.format([[
+ augroup %s
+ autocmd %s <buffer> lua vim.lsp.util._close_preview_window(%d)
+ augroup end
+ ]], augroup, table.concat(events, ','), winnr))
end
end
+---@private
+--- Closes the preview window
+---
+---@param winnr number window id of preview window
+---@param bufnrs table|nil optional list of ignored buffers
+function M._close_preview_window(winnr, bufnrs)
+ vim.schedule(function()
+ -- exit if we are in one of ignored buffers
+ if bufnrs and vim.tbl_contains(bufnrs, api.nvim_get_current_buf()) then
+ return
+ end
+
+ local augroup = 'preview_window_'..winnr
+ vim.cmd(string.format([[
+ augroup %s
+ autocmd!
+ augroup end
+ augroup! %s
+ ]], augroup, augroup))
+ pcall(vim.api.nvim_win_close, winnr, true)
+ end)
+end
+
---@internal
--- Computes size of float needed to show contents (with optional wrapping)
---
@@ -1223,18 +1404,21 @@ end
---
---@param contents table of lines to show in window
---@param syntax string of syntax to set for opened buffer
----@param opts dictionary with optional fields
---- - height of floating window
---- - width of floating window
---- - wrap boolean enable wrapping of long lines (defaults to true)
---- - wrap_at character to wrap at for computing height when wrap is enabled
---- - max_width maximal width of floating window
---- - max_height maximal height of floating window
---- - pad_top number of lines to pad contents at top
---- - pad_bottom number of lines to pad contents at bottom
---- - focus_id if a popup with this id is opened, then focus it
---- - close_events list of events that closes the floating window
---- - focusable (boolean, default true): Make float focusable
+---@param opts table with optional fields (additional keys are passed on to |vim.api.nvim_open_win()|)
+--- - height: (number) height of floating window
+--- - width: (number) width of floating window
+--- - wrap: (boolean, default true) wrap long lines
+--- - wrap_at: (string) character to wrap at for computing height when wrap is enabled
+--- - max_width: (number) maximal width of floating window
+--- - max_height: (number) maximal height of floating window
+--- - pad_top: (number) number of lines to pad contents at top
+--- - pad_bottom: (number) number of lines to pad contents at bottom
+--- - focus_id: (string) if a popup with this id is opened, then focus it
+--- - close_events: (table) list of events that closes the floating window
+--- - focusable: (boolean, default true) Make float focusable
+--- - focus: (boolean, default true) If `true`, and if {focusable}
+--- is also `true`, focus an existing floating window with the same
+--- {focus_id}
---@returns bufnr,winnr buffer and window number of the newly created floating
---preview window
function M.open_floating_preview(contents, syntax, opts)
@@ -1246,12 +1430,13 @@ function M.open_floating_preview(contents, syntax, opts)
opts = opts or {}
opts.wrap = opts.wrap ~= false -- wrapping by default
opts.stylize_markdown = opts.stylize_markdown ~= false
- opts.close_events = opts.close_events or {"CursorMoved", "CursorMovedI", "BufHidden", "InsertCharPre"}
+ opts.focus = opts.focus ~= false
+ opts.close_events = opts.close_events or {"CursorMoved", "CursorMovedI", "InsertCharPre"}
local bufnr = api.nvim_get_current_buf()
-- check if this popup is focusable and we need to focus
- if opts.focus_id and opts.focusable ~= false then
+ if opts.focus_id and opts.focusable ~= false and opts.focus then
-- Go back to previous window if we are in a focusable one
local current_winnr = api.nvim_get_current_win()
if npcall(api.nvim_win_get_var, current_winnr, opts.focus_id) then
@@ -1314,8 +1499,8 @@ function M.open_floating_preview(contents, syntax, opts)
api.nvim_buf_set_option(floating_bufnr, 'modifiable', false)
api.nvim_buf_set_option(floating_bufnr, 'bufhidden', 'wipe')
- api.nvim_buf_set_keymap(floating_bufnr, "n", "q", "<cmd>bdelete<cr>", {silent = true, noremap = true})
- M.close_preview_autocmd(opts.close_events, floating_winnr)
+ api.nvim_buf_set_keymap(floating_bufnr, "n", "q", "<cmd>bdelete<cr>", {silent = true, noremap = true, nowait = true})
+ close_preview_autocmd(opts.close_events, floating_winnr, {floating_bufnr, bufnr})
-- save focus_id
if opts.focus_id then
@@ -1331,21 +1516,23 @@ do --[[ References ]]
--- Removes document highlights from a buffer.
---
- ---@param bufnr buffer id
+ ---@param bufnr number Buffer id
function M.buf_clear_references(bufnr)
validate { bufnr = {bufnr, 'n', true} }
- api.nvim_buf_clear_namespace(bufnr, reference_ns, 0, -1)
+ api.nvim_buf_clear_namespace(bufnr or 0, reference_ns, 0, -1)
end
--- Shows a list of document highlights for a certain buffer.
---
- ---@param bufnr buffer id
- ---@param references List of `DocumentHighlight` objects to highlight
- ---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to utf-16
+ ---@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
function M.buf_highlight_references(bufnr, references, offset_encoding)
- validate { bufnr = {bufnr, 'n', true} }
- offset_encoding = offset_encoding or 'utf-16'
+ validate {
+ bufnr = {bufnr, 'n', true},
+ offset_encoding = { offset_encoding, 'string', false };
+ }
for _, reference in ipairs(references) do
local start_line, start_char = reference["range"]["start"]["line"], reference["range"]["start"]["character"]
local end_line, end_char = reference["range"]["end"]["line"], reference["range"]["end"]["character"]
@@ -1363,7 +1550,8 @@ do --[[ References ]]
reference_ns,
document_highlight_kind[kind],
{ start_line, start_idx },
- { end_line, end_idx })
+ { end_line, end_idx },
+ { priority = vim.highlight.priorities.user })
end
end
end
@@ -1372,97 +1560,20 @@ local position_sort = sort_by_key(function(v)
return {v.start.line, v.start.character}
end)
---- Gets the zero-indexed line from the given uri.
----@param uri string uri of the resource to get the line from
----@param row number zero-indexed line number
----@return string the line at row in filename
--- For non-file uris, we load the buffer and get the line.
--- If a loaded buffer exists, then that is used.
--- Otherwise we get the line using libuv which is a lot faster than loading the buffer.
-function M.get_line(uri, row)
- return M.get_lines(uri, { row })[row]
-end
-
---- Gets the zero-indexed lines from the given uri.
----@param uri string uri of the resource to get the lines from
----@param rows number[] zero-indexed line numbers
----@return table<number string> a table mapping rows to lines
--- For non-file uris, we load the buffer and get the lines.
--- If a loaded buffer exists, then that is used.
--- Otherwise we get the lines using libuv which is a lot faster than loading the buffer.
-function M.get_lines(uri, rows)
- rows = type(rows) == "table" and rows or { rows }
-
- local function buf_lines(bufnr)
- local lines = {}
- for _, row in pairs(rows) do
- lines[row] = (vim.api.nvim_buf_get_lines(bufnr, row, row + 1, false) or { "" })[1]
- end
- return lines
- end
-
- -- load the buffer if this is not a file uri
- -- Custom language server protocol extensions can result in servers sending URIs with custom schemes. Plugins are able to load these via `BufReadCmd` autocmds.
- if uri:sub(1, 4) ~= "file" then
- local bufnr = vim.uri_to_bufnr(uri)
- vim.fn.bufload(bufnr)
- return buf_lines(bufnr)
- end
-
- local filename = vim.uri_to_fname(uri)
-
- -- use loaded buffers if available
- if vim.fn.bufloaded(filename) == 1 then
- local bufnr = vim.fn.bufnr(filename, false)
- return buf_lines(bufnr)
- end
-
- -- get the data from the file
- local fd = uv.fs_open(filename, "r", 438)
- if not fd then return "" end
- local stat = uv.fs_fstat(fd)
- local data = uv.fs_read(fd, stat.size, 0)
- uv.fs_close(fd)
-
- local lines = {} -- rows we need to retrieve
- local need = 0 -- keep track of how many unique rows we need
- for _, row in pairs(rows) do
- if not lines[row] then
- need = need + 1
- end
- lines[row] = true
- end
-
- local found = 0
- local lnum = 0
-
- for line in string.gmatch(data, "([^\n]*)\n?") do
- if lines[lnum] == true then
- lines[lnum] = line
- found = found + 1
- if found == need then break end
- end
- lnum = lnum + 1
- end
-
- -- change any lines we didn't find to the empty string
- for i, line in pairs(lines) do
- if line == true then
- lines[i] = ""
- end
- end
- return lines
-end
-
--- Returns the items with the byte position calculated correctly and in sorted
--- order, for display in quickfix and location lists.
---
--- The result can be passed to the {list} argument of |setqflist()| or
--- |setloclist()|.
---
----@param locations (table) list of `Location`s or `LocationLink`s
+---@param locations table list of `Location`s or `LocationLink`s
+---@param offset_encoding string offset_encoding for locations utf-8|utf-16|utf-32
---@returns (table) list of items
-function M.locations_to_items(locations)
+function M.locations_to_items(locations, offset_encoding)
+ if offset_encoding == nil then
+ vim.notify_once("locations_to_items must be called with valid offset encoding", vim.log.levels.WARN)
+ end
+
local items = {}
local grouped = setmetatable({}, {
__index = function(t, k)
@@ -1496,13 +1607,13 @@ function M.locations_to_items(locations)
end
-- get all the lines for this uri
- local lines = M.get_lines(uri, uri_rows)
+ local lines = get_lines(vim.uri_to_bufnr(uri), uri_rows)
for _, temp in ipairs(rows) do
local pos = temp.start
local row = pos.line
local line = lines[row] or ""
- local col = pos.character
+ local col = M._str_byteindex_enc(line, pos.character, offset_encoding)
table.insert(items, {
filename = filename,
lnum = row + 1,
@@ -1522,6 +1633,7 @@ end
---
---@param items (table) list of items
function M.set_loclist(items, win_id)
+ vim.api.nvim_echo({{'vim.lsp.util.set_loclist is deprecated. See :h deprecated', 'WarningMsg'}}, true, {})
vim.fn.setloclist(win_id or 0, {}, ' ', {
title = 'Language Server';
items = items;
@@ -1535,13 +1647,14 @@ end
---
---@param items (table) list of items
function M.set_qflist(items)
+ vim.api.nvim_echo({{'vim.lsp.util.set_qflist is deprecated. See :h deprecated', 'WarningMsg'}}, true, {})
vim.fn.setqflist({}, ' ', {
title = 'Language Server';
items = items;
})
end
--- Acording to LSP spec, if the client set "symbolKind.valueSet",
+-- According to LSP spec, if the client set "symbolKind.valueSet",
-- the client must handle it properly even if it receives a value outside the specification.
-- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol
function M._get_symbol_kind_name(symbol_kind)
@@ -1586,7 +1699,7 @@ function M.symbols_to_items(symbols, bufnr)
end
return _items
end
- return _symbols_to_items(symbols, {}, bufnr)
+ return _symbols_to_items(symbols, {}, bufnr or 0)
end
--- Removes empty lines from the beginning and end.
@@ -1638,43 +1751,84 @@ function M.try_trim_markdown_code_blocks(lines)
return 'markdown'
end
-local str_utfindex = vim.str_utfindex
---@private
-local function make_position_param()
- local row, col = unpack(api.nvim_win_get_cursor(0))
+---@param window (optional, number): 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`
+local function make_position_param(window, offset_encoding)
+ window = window or 0
+ local buf = vim.api.nvim_win_get_buf(window)
+ local row, col = unpack(api.nvim_win_get_cursor(window))
+ offset_encoding = offset_encoding or M._get_offset_encoding(buf)
row = row - 1
- local line = api.nvim_buf_get_lines(0, row, row+1, true)[1]
+ local line = api.nvim_buf_get_lines(buf, row, row+1, true)[1]
if not line then
return { line = 0; character = 0; }
end
- -- TODO handle offset_encoding
- local _
- _, col = str_utfindex(line, col)
+
+ col = _str_utfindex_enc(line, col, offset_encoding)
+
return { line = row; character = col; }
end
--- Creates a `TextDocumentPositionParams` object for the current buffer and cursor position.
---
+---@param window (optional, number): 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`
---@returns `TextDocumentPositionParams` object
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentPositionParams
-function M.make_position_params()
+function M.make_position_params(window, offset_encoding)
+ window = window or 0
+ local buf = vim.api.nvim_win_get_buf(window)
+ offset_encoding = offset_encoding or M._get_offset_encoding(buf)
return {
- textDocument = M.make_text_document_params();
- position = make_position_param()
+ textDocument = M.make_text_document_params(buf);
+ position = make_position_param(window, offset_encoding)
}
end
+--- Utility function for getting the encoding of the first LSP client on the given buffer.
+---@param bufnr (number) buffer handle or 0 for current, defaults to current
+---@returns (string) encoding first client if there is one, nil otherwise
+function M._get_offset_encoding(bufnr)
+ validate {
+ bufnr = {bufnr, 'n', true};
+ }
+
+ local offset_encoding
+
+ for _, client in pairs(vim.lsp.buf_get_clients(bufnr)) do
+ if client.offset_encoding == nil then
+ vim.notify_once(
+ string.format("Client (id: %s) offset_encoding is nil. Do not unset offset_encoding.", client.id),
+ vim.log.levels.ERROR
+ )
+ end
+ local this_offset_encoding = client.offset_encoding
+ if not offset_encoding then
+ offset_encoding = this_offset_encoding
+ elseif offset_encoding ~= this_offset_encoding then
+ vim.notify("warning: multiple different client offset_encodings detected for buffer, this is not supported yet", vim.log.levels.WARN)
+ end
+ end
+
+ return offset_encoding
+end
+
--- Using the current position in the current buffer, creates an object that
--- can be used as a building block for several LSP requests, such as
--- `textDocument/codeAction`, `textDocument/colorPresentation`,
--- `textDocument/rangeFormatting`.
---
+---@param window (optional, number): 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`
---@returns { textDocument = { uri = `current_file_uri` }, range = { start =
---`current_position`, end = `current_position` } }
-function M.make_range_params()
- local position = make_position_param()
+function M.make_range_params(window, offset_encoding)
+ local buf = vim.api.nvim_win_get_buf(window or 0)
+ offset_encoding = offset_encoding or M._get_offset_encoding(buf)
+ local position = make_position_param(window, offset_encoding)
return {
- textDocument = M.make_text_document_params(),
+ textDocument = M.make_text_document_params(buf),
range = { start = position; ["end"] = position; }
}
end
@@ -1686,27 +1840,29 @@ end
---Defaults to the start of the last visual selection.
---@param end_pos ({number, number}, optional) mark-indexed position.
---Defaults to the end of the last visual selection.
+---@param bufnr (optional, number): buffer 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 `bufnr`
---@returns { textDocument = { uri = `current_file_uri` }, range = { start =
---`start_position`, end = `end_position` } }
-function M.make_given_range_params(start_pos, end_pos)
+function M.make_given_range_params(start_pos, end_pos, bufnr, offset_encoding)
validate {
start_pos = {start_pos, 't', true};
end_pos = {end_pos, 't', true};
+ offset_encoding = {offset_encoding, 's', true};
}
- local A = list_extend({}, start_pos or api.nvim_buf_get_mark(0, '<'))
- local B = list_extend({}, end_pos or api.nvim_buf_get_mark(0, '>'))
+ bufnr = bufnr or vim.api.nvim_get_current_buf()
+ offset_encoding = offset_encoding or M._get_offset_encoding(bufnr)
+ local A = list_extend({}, start_pos or api.nvim_buf_get_mark(bufnr, '<'))
+ local B = list_extend({}, end_pos or api.nvim_buf_get_mark(bufnr, '>'))
-- convert to 0-index
A[1] = A[1] - 1
B[1] = B[1] - 1
- -- account for encoding.
- -- TODO handle offset_encoding
+ -- account for offset_encoding.
if A[2] > 0 then
- local _, char = M.character_offset(0, A[1], A[2])
- A = {A[1], char}
+ A = {A[1], M.character_offset(bufnr, A[1], A[2], offset_encoding)}
end
if B[2] > 0 then
- local _, char = M.character_offset(0, B[1], B[2])
- B = {B[1], char}
+ B = {B[1], M.character_offset(bufnr, B[1], B[2], offset_encoding)}
end
-- we need to offset the end character position otherwise we loose the last
-- character of the selection, as LSP end position is exclusive
@@ -1715,7 +1871,7 @@ function M.make_given_range_params(start_pos, end_pos)
B[2] = B[2] + 1
end
return {
- textDocument = M.make_text_document_params(),
+ textDocument = M.make_text_document_params(bufnr),
range = {
start = {line = A[1], character = A[2]},
['end'] = {line = B[1], character = B[2]}
@@ -1725,10 +1881,11 @@ end
--- Creates a `TextDocumentIdentifier` object for the current buffer.
---
+---@param bufnr (optional, number): Buffer handle, defaults to current
---@returns `TextDocumentIdentifier`
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentIdentifier
-function M.make_text_document_params()
- return { uri = vim.uri_from_bufnr(0) }
+function M.make_text_document_params(bufnr)
+ return { uri = vim.uri_from_bufnr(bufnr or 0) }
end
--- Create the workspace params
@@ -1737,16 +1894,16 @@ end
function M.make_workspace_params(added, removed)
return { event = { added = added; removed = removed; } }
end
---- Returns visual width of tabstop.
+--- Returns indentation size.
---
----@see |softtabstop|
+---@see |shiftwidth|
---@param bufnr (optional, number): Buffer handle, defaults to current
----@returns (number) tabstop visual width
+---@returns (number) indentation size
function M.get_effective_tabstop(bufnr)
validate { bufnr = {bufnr, 'n', true} }
local bo = bufnr and vim.bo[bufnr] or vim.bo
- local sts = bo.softtabstop
- return (sts > 0 and sts) or (sts < 0 and bo.shiftwidth) or bo.tabstop
+ local sw = bo.shiftwidth
+ return (sw == 0 and bo.tabstop) or sw
end
--- Creates a `DocumentFormattingParams` object for the current buffer and cursor position.
@@ -1771,15 +1928,18 @@ end
---@param buf buffer id (0 for current)
---@param row 0-indexed line
---@param col 0-indexed byte offset in line
----@returns (number, number) UTF-32 and UTF-16 index of the character in line {row} column {col} in buffer {buf}
-function M.character_offset(bufnr, row, col)
- local uri = vim.uri_from_bufnr(bufnr)
- local line = M.get_line(uri, row)
+---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of `buf`
+---@returns (number, number) `offset_encoding` index of the character in line {row} column {col} in buffer {buf}
+function M.character_offset(buf, row, col, offset_encoding)
+ local line = get_line(buf, row)
+ if offset_encoding == nil then
+ vim.notify_once("character_offset must be called with valid offset encoding", vim.log.levels.WARN)
+ end
-- If the col is past the EOL, use the line length.
if col > #line then
- return str_utfindex(line)
+ return _str_utfindex_enc(line, nil, offset_encoding)
end
- return str_utfindex(line, col)
+ return _str_utfindex_enc(line, col, offset_encoding)
end
--- Helper function to return nested values in language server settings
@@ -1798,7 +1958,6 @@ function M.lookup_section(settings, section)
end
M._get_line_byte_from_position = get_line_byte_from_position
-M._warn_once = warn_once
M.buf_versions = {}
diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua
index 6e40b6ca52..f0dc34608c 100644
--- a/runtime/lua/vim/shared.lua
+++ b/runtime/lua/vim/shared.lua
@@ -1,8 +1,10 @@
-- Functions shared by Nvim and its test-suite.
--
--- The singular purpose of this module is to share code with the Nvim
--- test-suite. If, in the future, Nvim itself is used to run the test-suite
--- instead of "vanilla Lua", these functions could move to src/nvim/lua/vim.lua
+-- These are "pure" lua functions not depending of the state of the editor.
+-- Thus they should always be available whenever nvim-related lua code is run,
+-- regardless if it is code in the editor itself, or in worker threads/processes,
+-- or the test suite. (Eventually the test suite will be run in a worker process,
+-- so this wouldn't be a separate case to consider)
local vim = vim or {}
@@ -12,7 +14,7 @@ local vim = vim or {}
--- same functions as those in the input table. Userdata and threads are not
--- copied and will throw an error.
---
----@param orig Table to copy
+---@param orig table Table to copy
---@returns New table of copied keys and (nested) values.
function vim.deepcopy(orig) end -- luacheck: no unused
vim.deepcopy = (function()
@@ -21,17 +23,16 @@ vim.deepcopy = (function()
end
local deepcopy_funcs = {
- table = function(orig)
+ table = function(orig, cache)
+ if cache[orig] then return cache[orig] end
local copy = {}
- if vim._empty_dict_mt ~= nil and getmetatable(orig) == vim._empty_dict_mt then
- copy = vim.empty_dict()
- end
-
+ cache[orig] = copy
+ local mt = getmetatable(orig)
for k, v in pairs(orig) do
- copy[vim.deepcopy(k)] = vim.deepcopy(v)
+ copy[vim.deepcopy(k, cache)] = vim.deepcopy(v, cache)
end
- return copy
+ return setmetatable(copy, mt)
end,
number = _id,
string = _id,
@@ -40,10 +41,10 @@ vim.deepcopy = (function()
['function'] = _id,
}
- return function(orig)
+ return function(orig, cache)
local f = deepcopy_funcs[type(orig)]
if f then
- return f(orig)
+ return f(orig, cache or {})
else
error("Cannot deepcopy object of type "..type(orig))
end
@@ -330,7 +331,7 @@ end
--- Add the reverse lookup values to an existing table.
--- For example:
---- `tbl_add_reverse_lookup { A = 1 } == { [1] = 'A', A = 1 }`
+--- ``tbl_add_reverse_lookup { A = 1 } == { [1] = 'A', A = 1 }``
--
--Do note that it *modifies* the input.
---@param o table The table to add the reverse to.
@@ -346,6 +347,33 @@ function vim.tbl_add_reverse_lookup(o)
return o
end
+--- Index into a table (first argument) via string keys passed as subsequent arguments.
+--- Return `nil` if the key does not exist.
+--_
+--- Examples:
+--- <pre>
+--- vim.tbl_get({ key = { nested_key = true }}, 'key', 'nested_key') == true
+--- vim.tbl_get({ key = {}}, 'key', 'nested_key') == nil
+--- </pre>
+---
+---@param o Table to index
+---@param ... Optional strings (0 or more, variadic) via which to index the table
+---
+---@returns nested value indexed by key if it exists, else nil
+function vim.tbl_get(o, ...)
+ local keys = {...}
+ if #keys == 0 then
+ return
+ end
+ for _, k in ipairs(keys) do
+ o = o[k]
+ if o == nil then
+ return
+ end
+ end
+ return o
+end
+
--- Extends a list-like table with the values of another list-like table.
---
--- NOTE: This mutates dst!
@@ -527,13 +555,23 @@ end
--- => error('arg1: expected even number, got 3')
--- </pre>
---
----@param opt Map of parameter names to validations. Each key is a parameter
+--- If multiple types are valid they can be given as a list.
+--- <pre>
+--- vim.validate{arg1={{'foo'}, {'table', 'string'}}, arg2={'foo', {'table', 'string'}}}
+--- => NOP (success)
+---
+--- vim.validate{arg1={1, {'string', table'}}}
+--- => error('arg1: expected string|table, got number')
+---
+--- </pre>
+---
+---@param opt table of parameter names to validations. Each key is a parameter
--- name; each value is a tuple in one of these forms:
--- 1. (arg_value, type_name, optional)
--- - arg_value: argument value
---- - type_name: string type name, one of: ("table", "t", "string",
+--- - type_name: string|table type name, one of: ("table", "t", "string",
--- "s", "number", "n", "boolean", "b", "function", "f", "nil",
---- "thread", "userdata")
+--- "thread", "userdata") or list of them.
--- - optional: (optional) boolean, if true, `nil` is valid
--- 2. (arg_value, fn, msg)
--- - arg_value: argument value
@@ -560,6 +598,7 @@ do
return type(val) == t or (t == 'callable' and vim.is_callable(val))
end
+ ---@private
local function is_valid(opt)
if type(opt) ~= 'table' then
return false, string.format('opt: expected table, got %s', type(opt))
@@ -571,31 +610,43 @@ do
end
local val = spec[1] -- Argument value.
- local t = spec[2] -- Type name, or callable.
+ local types = spec[2] -- Type name, or callable.
local optional = (true == spec[3])
- if type(t) == 'string' then
- local t_name = type_names[t]
- if not t_name then
- return false, string.format('invalid type name: %s', t)
- end
+ if type(types) == 'string' then
+ types = {types}
+ end
- if (not optional or val ~= nil) and not _is_type(val, t_name) then
- return false, string.format("%s: expected %s, got %s", param_name, t_name, type(val))
- end
- elseif vim.is_callable(t) then
+ if vim.is_callable(types) then
-- Check user-provided validation function.
- local valid, optional_message = t(val)
+ local valid, optional_message = types(val)
if not valid then
- local error_message = string.format("%s: expected %s, got %s", param_name, (spec[3] or '?'), val)
+ local error_message = string.format("%s: expected %s, got %s", param_name, (spec[3] or '?'), tostring(val))
if optional_message ~= nil then
error_message = error_message .. string.format(". Info: %s", optional_message)
end
return false, error_message
end
+ elseif type(types) == 'table' then
+ local success = false
+ for i, t in ipairs(types) do
+ local t_name = type_names[t]
+ if not t_name then
+ return false, string.format('invalid type name: %s', t)
+ end
+ types[i] = t_name
+
+ if (optional and val == nil) or _is_type(val, t_name) then
+ success = true
+ break
+ end
+ end
+ if not success then
+ return false, string.format("%s: expected %s, got %s", param_name, table.concat(types, '|'), type(val))
+ end
else
- return false, string.format("invalid type name: %s", tostring(t))
+ return false, string.format("invalid type name: %s", tostring(types))
end
end
diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua
index 66999c5f7f..f9d539f028 100644
--- a/runtime/lua/vim/treesitter.lua
+++ b/runtime/lua/vim/treesitter.lua
@@ -11,6 +11,7 @@ local parsers = {}
local M = vim.tbl_extend("error", query, language)
M.language_version = vim._ts_get_language_version()
+M.minimum_language_version = vim._ts_get_minimum_language_version()
setmetatable(M, {
__index = function (t, k)
@@ -72,7 +73,7 @@ end
--- Gets the parser for this bufnr / ft combination.
---
--- If needed this will create the parser.
---- Unconditionnally attach the provided callback
+--- Unconditionally attach the provided callback
---
---@param bufnr The buffer the parser should be tied to
---@param lang The filetype of this parser
diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua
index 22b528838c..0ec4ab37ec 100644
--- a/runtime/lua/vim/treesitter/highlighter.lua
+++ b/runtime/lua/vim/treesitter/highlighter.lua
@@ -22,8 +22,25 @@ local _link_default_highlight_once = function(from, to)
return from
end
-TSHighlighter.hl_map = {
+-- If @definition.special does not exist use @definition instead
+local subcapture_fallback = {
+ __index = function(self, capture)
+ local rtn
+ local shortened = capture
+ while not rtn and shortened do
+ shortened = shortened:match('(.*)%.')
+ rtn = shortened and rawget(self, shortened)
+ end
+ rawset(self, capture, rtn or "__notfound")
+ return rtn
+ end
+}
+
+TSHighlighter.hl_map = setmetatable({
["error"] = "Error",
+ ["text.underline"] = "Underlined",
+ ["todo"] = "Todo",
+ ["debug"] = "Debug",
-- Miscs
["comment"] = "Comment",
@@ -35,10 +52,13 @@ TSHighlighter.hl_map = {
["constant"] = "Constant",
["constant.builtin"] = "Special",
["constant.macro"] = "Define",
+ ["define"] = "Define",
+ ["macro"] = "Macro",
["string"] = "String",
["string.regex"] = "String",
["string.escape"] = "SpecialChar",
["character"] = "Character",
+ ["character.special"] = "SpecialChar",
["number"] = "Number",
["boolean"] = "Boolean",
["float"] = "Float",
@@ -64,9 +84,13 @@ TSHighlighter.hl_map = {
["type"] = "Type",
["type.builtin"] = "Type",
+ ["type.qualifier"] = "Type",
+ ["type.definition"] = "Typedef",
+ ["storageclass"] = "StorageClass",
["structure"] = "Structure",
["include"] = "Include",
-}
+ ["preproc"] = "PreProc",
+}, subcapture_fallback)
---@private
local function is_highlight_name(capture_name)
@@ -260,7 +284,8 @@ local function on_line_impl(self, buf, line)
{ 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, -- Low but leaves room below
+ conceal = metadata.conceal,
})
end
if start_row > line then
diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua
index 89ddd6cd5a..8b106108df 100644
--- a/runtime/lua/vim/treesitter/language.lua
+++ b/runtime/lua/vim/treesitter/language.lua
@@ -14,7 +14,7 @@ function M.require_language(lang, path, silent)
return true
end
if path == nil then
- local fname = 'parser/' .. lang .. '.*'
+ local fname = 'parser/' .. vim.fn.fnameescape(lang) .. '.*'
local paths = a.nvim_get_runtime_file(fname, false)
if #paths == 0 then
if silent then
@@ -38,7 +38,7 @@ end
--- Inspects the provided language.
---
---- Inspecting provides some useful informations on the language like node names, ...
+--- Inspecting provides some useful information on the language like node names, ...
---
---@param lang The language.
function M.inspect_language(lang)
diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua
index 7e392f72a4..b83df65151 100644
--- a/runtime/lua/vim/treesitter/languagetree.lua
+++ b/runtime/lua/vim/treesitter/languagetree.lua
@@ -76,8 +76,8 @@ function LanguageTree:lang()
end
--- Determines whether this tree is valid.
---- If the tree is invalid, `parse()` must be called
---- to get the an updated tree.
+--- If the tree is invalid, call `parse()`.
+--- This will return the updated tree.
function LanguageTree:is_valid()
return self._valid
end
@@ -234,7 +234,9 @@ end
--- Destroys this language tree and all its children.
---
--- Any cleanup logic should be performed here.
---- Note, this DOES NOT remove this tree from a parent.
+---
+--- Note:
+--- This DOES NOT remove this tree from a parent. Instead,
--- `remove_child` must be called on the parent to remove it.
function LanguageTree:destroy()
-- Cleanup here
@@ -448,14 +450,14 @@ function LanguageTree:_on_detach(...)
self:_do_callback('detach', ...)
end
---- Registers callbacks for the parser
----@param cbs An `nvim_buf_attach`-like table argument with the following keys :
---- `on_bytes` : see `nvim_buf_attach`, but this will be called _after_ the parsers callback.
---- `on_changedtree` : a callback that will be called every time the tree has syntactical changes.
---- it will only be passed one argument, that is a table of the ranges (as node ranges) that
---- changed.
---- `on_child_added` : emitted when a child is added to the tree.
---- `on_child_removed` : emitted when a child is removed from the tree.
+--- Registers callbacks for the parser.
+---@param cbs table An |nvim_buf_attach()|-like table argument with the following keys :
+--- - `on_bytes` : see |nvim_buf_attach()|, but this will be called _after_ the parsers callback.
+--- - `on_changedtree` : a callback that will be called every time the tree has syntactical changes.
+--- It will only be passed one argument, which is a table of the ranges (as node ranges) that
+--- changed.
+--- - `on_child_added` : emitted when a child is added to the tree.
+--- - `on_child_removed` : emitted when a child is removed from the tree.
function LanguageTree:register_cbs(cbs)
if not cbs then return end
@@ -493,9 +495,9 @@ local function tree_contains(tree, range)
return false
end
---- Determines wether @param range is contained in this language tree
+--- Determines whether {range} is contained in this language tree
---
---- This goes down the tree to recursively check childs.
+--- This goes down the tree to recursively check children.
---
---@param range A range, that is a `{ start_line, start_col, end_line, end_col }` table.
function LanguageTree:contains(range)
@@ -508,7 +510,7 @@ function LanguageTree:contains(range)
return false
end
---- Gets the appropriate language that contains @param range
+--- Gets the appropriate language that contains {range}
---
---@param range A text range, see |LanguageTree:contains|
function LanguageTree:language_for_range(range)
diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua
index 66da179ea3..8383551b5f 100644
--- a/runtime/lua/vim/treesitter/query.lua
+++ b/runtime/lua/vim/treesitter/query.lua
@@ -48,7 +48,7 @@ function M.get_query_files(lang, query_name, is_included)
local base_langs = {}
-- Now get the base languages by looking at the first line of every file
- -- The syntax is the folowing :
+ -- The syntax is the following :
-- ;+ inherits: ({language},)*{language}
--
-- {language} ::= {lang} | ({lang})
@@ -138,48 +138,77 @@ function M.get_query(lang, query_name)
end
end
+local query_cache = setmetatable({}, {
+ __index = function(tbl, key)
+ rawset(tbl, key, {})
+ return rawget(tbl, key)
+ end
+})
+
--- Parse {query} as a string. (If the query is in a file, the caller
---- should read the contents into a string before calling).
+--- should read the contents into a string before calling).
---
--- Returns a `Query` (see |lua-treesitter-query|) object which can be used to
--- search nodes in the syntax tree for the patterns defined in {query}
--- using `iter_*` methods below.
---
---- Exposes `info` and `captures` with additional information about the {query}.
+--- Exposes `info` and `captures` with additional context about {query}.
--- - `captures` contains the list of unique capture names defined in
--- {query}.
--- -` info.captures` also points to `captures`.
--- - `info.patterns` contains information about predicates.
---
----@param lang The language
----@param query A string containing the query (s-expr syntax)
+---@param lang string The language
+---@param query string A string containing the query (s-expr syntax)
---
---@returns The query
function M.parse_query(lang, query)
language.require_language(lang)
- local self = setmetatable({}, Query)
- self.query = vim._ts_parse_query(lang, query)
- self.info = self.query:inspect()
- self.captures = self.info.captures
- return self
+ local cached = query_cache[lang][query]
+ if cached then
+ return cached
+ else
+ local self = setmetatable({}, Query)
+ self.query = vim._ts_parse_query(lang, query)
+ self.info = self.query:inspect()
+ self.captures = self.info.captures
+ query_cache[lang][query] = self
+ return self
+ end
end
--- TODO(vigoux): support multiline nodes too
-
--- Gets the text corresponding to a given node
---
---@param node the node
----@param bsource The buffer or string from which the node is extracted
+---@param source The buffer or string from which the node is extracted
function M.get_node_text(node, source)
local start_row, start_col, start_byte = node:start()
local end_row, end_col, end_byte = node:end_()
if type(source) == "number" then
- if start_row ~= end_row then
+ local lines
+ local eof_row = a.nvim_buf_line_count(source)
+ if start_row >= eof_row then
return nil
end
- local line = a.nvim_buf_get_lines(source, start_row, start_row+1, true)[1]
- return string.sub(line, start_col+1, end_col)
+
+ if end_col == 0 then
+ lines = a.nvim_buf_get_lines(source, start_row, end_row, true)
+ end_col = -1
+ else
+ lines = a.nvim_buf_get_lines(source, start_row, end_row + 1, true)
+ end
+
+ if #lines > 0 then
+ if #lines == 1 then
+ lines[1] = string.sub(lines[1], start_col+1, end_col)
+ else
+ lines[1] = string.sub(lines[1], start_col+1)
+ lines[#lines] = string.sub(lines[#lines], 1, end_col)
+ end
+ end
+
+ return table.concat(lines, "\n")
elseif type(source) == "string" then
return source:sub(start_byte+1, end_byte)
end
@@ -211,11 +240,6 @@ local predicate_handlers = {
["lua-match?"] = function(match, _, source, predicate)
local node = match[predicate[2]]
local regex = predicate[3]
- local start_row, _, end_row, _ = node:range()
- if start_row ~= end_row then
- return false
- end
-
return string.find(M.get_node_text(node, source), regex)
end,
@@ -239,13 +263,8 @@ local predicate_handlers = {
return function(match, _, source, pred)
local node = match[pred[2]]
- local start_row, start_col, end_row, end_col = node:range()
- if start_row ~= end_row then
- return false
- end
-
local regex = compiled_vim_regexes[pred[3]]
- return regex:match_line(source, start_row, start_col, end_col)
+ return regex:match_str(M.get_node_text(node, source))
end
end)(),
@@ -446,7 +465,7 @@ end
---
--- {source} is needed if the query contains predicates, then the caller
--- must ensure to use a freshly parsed tree consistent with the current
---- text of the buffer (if relevent). {start_row} and {end_row} can be used to limit
+--- text of the buffer (if relevant). {start_row} and {end_row} can be used to limit
--- matches inside a row range (this is typically used with root node
--- as the node, i e to get syntax highlight matches in the current
--- viewport). When omitted the start and end row values are used from the given node.
@@ -466,7 +485,7 @@ end
--- </pre>
---
---@param node The node under which the search will occur
----@param source The source buffer or string to exctract text from
+---@param source The source buffer or string to extract text from
---@param start The starting line of the search
---@param stop The stopping line of the search (end-exclusive)
---
diff --git a/runtime/lua/vim/ui.lua b/runtime/lua/vim/ui.lua
index 9568b60fd0..9d4b38f08a 100644
--- a/runtime/lua/vim/ui.lua
+++ b/runtime/lua/vim/ui.lua
@@ -18,6 +18,24 @@ local M = {}
--- Called once the user made a choice.
--- `idx` is the 1-based index of `item` within `item`.
--- `nil` if the user aborted the dialog.
+---
+---
+--- Example:
+--- <pre>
+--- vim.ui.select({ 'tabs', 'spaces' }, {
+--- prompt = 'Select tabs or spaces:',
+--- format_item = function(item)
+--- return "I'd like to choose " .. item
+--- end,
+--- }, function(choice)
+--- if choice == 'spaces' then
+--- vim.o.expandtab = true
+--- else
+--- vim.o.expandtab = false
+--- end
+--- end)
+--- </pre>
+
function M.select(items, opts, on_choice)
vim.validate {
items = { items, 'table', false },
@@ -57,6 +75,13 @@ end
--- Called once the user confirms or abort the input.
--- `input` is what the user typed.
--- `nil` if the user aborted the dialog.
+---
+--- Example:
+--- <pre>
+--- vim.ui.input({ prompt = 'Enter value for shiftwidth: ' }, function(input)
+--- vim.o.shiftwidth = tonumber(input)
+--- end)
+--- </pre>
function M.input(opts, on_confirm)
vim.validate {
on_confirm = { on_confirm, 'function', false },
diff --git a/runtime/lua/vim/uri.lua b/runtime/lua/vim/uri.lua
index d08d2a3ee3..11b661cd1a 100644
--- a/runtime/lua/vim/uri.lua
+++ b/runtime/lua/vim/uri.lua
@@ -74,8 +74,8 @@ local function uri_from_fname(path)
return table.concat(uri_parts)
end
-local URI_SCHEME_PATTERN = '^([a-zA-Z]+[a-zA-Z0-9+-.]*):.*'
-local WINDOWS_URI_SCHEME_PATTERN = '^([a-zA-Z]+[a-zA-Z0-9+-.]*):[a-zA-Z]:.*'
+local URI_SCHEME_PATTERN = '^([a-zA-Z]+[a-zA-Z0-9.+-]*):.*'
+local WINDOWS_URI_SCHEME_PATTERN = '^([a-zA-Z]+[a-zA-Z0-9.+-]*):[a-zA-Z]:.*'
--- Get a URI from a bufnr
---@param bufnr number