diff options
author | Josh Rahm <rahm@google.com> | 2022-07-18 19:37:18 +0000 |
---|---|---|
committer | Josh Rahm <rahm@google.com> | 2022-07-18 19:37:18 +0000 |
commit | 308e1940dcd64aa6c344c403d4f9e0dda58d9c5c (patch) | |
tree | 35fe43e01755e0f312650667004487a44d6b7941 /runtime/lua/vim | |
parent | 96a00c7c588b2f38a2424aeeb4ea3581d370bf2d (diff) | |
parent | e8c94697bcbe23a5c7b07c292b90a6b70aadfa87 (diff) | |
download | rneovim-308e1940dcd64aa6c344c403d4f9e0dda58d9c5c.tar.gz rneovim-308e1940dcd64aa6c344c403d4f9e0dda58d9c5c.tar.bz2 rneovim-308e1940dcd64aa6c344c403d4f9e0dda58d9c5c.zip |
Merge remote-tracking branch 'upstream/master' into rahm
Diffstat (limited to 'runtime/lua/vim')
35 files changed, 9220 insertions, 4415 deletions
diff --git a/runtime/lua/vim/F.lua b/runtime/lua/vim/F.lua index 9327c652db..bca5ddf68b 100644 --- a/runtime/lua/vim/F.lua +++ b/runtime/lua/vim/F.lua @@ -5,13 +5,17 @@ local F = {} ---@param a ---@param b function F.if_nil(a, b) - if a == nil then return b end + if a == nil then + return b + end return a end -- Use in combination with pcall function F.ok_or_nil(status, ...) - if not status then return end + if not status then + return + end return ... end @@ -29,7 +33,7 @@ end --- like {...} except preserve the length explicitly function F.pack_len(...) - return {n=select('#', ...), ...} + return { n = select('#', ...), ... } end --- like unpack() but use the length set by F.pack_len if present diff --git a/runtime/lua/vim/_editor.lua b/runtime/lua/vim/_editor.lua new file mode 100644 index 0000000000..442d7b07d8 --- /dev/null +++ b/runtime/lua/vim/_editor.lua @@ -0,0 +1,854 @@ +-- 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, + health = true, + fs = true, +}) do + vim._submodules[k] = v +end + +vim.log = { + levels = { + TRACE = 0, + DEBUG = 1, + INFO = 2, + WARN = 3, + ERROR = 4, + OFF = 5, + }, +} + +-- 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) + -- Escape control characters + local line1 = lines[1]:gsub('(%c)', '\022%1') + -- nvim_input() is affected by mappings, + -- so use nvim_feedkeys() with "n" flag to ignore mappings. + vim.api.nvim_feedkeys(line1, 'n', true) + 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 + +--- Execute Vim script commands. +--- +--- Example: +--- <pre> +--- vim.cmd('echo 42') +--- vim.cmd([[ +--- augroup My_group +--- autocmd! +--- autocmd FileType c setlocal cindent +--- augroup END +--- ]]) +--- vim.cmd({ cmd = 'echo', args = { '"foo"' } }) +--- </pre> +--- +---@param command string|table Command(s) to execute. +--- If a string, executes multiple lines of Vim script at once. In this +--- case, it is an alias to |nvim_exec()|, where `output` is set to +--- false. Thus it works identical to |:source|. +--- If a table, executes a single command. In this case, it is an alias +--- to |nvim_cmd()| where `opts` is empty. +---@see |ex-cmd-index| +function vim.cmd(command) + if type(command) == 'table' then + return vim.api.nvim_cmd(command, {}) + else + return vim.api.nvim_exec(command, false) + end +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() + if not timer:is_closing() then + timer:close() + end + + 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. + ---@return boolean true if message was displayed, else false + function vim.notify_once(msg, level, opts) + if not notified[msg] then + vim.notify(msg, level, opts) + notified[msg] = true + return true + end + return false + 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 + keys[k] = true + 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 + + keys = vim.tbl_keys(keys) + 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 + +--- Display a deprecation notification to the user. +--- +---@param name string Deprecated function. +---@param alternative string|nil Preferred alternative function. +---@param version string Version in which the deprecated function will +--- be removed. +---@param plugin string|nil Plugin name that the function will be removed +--- from. Defaults to "Nvim". +---@param backtrace boolean|nil Prints backtrace. Defaults to true. +function vim.deprecate(name, alternative, version, plugin, backtrace) + local message = name .. ' is deprecated' + plugin = plugin or 'Nvim' + message = alternative and (message .. ', use ' .. alternative .. ' instead.') or message + message = message + .. ' See :h deprecated\nThis function will be removed in ' + .. plugin + .. ' version ' + .. version + if vim.notify_once(message, vim.log.levels.WARN) and backtrace ~= false then + vim.notify(debug.traceback('', 2):sub(2), vim.log.levels.WARN) + end +end + +--- Create builtin mappings (incl. menus). +--- Called once on startup. +function vim._init_default_mappings() + -- mappings + + --@private + local function map(mode, lhs, rhs) + vim.api.nvim_set_keymap(mode, lhs, rhs, { noremap = true, desc = 'Nvim builtin' }) + end + + map('n', 'Y', 'y$') + -- Use normal! <C-L> to prevent inserting raw <C-L> when using i_<C-O>. #17473 + map('n', '<C-L>', '<Cmd>nohlsearch<Bar>diffupdate<Bar>normal! <C-L><CR>') + map('i', '<C-U>', '<C-G>u<C-U>') + map('i', '<C-W>', '<C-G>u<C-W>') + map('x', '*', 'y/\\V<C-R>"<CR>') + map('x', '#', 'y?\\V<C-R>"<CR>') + -- Use : instead of <Cmd> so that ranges are supported. #19365 + map('n', '&', ':&&<CR>') + + -- menus + + -- TODO VimScript, no l10n + vim.cmd([[ + aunmenu * + vnoremenu PopUp.Cut "+x + vnoremenu PopUp.Copy "+y + anoremenu PopUp.Paste "+gP + vnoremenu PopUp.Paste "+P + vnoremenu PopUp.Delete "_x + nnoremenu PopUp.Select\ All ggVG + vnoremenu PopUp.Select\ All gg0oG$ + inoremenu PopUp.Select\ All <C-Home><C-O>VG + anoremenu PopUp.-1- <Nop> + anoremenu PopUp.How-to\ disable\ mouse <Cmd>help disable-mouse<CR> + ]]) +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..7e3c73667e --- /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 522e26caa7..f1652718ee 100644 --- a/runtime/lua/vim/_meta.lua +++ b/runtime/lua/vim/_meta.lua @@ -5,41 +5,55 @@ local a = vim.api local validate = vim.validate local SET_TYPES = setmetatable({ - SET = 0, - LOCAL = 1, + SET = 0, + LOCAL = 1, GLOBAL = 2, }, { __index = error }) -local options_info = {} -for _, v in pairs(a.nvim_get_all_options_info()) do - options_info[v.name] = v - if v.shortname ~= "" then options_info[v.shortname] = v end -end +local options_info = nil +local buf_options = nil +local glb_options = nil +local win_options = nil -local get_scoped_options = function(scope) - local result = {} - for name, option_info in pairs(options_info) do - if option_info.scope == scope then - result[name] = true +local function _setup() + if options_info ~= nil then + return + end + options_info = {} + for _, v in pairs(a.nvim_get_all_options_info()) do + options_info[v.name] = v + if v.shortname ~= '' then + options_info[v.shortname] = v end end - return result -end + local function get_scoped_options(scope) + local result = {} + for name, option_info in pairs(options_info) do + if option_info.scope == scope then + result[name] = true + end + end + + return result + end -local buf_options = get_scoped_options("buf") -local glb_options = get_scoped_options("global") -local win_options = get_scoped_options("win") + buf_options = get_scoped_options('buf') + glb_options = get_scoped_options('global') + win_options = get_scoped_options('win') +end local function make_meta_accessor(get, set, del, validator) - validator = validator or function() return true end + validator = validator or function() + return true + end - validate { - get = {get, 'f'}; - set = {set, 'f'}; - del = {del, 'f', true}; - validator = {validator, 'f'}; - } + validate({ + get = { get, 'f' }, + set = { set, 'f' }, + del = { del, 'f', true }, + validator = { validator, 'f' }, + }) local mt = {} function mt:__newindex(k, v) @@ -73,23 +87,28 @@ end, vim.fn.setenv) do -- buffer option accessor local function new_buf_opt_accessor(bufnr) local function get(k) - if bufnr == nil and type(k) == "number" then + if bufnr == nil and type(k) == 'number' then return new_buf_opt_accessor(k) end - return a.nvim_buf_get_option(bufnr or 0, k) + return a.nvim_get_option_value(k, { buf = bufnr or 0 }) end local function set(k, v) - return a.nvim_buf_set_option(bufnr or 0, k, v) + return a.nvim_set_option_value(k, v, { buf = bufnr or 0 }) end return make_meta_accessor(get, set, nil, function(k) if type(k) == 'string' then + _setup() if win_options[k] then - error(string.format([['%s' is a window option, not a buffer option. See ":help %s"]], k, k)) + error( + string.format([['%s' is a window option, not a buffer option. See ":help %s"]], k, k) + ) elseif glb_options[k] then - error(string.format([['%s' is a global option, not a buffer option. See ":help %s"]], k, k)) + error( + string.format([['%s' is a global option, not a buffer option. See ":help %s"]], k, k) + ) end end @@ -103,22 +122,27 @@ end do -- window option accessor local function new_win_opt_accessor(winnr) local function get(k) - if winnr == nil and type(k) == "number" then + if winnr == nil and type(k) == 'number' then return new_win_opt_accessor(k) end - return a.nvim_win_get_option(winnr or 0, k) + return a.nvim_get_option_value(k, { win = winnr or 0 }) end local function set(k, v) - return a.nvim_win_set_option(winnr or 0, k, v) + return a.nvim_set_option_value(k, v, { win = winnr or 0 }) end return make_meta_accessor(get, set, nil, function(k) if type(k) == 'string' then + _setup() if buf_options[k] then - error(string.format([['%s' is a buffer option, not a window option. See ":help %s"]], k, k)) + error( + string.format([['%s' is a buffer option, not a window option. See ":help %s"]], k, k) + ) elseif glb_options[k] then - error(string.format([['%s' is a global option, not a window option. See ":help %s"]], k, k)) + error( + string.format([['%s' is a global option, not a window option. See ":help %s"]], k, k) + ) end end @@ -131,17 +155,19 @@ end -- vim global option -- this ONLY sets the global option. like `setglobal` -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.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 a.nvim_get_option_value(k, {}) end, - function(k, v) return a.nvim_set_option_value(k, v, {}) end -) +vim.o = make_meta_accessor(function(k) + return a.nvim_get_option_value(k, {}) +end, function(k, v) + return a.nvim_set_option_value(k, v, {}) +end) ---@brief [[ --- vim.opt, vim.opt_local and vim.opt_global implementation @@ -154,7 +180,9 @@ vim.o = make_meta_accessor( --- Preserves the order and does not mutate the original list local remove_duplicate_values = function(t) local result, seen = {}, {} - if type(t) == "function" then error(debug.traceback("asdf")) end + if type(t) == 'function' then + error(debug.traceback('asdf')) + end for _, v in ipairs(t) do if not seen[v] then table.insert(result, v) @@ -171,37 +199,41 @@ end local key_value_options = { fillchars = true, listchars = true, - winhl = true, + winhl = true, } ---@class OptionTypes --- Option Type Enum local OptionTypes = setmetatable({ BOOLEAN = 0, - NUMBER = 1, - STRING = 2, - ARRAY = 3, - MAP = 4, - SET = 5, + NUMBER = 1, + STRING = 2, + ARRAY = 3, + MAP = 4, + SET = 5, }, { - __index = function(_, k) error("Not a valid OptionType: " .. k) end, - __newindex = function(_, k) error("Cannot set a new OptionType: " .. k) end, + __index = function(_, k) + error('Not a valid OptionType: ' .. k) + end, + __newindex = function(_, k) + error('Cannot set a new OptionType: ' .. k) + end, }) --- Convert a vimoption_T style dictionary to the correct OptionType associated with it. ---@return OptionType local get_option_type = function(name, info) - if info.type == "boolean" then + if info.type == 'boolean' then return OptionTypes.BOOLEAN - elseif info.type == "number" then + elseif info.type == 'number' then return OptionTypes.NUMBER - elseif info.type == "string" then + elseif info.type == 'string' then if not info.commalist and not info.flaglist then return OptionTypes.STRING end if key_value_options[name] then - assert(info.commalist, "Must be a comma list to use key:value style") + assert(info.commalist, 'Must be a comma list to use key:value style') return OptionTypes.MAP end @@ -211,13 +243,12 @@ local get_option_type = function(name, info) return OptionTypes.ARRAY end - error("Fallthrough in OptionTypes") + error('Fallthrough in OptionTypes') else - error("Not a known info.type:" .. info.type) + error('Not a known info.type:' .. info.type) end end - -- Check whether the OptionTypes is allowed for vim.opt -- If it does not match, throw an error which indicates which option causes the error. local function assert_valid_value(name, value, types) @@ -228,16 +259,23 @@ local function assert_valid_value(name, value, types) end end - error(string.format("Invalid option type '%s' for '%s', should be %s", type_of_value, name, table.concat(types, " or "))) + error( + string.format( + "Invalid option type '%s' for '%s', should be %s", + type_of_value, + name, + table.concat(types, ' or ') + ) + ) end local valid_types = { - [OptionTypes.BOOLEAN] = { "boolean" }, - [OptionTypes.NUMBER] = { "number" }, - [OptionTypes.STRING] = { "string" }, - [OptionTypes.SET] = { "string", "table" }, - [OptionTypes.ARRAY] = { "string", "table" }, - [OptionTypes.MAP] = { "string", "table" }, + [OptionTypes.BOOLEAN] = { 'boolean' }, + [OptionTypes.NUMBER] = { 'number' }, + [OptionTypes.STRING] = { 'string' }, + [OptionTypes.SET] = { 'string', 'table' }, + [OptionTypes.ARRAY] = { 'string', 'table' }, + [OptionTypes.MAP] = { 'string', 'table' }, } --- Convert a lua value to a vimoption_T value @@ -245,12 +283,20 @@ local convert_value_to_vim = (function() -- Map of functions to take a Lua style value and convert to vimoption_T style value. -- Each function takes (info, lua_value) -> vim_value local to_vim_value = { - [OptionTypes.BOOLEAN] = function(_, value) return value end, - [OptionTypes.NUMBER] = function(_, value) return value end, - [OptionTypes.STRING] = function(_, value) return value end, + [OptionTypes.BOOLEAN] = function(_, value) + return value + end, + [OptionTypes.NUMBER] = function(_, value) + return value + end, + [OptionTypes.STRING] = function(_, value) + return value + end, [OptionTypes.SET] = function(info, value) - if type(value) == "string" then return value end + if type(value) == 'string' then + return value + end if info.flaglist and info.commalist then local keys = {} @@ -261,7 +307,7 @@ local convert_value_to_vim = (function() end table.sort(keys) - return table.concat(keys, ",") + return table.concat(keys, ',') else local result = '' for k, v in pairs(value) do @@ -275,23 +321,27 @@ local convert_value_to_vim = (function() end, [OptionTypes.ARRAY] = function(info, value) - if type(value) == "string" then return value end + if type(value) == 'string' then + return value + end if not info.allows_duplicates then value = remove_duplicate_values(value) end - return table.concat(value, ",") + return table.concat(value, ',') end, [OptionTypes.MAP] = function(_, value) - if type(value) == "string" then return value end + if type(value) == 'string' then + return value + end local result = {} for opt_key, opt_value in pairs(value) do - table.insert(result, string.format("%s:%s", opt_key, opt_value)) + table.insert(result, string.format('%s:%s', opt_key, opt_value)) end table.sort(result) - return table.concat(result, ",") + return table.concat(result, ',') end, } @@ -312,12 +362,18 @@ local convert_value_to_lua = (function() -- 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, - [OptionTypes.NUMBER] = function(_, value) return value end, - [OptionTypes.STRING] = function(_, value) return value end, + [OptionTypes.BOOLEAN] = function(_, value) + return value + end, + [OptionTypes.NUMBER] = function(_, value) + return value + end, + [OptionTypes.STRING] = function(_, value) + return value + end, [OptionTypes.ARRAY] = function(info, value) - if type(value) == "table" then + if type(value) == 'table' then if not info.allows_duplicates then value = remove_duplicate_values(value) end @@ -332,41 +388,43 @@ local convert_value_to_lua = (function() end -- Handles unescaped commas in a list. - if string.find(value, ",,,") then - local comma_split = vim.split(value, ",,,") + if string.find(value, ',,,') then + local comma_split = vim.split(value, ',,,') local left = comma_split[1] local right = comma_split[2] local result = {} - vim.list_extend(result, vim.split(left, ",")) - table.insert(result, ",") - vim.list_extend(result, vim.split(right, ",")) + vim.list_extend(result, vim.split(left, ',')) + table.insert(result, ',') + vim.list_extend(result, vim.split(right, ',')) table.sort(result) return result end - if string.find(value, ",^,,", 1, true) then - local comma_split = vim.split(value, ",^,,", true) + if string.find(value, ',^,,', 1, true) then + local comma_split = vim.split(value, ',^,,', true) local left = comma_split[1] local right = comma_split[2] local result = {} - vim.list_extend(result, vim.split(left, ",")) - table.insert(result, "^,") - vim.list_extend(result, vim.split(right, ",")) + vim.list_extend(result, vim.split(left, ',')) + table.insert(result, '^,') + vim.list_extend(result, vim.split(right, ',')) table.sort(result) return result end - return vim.split(value, ",") + return vim.split(value, ',') end, [OptionTypes.SET] = function(info, value) - if type(value) == "table" then return value end + if type(value) == 'table' then + return value + end -- Empty strings mean that there is nothing there, -- so empty table should be returned. @@ -374,10 +432,10 @@ local convert_value_to_lua = (function() return {} end - assert(info.flaglist, "That is the only one I know how to handle") + assert(info.flaglist, 'That is the only one I know how to handle') if info.flaglist and info.commalist then - local split_value = vim.split(value, ",") + local split_value = vim.split(value, ',') local result = {} for _, v in ipairs(split_value) do result[v] = true @@ -395,15 +453,17 @@ local convert_value_to_lua = (function() end, [OptionTypes.MAP] = function(info, raw_value) - if type(raw_value) == "table" then return raw_value end + if type(raw_value) == 'table' then + return raw_value + end - assert(info.commalist, "Only commas are supported currently") + assert(info.commalist, 'Only commas are supported currently') local result = {} - local comma_split = vim.split(raw_value, ",") + local comma_split = vim.split(raw_value, ',') for _, key_value_str in ipairs(comma_split) do - local key, value = unpack(vim.split(key_value_str, ":")) + local key, value = unpack(vim.split(key_value_str, ':')) key = vim.trim(key) result[key] = value @@ -443,17 +503,21 @@ local prepend_value = (function() end, [OptionTypes.MAP] = function(left, right) - return vim.tbl_extend("force", left, right) + return vim.tbl_extend('force', left, right) end, [OptionTypes.SET] = function(left, right) - return vim.tbl_extend("force", left, right) + return vim.tbl_extend('force', left, right) end, } return function(name, info, current, new) return value_mutator( - name, info, convert_value_to_lua(name, info, current), convert_value_to_lua(name, info, new), methods + name, + info, + convert_value_to_lua(name, info, current), + convert_value_to_lua(name, info, new), + methods ) end end)() @@ -478,17 +542,21 @@ local add_value = (function() end, [OptionTypes.MAP] = function(left, right) - return vim.tbl_extend("force", left, right) + return vim.tbl_extend('force', left, right) end, [OptionTypes.SET] = function(left, right) - return vim.tbl_extend("force", left, right) + return vim.tbl_extend('force', left, right) end, } return function(name, info, current, new) return value_mutator( - name, info, convert_value_to_lua(name, info, current), convert_value_to_lua(name, info, new), methods + name, + info, + convert_value_to_lua(name, info, current), + convert_value_to_lua(name, info, new), + methods ) end end)() @@ -518,11 +586,11 @@ local remove_value = (function() end, [OptionTypes.STRING] = function() - error("Subtraction not supported for strings.") + error('Subtraction not supported for strings.') end, [OptionTypes.ARRAY] = function(left, right) - if type(right) == "string" then + if type(right) == 'string' then remove_one_item(left, right) else for _, v in ipairs(right) do @@ -534,7 +602,7 @@ local remove_value = (function() end, [OptionTypes.MAP] = function(left, right) - if type(right) == "string" then + if type(right) == 'string' then left[right] = nil else for _, v in ipairs(right) do @@ -546,7 +614,7 @@ local remove_value = (function() end, [OptionTypes.SET] = function(left, right) - if type(right) == "string" then + if type(right) == 'string' then left[right] = nil else for _, v in ipairs(right) do @@ -567,9 +635,10 @@ local create_option_metatable = function(set_type) local set_mt, option_mt local make_option = function(name, value) - local info = assert(options_info[name], "Not a valid option name: " .. name) + _setup() + local info = assert(options_info[name], 'Not a valid option name: ' .. name) - if type(value) == "table" and getmetatable(value) == option_mt then + if type(value) == 'table' and getmetatable(value) == option_mt then assert(name == value._name, "must be the same value, otherwise that's weird.") value = value._value @@ -584,9 +653,9 @@ local create_option_metatable = function(set_type) local scope if set_type == SET_TYPES.GLOBAL then - scope = "global" + scope = 'global' elseif set_type == SET_TYPES.LOCAL then - scope = "local" + scope = 'local' end option_mt = { @@ -594,7 +663,7 @@ local create_option_metatable = function(set_type) -- opt[my_option] = value _set = function(self) local value = convert_value_to_vim(self._name, self._info, self._value) - a.nvim_set_option_value(self._name, value, {scope = scope}) + a.nvim_set_option_value(self._name, value, { scope = scope }) return self end, @@ -625,13 +694,13 @@ local create_option_metatable = function(set_type) __sub = function(self, right) return make_option(self._name, remove_value(self._name, self._info, self._value, right)) - end + end, } option_mt.__index = option_mt set_mt = { __index = function(_, k) - return make_option(k, a.nvim_get_option_value(k, {scope = scope})) + return make_option(k, a.nvim_get_option_value(k, { scope = scope })) end, __newindex = function(_, k, v) diff --git a/runtime/lua/vim/compat.lua b/runtime/lua/vim/compat.lua index 168979bb95..2c9786d491 100644 --- a/runtime/lua/vim/compat.lua +++ b/runtime/lua/vim/compat.lua @@ -7,6 +7,6 @@ local lua_version = _VERSION:sub(-3) -if lua_version >= "5.2" then - unpack = table.unpack -- luacheck: ignore 121 143 +if lua_version >= '5.2' then + unpack = table.unpack -- luacheck: ignore 121 143 end diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua index 417b661155..ae20b5c517 100644 --- a/runtime/lua/vim/diagnostic.lua +++ b/runtime/lua/vim/diagnostic.lua @@ -28,9 +28,9 @@ local global_diagnostic_options = { M.handlers = setmetatable({}, { __newindex = function(t, name, handler) - vim.validate { handler = {handler, "t" } } + 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, @@ -39,7 +39,7 @@ M.handlers = setmetatable({}, { -- 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") + assert(bufnr > 0, 'Invalid buffer number') t[bufnr] = {} return t[bufnr] end, @@ -47,11 +47,11 @@ local bufnr_and_namespace_cacher_mt = { local diagnostic_cache = setmetatable({}, { __index = function(t, bufnr) - assert(bufnr > 0, "Invalid buffer number") + assert(bufnr > 0, 'Invalid buffer number') vim.api.nvim_buf_attach(bufnr, false, { on_detach = function() rawset(t, bufnr, nil) -- clear cache - end + end, }) t[bufnr] = {} return t[bufnr] @@ -68,7 +68,10 @@ local all_namespaces = {} ---@private local function to_severity(severity) if type(severity) == 'string' then - return assert(M.severity[string.upper(severity)], string.format("Invalid severity: %s", severity)) + return assert( + M.severity[string.upper(severity)], + string.format('Invalid severity: %s', severity) + ) end return severity end @@ -79,15 +82,19 @@ local function filter_by_severity(severity, diagnostics) return diagnostics end - if type(severity) ~= "table" then + if type(severity) ~= 'table' then severity = to_severity(severity) - return vim.tbl_filter(function(t) return t.severity == severity end, diagnostics) + return vim.tbl_filter(function(t) + return t.severity == severity + end, diagnostics) end local min_severity = to_severity(severity.min) or M.severity.HINT local max_severity = to_severity(severity.max) or M.severity.ERROR - return vim.tbl_filter(function(t) return t.severity <= min_severity and t.severity >= max_severity end, diagnostics) + return vim.tbl_filter(function(t) + return t.severity <= min_severity and t.severity >= max_severity + end, diagnostics) end ---@private @@ -113,17 +120,17 @@ local function prefix_source(diagnostics) end local t = vim.deepcopy(d) - t.message = string.format("%s: %s", d.source, d.message) + t.message = string.format('%s: %s', d.source, d.message) return t end, diagnostics) end ---@private local function reformat_diagnostics(format, diagnostics) - vim.validate { - format = {format, 'f'}, - diagnostics = {diagnostics, 't'}, - } + vim.validate({ + format = { format, 'f' }, + diagnostics = { diagnostics, 't' }, + }) local formatted = vim.deepcopy(diagnostics) for _, diagnostic in ipairs(formatted) do @@ -135,11 +142,11 @@ end ---@private local function enabled_value(option, namespace) local ns = namespace and M.get_namespace(namespace) or {} - if ns.opts and type(ns.opts[option]) == "table" then + if ns.opts and type(ns.opts[option]) == 'table' then return ns.opts[option] end - if type(global_diagnostic_options[option]) == "table" then + if type(global_diagnostic_options[option]) == 'table' then return global_diagnostic_options[option] end @@ -162,7 +169,7 @@ local function resolve_optional_value(option, value, namespace, bufnr) elseif type(value) == 'table' then return value else - error("Unexpected option type: " .. vim.inspect(value)) + error('Unexpected option type: ' .. vim.inspect(value)) end end @@ -181,10 +188,10 @@ end -- Default diagnostic highlights local diagnostic_severities = { - [M.severity.ERROR] = { ctermfg = 1, guifg = "Red" }; - [M.severity.WARN] = { ctermfg = 3, guifg = "Orange" }; - [M.severity.INFO] = { ctermfg = 4, guifg = "LightBlue" }; - [M.severity.HINT] = { ctermfg = 7, guifg = "LightGrey" }; + [M.severity.ERROR] = { ctermfg = 1, guifg = 'Red' }, + [M.severity.WARN] = { ctermfg = 3, guifg = 'Orange' }, + [M.severity.INFO] = { ctermfg = 4, guifg = 'LightBlue' }, + [M.severity.HINT] = { ctermfg = 7, guifg = 'LightGrey' }, } -- Make a map from DiagnosticSeverity -> Highlight Name @@ -194,16 +201,16 @@ local function make_highlight_map(base_name) for k in pairs(diagnostic_severities) do local name = M.severity[k] name = name:sub(1, 1) .. name:sub(2):lower() - result[k] = "Diagnostic" .. base_name .. name + result[k] = 'Diagnostic' .. base_name .. name end return result end -local virtual_text_highlight_map = make_highlight_map("VirtualText") -local underline_highlight_map = make_highlight_map("Underline") -local floating_highlight_map = make_highlight_map("Floating") -local sign_highlight_map = make_highlight_map("Sign") +local virtual_text_highlight_map = make_highlight_map('VirtualText') +local underline_highlight_map = make_highlight_map('Underline') +local floating_highlight_map = make_highlight_map('Floating') +local sign_highlight_map = make_highlight_map('Sign') ---@private local define_default_signs = (function() @@ -244,7 +251,7 @@ local function is_disabled(namespace, bufnr) return true end - if type(diagnostic_disabled[bufnr]) == "table" then + if type(diagnostic_disabled[bufnr]) == 'table' then return diagnostic_disabled[bufnr][namespace] end return diagnostic_disabled[bufnr] @@ -271,9 +278,10 @@ 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 + 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 diagnostic.namespace = namespace @@ -285,7 +293,7 @@ end ---@private local function restore_extmarks(bufnr, last) for ns, extmarks in pairs(diagnostic_cache_extmarks[bufnr]) do - local extmarks_current = vim.api.nvim_buf_get_extmarks(bufnr, ns, 0, -1, {details = true}) + local extmarks_current = vim.api.nvim_buf_get_extmarks(bufnr, ns, 0, -1, { details = true }) local found = {} for _, extmark in ipairs(extmarks_current) do -- nvim_buf_set_lines will move any extmark to the line after the last @@ -314,10 +322,12 @@ local function save_extmarks(namespace, bufnr) end, on_detach = function() diagnostic_cache_extmarks[bufnr] = nil - end}) + end, + }) diagnostic_attached_buffers[bufnr] = true end - diagnostic_cache_extmarks[bufnr][namespace] = vim.api.nvim_buf_get_extmarks(bufnr, namespace, 0, -1, {details = true}) + diagnostic_cache_extmarks[bufnr][namespace] = + vim.api.nvim_buf_get_extmarks(bufnr, namespace, 0, -1, { details = true }) end local registered_autocmds = {} @@ -325,11 +335,37 @@ local registered_autocmds = {} ---@private local function make_augroup_key(namespace, bufnr) local ns = M.get_namespace(namespace) - return string.format("DiagnosticInsertLeave:%s:%s", bufnr, ns.name) + return string.format('DiagnosticInsertLeave:%s:%s', bufnr, ns.name) +end + +---@private +local function execute_scheduled_display(namespace, bufnr) + local args = bufs_waiting_to_update[bufnr][namespace] + if not args then + return + end + + -- Clear the args so we don't display unnecessarily. + bufs_waiting_to_update[bufnr][namespace] = nil + + M.show(namespace, bufnr, nil, args) +end + +--- @deprecated +--- Callback scheduled when leaving Insert mode. +--- +--- called from the Vimscript autocommand. +--- +--- See @ref schedule_display() +--- +---@private +function M._execute_scheduled_display(namespace, bufnr) + vim.deprecate('vim.diagnostic._execute_scheduled_display', nil, '0.9') + execute_scheduled_display(namespace, bufnr) end --- Table of autocmd events to fire the update for displaying new diagnostic information -local insert_leave_auto_cmds = { "InsertLeave", "CursorHoldI" } +local insert_leave_auto_cmds = { 'InsertLeave', 'CursorHoldI' } ---@private local function schedule_display(namespace, bufnr, args) @@ -337,15 +373,15 @@ local function schedule_display(namespace, bufnr, args) local key = make_augroup_key(namespace, bufnr) if not registered_autocmds[key] then - vim.cmd(string.format([[augroup %s - au! - autocmd %s <buffer=%s> lua vim.diagnostic._execute_scheduled_display(%s, %s) - augroup END]], - key, - table.concat(insert_leave_auto_cmds, ","), - bufnr, - namespace, - bufnr)) + local group = vim.api.nvim_create_augroup(key, { clear = true }) + vim.api.nvim_create_autocmd(insert_leave_auto_cmds, { + group = group, + buffer = bufnr, + callback = function() + execute_scheduled_display(namespace, bufnr) + end, + desc = 'vim.diagnostic: display diagnostics', + }) registered_autocmds[key] = true end end @@ -355,9 +391,7 @@ local function clear_scheduled_display(namespace, bufnr) local key = make_augroup_key(namespace, bufnr) if registered_autocmds[key] then - vim.cmd(string.format([[augroup %s - au! - augroup END]], key)) + vim.api.nvim_del_augroup_by_name(key) registered_autocmds[key] = nil end end @@ -382,10 +416,19 @@ local function get_diagnostics(bufnr, opts, clamp) 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 + if + d.lnum > line_count + or d.end_lnum > line_count + or d.lnum < 0 + or d.end_lnum < 0 + or d.col < 0 + or d.end_col < 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) + d.col = math.max(d.col, 0) + d.end_col = math.max(d.end_col, 0) end end table.insert(diagnostics, d) @@ -431,7 +474,7 @@ end local function set_list(loclist, opts) opts = opts or {} local open = vim.F.if_nil(opts.open, true) - local title = opts.title or "Diagnostics" + local title = opts.title or 'Diagnostics' local winnr = opts.winnr or 0 local bufnr if loclist then @@ -447,7 +490,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") + vim.api.nvim_command(loclist and 'lopen' or 'botright copen') end end @@ -457,7 +500,8 @@ 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 = get_diagnostics(bufnr, vim.tbl_extend("keep", opts, {namespace = namespace}), true) + 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) @@ -469,14 +513,22 @@ local function next_diagnostic(position, search_forward, bufnr, opts, namespace) lnum = (lnum + line_count) % line_count end if line_diagnostics[lnum] and not vim.tbl_isempty(line_diagnostics[lnum]) then - local line_length = #vim.api.nvim_buf_get_lines(bufnr, lnum, lnum + 1, true)[1] + local line_length = #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(d) return math.min(d.col, line_length - 1) > position[2] end + sort_diagnostics = function(a, b) + return a.col < b.col + 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(d) return math.min(d.col, line_length - 1) < position[2] end + sort_diagnostics = function(a, b) + return a.col > b.col + 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 @@ -500,28 +552,26 @@ local function diagnostic_move_pos(opts, pos) local win_id = opts.win_id or vim.api.nvim_get_current_win() if not pos then - vim.api.nvim_echo({{"No more valid diagnostics to move to", "WarningMsg"}}, true, {}) + vim.api.nvim_echo({ { 'No more valid diagnostics to move to', 'WarningMsg' } }, true, {}) return end 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]}) + vim.api.nvim_win_set_cursor(win_id, { pos[1] + 1, pos[2] }) -- Open folds under the cursor - vim.cmd("normal! zv") + vim.cmd('normal! zv') end) if float then - local float_opts = type(float) == "table" and float or {} + local float_opts = type(float) == 'table' and float or {} vim.schedule(function() - M.open_float( - vim.tbl_extend("keep", float_opts, { - bufnr = vim.api.nvim_win_get_buf(win_id), - scope = "cursor", - focus = false, - }) - ) + M.open_float(vim.tbl_extend('keep', float_opts, { + bufnr = vim.api.nvim_win_get_buf(win_id), + scope = 'cursor', + focus = false, + })) end) end end @@ -552,7 +602,8 @@ 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| @@ -598,10 +649,10 @@ end ---@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' }, + vim.validate({ + opts = { opts, 't', true }, namespace = { namespace, 'n', true }, - } + }) local t if namespace then @@ -611,10 +662,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 @@ -641,16 +695,16 @@ end ---@param diagnostics table A list of diagnostic items |diagnostic-structure| ---@param opts table|nil Display options to pass to |vim.diagnostic.show()| function M.set(namespace, bufnr, diagnostics, opts) - vim.validate { - namespace = {namespace, 'n'}, - bufnr = {bufnr, 'n'}, + vim.validate({ + namespace = { namespace, 'n' }, + bufnr = { bufnr, 'n' }, diagnostics = { diagnostics, vim.tbl_islist, - "a list of diagnostics", + 'a list of diagnostics', }, - opts = {opts, 't', true}, - } + opts = { opts, 't', true }, + }) bufnr = get_bufnr(bufnr) @@ -664,14 +718,10 @@ function M.set(namespace, bufnr, diagnostics, opts) M.show(namespace, bufnr, nil, opts) end - 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) + vim.api.nvim_exec_autocmds('DiagnosticChanged', { + modeline = false, + buffer = bufnr, + }) end --- Get namespace metadata. @@ -679,7 +729,7 @@ end ---@param namespace number Diagnostic namespace ---@return table Namespace metadata function M.get_namespace(namespace) - vim.validate { namespace = { namespace, 'n' } } + vim.validate({ namespace = { namespace, 'n' } }) if not all_namespaces[namespace] then local name for k, v in pairs(vim.api.nvim_get_namespaces()) do @@ -689,7 +739,7 @@ function M.get_namespace(namespace) end end - assert(name, "namespace does not exist or is anonymous") + assert(name, 'namespace does not exist or is anonymous') all_namespaces[namespace] = { name = name, @@ -717,10 +767,10 @@ end --- - severity: See |diagnostic-severity|. ---@return table A list of diagnostic items |diagnostic-structure|. function M.get(bufnr, opts) - vim.validate { + vim.validate({ bufnr = { bufnr, 'n', true }, opts = { opts, 't', true }, - } + }) return get_diagnostics(bufnr, opts, false) end @@ -749,16 +799,13 @@ function M.get_prev_pos(opts) return false end - return {prev.lnum, prev.col} + return { prev.lnum, prev.col } end --- Move to the previous diagnostic in the current buffer. ---@param opts table See |vim.diagnostic.goto_next()| function M.goto_prev(opts) - return diagnostic_move_pos( - opts, - M.get_prev_pos(opts) - ) + return diagnostic_move_pos(opts, M.get_prev_pos(opts)) end --- Get the next diagnostic closest to the cursor position. @@ -785,7 +832,7 @@ function M.get_next_pos(opts) return false end - return {next.lnum, next.col} + return { next.lnum, next.col } end --- Move to the next diagnostic. @@ -803,26 +850,24 @@ end --- the "scope" option). --- - win_id: (number, default 0) Window ID function M.goto_next(opts) - return diagnostic_move_pos( - opts, - M.get_next_pos(opts) - ) + return diagnostic_move_pos(opts, M.get_next_pos(opts)) end M.handlers.signs = { show = function(namespace, bufnr, diagnostics, opts) - vim.validate { - namespace = {namespace, 'n'}, - bufnr = {bufnr, 'n'}, + vim.validate({ + namespace = { namespace, 'n' }, + bufnr = { bufnr, 'n' }, diagnostics = { diagnostics, vim.tbl_islist, - "a list of diagnostics", + 'a list of diagnostics', }, - opts = {opts, 't', true}, - } + 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) @@ -834,7 +879,7 @@ M.handlers.signs = { local priority = opts.signs and opts.signs.priority or 10 local get_priority if opts.severity_sort then - if type(opts.severity_sort) == "table" and opts.severity_sort.reverse then + if type(opts.severity_sort) == 'table' and opts.severity_sort.reverse then get_priority = function(severity) return priority + (severity - vim.diagnostic.severity.ERROR) end @@ -851,45 +896,40 @@ M.handlers.signs = { local ns = M.get_namespace(namespace) if not ns.user_data.sign_group then - ns.user_data.sign_group = string.format("vim.diagnostic.%s", ns.name) + ns.user_data.sign_group = string.format('vim.diagnostic.%s', ns.name) end local sign_group = ns.user_data.sign_group for _, diagnostic in ipairs(diagnostics) do - vim.fn.sign_place( - 0, - sign_group, - sign_highlight_map[diagnostic.severity], - bufnr, - { - priority = get_priority(diagnostic.severity), - lnum = diagnostic.lnum + 1 - } - ) + vim.fn.sign_place(0, sign_group, sign_highlight_map[diagnostic.severity], bufnr, { + priority = get_priority(diagnostic.severity), + lnum = diagnostic.lnum + 1, + }) end end, hide = function(namespace, bufnr) local ns = M.get_namespace(namespace) if ns.user_data.sign_group then - vim.fn.sign_unplace(ns.user_data.sign_group, {buffer=bufnr}) + vim.fn.sign_unplace(ns.user_data.sign_group, { buffer = bufnr }) end end, } M.handlers.underline = { show = function(namespace, bufnr, diagnostics, opts) - vim.validate { - namespace = {namespace, 'n'}, - bufnr = {bufnr, 'n'}, + vim.validate({ + namespace = { namespace, 'n' }, + bufnr = { bufnr, 'n' }, diagnostics = { diagnostics, vim.tbl_islist, - "a list of diagnostics", + 'a list of diagnostics', }, - opts = {opts, 't', true}, - } + 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) @@ -897,7 +937,7 @@ M.handlers.underline = { local ns = M.get_namespace(namespace) if not ns.user_data.underline_ns then - ns.user_data.underline_ns = vim.api.nvim_create_namespace("") + ns.user_data.underline_ns = vim.api.nvim_create_namespace('') end local underline_ns = ns.user_data.underline_ns @@ -914,7 +954,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) @@ -925,23 +966,24 @@ M.handlers.underline = { diagnostic_cache_extmarks[bufnr][ns.user_data.underline_ns] = {} vim.api.nvim_buf_clear_namespace(bufnr, ns.user_data.underline_ns, 0, -1) end - end + end, } M.handlers.virtual_text = { show = function(namespace, bufnr, diagnostics, opts) - vim.validate { - namespace = {namespace, 'n'}, - bufnr = {bufnr, 'n'}, + vim.validate({ + namespace = { namespace, 'n' }, + bufnr = { bufnr, 'n' }, diagnostics = { diagnostics, vim.tbl_islist, - "a list of diagnostics", + 'a list of diagnostics', }, - opts = {opts, 't', true}, - } + opts = { opts, 't', true }, + }) bufnr = get_bufnr(bufnr) + opts = opts or {} local severity if opts.virtual_text then @@ -950,7 +992,7 @@ M.handlers.virtual_text = { end if opts.virtual_text.source - and (opts.virtual_text.source ~= "if_many" or count_sources(bufnr) > 1) + and (opts.virtual_text.source ~= 'if_many' or count_sources(bufnr) > 1) then diagnostics = prefix_source(diagnostics) end @@ -961,7 +1003,7 @@ M.handlers.virtual_text = { local ns = M.get_namespace(namespace) if not ns.user_data.virt_text_ns then - ns.user_data.virt_text_ns = vim.api.nvim_create_namespace("") + ns.user_data.virt_text_ns = vim.api.nvim_create_namespace('') end local virt_text_ns = ns.user_data.virt_text_ns @@ -974,7 +1016,7 @@ M.handlers.virtual_text = { if virt_texts then vim.api.nvim_buf_set_extmark(bufnr, virt_text_ns, line, 0, { - hl_mode = "combine", + hl_mode = 'combine', virt_text = virt_texts, }) end @@ -1002,52 +1044,29 @@ function M._get_virt_text_chunks(line_diags, opts) end opts = opts or {} - local prefix = opts.prefix or "■" + local prefix = opts.prefix or '■' local spacing = opts.spacing or 4 -- Create a little more space between virtual text and contents - local virt_texts = {{string.rep(" ", spacing)}} + local virt_texts = { { string.rep(' ', spacing) } } for i = 1, #line_diags - 1 do - table.insert(virt_texts, {prefix, virtual_text_highlight_map[line_diags[i].severity]}) + table.insert(virt_texts, { prefix, virtual_text_highlight_map[line_diags[i].severity] }) end local last = line_diags[#line_diags] -- TODO(tjdevries): Allow different servers to be shown first somehow? -- TODO(tjdevries): Display server name associated with these? if last.message then - table.insert( - virt_texts, - { - string.format("%s %s", prefix, last.message:gsub("\r", ""):gsub("\n", " ")), - virtual_text_highlight_map[last.severity] - } - ) + table.insert(virt_texts, { + string.format('%s %s', prefix, last.message:gsub('\r', ''):gsub('\n', ' ')), + virtual_text_highlight_map[last.severity], + }) return virt_texts end end ---- Callback scheduled when leaving Insert mode. ---- ---- This function must be exported publicly so that it is available to be ---- called from the Vimscript autocommand. ---- ---- See @ref schedule_display() ---- ----@private -function M._execute_scheduled_display(namespace, bufnr) - local args = bufs_waiting_to_update[bufnr][namespace] - if not args then - return - end - - -- Clear the args so we don't display unnecessarily. - bufs_waiting_to_update[bufnr][namespace] = nil - - M.show(namespace, bufnr, nil, args) -end - --- Hide currently displayed diagnostics. --- --- This only clears the decorations displayed in the buffer. Diagnostics can @@ -1062,14 +1081,14 @@ end ---@param bufnr number|nil Buffer number, or 0 for current buffer. When --- omitted, hide diagnostics in all buffers. function M.hide(namespace, bufnr) - vim.validate { + vim.validate({ namespace = { namespace, 'n', true }, bufnr = { bufnr, 'n', true }, - } + }) - local buffers = bufnr and {get_bufnr(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]) + local namespaces = namespace and { namespace } or vim.tbl_keys(diagnostic_cache[iter_bufnr]) for _, iter_namespace in ipairs(namespaces) do for _, handler in pairs(M.handlers) do if handler.hide then @@ -1094,19 +1113,21 @@ end --- or {bufnr} is nil. ---@param opts table|nil Display options. See |vim.diagnostic.config()|. function M.show(namespace, bufnr, diagnostics, opts) - vim.validate { + vim.validate({ namespace = { namespace, 'n', true }, bufnr = { bufnr, 'n', true }, diagnostics = { diagnostics, - function(v) return v == nil or vim.tbl_islist(v) end, - "a list of diagnostics", + function(v) + return v == nil or vim.tbl_islist(v) + end, + 'a list of diagnostics', }, opts = { opts, 't', true }, - } + }) if not bufnr or not namespace then - assert(not diagnostics, "Cannot show diagnostics without a buffer and namespace") + assert(not diagnostics, 'Cannot show diagnostics without a buffer and namespace') if not bufnr then for iter_bufnr in pairs(diagnostic_cache) do M.show(namespace, iter_bufnr, nil, opts) @@ -1127,7 +1148,7 @@ function M.show(namespace, bufnr, diagnostics, opts) M.hide(namespace, bufnr) - diagnostics = diagnostics or get_diagnostics(bufnr, {namespace=namespace}, true) + diagnostics = diagnostics or get_diagnostics(bufnr, { namespace = namespace }, true) if not diagnostics or vim.tbl_isempty(diagnostics) then return @@ -1146,10 +1167,14 @@ function M.show(namespace, bufnr, diagnostics, opts) end if vim.F.if_nil(opts.severity_sort, false) then - if type(opts.severity_sort) == "table" and opts.severity_sort.reverse then - table.sort(diagnostics, function(a, b) return a.severity < b.severity end) + if type(opts.severity_sort) == 'table' and opts.severity_sort.reverse then + table.sort(diagnostics, function(a, b) + return a.severity < b.severity + end) else - table.sort(diagnostics, function(a, b) return a.severity > b.severity end) + table.sort(diagnostics, function(a, b) + return a.severity > b.severity + end) end end @@ -1205,53 +1230,54 @@ end function M.open_float(opts, ...) -- Support old (bufnr, opts) signature local bufnr - if opts == nil or type(opts) == "number" then + if opts == nil or type(opts) == 'number' then bufnr = opts opts = ... else - vim.validate { + vim.validate({ opts = { opts, 't', true }, - } + }) end opts = opts or {} bufnr = get_bufnr(bufnr or opts.bufnr) - local scope = ({l = "line", c = "cursor", b = "buffer"})[opts.scope] or opts.scope or "line" + + 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 scope == 'line' or scope == 'cursor' then if not opts.pos then local pos = vim.api.nvim_win_get_cursor(0) lnum = pos[1] - 1 col = pos[2] - elseif type(opts.pos) == "number" then + elseif type(opts.pos) == 'number' then lnum = opts.pos - elseif type(opts.pos) == "table" then + elseif type(opts.pos) == 'table' then lnum, col = unpack(opts.pos) else error("Invalid value for option 'pos'") end - elseif scope ~= "buffer" then + elseif scope ~= 'buffer' then 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 = get_diagnostics(bufnr, opts, true) - if scope == "line" then + if scope == 'line' then diagnostics = vim.tbl_filter(function(d) return d.lnum == lnum end, diagnostics) - elseif scope == "cursor" then + elseif scope == 'cursor' then -- LSP servers can send diagnostics with `end_col` past the length of the line local line_length = #vim.api.nvim_buf_get_lines(bufnr, lnum, lnum + 1, true)[1] diagnostics = vim.tbl_filter(function(d) @@ -1267,29 +1293,39 @@ function M.open_float(opts, ...) local severity_sort = vim.F.if_nil(opts.severity_sort, global_diagnostic_options.severity_sort) if severity_sort then - if type(severity_sort) == "table" and severity_sort.reverse then - table.sort(diagnostics, function(a, b) return a.severity > b.severity end) + if type(severity_sort) == 'table' and severity_sort.reverse then + table.sort(diagnostics, function(a, b) + return a.severity > b.severity + end) else - table.sort(diagnostics, function(a, b) return a.severity < b.severity end) + table.sort(diagnostics, function(a, b) + return a.severity < b.severity + end) end end local lines = {} local highlights = {} - local header = if_nil(opts.header, "Diagnostics:") + local header = if_nil(opts.header, 'Diagnostics:') if header then - vim.validate { header = { header, function(v) - return type(v) == "string" or type(v) == "table" - end, "'string' or 'table'" } } - if type(header) == "table" then + vim.validate({ + header = { + header, + function(v) + return type(v) == 'string' or type(v) == 'table' + end, + "'string' or 'table'", + }, + }) + if type(header) == 'table' then -- Don't insert any lines for an empty string - if string.len(if_nil(header[1], "")) > 0 then + if string.len(if_nil(header[1], '')) > 0 then table.insert(lines, header[1]) - table.insert(highlights, {0, header[2] or "Bold"}) + table.insert(highlights, { 0, header[2] or 'Bold' }) end elseif #header > 0 then table.insert(lines, header) - table.insert(highlights, {0, "Bold"}) + table.insert(highlights, { 0, 'Bold' }) end end @@ -1297,38 +1333,45 @@ function M.open_float(opts, ...) diagnostics = reformat_diagnostics(opts.format, diagnostics) end - if opts.source and (opts.source ~= "if_many" or count_sources(bufnr) > 1) then + 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) - return string.format("%d. ", i) - end) + local prefix_opt = + if_nil(opts.prefix, (scope == 'cursor' and #diagnostics <= 1) and '' or function(_, i) + return string.format('%d. ', i) + end) local prefix, prefix_hl_group if prefix_opt then - vim.validate { prefix = { prefix_opt, function(v) - return type(v) == "string" or type(v) == "table" or type(v) == "function" - end, "'string' or 'table' or 'function'" } } - if type(prefix_opt) == "string" then - prefix, prefix_hl_group = prefix_opt, "NormalFloat" - elseif type(prefix_opt) == "table" then - prefix, prefix_hl_group = prefix_opt[1] or "", prefix_opt[2] or "NormalFloat" + vim.validate({ + prefix = { + prefix_opt, + function(v) + return type(v) == 'string' or type(v) == 'table' or type(v) == 'function' + end, + "'string' or 'table' or 'function'", + }, + }) + if type(prefix_opt) == 'string' then + prefix, prefix_hl_group = prefix_opt, 'NormalFloat' + elseif type(prefix_opt) == 'table' then + prefix, prefix_hl_group = prefix_opt[1] or '', prefix_opt[2] or 'NormalFloat' end end for i, diagnostic in ipairs(diagnostics) do - if prefix_opt and type(prefix_opt) == "function" then + if prefix_opt and type(prefix_opt) == 'function' then prefix, prefix_hl_group = prefix_opt(diagnostic, i, #diagnostics) - prefix, prefix_hl_group = prefix or "", prefix_hl_group or "NormalFloat" + prefix, prefix_hl_group = prefix or '', prefix_hl_group or 'NormalFloat' end local hiname = floating_highlight_map[diagnostic.severity] local message_lines = vim.split(diagnostic.message, '\n') - table.insert(lines, prefix..message_lines[1]) - table.insert(highlights, {#prefix, hiname, prefix_hl_group}) + table.insert(lines, prefix .. message_lines[1]) + table.insert(highlights, { #prefix, hiname, prefix_hl_group }) for j = 2, #message_lines do table.insert(lines, string.rep(' ', #prefix) .. message_lines[j]) - table.insert(highlights, {0, hiname}) + table.insert(highlights, { 0, hiname }) end end @@ -1340,9 +1383,9 @@ function M.open_float(opts, ...) for i, hi in ipairs(highlights) do local prefixlen, hiname, prefix_hiname = unpack(hi) if prefix_hiname then - vim.api.nvim_buf_add_highlight(float_bufnr, -1, prefix_hiname, i-1, 0, prefixlen) + vim.api.nvim_buf_add_highlight(float_bufnr, -1, prefix_hiname, i - 1, 0, prefixlen) end - vim.api.nvim_buf_add_highlight(float_bufnr, -1, hiname, i-1, prefixlen, -1) + vim.api.nvim_buf_add_highlight(float_bufnr, -1, hiname, i - 1, prefixlen, -1) end return float_bufnr, winnr @@ -1360,27 +1403,23 @@ end ---@param bufnr number|nil Remove diagnostics for the given buffer. When omitted, --- diagnostics are removed for all buffers. function M.reset(namespace, bufnr) - vim.validate { - namespace = {namespace, 'n', true}, - bufnr = {bufnr, 'n', true}, - } + vim.validate({ + namespace = { namespace, 'n', true }, + bufnr = { bufnr, 'n', true }, + }) - local buffers = bufnr and {get_bufnr(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]) + local namespaces = namespace and { namespace } or vim.tbl_keys(diagnostic_cache[iter_bufnr]) for _, iter_namespace in ipairs(namespaces) do diagnostic_cache[iter_bufnr][iter_namespace] = nil M.hide(iter_namespace, iter_bufnr) end - 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) + vim.api.nvim_exec_autocmds('DiagnosticChanged', { + modeline = false, + buffer = iter_bufnr, + }) end end @@ -1413,7 +1452,7 @@ end --- omitted, disable diagnostics in all buffers. ---@param namespace number|nil Only disable diagnostics for the given namespace. function M.disable(bufnr, namespace) - vim.validate { bufnr = {bufnr, 'n', true}, namespace = {namespace, 'n', true} } + vim.validate({ bufnr = { bufnr, 'n', true }, namespace = { namespace, 'n', true } }) if bufnr == nil then if namespace == nil then -- Disable everything (including as yet non-existing buffers and @@ -1421,7 +1460,9 @@ function M.disable(bufnr, namespace) -- its metatable to always return true. This metatable is removed -- in enable() diagnostic_disabled = setmetatable({}, { - __index = function() return true end, + __index = function() + return true + end, }) else local ns = M.get_namespace(namespace) @@ -1432,7 +1473,7 @@ function M.disable(bufnr, namespace) if namespace == nil then diagnostic_disabled[bufnr] = true else - if type(diagnostic_disabled[bufnr]) ~= "table" then + if type(diagnostic_disabled[bufnr]) ~= 'table' then diagnostic_disabled[bufnr] = {} end diagnostic_disabled[bufnr][namespace] = true @@ -1448,7 +1489,7 @@ end --- omitted, enable diagnostics in all buffers. ---@param namespace number|nil Only enable diagnostics for the given namespace. function M.enable(bufnr, namespace) - vim.validate { bufnr = {bufnr, 'n', true}, namespace = {namespace, 'n', true} } + vim.validate({ bufnr = { bufnr, 'n', true }, namespace = { namespace, 'n', true } }) if bufnr == nil then if namespace == nil then -- Enable everything by setting diagnostic_disabled to an empty table @@ -1462,7 +1503,7 @@ function M.enable(bufnr, namespace) if namespace == nil then diagnostic_disabled[bufnr] = nil else - if type(diagnostic_disabled[bufnr]) ~= "table" then + if type(diagnostic_disabled[bufnr]) ~= 'table' then return end diagnostic_disabled[bufnr][namespace] = nil @@ -1499,33 +1540,33 @@ end --- ERROR. ---@return diagnostic |diagnostic-structure| or `nil` if {pat} fails to match {str}. function M.match(str, pat, groups, severity_map, defaults) - vim.validate { + vim.validate({ str = { str, 's' }, pat = { pat, 's' }, groups = { groups, 't' }, severity_map = { severity_map, 't', true }, defaults = { defaults, 't', true }, - } + }) severity_map = severity_map or M.severity local diagnostic = {} - local matches = {string.match(str, pat)} + local matches = { string.match(str, pat) } if vim.tbl_isempty(matches) then return end for i, match in ipairs(matches) do local field = groups[i] - if field == "severity" then + if field == 'severity' then match = severity_map[match] - elseif field == "lnum" or field == "end_lnum" or field == "col" or field == "end_col" then + elseif field == 'lnum' or field == 'end_lnum' or field == 'col' or field == 'end_col' then match = assert(tonumber(match)) - 1 end diagnostic[field] = match end - diagnostic = vim.tbl_extend("keep", diagnostic, defaults or {}) + diagnostic = vim.tbl_extend('keep', diagnostic, defaults or {}) diagnostic.severity = diagnostic.severity or M.severity.ERROR diagnostic.col = diagnostic.col or 0 diagnostic.end_lnum = diagnostic.end_lnum or diagnostic.lnum @@ -1546,13 +1587,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 { + vim.validate({ diagnostics = { diagnostics, vim.tbl_islist, - "a list of diagnostics", + 'a list of diagnostics', }, - } + }) local list = {} for _, v in ipairs(diagnostics) do @@ -1583,13 +1624,13 @@ end --- |getloclist()|. ---@return array of diagnostics |diagnostic-structure| function M.fromqflist(list) - vim.validate { + vim.validate({ list = { list, vim.tbl_islist, - "a list of quickfix items", + 'a list of quickfix items', }, - } + }) local diagnostics = {} for _, item in ipairs(list) do @@ -1598,7 +1639,7 @@ function M.fromqflist(list) local col = math.max(0, item.col - 1) local end_lnum = item.end_lnum > 0 and (item.end_lnum - 1) or lnum local end_col = item.end_col > 0 and (item.end_col - 1) or col - local severity = item.type ~= "" and M.severity[item.type] or M.severity.ERROR + local severity = item.type ~= '' and M.severity[item.type] or M.severity.ERROR table.insert(diagnostics, { bufnr = item.bufnr, lnum = lnum, diff --git a/runtime/lua/vim/filetype.lua b/runtime/lua/vim/filetype.lua index 3d91abc406..70c8cd15eb 100644 --- a/runtime/lua/vim/filetype.lua +++ b/runtime/lua/vim/filetype.lua @@ -3,1361 +3,2187 @@ 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 function starsetf(ft, opts) + return { + function(path, bufnr) + local f = type(ft) == 'function' and ft(path, bufnr) or ft + if not vim.g.ft_ignore_pat then + return f + end - local re = vim.regex(vim.g.fg_ignore_pat) - if re:match_str(path) then - return ft + local re = vim.regex(vim.g.ft_ignore_pat) + if not re:match_str(path) then + return f + end + end, + { + -- Starset matches should have lowest priority by default + priority = (opts and opts.priority) or -math.huge, + }, + } +end + +---@private +--- Get a single line or line range from the buffer. +--- If only start_lnum is specified, return a single line as a string. +--- If both start_lnum and end_lnum are omitted, return all lines from the buffer. +--- +---@param bufnr number|nil The buffer to get the lines from +---@param start_lnum number|nil The line number of the first line (inclusive, 1-based) +---@param end_lnum number|nil The line number of the last line (inclusive, 1-based) +---@return table<string>|string Array of lines, or string when end_lnum is omitted +function M.getlines(bufnr, start_lnum, end_lnum) + if end_lnum then + -- Return a line range + return api.nvim_buf_get_lines(bufnr, start_lnum - 1, end_lnum, false) + end + if start_lnum then + -- Return a single line + return api.nvim_buf_get_lines(bufnr, start_lnum - 1, start_lnum, false)[1] or '' + else + -- Return all lines + return api.nvim_buf_get_lines(bufnr, 0, -1, false) + end +end + +---@private +--- Check whether a string matches any of the given Lua patterns. +--- +---@param s string The string to check +---@param patterns table<string> A list of Lua patterns +---@return boolean `true` if s matched a pattern, else `false` +function M.findany(s, patterns) + if s == nil then + return false + end + for _, v in ipairs(patterns) do + if s:find(v) then + return true end - end, { - -- Starset matches should always have lowest priority - priority = -math.huge, - }} + end + return false end ---@private -local function getline(bufnr, lnum) - return api.nvim_buf_get_lines(bufnr, lnum-1, lnum, false)[1] +--- Get the next non-whitespace line in the buffer. +--- +---@param bufnr number The buffer to get the line from +---@param start_lnum number The line number of the first line to start from (inclusive, 1-based) +---@return string|nil The first non-blank line if found or `nil` otherwise +function M.nextnonblank(bufnr, start_lnum) + for _, line in ipairs(M.getlines(bufnr, start_lnum, -1)) do + if not line:find('^%s*$') then + return line + end + end + return nil end --- Filetypes based on file extension +---@private +--- Check whether the given string matches the Vim regex pattern. +M.matchregex = (function() + local cache = {} + return function(s, pattern) + if s == nil then + return nil + end + if not cache[pattern] then + cache[pattern] = vim.regex(pattern) + end + return cache[pattern]:match_str(s) + end +end)() + -- luacheck: push no unused args +-- luacheck: push ignore 122 + +-- Filetypes based on file extension +---@diagnostic disable: unused-local 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", - 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", - cfg = "cfg", - 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", - 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() - if vim.g.cynlib_syntax_for_cc then - return "cynlib" + ['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', + asa = function(path, bufnr) + if vim.g.filetype_asa then + return vim.g.filetype_asa 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", - sys = "dosbatch", - 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", - 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", - bi = "freebasic", - fb = "freebasic", - fsi = "fsharp", - fsx = "fsharp", - gdmo = "gdmo", - mo = "gdmo", - ged = "gedcom", - gmi = "gemtext", - gemini = "gemtext", - gift = "gift", - gpi = "gnuplot", - go = "go", - gp = "gp", - gs = "grads", - gretl = "gretl", - gradle = "groovy", - groovy = "groovy", - gsp = "gsp", - haml = "haml", - hsm = "hamster", - ["hs-boot"] = "haskell", - hsig = "haskell", - hsc = "haskell", - hs = "haskell", - ht = "haste", - htpp = "hastepreproc", - hb = "hb", - sum = "hercules", - errsum = "hercules", - ev = "hercules", - vc = "hercules", - hex = "hex", - ["h32"] = "hex", - 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", - 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", - 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", - MOD = "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", - 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", - pxsl = "papp", - papp = "papp", - pxml = "papp", - pas = "pascal", - lpr = "pascal", - dpr = "pascal", - pbtxt = "pbtxt", - g = "pccts", - pcmk = "pcmk", - pdf = "pdf", - plx = "perl", - psgi = "perl", - al = "perl", - ctp = "php", - php = "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", - arr = "pyret", - pxd = "pyrex", - pyx = "pyrex", - pyw = "python", - py = "python", - pyi = "python", - ptl = "python", - 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", - 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", - sc = "scala", - scd = "scdoc", - 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", - 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", - 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", - tmpl = "template", - ti = "terminfo", - dtx = "tex", - ltx = "tex", - bbl = "tex", - latex = "tex", - sty = "tex", - texi = "texinfo", - txi = "texinfo", - texinfo = "texinfo", - text = "text", - tf = "tf", - 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" + return 'aspvbs' + end, + asm = function(path, bufnr) + return require('vim.filetype.detect').asm(bufnr) + end, + lst = function(path, bufnr) + return require('vim.filetype.detect').asm(bufnr) + end, + mac = function(path, bufnr) + return require('vim.filetype.detect').asm(bufnr) + end, + ['asn1'] = 'asn', + asn = 'asn', + asp = function(path, bufnr) + return require('vim.filetype.detect').asp(bufnr) + end, + atl = 'atlas', + as = 'atlas', + ahk = 'autohotkey', + ['au3'] = 'autoit', + ave = 'ave', + gawk = 'awk', + awk = 'awk', + ref = 'b', + imp = 'b', + mch = 'b', + bas = function(path, bufnr) + return require('vim.filetype.detect').bas(bufnr) + end, + bi = function(path, bufnr) + return require('vim.filetype.detect').bas(bufnr) + end, + bm = function(path, bufnr) + return require('vim.filetype.detect').bas(bufnr) + end, + bc = 'bc', + bdf = 'bdf', + beancount = 'beancount', + bib = 'bib', + com = function(path, bufnr) + return require('vim.filetype.detect').bindzone(bufnr, 'dcl') + end, + db = function(path, bufnr) + return require('vim.filetype.detect').bindzone(bufnr, '') + end, + bicep = 'bicep', + bb = 'bitbake', + bbappend = 'bitbake', + bbclass = 'bitbake', + bl = 'blank', + bsdl = 'bsdl', + bst = 'bst', + btm = function(path, bufnr) + return (vim.g.dosbatch_syntax_for_btm and vim.g.dosbatch_syntax_for_btm ~= 0) and 'dosbatch' + or 'btm' + end, + 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', + ch = function(path, bufnr) + return require('vim.filetype.detect').change(bufnr) + end, + chs = 'chaskell', + chopro = 'chordpro', + crd = 'chordpro', + crdpro = 'chordpro', + cho = 'chordpro', + chordpro = 'chordpro', + eni = 'cl', + 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', + hook = function(path, bufnr) + return M.getlines(bufnr, 1) == '[Trigger]' and 'conf' + end, + mklx = 'context', + mkiv = 'context', + mkii = 'context', + mkxl = 'context', + mkvi = 'context', + control = function(path, bufnr) + return require('vim.filetype.detect').control(bufnr) + end, + copyright = function(path, bufnr) + return require('vim.filetype.detect').copyright(bufnr) + end, + csh = function(path, bufnr) + return require('vim.filetype.detect').csh(path, bufnr) + end, + 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) + return vim.g.cynlib_syntax_for_cpp and 'cynlib' or 'cpp' + end, + cc = function(path, bufnr) + return vim.g.cynlib_syntax_for_cc and 'cynlib' or 'cpp' + end, + crm = 'crm', + csx = 'cs', + cs = 'cs', + csc = 'csc', + csdl = 'csdl', + cshtml = 'html', + fdr = 'csp', + csp = 'csp', + css = 'css', + csv = 'csv', + con = 'cterm', + feature = 'cucumber', + cuh = 'cuda', + cu = 'cuda', + pld = 'cupl', + si = 'cuplsim', + cyn = 'cynpp', + dart = 'dart', + drt = 'dart', + ds = 'datascript', + dcd = 'dcd', + decl = function(path, bufnr) + return require('vim.filetype.detect').decl(bufnr) + end, + dec = function(path, bufnr) + return require('vim.filetype.detect').decl(bufnr) + end, + dcl = function(path, bufnr) + return require('vim.filetype.detect').decl(bufnr) or 'clean' + end, + def = 'def', + desc = 'desc', + directory = 'desktop', + desktop = 'desktop', + diff = 'diff', + rej = 'diff', + Dockerfile = 'dockerfile', + dockerfile = 'dockerfile', + bat = 'dosbatch', + wrap = 'dosini', + ini = 'dosini', + dot = 'dot', + gv = 'dot', + drac = 'dracula', + drc = 'dracula', + dtd = 'dtd', + d = function(path, bufnr) + return require('vim.filetype.detect').dtrace(bufnr) + end, + dts = 'dts', + dtsi = 'dts', + dylan = 'dylan', + intr = 'dylanintr', + lid = 'dylanlid', + e = function(path, bufnr) + return require('vim.filetype.detect').e(bufnr) + end, + E = function(path, bufnr) + return require('vim.filetype.detect').e(bufnr) + end, + ecd = 'ecd', + edf = 'edif', + edif = 'edif', + edo = 'edif', + edn = function(path, bufnr) + return require('vim.filetype.detect').edn(bufnr) + end, + eex = 'eelixir', + leex = 'eelixir', + am = function(path, bufnr) + if not path:lower():find('makefile%.am$') then + return 'elf' 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", - ["z8a"] = "z8a", - zig = "zig", - zu = "zimbu", - zut = "zimbutempl", - zsh = "zsh", - 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#FTVB"]("basic") 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#FTVB"]("form") 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, - 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, - 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, - 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, + exs = 'elixir', + elm = 'elm', + elv = 'elvish', + ent = function(path, bufnr) + return require('vim.filetype.detect').ent(bufnr) + end, + epp = 'epuppet', + erl = 'erlang', + hrl = 'erlang', + yaws = 'erlang', + erb = 'eruby', + rhtml = 'eruby', + ec = 'esqlc', + EC = 'esqlc', + strl = 'esterel', + eu = function(path, bufnr) + return vim.g.filetype_euphoria or 'euphoria3' + end, + EU = function(path, bufnr) + return vim.g.filetype_euphoria or 'euphoria3' + end, + ew = function(path, bufnr) + return vim.g.filetype_euphoria or 'euphoria3' + end, + EW = function(path, bufnr) + return vim.g.filetype_euphoria or 'euphoria3' + end, + EX = function(path, bufnr) + return vim.g.filetype_euphoria or 'euphoria3' + end, + exu = function(path, bufnr) + return vim.g.filetype_euphoria or 'euphoria3' + end, + EXU = function(path, bufnr) + return vim.g.filetype_euphoria or 'euphoria3' + end, + exw = function(path, bufnr) + return vim.g.filetype_euphoria or 'euphoria3' + end, + EXW = function(path, bufnr) + return vim.g.filetype_euphoria or 'euphoria3' + end, + ex = function(path, bufnr) + return require('vim.filetype.detect').ex(bufnr) + end, + 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', + frm = function(path, bufnr) + return require('vim.filetype.detect').frm(bufnr) + end, + fb = 'freebasic', + fs = function(path, bufnr) + return require('vim.filetype.detect').fs(bufnr) + end, + 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', + gleam = 'gleam', + glsl = 'glsl', + gpi = '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', + ha = 'hare', + ['hs-boot'] = 'haskell', + hsig = 'haskell', + hsc = 'haskell', + hs = 'haskell', + ht = 'haste', + htpp = 'hastepreproc', + hb = 'hb', + h = function(path, bufnr) + return require('vim.filetype.detect').header(bufnr) + end, + sum = 'hercules', + errsum = 'hercules', + ev = 'hercules', + vc = 'hercules', + hcl = 'hcl', + heex = 'heex', + hex = 'hex', + ['h32'] = 'hex', + hjson = 'hjson', + hog = 'hog', + hws = 'hollywood', + hoon = 'hoon', + cpt = function(path, bufnr) + return require('vim.filetype.detect').html(bufnr) + end, + dtml = function(path, bufnr) + return require('vim.filetype.detect').html(bufnr) + end, + htm = function(path, bufnr) + return require('vim.filetype.detect').html(bufnr) + end, + html = function(path, bufnr) + return require('vim.filetype.detect').html(bufnr) + end, + pt = function(path, bufnr) + return require('vim.filetype.detect').html(bufnr) + end, + shtml = function(path, bufnr) + return require('vim.filetype.detect').html(bufnr) + end, + stm = function(path, bufnr) + return require('vim.filetype.detect').html(bufnr) + end, + htt = 'httest', + htb = 'httest', + hw = function(path, bufnr) + return require('vim.filetype.detect').hw(bufnr) + end, + module = function(path, bufnr) + return require('vim.filetype.detect').hw(bufnr) + end, + pkg = function(path, bufnr) + return require('vim.filetype.detect').hw(bufnr) + end, + iba = 'ibasic', + ibi = 'ibasic', + icn = 'icon', + idl = function(path, bufnr) + return require('vim.filetype.detect').idl(bufnr) + end, + inc = function(path, bufnr) + return require('vim.filetype.detect').inc(bufnr) + end, + inf = 'inform', + INF = 'inform', + ii = 'initng', + inp = function(path, bufnr) + return require('vim.filetype.detect').inp(bufnr) + end, + ms = function(path, bufnr) + return require('vim.filetype.detect').nroff(bufnr) or 'xmath' + end, + 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', + ly = 'lilypond', + ily = 'lilypond', + 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', + c = function(path, bufnr) + return require('vim.filetype.detect').lpc(bufnr) + end, + lsl = 'lsl', + lss = 'lss', + nse = 'lua', + rockspec = 'lua', + lua = 'lua', + m = function(path, bufnr) + return require('vim.filetype.detect').m(bufnr) + end, + at = 'm4', + mc = function(path, bufnr) + return require('vim.filetype.detect').mc(bufnr) + end, + quake = 'm3quake', + ['m4'] = function(path, bufnr) + path = path:lower() + return not (path:find('html%.m4$') or path:find('fvwm2rc')) and 'm4' + end, + 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', + demo = 'maxima', + dm1 = 'maxima', + dm2 = 'maxima', + dm3 = 'maxima', + dmt = 'maxima', + wxm = 'maxima', + mel = 'mel', + mf = 'mf', + mgl = 'mgl', + mgp = 'mgp', + my = 'mib', + mib = 'mib', + mix = 'mix', + mixal = 'mix', + mm = function(path, bufnr) + return require('vim.filetype.detect').mm(bufnr) + end, + nb = 'mma', + mmp = 'mmp', + mms = function(path, bufnr) + return require('vim.filetype.detect').mms(bufnr) + end, + DEF = 'modula2', + ['m2'] = 'modula2', + mi = 'modula2', + ssc = 'monk', + monk = 'monk', + tsc = 'monk', + isc = 'monk', + moo = 'moo', + moon = 'moonscript', + 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', + scad = 'openscad', + 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', + PL = function(path, bufnr) + return require('vim.filetype.detect').pl(bufnr) + end, + 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', + R = function(path, bufnr) + return require('vim.filetype.detect').r(bufnr) + end, + 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', + robot = 'robot', + resource = 'robot', + 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', + bash = function(path, bufnr) + return require('vim.filetype.detect').sh(path, M.getlines(bufnr), 'bash') + end, + ebuild = function(path, bufnr) + return require('vim.filetype.detect').sh(path, M.getlines(bufnr), 'bash') + end, + eclass = function(path, bufnr) + return require('vim.filetype.detect').sh(path, M.getlines(bufnr), 'bash') + end, + env = function(path, bufnr) + return require('vim.filetype.detect').sh(path, M.getlines(bufnr)) + end, + ksh = function(path, bufnr) + return require('vim.filetype.detect').sh(path, M.getlines(bufnr), 'ksh') + end, + sh = function(path, bufnr) + return require('vim.filetype.detect').sh(path, M.getlines(bufnr)) + end, + sieve = 'sieve', + siv = 'sieve', + sig = function(path, bufnr) + return require('vim.filetype.detect').sig(bufnr) + end, + 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', + cls = function(path, bufnr) + return require('vim.filetype.detect').cls(bufnr) + end, + 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', + tsv = 'tsv', + tutor = 'tutor', + twig = 'twig', + ts = function(path, bufnr) + return M.getlines(bufnr, 1):find('<%?xml') and 'xml' or 'typescript' + 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', + xpm = function(path, bufnr) + return M.getlines(bufnr, 1):find('XPM2') and 'xpm2' or 'xpm' + end, + ['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', + web = function(path, bufnr) + return require('vim.filetype.detect').web(bufnr) + end, + pl = function(path, bufnr) + return require('vim.filetype.detect').pl(bufnr) + end, + pp = function(path, bufnr) + return require('vim.filetype.detect').pp(bufnr) + end, + i = function(path, bufnr) + return require('vim.filetype.detect').progress_asm(bufnr) + end, + w = function(path, bufnr) + return require('vim.filetype.detect').progress_cweb(bufnr) + end, + p = function(path, bufnr) + return require('vim.filetype.detect').progress_pascal(bufnr) + end, + pro = function(path, bufnr) + return require('vim.filetype.detect').proto(bufnr, 'idlang') + end, + patch = function(path, bufnr) + return require('vim.filetype.detect').patch(bufnr) + end, + r = function(path, bufnr) + return require('vim.filetype.detect').r(bufnr) + end, + rdf = function(path, bufnr) + return require('vim.filetype.detect').redif(bufnr) + end, + rules = function(path, bufnr) + return require('vim.filetype.detect').rules(path) + end, + sc = function(path, bufnr) + return require('vim.filetype.detect').sc(bufnr) + end, + scd = function(path, bufnr) + return require('vim.filetype.detect').scd(bufnr) + end, + tcsh = function(path, bufnr) + return require('vim.filetype.detect').shell(path, M.getlines(bufnr), 'tcsh') + end, + sql = function(path, bufnr) + return vim.g.filetype_sql and vim.g.filetype_sql or 'sql' + end, + zsql = function(path, bufnr) + return vim.g.filetype_sql and vim.g.filetype_sql or 'sql' + end, + tex = function(path, bufnr) + return require('vim.filetype.detect').tex(path, bufnr) + end, + tf = function(path, bufnr) + return require('vim.filetype.detect').tf(bufnr) + 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" + return require('vim.filetype.detect').txt(bufnr) + end, + xml = function(path, bufnr) + return require('vim.filetype.detect').xml(bufnr) + end, + y = function(path, bufnr) + return require('vim.filetype.detect').y(bufnr) + end, + cmd = function(path, bufnr) + return M.getlines(bufnr, 1):find('^/%*') and 'rexx' or 'dosbatch' + end, + rul = function(path, bufnr) + return require('vim.filetype.detect').rul(bufnr) + end, + cpy = function(path, bufnr) + return M.getlines(bufnr, 1):find('^##') and 'python' or 'cobol' + end, + dsl = function(path, bufnr) + return M.getlines(bufnr, 1):find('^%s*<!') and 'dsl' or 'structurizr' + end, + smil = function(path, bufnr) + return M.getlines(bufnr, 1):find('<%?%s*xml.*%?>') and 'xml' or 'smil' + end, + smi = function(path, bufnr) + return require('vim.filetype.detect').smi(bufnr) + end, + install = function(path, bufnr) + return require('vim.filetype.detect').install(path, bufnr) + end, + pm = function(path, bufnr) + return require('vim.filetype.detect').pm(bufnr) + end, + me = function(path, bufnr) + return require('vim.filetype.detect').me(path) + end, + reg = function(path, bufnr) + return require('vim.filetype.detect').reg(bufnr) + end, + ttl = function(path, bufnr) + return require('vim.filetype.detect').ttl(bufnr) + end, + rc = function(path, bufnr) + if not path:find('/etc/Muttrc%.d/') then + return 'rc' + end + end, + rch = function(path, bufnr) + if not path:find('/etc/Muttrc%.d/') then + return 'rc' end end, + class = function(path, bufnr) + require('vim.filetype.detect').class(bufnr) + end, + sgml = function(path, bufnr) + return require('vim.filetype.detect').sgml(bufnr) + end, + sgm = function(path, bufnr) + return require('vim.filetype.detect').sgml(bufnr) + end, + t = function(path, bufnr) + local nroff = require('vim.filetype.detect').nroff(bufnr) + return nroff or require('vim.filetype.detect').perl(path, bufnr) or 'tads' + end, + -- Ignored extensions + bak = function(path, bufnr) + local root = vim.fn.fnamemodify(path, ':r') + return M.match({ buf = bufnr, filename = root }) + end, + ['dpkg-bak'] = function(path, bufnr) + local root = vim.fn.fnamemodify(path, ':r') + return M.match({ buf = bufnr, filename = root }) + end, + ['dpkg-dist'] = function(path, bufnr) + local root = vim.fn.fnamemodify(path, ':r') + return M.match({ buf = bufnr, filename = root }) + end, + ['dpkg-old'] = function(path, bufnr) + local root = vim.fn.fnamemodify(path, ':r') + return M.match({ buf = bufnr, filename = root }) + end, + ['dpkg-new'] = function(path, bufnr) + local root = vim.fn.fnamemodify(path, ':r') + return M.match({ buf = bufnr, filename = root }) + end, + ['in'] = function(path, bufnr) + if vim.fs.basename(path) ~= 'configure.in' then + local root = vim.fn.fnamemodify(path, ':r') + return M.match({ buf = bufnr, filename = root }) + end + end, + new = function(path, bufnr) + local root = vim.fn.fnamemodify(path, ':r') + return M.match({ buf = bufnr, filename = root }) + end, + old = function(path, bufnr) + local root = vim.fn.fnamemodify(path, ':r') + return M.match({ buf = bufnr, filename = root }) + end, + orig = function(path, bufnr) + local root = vim.fn.fnamemodify(path, ':r') + return M.match({ buf = bufnr, filename = root }) + end, + pacsave = function(path, bufnr) + local root = vim.fn.fnamemodify(path, ':r') + return M.match({ buf = bufnr, filename = root }) + end, + pacnew = function(path, bufnr) + local root = vim.fn.fnamemodify(path, ':r') + return M.match({ buf = bufnr, filename = root }) + end, + rpmsave = function(path, bufnr) + local root = vim.fn.fnamemodify(path, ':r') + return M.match({ buf = bufnr, filename = root }) + end, + rmpnew = function(path, bufnr) + local root = vim.fn.fnamemodify(path, ':r') + return M.match({ buf = bufnr, filename = root }) + 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", - ["lltxxxxx.txt"] = "gedcom", - ["TAG_EDITMSG"] = "gitcommit", - ["MERGE_MSG"] = "gitcommit", - ["COMMIT_EDITMSG"] = "gitcommit", - ["NOTES_EDITMSG"] = "gitcommit", - ["EDIT_DESCRIPTION"] = "gitcommit", - [".gitconfig"] = "gitconfig", - [".gitmodules"] = "gitconfig", - ["/.config/git/config"] = "gitconfig", - ["/etc/gitconfig"] = "gitconfig", - ["gitolite.conf"] = "gitolite", - ["git-rebase-todo"] = "gitrebase", - gkrellmrc = "gkrellmrc", - [".gnashrc"] = "gnash", - [".gnashpluginrc"] = "gnash", - gnashpluginrc = "gnash", - gnashrc = "gnash", - [".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", - ["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", - ["/.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, - [".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, + ['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.project'] = 'cabalproject', + [vim.env.HOME .. '/cabal.config'] = 'cabalconfig', + ['cabal.config'] = 'cabalconfig', + calendar = 'calendar', + catalog = 'catalog', + ['/etc/cdrdao.conf'] = 'cdrdaoconf', + ['.cdrdao'] = 'cdrdaoconf', + ['/etc/default/cdrdao'] = 'cdrdaoconf', + ['/etc/defaults/cdrdao'] = 'cdrdaoconf', + ['cfengine.conf'] = 'cfengine', + ['CMakeLists.txt'] = 'cmake', + ['.alias'] = function(path, bufnr) + return require('vim.filetype.detect').csh(path, bufnr) + end, + ['.cshrc'] = function(path, bufnr) + return require('vim.filetype.detect').csh(path, bufnr) + end, + ['.login'] = function(path, bufnr) + return require('vim.filetype.detect').csh(path, bufnr) + end, + ['csh.cshrc'] = function(path, bufnr) + return require('vim.filetype.detect').csh(path, bufnr) + end, + ['csh.login'] = function(path, bufnr) + return require('vim.filetype.detect').csh(path, bufnr) + end, + ['csh.logout'] = function(path, bufnr) + return require('vim.filetype.detect').csh(path, bufnr) + end, + ['auto.master'] = 'conf', + ['configure.in'] = 'config', + ['configure.ac'] = 'config', + crontab = 'crontab', + ['.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', + Dockerfile = 'dockerfile', + npmrc = 'dosini', + ['/etc/yum.conf'] = 'dosini', + ['.npmrc'] = 'dosini', + ['.editorconfig'] = 'dosini', + ['/etc/pacman.conf'] = 'confini', + ['mpv.conf'] = 'confini', + dune = 'dune', + jbuild = 'dune', + ['dune-workspace'] = 'dune', + ['dune-project'] = 'dune', + ['elinks.conf'] = 'elinks', + ['mix.lock'] = 'elixir', + ['filter-rules'] = 'elmfilt', + ['exim.conf'] = 'exim', + exports = 'exports', + ['.fetchmailrc'] = 'fetchmail', + fvSchemes = function(path, bufnr) + return require('vim.filetype.detect').foam(bufnr) + end, + fvSolution = function(path, bufnr) + return require('vim.filetype.detect').foam(bufnr) + end, + fvConstraints = function(path, bufnr) + return require('vim.filetype.detect').foam(bufnr) + end, + fvModels = function(path, bufnr) + return require('vim.filetype.detect').foam(bufnr) + 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', + ['.gnuplot'] = 'gnuplot', + ['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', + ['maxima-init.mac'] = 'maxima', + ['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', + NEWS = function(path, bufnr) + return require('vim.filetype.detect').news(bufnr) + end, + ['.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', + ['printcap'] = function(path, bufnr) + return 'ptcap', function(b) + vim.b[b].ptcap_type = 'print' + end + end, + ['termcap'] = function(path, bufnr) + return 'ptcap', function(b) + vim.b[b].ptcap_type = 'term' + end + end, + ['.procmailrc'] = 'procmail', + ['.procmail'] = 'procmail', + ['indent.pro'] = function(path, bufnr) + return require('vim.filetype.detect').proto(bufnr, 'indent') + end, + ['/etc/protocols'] = 'protocols', + ['INDEX'] = function(path, bufnr) + return require('vim.filetype.detect').psf(bufnr) + end, + ['INFO'] = function(path, bufnr) + return require('vim.filetype.detect').psf(bufnr) + end, + ['.pythonstartup'] = 'python', + ['.pythonrc'] = 'python', + SConstruct = 'python', + ratpoisonrc = 'ratpoison', + ['.ratpoisonrc'] = 'ratpoison', + 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', + ['bash.bashrc'] = function(path, bufnr) + return require('vim.filetype.detect').sh(path, M.getlines(bufnr), 'bash') + end, + bashrc = function(path, bufnr) + return require('vim.filetype.detect').sh(path, M.getlines(bufnr), 'bash') + end, + ['.bashrc'] = function(path, bufnr) + return require('vim.filetype.detect').sh(path, M.getlines(bufnr), 'bash') + end, + ['.env'] = function(path, bufnr) + return require('vim.filetype.detect').sh(path, M.getlines(bufnr)) + end, + ['.kshrc'] = function(path, bufnr) + return require('vim.filetype.detect').sh(path, M.getlines(bufnr), 'ksh') + end, + ['.profile'] = function(path, bufnr) + return require('vim.filetype.detect').sh(path, M.getlines(bufnr)) + end, + ['/etc/profile'] = function(path, bufnr) + return require('vim.filetype.detect').sh(path, M.getlines(bufnr)) + end, + APKBUILD = function(path, bufnr) + return require('vim.filetype.detect').sh(path, M.getlines(bufnr), 'bash') + end, + PKGBUILD = function(path, bufnr) + return require('vim.filetype.detect').sh(path, M.getlines(bufnr), 'bash') + end, + ['.tcshrc'] = function(path, bufnr) + return require('vim.filetype.detect').shell(path, M.getlines(bufnr), 'tcsh') + end, + ['tcsh.login'] = function(path, bufnr) + return require('vim.filetype.detect').shell(path, M.getlines(bufnr), 'tcsh') + end, + ['tcsh.tcshrc'] = function(path, bufnr) + return require('vim.filetype.detect').shell(path, M.getlines(bufnr), 'tcsh') + end, + ['/etc/slp.conf'] = 'slpconf', + ['/etc/slp.reg'] = 'slpreg', + ['/etc/slp.spi'] = 'slpspi', + ['.slrnrc'] = 'slrnrc', + ['sendmail.cf'] = 'sm', + ['squid.conf'] = 'squid', + ['ssh_config'] = 'sshconfig', + ['sshd_config'] = 'sshdconfig', + ['/etc/sudoers'] = 'sudoers', + ['sudoers.tmp'] = 'sudoers', + ['/etc/sysctl.conf'] = 'sysctl', + tags = 'tags', + ['pending.data'] = 'taskdata', + ['completed.data'] = 'taskdata', + ['undo.data'] = 'taskdata', + ['.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', + ['.wget2rc'] = 'wget2', + wgetrc = 'wget', + wget2rc = 'wget2', + ['.wvdialrc'] = 'wvdial', + ['wvdial.conf'] = 'wvdial', + ['.Xresources'] = 'xdefaults', + ['.Xpdefaults'] = 'xdefaults', + ['xdm-config'] = 'xdefaults', + ['.Xdefaults'] = 'xdefaults', + ['xorg.conf'] = function(path, bufnr) + return 'xf86conf', function(b) + vim.b[b].xf86conf_xfree86_version = 4 + end + end, + ['xorg.conf-4'] = function(path, bufnr) + return 'xf86conf', function(b) + vim.b[b].xf86conf_xfree86_version = 4 + end + end, + ['XF86Config'] = function(path, bufnr) + return require('vim.filetype.detect').xfree86() + end, + ['/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', -- 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", - [".*esmtprc"] = "esmtprc", - [".*Eterm/.*%.cfg"] = "eterm", - [".*%.git/modules/.*/config"] = "gitconfig", - [".*%.git/config"] = "gitconfig", - [".*/%.config/git/config"] = "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", - [".*/%.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', - ["[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/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', + ['.*/etc/apache2/.*%.conf.*'] = starsetf('apache'), + ['.*/etc/apache2/conf%..*/.*'] = starsetf('apache'), + ['.*/etc/apache2/mods%-.*/.*'] = starsetf('apache'), + ['.*/etc/apache2/sites%-.*/.*'] = starsetf('apache'), + ['access%.conf.*'] = starsetf('apache'), + ['apache%.conf.*'] = starsetf('apache'), + ['apache2%.conf.*'] = starsetf('apache'), + ['httpd%.conf.*'] = starsetf('apache'), + ['srm%.conf.*'] = starsetf('apache'), + ['.*/etc/httpd/conf%..*/.*'] = starsetf('apache'), + ['.*/etc/httpd/conf%.d/.*%.conf.*'] = starsetf('apache'), + ['.*/etc/httpd/mods%-.*/.*'] = starsetf('apache'), + ['.*/etc/httpd/sites%-.*/.*'] = starsetf('apache'), + ['.*/etc/proftpd/.*%.conf.*'] = starsetf('apachestyle'), + ['.*/etc/proftpd/conf%..*/.*'] = starsetf('apachestyle'), + ['proftpd%.conf.*'] = starsetf('apachestyle'), + ['.*asterisk/.*%.conf.*'] = starsetf('asterisk'), + ['.*asterisk.*/.*voicemail%.conf.*'] = starsetf('asteriskvm'), + ['.*/%.aptitude/config'] = 'aptconf', + ['.*%.[aA]'] = function(path, bufnr) + return require('vim.filetype.detect').asm(bufnr) + end, + ['.*%.[sS]'] = function(path, bufnr) + return require('vim.filetype.detect').asm(bufnr) + end, + ['[mM]akefile%.am'] = 'automake', + ['.*/bind/db%..*'] = starsetf('bindzone'), + ['.*/named/db%..*'] = starsetf('bindzone'), + ['.*/build/conf/.*%.conf'] = 'bitbake', + ['.*/meta/conf/.*%.conf'] = 'bitbake', + ['.*/meta%-.*/conf/.*%.conf'] = 'bitbake', + ['.*bsd'] = 'bsdl', + ['bzr_log%..*'] = 'bzr', + ['.*enlightenment/.*%.cfg'] = 'c', + ['cabal%.project%..*'] = starsetf('cabalproject'), + ['.*/%.calendar/.*'] = starsetf('calendar'), + ['.*/share/calendar/.*/calendar%..*'] = starsetf('calendar'), + ['.*/share/calendar/calendar%..*'] = starsetf('calendar'), + ['sgml%.catalog.*'] = starsetf('catalog'), + ['.*/etc/defaults/cdrdao'] = 'cdrdaoconf', + ['.*/etc/cdrdao%.conf'] = 'cdrdaoconf', + ['.*/etc/default/cdrdao'] = 'cdrdaoconf', + ['.*hgrc'] = 'cfg', + ['.*%.[Cc][Ff][Gg]'] = { + function(path, bufnr) + return require('vim.filetype.detect').cfg(bufnr) + end, + -- Decrease priority to avoid conflicts with more specific patterns + -- such as '.*/etc/a2ps/.*%.cfg', '.*enlightenment/.*%.cfg', etc. + { priority = -1 }, + }, + ['[cC]hange[lL]og.*'] = starsetf(function(path, bufnr) + require('vim.filetype.detect').changelog(bufnr) + end), + ['.*%.%.ch'] = 'chill', + ['.*%.cmake%.in'] = 'cmake', + -- */cmus/rc and */.cmus/rc + ['.*/%.?cmus/rc'] = 'cmusrc', + -- */cmus/*.theme and */.cmus/*.theme + ['.*/%.?cmus/.*%.theme'] = 'cmusrc', + ['.*/%.cmus/autosave'] = 'cmusrc', + ['.*/%.cmus/command%-history'] = 'cmusrc', + ['.*/etc/hostname%..*'] = starsetf('config'), + ['crontab%..*'] = starsetf('crontab'), + ['.*/etc/cron%.d/.*'] = starsetf('crontab'), + ['%.cshrc.*'] = function(path, bufnr) + return require('vim.filetype.detect').csh(path, bufnr) + end, + ['%.login.*'] = function(path, bufnr) + return require('vim.filetype.detect').csh(path, bufnr) + end, + ['cvs%d+'] = 'cvs', + ['.*%.[Dd][Aa][Tt]'] = function(path, bufnr) + return require('vim.filetype.detect').dat(path, bufnr) + end, + ['.*/debian/patches/.*'] = function(path, bufnr) + return require('vim.filetype.detect').dep3patch(path, bufnr) + end, + ['.*/etc/dnsmasq%.d/.*'] = starsetf('dnsmasq'), + ['Containerfile%..*'] = starsetf('dockerfile'), + ['Dockerfile%..*'] = starsetf('dockerfile'), + ['.*/etc/yum%.repos%.d/.*'] = starsetf('dosini'), + ['drac%..*'] = starsetf('dracula'), + ['.*/debian/changelog'] = 'debchangelog', + ['.*/debian/control'] = 'debcontrol', + ['.*/debian/copyright'] = 'debcopyright', + ['.*/etc/apt/sources%.list%.d/.*%.list'] = 'debsources', + ['.*/etc/apt/sources%.list'] = 'debsources', + ['.*%.directory'] = 'desktop', + ['.*%.desktop'] = 'desktop', + ['dictd.*%.conf'] = 'dictdconf', + ['.*/etc/DIR_COLORS'] = 'dircolors', + ['.*/etc/dnsmasq%.conf'] = 'dnsmasq', + ['php%.ini%-.*'] = 'dosini', + ['.*/etc/pacman%.conf'] = 'confini', + ['.*/etc/yum%.conf'] = 'dosini', + ['.*lvs'] = 'dracula', + ['.*lpe'] = 'dracula', + ['.*/dtrace/.*%.d'] = 'dtrace', + ['.*esmtprc'] = 'esmtprc', + ['.*Eterm/.*%.cfg'] = 'eterm', + ['[a-zA-Z0-9].*Dict'] = function(path, bufnr) + return require('vim.filetype.detect').foam(bufnr) + end, + ['[a-zA-Z0-9].*Dict%..*'] = function(path, bufnr) + return require('vim.filetype.detect').foam(bufnr) + end, + ['[a-zA-Z].*Properties'] = function(path, bufnr) + return require('vim.filetype.detect').foam(bufnr) + end, + ['[a-zA-Z].*Properties%..*'] = function(path, bufnr) + return require('vim.filetype.detect').foam(bufnr) + end, + ['.*Transport%..*'] = function(path, bufnr) + return require('vim.filetype.detect').foam(bufnr) + end, + ['.*/constant/g'] = function(path, bufnr) + return require('vim.filetype.detect').foam(bufnr) + end, + ['.*/0/.*'] = function(path, bufnr) + return require('vim.filetype.detect').foam(bufnr) + end, + ['.*/0%.orig/.*'] = function(path, bufnr) + return require('vim.filetype.detect').foam(bufnr) + end, + ['.*/%.fvwm/.*'] = starsetf('fvwm'), + ['.*fvwmrc.*'] = starsetf(function(path, bufnr) + return 'fvwm', function(b) + vim.b[b].fvwm_version = 1 + end + end), + ['.*fvwm95.*%.hook'] = starsetf(function(path, bufnr) + return 'fvwm', function(b) + vim.b[b].fvwm_version = 1 + end + end), + ['.*fvwm2rc.*'] = starsetf(function(path, bufnr) + return require('vim.filetype.detect').fvwm(path) + end), + ['.*/tmp/lltmp.*'] = starsetf('gedcom'), + ['.*/etc/gitconfig%.d/.*'] = starsetf('gitconfig'), + ['.*/gitolite%-admin/conf/.*'] = starsetf('gitolite'), + ['tmac%..*'] = starsetf('nroff'), + ['.*/%.gitconfig%.d/.*'] = starsetf('gitconfig'), + ['.*%.git/.*'] = { + function(path, bufnr) + return require('vim.filetype.detect').git(bufnr) + end, + -- Decrease priority to run after simple pattern checks + { priority = -1 }, + }, + ['.*%.git/modules/.*/config'] = 'gitconfig', + ['.*%.git/modules/config'] = 'gitconfig', + ['.*%.git/config'] = 'gitconfig', + ['.*/etc/gitconfig'] = 'gitconfig', + ['.*/%.config/git/config'] = 'gitconfig', + ['.*%.git/config%.worktree'] = 'gitconfig', + ['.*%.git/worktrees/.*/config%.worktree'] = 'gitconfig', + ['.*/git/config'] = function(path, bufnr) + if vim.env.XDG_CONFIG_HOME and path:find(vim.env.XDG_CONFIG_HOME .. '/git/config') then + return 'gitconfig' + end + end, + ['%.gitsendemail%.msg%.......'] = 'gitsendemail', + ['gkrellmrc_.'] = 'gkrellmrc', + ['.*/usr/.*/gnupg/options%.skel'] = 'gpg', + ['.*/%.gnupg/options'] = 'gpg', + ['.*/%.gnupg/gpg%.conf'] = 'gpg', + ['.*/options'] = function(path, bufnr) + if vim.env.GNUPGHOME and path:find(vim.env.GNUPGHOME .. '/options') then + return 'gpg' + end + end, + ['.*/gpg%.conf'] = function(path, bufnr) + if vim.env.GNUPGHOME and path:find(vim.env.GNUPGHOME .. '/gpg%.conf') then + return 'gpg' + end + end, + ['.*/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', + -- gtkrc* and .gtkrc* + ['%.?gtkrc.*'] = starsetf('gtkrc'), + [vim.env.VIMRUNTIME .. '/doc/.*%.txt'] = 'help', + ['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', + ['JAM.*%..*'] = starsetf('jam'), + ['Prl.*%..*'] = starsetf('jam'), + ['.*%.properties_..'] = 'jproperties', + ['.*%.properties_.._..'] = 'jproperties', + ['.*%.properties_.._.._.*'] = starsetf('jproperties'), + ['Kconfig%..*'] = starsetf('kconfig'), + ['.*%.[Ss][Uu][Bb]'] = 'krl', + ['lilo%.conf.*'] = starsetf('lilo'), + ['.*/etc/logcheck/.*%.d.*/.*'] = starsetf('logcheck'), + ['.*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', + ['%.letter%.%d+'] = 'mail', + ['%.article%.%d+'] = 'mail', + ['/tmp/SLRN[0-9A-Z.]+'] = 'mail', + ['ae%d+%.txt'] = 'mail', + ['pico%.%d+'] = 'mail', + ['mutt%-.*%-%w+'] = 'mail', + ['muttng%-.*%-%w+'] = 'mail', + ['neomutt%-.*%-%w+'] = 'mail', + ['mutt' .. string.rep('[%w_-]', 6)] = 'mail', + ['neomutt' .. string.rep('[%w_-]', 6)] = 'mail', + ['snd%.%d+'] = 'mail', + ['reportbug%-.*'] = starsetf('mail'), + ['.*/etc/mail/aliases'] = 'mailaliases', + ['.*/etc/aliases'] = 'mailaliases', + ['.*[mM]akefile'] = 'make', + ['[mM]akefile.*'] = starsetf('make'), + ['.*/etc/man%.conf'] = 'manconf', + ['.*/log/auth'] = 'messages', + ['.*/log/cron'] = 'messages', + ['.*/log/daemon'] = 'messages', + ['.*/log/debug'] = 'messages', + ['.*/log/kern'] = 'messages', + ['.*/log/lpr'] = 'messages', + ['.*/log/mail'] = 'messages', + ['.*/log/messages'] = 'messages', + ['.*/log/news/news'] = 'messages', + ['.*/log/syslog'] = 'messages', + ['.*/log/user'] = 'messages', + ['.*/log/auth%.log'] = 'messages', + ['.*/log/cron%.log'] = 'messages', + ['.*/log/daemon%.log'] = 'messages', + ['.*/log/debug%.log'] = 'messages', + ['.*/log/kern%.log'] = 'messages', + ['.*/log/lpr%.log'] = 'messages', + ['.*/log/mail%.log'] = 'messages', + ['.*/log/messages%.log'] = 'messages', + ['.*/log/news/news%.log'] = 'messages', + ['.*/log/syslog%.log'] = 'messages', + ['.*/log/user%.log'] = 'messages', + ['.*/log/auth%.err'] = 'messages', + ['.*/log/cron%.err'] = 'messages', + ['.*/log/daemon%.err'] = 'messages', + ['.*/log/debug%.err'] = 'messages', + ['.*/log/kern%.err'] = 'messages', + ['.*/log/lpr%.err'] = 'messages', + ['.*/log/mail%.err'] = 'messages', + ['.*/log/messages%.err'] = 'messages', + ['.*/log/news/news%.err'] = 'messages', + ['.*/log/syslog%.err'] = 'messages', + ['.*/log/user%.err'] = 'messages', + ['.*/log/auth%.info'] = 'messages', + ['.*/log/cron%.info'] = 'messages', + ['.*/log/daemon%.info'] = 'messages', + ['.*/log/debug%.info'] = 'messages', + ['.*/log/kern%.info'] = 'messages', + ['.*/log/lpr%.info'] = 'messages', + ['.*/log/mail%.info'] = 'messages', + ['.*/log/messages%.info'] = 'messages', + ['.*/log/news/news%.info'] = 'messages', + ['.*/log/syslog%.info'] = 'messages', + ['.*/log/user%.info'] = 'messages', + ['.*/log/auth%.warn'] = 'messages', + ['.*/log/cron%.warn'] = 'messages', + ['.*/log/daemon%.warn'] = 'messages', + ['.*/log/debug%.warn'] = 'messages', + ['.*/log/kern%.warn'] = 'messages', + ['.*/log/lpr%.warn'] = 'messages', + ['.*/log/mail%.warn'] = 'messages', + ['.*/log/messages%.warn'] = 'messages', + ['.*/log/news/news%.warn'] = 'messages', + ['.*/log/syslog%.warn'] = 'messages', + ['.*/log/user%.warn'] = 'messages', + ['.*/log/auth%.crit'] = 'messages', + ['.*/log/cron%.crit'] = 'messages', + ['.*/log/daemon%.crit'] = 'messages', + ['.*/log/debug%.crit'] = 'messages', + ['.*/log/kern%.crit'] = 'messages', + ['.*/log/lpr%.crit'] = 'messages', + ['.*/log/mail%.crit'] = 'messages', + ['.*/log/messages%.crit'] = 'messages', + ['.*/log/news/news%.crit'] = 'messages', + ['.*/log/syslog%.crit'] = 'messages', + ['.*/log/user%.crit'] = 'messages', + ['.*/log/auth%.notice'] = 'messages', + ['.*/log/cron%.notice'] = 'messages', + ['.*/log/daemon%.notice'] = 'messages', + ['.*/log/debug%.notice'] = 'messages', + ['.*/log/kern%.notice'] = 'messages', + ['.*/log/lpr%.notice'] = 'messages', + ['.*/log/mail%.notice'] = 'messages', + ['.*/log/messages%.notice'] = 'messages', + ['.*/log/news/news%.notice'] = 'messages', + ['.*/log/syslog%.notice'] = 'messages', + ['.*/log/user%.notice'] = 'messages', + ['.*%.[Mm][Oo][Dd]'] = function(path, bufnr) + return require('vim.filetype.detect').mod(path, bufnr) + end, + ['.*/etc/modules%.conf'] = 'modconf', + ['.*/etc/conf%.modules'] = 'modconf', + ['.*/etc/modules'] = 'modconf', + ['.*/etc/modprobe%..*'] = starsetf('modconf'), + ['.*/etc/modutils/.*'] = starsetf(function(path, bufnr) + if vim.fn.executable(vim.fn.expand(path)) ~= 1 then + return 'modconf' + end + end), + ['.*%.[mi][3g]'] = 'modula3', + ['Muttrc'] = 'muttrc', + ['Muttngrc'] = 'muttrc', + ['.*/etc/Muttrc%.d/.*'] = starsetf('muttrc'), + ['.*/%.mplayer/config'] = 'mplayerconf', + ['Muttrc.*'] = starsetf('muttrc'), + ['Muttngrc.*'] = starsetf('muttrc'), + -- muttrc* and .muttrc* + ['%.?muttrc.*'] = starsetf('muttrc'), + -- muttngrc* and .muttngrc* + ['%.?muttngrc.*'] = starsetf('muttrc'), + ['.*/%.mutt/muttrc.*'] = starsetf('muttrc'), + ['.*/%.muttng/muttrc.*'] = starsetf('muttrc'), + ['.*/%.muttng/muttngrc.*'] = starsetf('muttrc'), + ['rndc.*%.conf'] = 'named', + ['rndc.*%.key'] = 'named', + ['named.*%.conf'] = 'named', + ['.*/etc/nanorc'] = 'nanorc', + ['.*%.NS[ACGLMNPS]'] = 'natural', + ['Neomuttrc.*'] = starsetf('neomuttrc'), + -- neomuttrc* and .neomuttrc* + ['%.?neomuttrc.*'] = starsetf('neomuttrc'), + ['.*/%.neomutt/neomuttrc.*'] = starsetf('neomuttrc'), + ['nginx.*%.conf'] = 'nginx', + ['.*/etc/nginx/.*'] = 'nginx', + ['.*nginx%.conf'] = 'nginx', + ['.*/nginx/.*%.conf'] = 'nginx', + ['.*/usr/local/nginx/conf/.*'] = 'nginx', + ['.*%.[1-9]'] = function(path, bufnr) + return require('vim.filetype.detect').nroff(bufnr) + end, + ['.*%.ml%.cppo'] = 'ocaml', + ['.*%.mli%.cppo'] = 'ocaml', + ['.*%.opam%.template'] = 'opam', + ['.*%.[Oo][Pp][Ll]'] = 'opl', + ['.*/etc/pam%.conf'] = 'pamconf', + ['.*/etc/pam%.d/.*'] = starsetf('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', + ['%.?gitolite%.rc'] = 'perl', + ['example%.gitolite%.rc'] = 'perl', + ['.*%.php%d'] = 'php', + ['.*/%.pinforc'] = 'pinfo', + ['.*/etc/pinforc'] = 'pinfo', + ['.*%.[Pp][Rr][Gg]'] = function(path, bufnr) + return require('vim.filetype.detect').prg(bufnr) + end, + ['.*/etc/protocols'] = 'protocols', + ['.*printcap.*'] = starsetf(function(path, bufnr) + return require('vim.filetype.detect').printcap('print') + end), + ['.*baseq[2-3]/.*%.cfg'] = 'quake', + ['.*quake[1-3]/.*%.cfg'] = 'quake', + ['.*id1/.*%.cfg'] = 'quake', + ['.*/queries/.*%.scm'] = 'query', -- tree-sitter queries (Neovim only) + ['.*,v'] = 'rcs', + ['%.reminders.*'] = starsetf('remind'), + ['[rR]akefile.*'] = starsetf('ruby'), + ['[rR]antfile'] = 'ruby', + ['[rR]akefile'] = 'ruby', + ['.*/etc/sensors%.d/[^.].*'] = starsetf('sensors'), + ['.*/etc/sensors%.conf'] = 'sensors', + ['.*/etc/sensors3%.conf'] = 'sensors', + ['.*/etc/services'] = 'services', + ['.*/etc/serial%.conf'] = 'setserial', + ['.*/etc/udev/cdsymlinks%.conf'] = 'sh', + ['%.bash[_%-]aliases'] = function(path, bufnr) + return require('vim.filetype.detect').sh(path, M.getlines(bufnr), 'bash') + end, + ['%.bash[_%-]logout'] = function(path, bufnr) + return require('vim.filetype.detect').sh(path, M.getlines(bufnr), 'bash') + end, + ['%.bash[_%-]profile'] = function(path, bufnr) + return require('vim.filetype.detect').sh(path, M.getlines(bufnr), 'bash') + end, + ['%.kshrc.*'] = function(path, bufnr) + return require('vim.filetype.detect').sh(path, M.getlines(bufnr), 'ksh') + end, + ['%.profile.*'] = function(path, bufnr) + return require('vim.filetype.detect').sh(path, M.getlines(bufnr)) + end, + ['.*/etc/profile'] = function(path, bufnr) + return require('vim.filetype.detect').sh(path, M.getlines(bufnr)) + end, + ['bash%-fc[%-%.]'] = function(path, bufnr) + return require('vim.filetype.detect').sh(path, M.getlines(bufnr), 'bash') + end, + ['%.tcshrc.*'] = function(path, bufnr) + return require('vim.filetype.detect').sh(path, M.getlines(bufnr), 'tcsh') + end, + ['.*/etc/sudoers%.d/.*'] = starsetf('sudoers'), + ['.*%._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', + ['.*/%.ssh/.*%.conf'] = 'sshconfig', + ['.*/etc/ssh/sshd_config%.d/.*%.conf'] = 'sshdconfig', + ['.*%.[Ss][Rr][Cc]'] = function(path, bufnr) + return require('vim.filetype.detect').src(bufnr) + end, + ['.*/etc/sudoers'] = 'sudoers', + ['svn%-commit.*%.tmp'] = 'svn', + ['.*%.swift%.gyb'] = 'swiftgyb', + ['.*%.[Ss][Yy][Ss]'] = function(path, bufnr) + return require('vim.filetype.detect').sys(bufnr) + end, + ['.*/etc/sysctl%.conf'] = 'sysctl', + ['.*/etc/sysctl%.d/.*%.conf'] = 'sysctl', + ['.*/systemd/.*%.automount'] = 'systemd', + ['.*/systemd/.*%.dnssd'] = 'systemd', + ['.*/systemd/.*%.link'] = 'systemd', + ['.*/systemd/.*%.mount'] = 'systemd', + ['.*/systemd/.*%.netdev'] = 'systemd', + ['.*/systemd/.*%.network'] = 'systemd', + ['.*/systemd/.*%.nspawn'] = 'systemd', + ['.*/systemd/.*%.path'] = 'systemd', + ['.*/systemd/.*%.service'] = 'systemd', + ['.*/systemd/.*%.slice'] = 'systemd', + ['.*/systemd/.*%.socket'] = 'systemd', + ['.*/systemd/.*%.swap'] = 'systemd', + ['.*/systemd/.*%.target'] = 'systemd', + ['.*/systemd/.*%.timer'] = 'systemd', + ['.*/etc/systemd/.*%.conf%.d/.*%.conf'] = 'systemd', + ['.*/%.config/systemd/user/.*%.d/.*%.conf'] = 'systemd', + ['.*/etc/systemd/system/.*%.d/.*%.conf'] = 'systemd', + ['.*/etc/systemd/system/.*%.d/%.#.*'] = 'systemd', + ['.*/etc/systemd/system/%.#.*'] = 'systemd', + ['.*/%.config/systemd/user/.*%.d/%.#.*'] = 'systemd', + ['.*/%.config/systemd/user/%.#.*'] = 'systemd', + ['.*termcap.*'] = starsetf(function(path, bufnr) + return require('vim.filetype.detect').printcap('term') + end), + ['.*%.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', + ['.*%.[Ll][Oo][Gg]'] = function(path, bufnr) + return require('vim.filetype.detect').log(path) + end, + ['.*%.vhdl_[0-9].*'] = starsetf('vhdl'), + ['.*%.ws[fc]'] = 'wsh', + ['.*/Xresources/.*'] = starsetf('xdefaults'), + ['.*/app%-defaults/.*'] = starsetf('xdefaults'), + ['.*/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', + ['.*vimrc.*'] = starsetf('vim'), + ['Xresources.*'] = starsetf('xdefaults'), + ['.*/etc/xinetd%.d/.*'] = starsetf('xinetd'), + ['.*xmodmap.*'] = starsetf('xmodmap'), + ['.*/xorg%.conf%.d/.*%.conf'] = function(path, bufnr) + return 'xf86config', function(b) + vim.b[b].xf86conf_xfree86_version = 4 + end + end, + -- Increase priority to run before the pattern below + ['XF86Config%-4.*'] = starsetf(function(path, bufnr) + return 'xf86conf', function(b) + vim.b[b].xf86conf_xfree86_version = 4 + end + end, { priority = -math.huge + 1 }), + ['XF86Config.*'] = starsetf(function(path, bufnr) + return require('vim.filetype.detect').xfree86() + end), + ['%.zcompdump.*'] = starsetf('zsh'), + -- .zlog* and zlog* + ['%.?zlog.*'] = starsetf('zsh'), + -- .zsh* and zsh* + ['%.?zsh.*'] = starsetf('zsh'), + -- Ignored extension + ['.*~'] = function(path, bufnr) + local short = path:gsub('~$', '', 1) + if path ~= short and short ~= '' then + return M.match({ buf = bufnr, filename = vim.fn.fnameescape(short) }) + end + end, -- END PATTERN } -- luacheck: pop +-- 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 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 {} + local opts = (type(v) == 'table' and type(v[2]) == 'table') and v[2] or {} if not opts.priority then opts.priority = 0 end @@ -1372,8 +2198,18 @@ end local pattern_sorted = sort_by_priority(pattern) ---@private -local function normalize_path(path) - return (path:gsub("\\", "/")) +local function normalize_path(path, as_pattern) + local normal = path:gsub('\\', '/') + if normal:find('^~') then + if as_pattern then + -- Escape Lua's metacharacters when $HOME is used in a pattern. + -- The rest of path should already be properly escaped. + normal = vim.pesc(vim.env.HOME) .. normal:sub(2) + else + normal = vim.env.HOME .. normal:sub(2) + end + end + return normal end --- Add new filetype mappings. @@ -1381,7 +2217,7 @@ end --- 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) +--- the filename is matched against the list of |lua-patterns| (sorted by priority) --- until a match is found. Lastly, if pattern matching does not find a --- filetype, then the file extension is used. --- @@ -1389,7 +2225,9 @@ end --- 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. +--- buffer's filetype. Optionally, the function can return a second function +--- value which, when called, modifies the state of the buffer. This can be used +--- to, for example, set filetype-specific buffer variables. --- --- Filename patterns can specify an optional priority to resolve cases when a --- file path matches multiple patterns. Higher priorities are matched first. @@ -1397,8 +2235,7 @@ end --- --- 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. +--- Note that Lua filetype detection is disabled when |g:do_legacy_filetype| is set. --- --- Example: --- <pre> @@ -1407,7 +2244,10 @@ end --- foo = "fooscript", --- bar = function(path, bufnr) --- if some_condition() then ---- return "barscript" +--- return "barscript", function(bufnr) +--- -- Set a buffer variable +--- vim.b[bufnr].barscript_version = 2 +--- end --- end --- return "bar" --- end, @@ -1431,6 +2271,25 @@ end --- }) --- </pre> --- +--- To add a fallback match on contents (see |new-filetype-scripts|), use +--- <pre> +--- vim.filetype.add { +--- pattern = { +--- ['.*'] = { +--- priority = -math.huge, +--- function(path, bufnr) +--- local content = vim.filetype.getlines(bufnr, 1) +--- if vim.filetype.matchregex(content, { [[^#!.*\\<mine\\>]] }) then +--- return 'mine' +--- elseif vim.filetype.matchregex(content, { [[\\<drawing\\>]] }) then +--- return 'drawing' +--- 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 @@ -1442,7 +2301,7 @@ function M.add(filetypes) end for k, v in pairs(filetypes.pattern or {}) do - pattern[normalize_path(k)] = v + pattern[normalize_path(k, true)] = v end if filetypes.pattern then @@ -1452,27 +2311,34 @@ end ---@private local function dispatch(ft, path, bufnr, ...) - if type(ft) == "function" then - ft = ft(path, bufnr, ...) + local on_detect + if type(ft) == 'function' then + if bufnr then + ft, on_detect = ft(path, bufnr, ...) + else + -- If bufnr is nil (meaning we are matching only against the filename), set it to an invalid + -- value (-1) and catch any errors from the filetype detection function. If the function tries + -- to use the buffer then it will fail, but this enables functions which do not need a buffer + -- to still work. + local ok + ok, ft, on_detect = pcall(ft, path, -1, ...) + if not ok then + return + end + end end - if type(ft) == "string" then - api.nvim_buf_set_option(bufnr, "filetype", ft) - return true + if type(ft) == 'string' then + return ft, on_detect 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 fullpat = '^' .. pat .. '$' local matches - if pat:find("/") then + 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) @@ -1483,28 +2349,87 @@ local function match_pattern(name, path, tail, pat) return matches end ---- Set the filetype for the given buffer from a file name. +--- Perform filetype detection. +--- +--- The filetype can be detected using one of three methods: +--- 1. Using an existing buffer +--- 2. Using only a file name +--- 3. Using only file contents +--- +--- Of these, option 1 provides the most accurate result as it uses both the buffer's filename and +--- (optionally) the buffer contents. Options 2 and 3 can be used without an existing buffer, but +--- may not always provide a match in cases where the filename (or contents) cannot unambiguously +--- determine the filetype. +--- +--- Each of the three options is specified using a key to the single argument of this function. +--- Example: +--- +--- <pre> +--- -- Using a buffer number +--- vim.filetype.match({ buf = 42 }) +--- +--- -- Override the filename of the given buffer +--- vim.filetype.match({ buf = 42, filename = 'foo.c' }) --- ----@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() +--- -- Using a filename without a buffer +--- vim.filetype.match({ filename = 'main.lua' }) +--- +--- -- Using file contents +--- vim.filetype.match({ contents = {'#!/usr/bin/env bash'} }) +--- </pre> +--- +---@param args table Table specifying which matching strategy to use. Accepted keys are: +--- * buf (number): Buffer number to use for matching. Mutually exclusive with +--- {contents} +--- * filename (string): Filename to use for matching. When {buf} is given, +--- defaults to the filename of the given buffer number. The +--- file need not actually exist in the filesystem. When used +--- without {buf} only the name of the file is used for +--- filetype matching. This may result in failure to detect +--- the filetype in cases where the filename alone is not +--- enough to disambiguate the filetype. +--- * contents (table): An array of lines representing file contents to use for +--- matching. Can be used with {filename}. Mutually exclusive +--- with {buf}. +---@return string|nil If a match was found, the matched filetype. +---@return function|nil A function that modifies buffer state when called (for example, to set some +--- filetype specific buffer variables). The function accepts a buffer number as +--- its only argument. +function M.match(args) + vim.validate({ + arg = { args, 't' }, + }) + + if not (args.buf or args.filename or args.contents) then + error('At least one of "buf", "filename", or "contents" must be given') + end + + local bufnr = args.buf + local name = args.filename + local contents = args.contents + + if bufnr and not name then + name = api.nvim_buf_get_name(bufnr) + end - name = normalize_path(name) + if name then + name = normalize_path(name) + end + + local ft, on_detect -- 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 + local path = vim.fn.fnamemodify(name, ':p') + ft, on_detect = dispatch(filename[path], path, bufnr) + if ft then + return ft, on_detect end -- Next check against just the file name - local tail = vim.fn.fnamemodify(name, ":t") - if dispatch(filename[tail], path, bufnr) then - return + local tail = vim.fn.fnamemodify(name, ':t') + ft, on_detect = dispatch(filename[tail], path, bufnr) + if ft then + return ft, on_detect end -- Next, check the file path against available patterns with non-negative priority @@ -1517,34 +2442,50 @@ function M.match(name, bufnr) break end - local ft = v[k][1] + local filetype = v[k][1] local matches = match_pattern(name, path, tail, k) if matches then - if dispatch(ft, path, bufnr, matches) then - return + ft, on_detect = dispatch(filetype, path, bufnr, matches) + if ft then + return ft, on_detect end end end -- Next, check file extension - local ext = vim.fn.fnamemodify(name, ":e") - if dispatch(extension[ext], path, bufnr) then - return + local ext = vim.fn.fnamemodify(name, ':e') + ft, on_detect = dispatch(extension[ext], path, bufnr) + if ft then + return ft, on_detect end - -- Finally, check patterns with negative priority + -- Next, 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 filetype = v[k][1] local matches = match_pattern(name, path, tail, k) if matches then - if dispatch(ft, path, bufnr, matches) then - return + ft, on_detect = dispatch(filetype, path, bufnr, matches) + if ft then + return ft, on_detect end end end + + -- Finally, check file contents + if contents or bufnr then + contents = contents or M.getlines(bufnr) + -- If name is nil, catch any errors from the contents filetype detection function. + -- If the function tries to use the filename that is nil then it will fail, + -- but this enables checks which do not need a filename to still work. + local ok + ok, ft = pcall(require('vim.filetype.detect').match_contents, contents, name) + if ok and ft then + return ft + end + end end return M diff --git a/runtime/lua/vim/filetype/detect.lua b/runtime/lua/vim/filetype/detect.lua new file mode 100644 index 0000000000..8c10517687 --- /dev/null +++ b/runtime/lua/vim/filetype/detect.lua @@ -0,0 +1,1630 @@ +-- Contains filetype detection functions for use in filetype.lua that are either: +-- * used more than once or +-- * complex (e.g. check more than one line or use conditionals). +-- Simple one-line checks, such as a check for a string in the first line are better inlined in filetype.lua. + +-- A few guidelines to follow when porting a new function: +-- * Sort the function alphabetically and omit 'ft' or 'check' from the new function name. +-- * Use ':find' instead of ':match' / ':sub' if possible. +-- * When '=~' is used to match a pattern, there are two possibilities: +-- - If the pattern only contains lowercase characters, treat the comparison as case-insensitive. +-- - Otherwise, treat it as case-sensitive. +-- (Basically, we apply 'smartcase': if upper case characters are used in the original pattern, then +-- it's likely that case does matter). +-- * When '\k', '\<' or '\>' is used in a pattern, use the 'matchregex' function. +-- Note that vim.regex is case-sensitive by default, so add the '\c' flag if only lowercase letters +-- are present in the pattern: +-- Example: +-- `if line =~ '^\s*unwind_protect\>'` => `if matchregex(line, [[\c^\s*unwind_protect\>]])` + +local M = {} + +local getlines = vim.filetype.getlines +local findany = vim.filetype.findany +local nextnonblank = vim.filetype.nextnonblank +local matchregex = vim.filetype.matchregex + +-- luacheck: push no unused args +-- luacheck: push ignore 122 + +-- This function checks for the kind of assembly that is wanted by the user, or +-- can be detected from the first five lines of the file. +function M.asm(bufnr) + local syntax = vim.b[bufnr].asmsyntax + if not syntax or syntax == '' then + syntax = M.asm_syntax(bufnr) + end + + -- If b:asmsyntax still isn't set, default to asmsyntax or GNU + if not syntax or syntax == '' then + if vim.g.asmsyntax and vim.g.asmsyntax ~= 0 then + syntax = vim.g.asmsyntax + else + syntax = 'asm' + end + end + return syntax, function(b) + vim.b[b].asmsyntax = syntax + end +end + +-- Active Server Pages (with Perl or Visual Basic Script) +function M.asp(bufnr) + if vim.g.filetype_asp then + return vim.g.filetype_asp + elseif table.concat(getlines(bufnr, 1, 3)):lower():find('perlscript') then + return 'aspperl' + else + return 'aspvbs' + end +end + +-- Checks the first 5 lines for a asmsyntax=foo override. +-- Only whitespace characters can be present immediately before or after this statement. +function M.asm_syntax(bufnr) + local lines = ' ' .. table.concat(getlines(bufnr, 1, 5), ' '):lower() .. ' ' + local match = lines:match('%sasmsyntax=([a-zA-Z0-9]+)%s') + if match then + return match + elseif findany(lines, { '%.title', '%.ident', '%.macro', '%.subtitle', '%.library' }) then + return 'vmasm' + end +end + +local visual_basic_content = + { 'vb_name', 'begin vb%.form', 'begin vb%.mdiform', 'begin vb%.usercontrol' } + +-- See frm() for Visual Basic form file detection +function M.bas(bufnr) + if vim.g.filetype_bas then + return vim.g.filetype_bas + end + + -- Most frequent FreeBASIC-specific keywords in distro files + local fb_keywords = + [[\c^\s*\%(extern\|var\|enum\|private\|scope\|union\|byref\|operator\|constructor\|delete\|namespace\|public\|property\|with\|destructor\|using\)\>\%(\s*[:=(]\)\@!]] + local fb_preproc = + [[\c^\s*\%(#\s*\a\+\|option\s\+\%(byval\|dynamic\|escape\|\%(no\)\=gosub\|nokeyword\|private\|static\)\>\|\%(''\|rem\)\s*\$lang\>\|def\%(byte\|longint\|short\|ubyte\|uint\|ulongint\|ushort\)\>\)]] + + local fb_comment = "^%s*/'" + -- OPTION EXPLICIT, without the leading underscore, is common to many dialects + local qb64_preproc = [[\c^\s*\%($\a\+\|option\s\+\%(_explicit\|_\=explicitarray\)\>\)]] + + for _, line in ipairs(getlines(bufnr, 1, 100)) do + if findany(line:lower(), visual_basic_content) then + return 'vb' + elseif + line:find(fb_comment) + or matchregex(line, fb_preproc) + or matchregex(line, fb_keywords) + then + return 'freebasic' + elseif matchregex(line, qb64_preproc) then + return 'qb64' + end + end + return 'basic' +end + +function M.bindzone(bufnr, default) + local lines = table.concat(getlines(bufnr, 1, 4)) + if findany(lines, { '^; <<>> DiG [0-9%.]+.* <<>>', '%$ORIGIN', '%$TTL', 'IN%s+SOA' }) then + return 'bindzone' + end + return default +end + +-- Returns true if file content looks like RAPID +local function is_rapid(bufnr, extension) + if extension == 'cfg' then + local line = getlines(bufnr, 1):lower() + return findany(line, { 'eio:cfg', 'mmc:cfg', 'moc:cfg', 'proc:cfg', 'sio:cfg', 'sys:cfg' }) + end + local line = nextnonblank(bufnr, 1) + if line then + -- Called from mod, prg or sys functions + return matchregex(line:lower(), [[\c\v^\s*%(\%{3}|module\s+\k+\s*%(\(|$))]]) + end + return false +end + +function M.cfg(bufnr) + if vim.g.filetype_cfg then + return vim.g.filetype_cfg + elseif is_rapid(bufnr, 'cfg') then + return 'rapid' + else + return 'cfg' + end +end + +-- This function checks if one of the first ten lines start with a '@'. In +-- that case it is probably a change file. +-- If the first line starts with # or ! it's probably a ch file. +-- If a line has "main", "include", "//" or "/*" it's probably ch. +-- Otherwise CHILL is assumed. +function M.change(bufnr) + local first_line = getlines(bufnr, 1) + if findany(first_line, { '^#', '^!' }) then + return 'ch' + end + for _, line in ipairs(getlines(bufnr, 1, 10)) do + if line:find('^@') then + return 'change' + end + if line:find('MODULE') then + return 'chill' + elseif findany(line:lower(), { 'main%s*%(', '#%s*include', '//' }) then + return 'ch' + end + end + return 'chill' +end + +function M.changelog(bufnr) + local line = getlines(bufnr, 1):lower() + if line:find('; urgency=') then + return 'debchangelog' + end + return 'changelog' +end + +function M.class(bufnr) + -- Check if not a Java class (starts with '\xca\xfe\xba\xbe') + if not getlines(bufnr, 1):find('^\202\254\186\190') then + return 'stata' + end +end + +function M.cls(bufnr) + if vim.g.filetype_cls then + return vim.g.filetype_cls + end + local line = getlines(bufnr, 1) + if line:find('^%%') then + return 'tex' + elseif line:find('^#') and line:lower():find('rexx') then + return 'rexx' + elseif line == 'VERSION 1.0 CLASS' then + return 'vb' + else + return 'st' + end +end + +function M.conf(path, bufnr) + if vim.fn.did_filetype() ~= 0 or path:find(vim.g.ft_ignore_pat) then + return + end + for _, line in ipairs(getlines(bufnr, 1, 5)) do + if line:find('^#') then + return 'conf' + end + end +end + +-- Debian Control +function M.control(bufnr) + if getlines(bufnr, 1):find('^Source:') then + return 'debcontrol' + end +end + +-- Debian Copyright +function M.copyright(bufnr) + if getlines(bufnr, 1):find('^Format:') then + return 'debcopyright' + end +end + +function M.csh(path, bufnr) + if vim.fn.did_filetype() ~= 0 then + -- Filetype was already detected + return + end + local contents = getlines(bufnr) + if vim.g.filetype_csh then + return M.shell(path, contents, vim.g.filetype_csh) + elseif string.find(vim.o.shell, 'tcsh') then + return M.shell(path, contents, 'tcsh') + else + return M.shell(path, contents, 'csh') + end +end + +local function cvs_diff(path, contents) + for _, line in ipairs(contents) do + if not line:find('^%? ') then + if matchregex(line, [[^Index:\s\+\f\+$]]) then + -- CVS diff + return 'diff' + elseif + -- Locale input files: Formal Definitions of Cultural Conventions + -- Filename must be like en_US, fr_FR@euro or en_US.UTF-8 + findany(path, { + '%a%a_%a%a$', + '%a%a_%a%a[%.@]', + '%a%a_%a%ai18n$', + '%a%a_%a%aPOSIX$', + '%a%a_%a%atranslit_', + }) + then + -- Only look at the first 100 lines + for line_nr = 1, 100 do + if not contents[line_nr] then + break + elseif + findany(contents[line_nr], { + '^LC_IDENTIFICATION$', + '^LC_CTYPE$', + '^LC_COLLATE$', + '^LC_MONETARY$', + '^LC_NUMERIC$', + '^LC_TIME$', + '^LC_MESSAGES$', + '^LC_PAPER$', + '^LC_TELEPHONE$', + '^LC_MEASUREMENT$', + '^LC_NAME$', + '^LC_ADDRESS$', + }) + then + return 'fdcc' + end + end + end + end + end +end + +function M.dat(path, bufnr) + local file_name = vim.fn.fnamemodify(path, ':t'):lower() + -- Innovation data processing + if findany(file_name, { '^upstream%.dat$', '^upstream%..*%.dat$', '^.*%.upstream%.dat$' }) then + return 'upstreamdat' + end + if vim.g.filetype_dat then + return vim.g.filetype_dat + end + -- Determine if a *.dat file is Kuka Robot Language + local line = nextnonblank(bufnr, 1) + if matchregex(line, [[\c\v^\s*%(\&\w+|defdat>)]]) then + return 'krl' + end +end + +function M.decl(bufnr) + for _, line in ipairs(getlines(bufnr, 1, 3)) do + if line:lower():find('^<!sgml') then + return 'sgmldecl' + end + end +end + +-- This function is called for all files under */debian/patches/*, make sure not +-- to non-dep3patch files, such as README and other text files. +function M.dep3patch(path, bufnr) + local file_name = vim.fn.fnamemodify(path, ':t') + if file_name == 'series' then + return + end + + for _, line in ipairs(getlines(bufnr, 1, 100)) do + if + findany(line, { + '^Description:', + '^Subject:', + '^Origin:', + '^Bug:', + '^Forwarded:', + '^Author:', + '^From:', + '^Reviewed%-by:', + '^Acked%-by:', + '^Last%-Updated:', + '^Applied%-Upstream:', + }) + then + return 'dep3patch' + elseif line:find('^%-%-%-') then + -- End of headers found. stop processing + return + end + end +end + +local function diff(contents) + if + contents[1]:find('^%-%-%- ') and contents[2]:find('^%+%+%+ ') + or contents[1]:find('^%* looking for ') and contents[2]:find('^%* comparing to ') + or contents[1]:find('^%*%*%* ') and contents[2]:find('^%-%-%- ') + or contents[1]:find('^=== ') and ((contents[2]:find('^' .. string.rep('=', 66)) and contents[3]:find( + '^%-%-% ' + ) and contents[4]:find('^%+%+%+')) or (contents[2]:find('^%-%-%- ') and contents[3]:find( + '^%+%+%+ ' + ))) + or findany(contents[1], { '^=== removed', '^=== added', '^=== renamed', '^=== modified' }) + then + return 'diff' + end +end + +function M.dns_zone(contents) + if + findany( + contents[1] .. contents[2] .. contents[3] .. contents[4], + { '^; <<>> DiG [0-9%.]+.* <<>>', '%$ORIGIN', '%$TTL', 'IN%s+SOA' } + ) + then + return 'bindzone' + end + -- BAAN + if -- Check for 1 to 80 '*' characters + contents[1]:find('|%*' .. string.rep('%*?', 79)) and contents[2]:find('VRC ') + or contents[2]:find('|%*' .. string.rep('%*?', 79)) and contents[3]:find('VRC ') + then + return 'baan' + end +end + +function M.dtrace(bufnr) + if vim.fn.did_filetype() ~= 0 then + -- Filetype was already detected + return + end + for _, line in ipairs(getlines(bufnr, 1, 100)) do + if matchregex(line, [[\c^module\>\|^import\>]]) then + -- D files often start with a module and/or import statement. + return 'd' + elseif findany(line, { '^#!%S+dtrace', '#pragma%s+D%s+option', ':%S-:%S-:' }) then + return 'dtrace' + end + end + return 'd' +end + +function M.e(bufnr) + if vim.g.filetype_euphoria then + return vim.g.filetype_euphoria + end + for _, line in ipairs(getlines(bufnr, 1, 100)) do + if findany(line, { "^%s*<'%s*$", "^%s*'>%s*$" }) then + return 'specman' + end + end + return 'eiffel' +end + +function M.edn(bufnr) + local line = getlines(bufnr, 1) + if matchregex(line, [[\c^\s*(\s*edif\>]]) then + return 'edif' + else + return 'clojure' + end +end + +-- This function checks for valid cl syntax in the first five lines. +-- Look for either an opening comment, '#', or a block start, '{'. +-- If not found, assume SGML. +function M.ent(bufnr) + for _, line in ipairs(getlines(bufnr, 1, 5)) do + if line:find('^%s*[#{]') then + return 'cl' + elseif not line:find('^%s*$') then + -- Not a blank line, not a comment, and not a block start, + -- so doesn't look like valid cl code. + break + end + end + return 'dtd' +end + +function M.ex(bufnr) + if vim.g.filetype_euphoria then + return vim.g.filetype_euphoria + else + for _, line in ipairs(getlines(bufnr, 1, 100)) do + if matchregex(line, [[\c^--\|^ifdef\>\|^include\>]]) then + return 'euphoria3' + end + end + return 'elixir' + end +end + +-- This function checks the first 15 lines for appearance of 'FoamFile' +-- and then 'object' in a following line. +-- In that case, it's probably an OpenFOAM file +function M.foam(bufnr) + local foam_file = false + for _, line in ipairs(getlines(bufnr, 1, 15)) do + if line:find('^FoamFile') then + foam_file = true + elseif foam_file and line:find('^%s*object') then + return 'foam' + end + end +end + +function M.frm(bufnr) + if vim.g.filetype_frm then + return vim.g.filetype_frm + end + local lines = table.concat(getlines(bufnr, 1, 5)):lower() + if findany(lines, visual_basic_content) then + return 'vb' + else + return 'form' + end +end + +function M.fvwm(path) + if vim.fn.fnamemodify(path, ':e') == 'm4' then + return 'fvwm2m4' + end + return 'fvwm', function(bufnr) + vim.b[bufnr].fvwm_version = 2 + end +end + +-- Distinguish between Forth and F#. +function M.fs(bufnr) + if vim.g.filetype_fs then + return vim.g.filetype_fs + end + local line = nextnonblank(bufnr, 1) + if findany(line, { '^%s*%.?%( ', '^%s*\\G? ', '^\\$', '^%s*: %S' }) then + return 'forth' + else + return 'fsharp' + end +end + +function M.git(bufnr) + local line = getlines(bufnr, 1) + if matchregex(line, [[^\x\{40,\}\>\|^ref: ]]) then + return 'git' + end +end + +function M.header(bufnr) + for _, line in ipairs(getlines(bufnr, 1, 200)) do + if findany(line:lower(), { '^@interface', '^@end', '^@class' }) then + if vim.g.c_syntax_for_h then + return 'objc' + else + return 'objcpp' + end + end + end + if vim.g.c_syntax_for_h then + return 'c' + elseif vim.g.ch_syntax_for_h then + return 'ch' + else + return 'cpp' + end +end + +function M.html(bufnr) + for _, line in ipairs(getlines(bufnr, 1, 10)) do + if matchregex(line, [[\<DTD\s\+XHTML\s]]) then + return 'xhtml' + elseif matchregex(line, [[\c{%\s*\(extends\|block\|load\)\>\|{#\s\+]]) then + return 'htmldjango' + end + end + return 'html' +end + +-- Virata Config Script File or Drupal module +function M.hw(bufnr) + if getlines(bufnr, 1):lower():find('<%?php') then + return 'php' + end + return 'virata' +end + +function M.idl(bufnr) + for _, line in ipairs(getlines(bufnr, 1, 50)) do + if findany(line:lower(), { '^%s*import%s+"unknwn"%.idl', '^%s*import%s+"objidl"%.idl' }) then + return 'msidl' + end + end + return 'idl' +end + +local pascal_comments = { '^%s*{', '^%s*%(%*', '^%s*//' } +local pascal_keywords = + [[\c^\s*\%(program\|unit\|library\|uses\|begin\|procedure\|function\|const\|type\|var\)\>]] + +function M.inc(bufnr) + if vim.g.filetype_inc then + return vim.g.filetype_inc + end + local lines = table.concat(getlines(bufnr, 1, 3)) + if lines:lower():find('perlscript') then + return 'aspperl' + elseif lines:find('<%%') then + return 'aspvbs' + elseif lines:find('<%?') then + return 'php' + -- Pascal supports // comments but they're vary rarely used for file + -- headers so assume POV-Ray + elseif findany(lines, { '^%s{', '^%s%(%*' }) or matchregex(lines, pascal_keywords) then + return 'pascal' + elseif findany(lines, { '^%s*inherit ', '^%s*require ', '^%s*%w+%s+= ' }) then + return 'bitbake' + else + local syntax = M.asm_syntax(bufnr) + if not syntax or syntax == '' then + return 'pov' + end + return syntax, function(b) + vim.b[b].asmsyntax = syntax + end + end +end + +function M.inp(bufnr) + if getlines(bufnr, 1):find('^%*') then + return 'abaqus' + else + for _, line in ipairs(getlines(bufnr, 1, 500)) do + if line:lower():find('^header surface data') then + return 'trasys' + end + end + end +end + +function M.install(path, bufnr) + if getlines(bufnr, 1):lower():find('<%?php') then + return 'php' + end + return M.sh(path, getlines(bufnr), 'bash') +end + +-- Innovation Data Processing +-- (refactor of filetype.vim since the patterns are case-insensitive) +function M.log(path) + path = path:lower() + if + findany( + path, + { 'upstream%.log', 'upstream%..*%.log', '.*%.upstream%.log', 'upstream%-.*%.log' } + ) + then + return 'upstreamlog' + elseif + findany( + path, + { 'upstreaminstall%.log', 'upstreaminstall%..*%.log', '.*%.upstreaminstall%.log' } + ) + then + return 'upstreaminstalllog' + elseif findany(path, { 'usserver%.log', 'usserver%..*%.log', '.*%.usserver%.log' }) then + return 'usserverlog' + elseif findany(path, { 'usw2kagt%.log', 'usw2kagt%..*%.log', '.*%.usw2kagt%.log' }) then + return 'usw2kagtlog' + end +end + +function M.lpc(bufnr) + if vim.g.lpc_syntax_for_c then + for _, line in ipairs(getlines(bufnr, 1, 12)) do + if + findany(line, { + '^//', + '^inherit', + '^private', + '^protected', + '^nosave', + '^string', + '^object', + '^mapping', + '^mixed', + }) + then + return 'lpc' + end + end + end + return 'c' +end + +function M.m(bufnr) + if vim.g.filetype_m then + return vim.g.filetype_m + end + + -- Excluding end(for|function|if|switch|while) common to Murphi + local octave_block_terminators = + [[\<end\%(_try_catch\|classdef\|enumeration\|events\|methods\|parfor\|properties\)\>]] + local objc_preprocessor = + [[\c^\s*#\s*\%(import\|include\|define\|if\|ifn\=def\|undef\|line\|error\|pragma\)\>]] + + -- Whether we've seen a multiline comment leader + local saw_comment = false + for _, line in ipairs(getlines(bufnr, 1, 100)) do + if line:find('^%s*/%*') then + -- /* ... */ is a comment in Objective C and Murphi, so we can't conclude + -- it's either of them yet, but track this as a hint in case we don't see + -- anything more definitive. + saw_comment = true + end + if + line:find('^%s*//') + or matchregex(line, [[\c^\s*@import\>]]) + or matchregex(line, objc_preprocessor) + then + return 'objc' + end + if + findany(line, { '^%s*#', '^%s*%%!' }) + or matchregex(line, [[\c^\s*unwind_protect\>]]) + or matchregex(line, [[\c\%(^\|;\)\s*]] .. octave_block_terminators) + then + return 'octave' + elseif line:find('^%s*%%') then + return 'matlab' + elseif line:find('^%s*%(%*') then + return 'mma' + elseif matchregex(line, [[\c^\s*\(\(type\|var\)\>\|--\)]]) then + return 'murphi' + end + end + + if saw_comment then + -- We didn't see anything definitive, but this looks like either Objective C + -- or Murphi based on the comment leader. Assume the former as it is more + -- common. + return 'objc' + else + -- Default is Matlab + return 'matlab' + end +end + +local function m4(contents) + for _, line in ipairs(contents) do + if matchregex(line, [[^\s*dnl\>]]) then + return 'm4' + end + end + if vim.env.TERM == 'amiga' and findany(contents[1]:lower(), { '^;', '^%.bra' }) then + -- AmigaDos scripts + return 'amiga' + end +end + +-- Rely on the file to start with a comment. +-- MS message text files use ';', Sendmail files use '#' or 'dnl' +function M.mc(bufnr) + for _, line in ipairs(getlines(bufnr, 1, 20)) do + if findany(line:lower(), { '^%s*#', '^%s*dnl' }) then + -- Sendmail .mc file + return 'm4' + elseif line:find('^%s*;') then + return 'msmessages' + end + end + -- Default: Sendmail .mc file + return 'm4' +end + +function M.me(path) + local filename = vim.fn.fnamemodify(path, ':t'):lower() + if filename ~= 'read.me' and filename ~= 'click.me' then + return 'nroff' + end +end + +function M.mm(bufnr) + for _, line in ipairs(getlines(bufnr, 1, 20)) do + if matchregex(line, [[\c^\s*\(#\s*\(include\|import\)\>\|@import\>\|/\*\)]]) then + return 'objcpp' + end + end + return 'nroff' +end + +function M.mms(bufnr) + for _, line in ipairs(getlines(bufnr, 1, 20)) do + if findany(line, { '^%s*%%', '^%s*//', '^%*' }) then + return 'mmix' + elseif line:find('^%s*#') then + return 'make' + end + end + return 'mmix' +end + +-- Returns true if file content looks like LambdaProlog +local function is_lprolog(bufnr) + -- Skip apparent comments and blank lines, what looks like + -- LambdaProlog comment may be RAPID header + for _, line in ipairs(getlines(bufnr)) do + -- The second pattern matches a LambdaProlog comment + if not findany(line, { '^%s*$', '^%s*%%' }) then + -- The pattern must not catch a go.mod file + return matchregex(line, [[\c\<module\s\+\w\+\s*\.\s*\(%\|$\)]]) ~= nil + end + end +end + +-- Determine if *.mod is ABB RAPID, LambdaProlog, Modula-2, Modsim III or go.mod +function M.mod(path, bufnr) + if vim.g.filetype_mod then + return vim.g.filetype_mod + elseif is_lprolog(bufnr) then + return 'lprolog' + elseif matchregex(nextnonblank(bufnr, 1), [[\%(\<MODULE\s\+\w\+\s*;\|^\s*(\*\)]]) then + return 'modula2' + elseif is_rapid(bufnr) then + return 'rapid' + elseif matchregex(path, [[\c\<go\.mod$]]) then + return 'gomod' + else + -- Nothing recognized, assume modsim3 + return 'modsim3' + end +end + +function M.news(bufnr) + if getlines(bufnr, 1):lower():find('; urgency=') then + return 'debchangelog' + end +end + +-- This function checks if one of the first five lines start with a dot. In +-- that case it is probably an nroff file. +function M.nroff(bufnr) + for _, line in ipairs(getlines(bufnr, 1, 5)) do + if line:find('^%.') then + return 'nroff' + end + end +end + +function M.patch(bufnr) + local firstline = getlines(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 + +-- If the file has an extension of 't' and is in a directory 't' or 'xt' then +-- it is almost certainly a Perl test file. +-- If the first line starts with '#' and contains 'perl' it's probably a Perl file. +-- (Slow test) If a file contains a 'use' statement then it is almost certainly a Perl file. +function M.perl(path, bufnr) + local dir_name = vim.fs.dirname(path) + if vim.fn.expand(path, '%:e') == 't' and (dir_name == 't' or dir_name == 'xt') then + return 'perl' + end + local first_line = getlines(bufnr, 1) + if first_line:find('^#') and first_line:lower():find('perl') then + return 'perl' + end + for _, line in ipairs(getlines(bufnr, 1, 30)) do + if matchregex(line, [[\c^use\s\s*\k]]) then + return 'perl' + end + end +end + +function M.pl(bufnr) + if vim.g.filetype_pl then + return vim.g.filetype_pl + end + -- Recognize Prolog by specific text in the first non-empty line; + -- require a blank after the '%' because Perl uses "%list" and "%translate" + local line = nextnonblank(bufnr, 1) + if + line and line:find(':%-') + or matchregex(line, [[\c\<prolog\>]]) + or findany(line, { '^%s*%%+%s', '^%s*%%+$', '^%s*/%*' }) + then + return 'prolog' + else + return 'perl' + end +end + +function M.pm(bufnr) + local line = getlines(bufnr, 1) + if line:find('XPM2') then + return 'xpm2' + elseif line:find('XPM') then + return 'xpm' + else + return 'perl' + end +end + +function M.pp(bufnr) + if vim.g.filetype_pp then + return vim.g.filetype_pp + end + local line = nextnonblank(bufnr, 1) + if findany(line, pascal_comments) or matchregex(line, pascal_keywords) then + return 'pascal' + else + return 'puppet' + end +end + +function M.prg(bufnr) + if vim.g.filetype_prg then + return vim.g.filetype_prg + elseif is_rapid(bufnr) then + return 'rapid' + else + -- Nothing recognized, assume Clipper + return 'clipper' + end +end + +function M.printcap(ptcap_type) + if vim.fn.did_filetype() == 0 then + return 'ptcap', function(bufnr) + vim.b[bufnr].ptcap_type = ptcap_type + end + end +end + +-- This function checks for an assembly comment in the first ten lines. +-- If not found, assume Progress. +function M.progress_asm(bufnr) + if vim.g.filetype_i then + return vim.g.filetype_i + end + + for _, line in ipairs(getlines(bufnr, 1, 10)) do + if line:find('^%s*;') or line:find('^/%*') then + return M.asm(bufnr) + elseif not line:find('^%s*$') or line:find('^/%*') then + -- Not an empty line: doesn't look like valid assembly code + -- or it looks like a Progress /* comment. + break + end + end + return 'progress' +end + +function M.progress_cweb(bufnr) + if vim.g.filetype_w then + return vim.g.filetype_w + else + if + getlines(bufnr, 1):lower():find('^&analyze') + or getlines(bufnr, 3):lower():find('^&global%-define') + then + return 'progress' + else + return 'cweb' + end + end +end + +-- This function checks for valid Pascal syntax in the first 10 lines. +-- Look for either an opening comment or a program start. +-- If not found, assume Progress. +function M.progress_pascal(bufnr) + if vim.g.filetype_p then + return vim.g.filetype_p + end + for _, line in ipairs(getlines(bufnr, 1, 10)) do + if findany(line, pascal_comments) or matchregex(line, pascal_keywords) then + return 'pascal' + elseif not line:find('^%s*$') or line:find('^/%*') then + -- Not an empty line: Doesn't look like valid Pascal code. + -- Or it looks like a Progress /* comment + break + end + end + return 'progress' +end + +-- Distinguish between "default" and Cproto prototype file. +function M.proto(bufnr, default) + -- Cproto files have a comment in the first line and a function prototype in + -- the second line, it always ends in ";". Indent files may also have + -- comments, thus we can't match comments to see the difference. + -- IDL files can have a single ';' in the second line, require at least one + -- character before the ';'. + if getlines(bufnr, 2):find('.;$') then + return 'cpp' + else + return default + end +end + +-- Software Distributor Product Specification File (POSIX 1387.2-1995) +function M.psf(bufnr) + local line = getlines(bufnr, 1):lower() + if + findany(line, { + '^%s*distribution%s*$', + '^%s*installed_software%s*$', + '^%s*root%s*$', + '^%s*bundle%s*$', + '^%s*product%s*$', + }) + then + return 'psf' + end +end + +function M.r(bufnr) + local lines = getlines(bufnr, 1, 50) + -- Rebol is easy to recognize, check for that first + if matchregex(table.concat(lines), [[\c\<rebol\>]]) then + return 'rebol' + end + + for _, line in ipairs(lines) do + -- R has # comments + if line:find('^%s*#') then + return 'r' + end + -- Rexx has /* comments */ + if line:find('^%s*/%*') then + return 'rexx' + end + end + + -- Nothing recognized, use user default or assume R + if vim.g.filetype_r then + return vim.g.filetype_r + else + -- Rexx used to be the default, but R appears to be much more popular. + return 'r' + end +end + +function M.redif(bufnr) + for _, line in ipairs(getlines(bufnr, 1, 5)) do + if line:lower():find('^template%-type:') then + return 'redif' + end + end +end + +function M.reg(bufnr) + local line = getlines(bufnr, 1):lower() + if + line:find('^regedit[0-9]*%s*$') or line:find('^windows registry editor version %d*%.%d*%s*$') + then + return 'registry' + end +end + +-- Diva (with Skill) or InstallShield +function M.rul(bufnr) + if table.concat(getlines(bufnr, 1, 6)):lower():find('installshield') then + return 'ishd' + end + return 'diva' +end + +local udev_rules_pattern = '^%s*udev_rules%s*=%s*"([%^"]+)/*".*' +function M.rules(path) + path = path:lower() + if + findany(path, { + '/etc/udev/.*%.rules$', + '/etc/udev/rules%.d/.*$.rules$', + '/usr/lib/udev/.*%.rules$', + '/usr/lib/udev/rules%.d/.*%.rules$', + '/lib/udev/.*%.rules$', + '/lib/udev/rules%.d/.*%.rules$', + }) + then + return 'udevrules' + elseif path:find('^/etc/ufw/') then + -- Better than hog + return 'conf' + elseif findany(path, { '^/etc/polkit%-1/rules%.d', '/usr/share/polkit%-1/rules%.d' }) then + return 'javascript' + else + local ok, config_lines = pcall(vim.fn.readfile, '/etc/udev/udev.conf') + if not ok then + return 'hog' + end + local dir = vim.fn.expand(path, ':h') + for _, line in ipairs(config_lines) do + local match = line:match(udev_rules_pattern) + if match then + local udev_rules = line:gsub(udev_rules_pattern, match, 1) + if dir == udev_rules then + return 'udevrules' + end + end + end + return 'hog' + end +end + +-- LambdaProlog and Standard ML signature files +function M.sig(bufnr) + if vim.g.filetype_sig then + return vim.g.filetype_sig + end + + local line = nextnonblank(bufnr, 1) + + -- LambdaProlog comment or keyword + if findany(line, { '^%s*/%*', '^%s*%%', '^%s*sig%s+%a' }) then + return 'lprolog' + -- SML comment or keyword + elseif findany(line, { '^%s*%(%*', '^%s*signature%s+%a', '^%s*structure%s+%a' }) then + return 'sml' + end +end + +-- This function checks the first 25 lines of file extension "sc" to resolve +-- detection between scala and SuperCollider +function M.sc(bufnr) + for _, line in ipairs(getlines(bufnr, 1, 25)) do + if + findany(line, { + '[A-Za-z0-9]*%s:%s[A-Za-z0-9]', + 'var%s<', + 'classvar%s<', + '%^this.*', + '|%w*|', + '%+%s%w*%s{', + '%*ar%s', + }) + then + return 'supercollider' + end + end + return 'scala' +end + +-- This function checks the first line of file extension "scd" to resolve +-- detection between scdoc and SuperCollider +function M.scd(bufnr) + local first = '^%S+%(%d[0-9A-Za-z]*%)' + local opt = [[%s+"[^"]*"]] + local line = getlines(bufnr, 1) + if findany(line, { first .. '$', first .. opt .. '$', first .. opt .. opt .. '$' }) then + return 'scdoc' + else + return 'supercollider' + end +end + +function M.sgml(bufnr) + local lines = table.concat(getlines(bufnr, 1, 5)) + if lines:find('linuxdoc') then + return 'smgllnx' + elseif lines:find('<!DOCTYPE.*DocBook') then + return 'docbk', + function(b) + vim.b[b].docbk_type = 'sgml' + vim.b[b].docbk_ver = 4 + end + else + return 'sgml' + end +end + +function M.sh(path, contents, name) + -- Path may be nil, do not fail in that case + if vim.fn.did_filetype() ~= 0 or (path or ''):find(vim.g.ft_ignore_pat) then + -- Filetype was already detected or detection should be skipped + return + end + + local on_detect + + -- Get the name from the first line if not specified + name = name or contents[1] + if matchregex(name, [[\<csh\>]]) then + -- Some .sh scripts contain #!/bin/csh. + return M.shell(path, contents, 'csh') + -- Some .sh scripts contain #!/bin/tcsh. + elseif matchregex(name, [[\<tcsh\>]]) then + return M.shell(path, contents, 'tcsh') + -- Some .sh scripts contain #!/bin/zsh. + elseif matchregex(name, [[\<zsh\>]]) then + return M.shell(path, contents, 'zsh') + elseif matchregex(name, [[\<ksh\>]]) then + on_detect = function(b) + vim.b[b].is_kornshell = 1 + vim.b[b].is_bash = nil + vim.b[b].is_sh = nil + end + elseif vim.g.bash_is_sh or matchregex(name, [[\<bash\>]]) or matchregex(name, [[\<bash2\>]]) then + on_detect = function(b) + vim.b[b].is_bash = 1 + vim.b[b].is_kornshell = nil + vim.b[b].is_sh = nil + end + elseif matchregex(name, [[\<sh\>]]) then + on_detect = function(b) + vim.b[b].is_sh = 1 + vim.b[b].is_kornshell = nil + vim.b[b].is_bash = nil + end + end + return M.shell(path, contents, 'sh'), on_detect +end + +-- For shell-like file types, check for an "exec" command hidden in a comment, as used for Tcl. +function M.shell(path, contents, name) + if vim.fn.did_filetype() ~= 0 or matchregex(path, vim.g.ft_ignore_pat) then + -- Filetype was already detected or detection should be skipped + return + end + + local prev_line = '' + for line_nr, line in ipairs(contents) do + -- Skip the first line + if line_nr ~= 1 then + line = line:lower() + if line:find('%s*exec%s') and not prev_line:find('^%s*#.*\\$') then + -- Found an "exec" line after a comment with continuation + local n = line:gsub('%s*exec%s+([^ ]*/)?', '', 1) + if matchregex(n, [[\c\<tclsh\|\<wish]]) then + return 'tcl' + end + end + prev_line = line + end + end + return name +end + +-- SMIL or SNMP MIB file +function M.smi(bufnr) + local line = getlines(bufnr, 1) + if matchregex(line, [[\c\<smil\>]]) then + return 'smil' + else + return 'mib' + end +end + +-- Determine if a *.src file is Kuka Robot Language +function M.src(bufnr) + if vim.g.filetype_src then + return vim.g.filetype_src + end + local line = nextnonblank(bufnr, 1) + if matchregex(line, [[\c\v^\s*%(\&\w+|%(global\s+)?def%(fct)?>)]]) then + return 'krl' + end +end + +function M.sys(bufnr) + if vim.g.filetype_sys then + return vim.g.filetype_sys + elseif is_rapid(bufnr) then + return 'rapid' + else + return 'bat' + end +end + +-- Choose context, plaintex, or tex (LaTeX) based on these rules: +-- 1. Check the first line of the file for "%&<format>". +-- 2. Check the first 1000 non-comment lines for LaTeX or ConTeXt keywords. +-- 3. Default to "plain" or to g:tex_flavor, can be set in user's vimrc. +function M.tex(path, bufnr) + local format = getlines(bufnr, 1):find('^%%&%s*(%a+)') + if format then + format = format:lower():gsub('pdf', '', 1) + if format == 'tex' then + return 'tex' + elseif format == 'plaintex' then + return 'plaintex' + end + elseif path:lower():find('tex/context/.*/.*%.tex') then + return 'context' + else + local lpat = [[documentclass\>\|usepackage\>\|begin{\|newcommand\>\|renewcommand\>]] + local cpat = + [[start\a\+\|setup\a\+\|usemodule\|enablemode\|enableregime\|setvariables\|useencoding\|usesymbols\|stelle\a\+\|verwende\a\+\|stel\a\+\|gebruik\a\+\|usa\a\+\|imposta\a\+\|regle\a\+\|utilisemodule\>]] + + for i, l in ipairs(getlines(bufnr, 1, 1000)) do + -- Find first non-comment line + if not l:find('^%s*%%%S') then + -- Check the next thousand lines for a LaTeX or ConTeXt keyword. + for _, line in ipairs(getlines(bufnr, i + 1, i + 1000)) do + local lpat_match, cpat_match = + matchregex(line, [[\c^\s*\\\%(]] .. lpat .. [[\)\|^\s*\\\(]] .. cpat .. [[\)]]) + if lpat_match then + return 'tex' + elseif cpat_match then + return 'context' + end + end + end + end + -- TODO: add AMSTeX, RevTex, others? + if not vim.g.tex_flavor or vim.g.tex_flavor == 'plain' then + return 'plaintex' + elseif vim.g.tex_flavor == 'context' then + return 'context' + else + -- Probably LaTeX + return 'tex' + end + end +end + +-- Determine if a *.tf file is TF mud client or terraform +function M.tf(bufnr) + for _, line in ipairs(getlines(bufnr)) do + -- Assume terraform file on a non-empty line (not whitespace-only) + -- and when the first non-whitespace character is not a ; or / + if not line:find('^%s*$') and not line:find('^%s*[;/]') then + return 'terraform' + end + end + return 'tf' +end + +function M.ttl(bufnr) + local line = getlines(bufnr, 1):lower() + if line:find('^@?prefix') or line:find('^@?base') then + return 'turtle' + end + return 'teraterm' +end + +function M.txt(bufnr) + -- helpfiles match *.txt, but should have a modeline as last line + if not getlines(bufnr, -1):find('vim:.*ft=help') then + return 'text' + end +end + +-- WEB (*.web is also used for Winbatch: Guess, based on expecting "%" comment +-- lines in a WEB file). +function M.web(bufnr) + for _, line in ipairs(getlines(bufnr, 1, 5)) do + if line:find('^%%') then + return 'web' + end + end + return 'winbatch' +end + +-- XFree86 config +function M.xfree86() + return 'xf86conf', + function(bufnr) + local line = getlines(bufnr, 1) + if matchregex(line, [[\<XConfigurator\>]]) then + vim.b[bufnr].xf86conf_xfree86_version = 3 + end + end +end + +function M.xml(bufnr) + for _, line in ipairs(getlines(bufnr, 1, 100)) do + local is_docbook4 = line:find('<!DOCTYPE.*DocBook') + line = line:lower() + local is_docbook5 = line:find([[ xmlns="http://docbook.org/ns/docbook"]]) + if is_docbook4 or is_docbook5 then + return 'docbk', + function(b) + vim.b[b].docbk_type = 'xml' + vim.b[b].docbk_ver = is_docbook4 and 4 or 5 + end + end + if line:find([[xmlns:xbl="http://www.mozilla.org/xbl"]]) then + return 'xbl' + end + end + return 'xml' +end + +function M.y(bufnr) + for _, line in ipairs(getlines(bufnr, 1, 100)) do + if line:find('^%s*%%') then + return 'yacc' + end + if matchregex(line, [[\c^\s*\(#\|class\>\)]]) and not line:lower():find('^%s*#%s*include') then + return 'racc' + end + end + return 'yacc' +end + +-- luacheck: pop +-- luacheck: pop + +local patterns_hashbang = { + ['^zsh\\>'] = { 'zsh', { vim_regex = true } }, + ['^\\(tclsh\\|wish\\|expectk\\|itclsh\\|itkwish\\)\\>'] = { 'tcl', { vim_regex = true } }, + ['^expect\\>'] = { 'expect', { vim_regex = true } }, + ['^gnuplot\\>'] = { 'gnuplot', { vim_regex = true } }, + ['make\\>'] = { 'make', { vim_regex = true } }, + ['^pike\\%(\\>\\|[0-9]\\)'] = { 'pike', { vim_regex = true } }, + lua = 'lua', + perl = 'perl', + php = 'php', + python = 'python', + ['^groovy\\>'] = { 'groovy', { vim_regex = true } }, + raku = 'raku', + ruby = 'ruby', + ['node\\(js\\)\\=\\>\\|js\\>'] = { 'javascript', { vim_regex = true } }, + ['rhino\\>'] = { 'javascript', { vim_regex = true } }, + -- BC calculator + ['^bc\\>'] = { 'bc', { vim_regex = true } }, + ['sed\\>'] = { 'sed', { vim_regex = true } }, + ocaml = 'ocaml', + -- Awk scripts; also finds "gawk" + ['awk\\>'] = { 'awk', { vim_regex = true } }, + wml = 'wml', + scheme = 'scheme', + cfengine = 'cfengine', + escript = 'erlang', + haskell = 'haskell', + clojure = 'clojure', + ['scala\\>'] = { 'scala', { vim_regex = true } }, + -- Free Pascal + ['instantfpc\\>'] = { 'pascal', { vim_regex = true } }, + ['fennel\\>'] = { 'fennel', { vim_regex = true } }, + -- MikroTik RouterOS script + ['rsc\\>'] = { 'routeros', { vim_regex = true } }, + ['fish\\>'] = { 'fish', { vim_regex = true } }, + ['gforth\\>'] = { 'forth', { vim_regex = true } }, + ['icon\\>'] = { 'icon', { vim_regex = true } }, + guile = 'scheme', +} + +---@private +-- File starts with "#!". +local function match_from_hashbang(contents, path) + local first_line = contents[1] + -- Check for a line like "#!/usr/bin/env {options} bash". Turn it into + -- "#!/usr/bin/bash" to make matching easier. + -- Recognize only a few {options} that are commonly used. + if matchregex(first_line, [[^#!\s*\S*\<env\s]]) then + first_line = first_line:gsub('%S+=%S+', '') + first_line = first_line + :gsub('%-%-ignore%-environment', '', 1) + :gsub('%-%-split%-string', '', 1) + :gsub('%-[iS]', '', 1) + first_line = vim.fn.substitute(first_line, [[\<env\s\+]], '', '') + end + + -- Get the program name. + -- Only accept spaces in PC style paths: "#!c:/program files/perl [args]". + -- If the word env is used, use the first word after the space: + -- "#!/usr/bin/env perl [path/args]" + -- If there is no path use the first word: "#!perl [path/args]". + -- Otherwise get the last word after a slash: "#!/usr/bin/perl [path/args]". + local name + if first_line:find('^#!%s*%a:[/\\]') then + name = vim.fn.substitute(first_line, [[^#!.*[/\\]\(\i\+\).*]], '\\1', '') + elseif matchregex(first_line, [[^#!.*\<env\>]]) then + name = vim.fn.substitute(first_line, [[^#!.*\<env\>\s\+\(\i\+\).*]], '\\1', '') + elseif matchregex(first_line, [[^#!\s*[^/\\ ]*\>\([^/\\]\|$\)]]) then + name = vim.fn.substitute(first_line, [[^#!\s*\([^/\\ ]*\>\).*]], '\\1', '') + else + name = vim.fn.substitute(first_line, [[^#!\s*\S*[/\\]\(\i\+\).*]], '\\1', '') + end + + -- tcl scripts may have #!/bin/sh in the first line and "exec wish" in the + -- third line. Suggested by Steven Atkinson. + if contents[3] and contents[3]:find('^exec wish') then + name = 'wish' + end + + if matchregex(name, [[^\(bash\d*\|\|ksh\d*\|sh\)\>]]) then + -- Bourne-like shell scripts: bash bash2 ksh ksh93 sh + return require('vim.filetype.detect').sh(path, contents, first_line) + elseif matchregex(name, [[^csh\>]]) then + return require('vim.filetype.detect').shell(path, contents, vim.g.filetype_csh or 'csh') + elseif matchregex(name, [[^tcsh\>]]) then + return require('vim.filetype.detect').shell(path, contents, 'tcsh') + end + + for k, v in pairs(patterns_hashbang) do + local ft = type(v) == 'table' and v[1] or v + local opts = type(v) == 'table' and v[2] or {} + if opts.vim_regex and matchregex(name, k) or name:find(k) then + return ft + end + end +end + +local patterns_text = { + ['^#compdef\\>'] = { 'zsh', { vim_regex = true } }, + ['^#autoload\\>'] = { 'zsh', { vim_regex = true } }, + -- ELM Mail files + ['^From [a-zA-Z][a-zA-Z_0-9%.=%-]*(@[^ ]*)? .* 19%d%d$'] = 'mail', + ['^From [a-zA-Z][a-zA-Z_0-9%.=%-]*(@[^ ]*)? .* 20%d%d$'] = 'mail', + ['^From %- .* 19%d%d$'] = 'mail', + ['^From %- .* 20%d%d$'] = 'mail', + -- Mason + ['^<[%%&].*>'] = 'mason', + -- Vim scripts (must have '" vim' as the first line to trigger this) + ['^" *[vV]im$['] = 'vim', + -- libcxx and libstdc++ standard library headers like ["iostream["] do not have + -- an extension, recognize the Emacs file mode. + ['%-%*%-.*[cC]%+%+.*%-%*%-'] = 'cpp', + ['^\\*\\* LambdaMOO Database, Format Version \\%([1-3]\\>\\)\\@!\\d\\+ \\*\\*$'] = { + 'moo', + { vim_regex = true }, + }, + -- Diff file: + -- - "diff" in first line (context diff) + -- - "Only in " in first line + -- - "--- " in first line and "+++ " in second line (unified diff). + -- - "*** " in first line and "--- " in second line (context diff). + -- - "# It was generated by makepatch " in the second line (makepatch diff). + -- - "Index: <filename>" in the first line (CVS file) + -- - "=== ", line of "=", "---", "+++ " (SVK diff) + -- - "=== ", "--- ", "+++ " (bzr diff, common case) + -- - "=== (removed|added|renamed|modified)" (bzr diff, alternative) + -- - "# HG changeset patch" in first line (Mercurial export format) + ['^\\(diff\\>\\|Only in \\|\\d\\+\\(,\\d\\+\\)\\=[cda]\\d\\+\\>\\|# It was generated by makepatch \\|Index:\\s\\+\\f\\+\\r\\=$\\|===== \\f\\+ \\d\\+\\.\\d\\+ vs edited\\|==== //\\f\\+#\\d\\+\\|# HG changeset patch\\)'] = { + 'diff', + { vim_regex = true }, + }, + function(contents) + return diff(contents) + end, + -- PostScript Files (must have %!PS as the first line, like a2ps output) + ['^%%![ \t]*PS'] = 'postscr', + function(contents) + return m4(contents) + end, + -- SiCAD scripts (must have procn or procd as the first line to trigger this) + ['^ *proc[nd] *$'] = { 'sicad', { ignore_case = true } }, + ['^%*%*%*%* Purify'] = 'purifylog', + -- XML + ['<%?%s*xml.*%?>'] = 'xml', + -- XHTML (e.g.: PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN") + ['\\<DTD\\s\\+XHTML\\s'] = 'xhtml', + -- HTML (e.g.: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN") + -- Avoid "doctype html", used by slim. + ['\\c<!DOCTYPE\\s\\+html\\>'] = { 'html', { vim_regex = true } }, + -- PDF + ['^%%PDF%-'] = 'pdf', + -- XXD output + ['^%x%x%x%x%x%x%x: %x%x ?%x%x ?%x%x ?%x%x '] = 'xxd', + -- RCS/CVS log output + ['^RCS file:'] = { 'rcslog', { start_lnum = 1, end_lnum = 2 } }, + -- CVS commit + ['^CVS:'] = { 'cvs', { start_lnum = 2 } }, + ['^CVS: '] = { 'cvs', { start_lnum = -1 } }, + -- Prescribe + ['^!R!'] = 'prescribe', + -- Send-pr + ['^SEND%-PR:'] = 'sendpr', + -- SNNS files + ['^SNNS network definition file'] = 'snnsnet', + ['^SNNS pattern definition file'] = 'snnspat', + ['^SNNS result file'] = 'snnsres', + ['^%%.-[Vv]irata'] = { 'virata', { start_lnum = 1, end_lnum = 5 } }, + ['[0-9:%.]* *execve%('] = 'strace', + ['^__libc_start_main'] = 'strace', + -- VSE JCL + ['^\\* $$ JOB\\>'] = { 'vsejcl', { vim_regex = true } }, + ['^// *JOB\\>'] = { 'vsejcl', { vim_regex = true } }, + -- TAK and SINDA + ['K & K Associates'] = { 'takout', { start_lnum = 4 } }, + ['TAK 2000'] = { 'takout', { start_lnum = 2 } }, + ['S Y S T E M S I M P R O V E D '] = { 'syndaout', { start_lnum = 3 } }, + ['Run Date: '] = { 'takcmp', { start_lnum = 6 } }, + ['Node File 1'] = { 'sindacmp', { start_lnum = 9 } }, + function(contents) + require('vim.filetype.detect').dns_zone(contents) + end, + -- Valgrind + ['^==%d+== valgrind'] = 'valgrind', + ['^==%d+== Using valgrind'] = { 'valgrind', { start_lnum = 3 } }, + -- Go docs + ['PACKAGE DOCUMENTATION$'] = 'godoc', + -- Renderman Interface Bytestream + ['^##RenderMan'] = 'rib', + -- Scheme scripts + ['exec%s%+%S*scheme'] = { 'scheme', { start_lnum = 1, end_lnum = 2 } }, + -- Git output + ['^\\(commit\\|tree\\|object\\) \\x\\{40,\\}\\>\\|^tag \\S\\+$'] = { + 'git', + { vim_regex = true }, + }, + function(lines) + -- Gprof (gnu profiler) + if + lines[1] == 'Flat profile:' + and lines[2] == '' + and lines[3]:find('^Each sample counts as .* seconds%.$') + then + return 'gprof' + end + end, + -- Erlang terms + -- (See also: http://www.gnu.org/software/emacs/manual/html_node/emacs/Choosing-Modes.html#Choosing-Modes) + ['%-%*%-.*erlang.*%-%*%-'] = { 'erlang', { ignore_case = true } }, + -- YAML + ['^%%YAML'] = 'yaml', + -- MikroTik RouterOS script + ['^#.*by RouterOS'] = 'routeros', + -- Sed scripts + -- #ncomment is allowed but most likely a false positive so require a space before any trailing comment text + ['^#n%s'] = 'sed', + ['^#n$'] = 'sed', +} + +---@private +-- File does not start with "#!". +local function match_from_text(contents, path) + if contents[1]:find('^:$') then + -- Bourne-like shell scripts: sh ksh bash bash2 + return M.sh(path, contents) + elseif + matchregex( + '\n' .. table.concat(contents, '\n'), + [[\n\s*emulate\s\+\%(-[LR]\s\+\)\=[ckz]\=sh\>]] + ) + then + -- Z shell scripts + return 'zsh' + end + + for k, v in pairs(patterns_text) do + if type(v) == 'string' then + -- Check the first line only + if contents[1]:find(k) then + return v + end + elseif type(v) == 'function' then + -- If filetype detection fails, continue with the next pattern + local ok, ft = pcall(v, contents) + if ok and ft then + return ft + end + else + local opts = type(v) == 'table' and v[2] or {} + if opts.start_lnum and opts.end_lnum then + assert( + not opts.ignore_case, + 'ignore_case=true is ignored when start_lnum is also present, needs refactor' + ) + for i = opts.start_lnum, opts.end_lnum do + if not contents[i] then + break + elseif contents[i]:find(k) then + return v[1] + end + end + else + local line_nr = opts.start_lnum == -1 and #contents or opts.start_lnum or 1 + if contents[line_nr] then + local line = opts.ignore_case and contents[line_nr]:lower() or contents[line_nr] + if opts.vim_regex and matchregex(line, k) or line:find(k) then + return v[1] + end + end + end + end + end + return cvs_diff(path, contents) +end + +M.match_contents = function(contents, path) + local first_line = contents[1] + if first_line:find('^#!') then + return match_from_hashbang(contents, path) + else + return match_from_text(contents, path) + end +end + +return M diff --git a/runtime/lua/vim/fs.lua b/runtime/lua/vim/fs.lua new file mode 100644 index 0000000000..ce845eda15 --- /dev/null +++ b/runtime/lua/vim/fs.lua @@ -0,0 +1,209 @@ +local M = {} + +--- Iterate over all the parents of the given file or directory. +--- +--- Example: +--- <pre> +--- local root_dir +--- for dir in vim.fs.parents(vim.api.nvim_buf_get_name(0)) do +--- if vim.fn.isdirectory(dir .. "/.git") == 1 then +--- root_dir = dir +--- break +--- end +--- end +--- +--- if root_dir then +--- print("Found git repository at", root_dir) +--- end +--- </pre> +--- +---@param start (string) Initial file or directory. +---@return (function) Iterator +function M.parents(start) + return function(_, dir) + local parent = M.dirname(dir) + if parent == dir then + return nil + end + + return parent + end, + nil, + start +end + +--- Return the parent directory of the given file or directory +--- +---@param file (string) File or directory +---@return (string) Parent directory of {file} +function M.dirname(file) + if file == nil then + return nil + end + return vim.fn.fnamemodify(file, ':h') +end + +--- Return the basename of the given file or directory +--- +---@param file (string) File or directory +---@return (string) Basename of {file} +function M.basename(file) + return vim.fn.fnamemodify(file, ':t') +end + +--- Return an iterator over the files and directories located in {path} +--- +---@param path (string) An absolute or relative path to the directory to iterate +--- over. The path is first normalized |vim.fs.normalize()|. +---@return Iterator over files and directories in {path}. Each iteration yields +--- two values: name and type. Each "name" is the basename of the file or +--- directory relative to {path}. Type is one of "file" or "directory". +function M.dir(path) + return function(fs) + return vim.loop.fs_scandir_next(fs) + end, + vim.loop.fs_scandir(M.normalize(path)) +end + +--- Find files or directories in the given path. +--- +--- Finds any files or directories given in {names} starting from {path}. If +--- {upward} is "true" then the search traverses upward through parent +--- directories; otherwise, the search traverses downward. Note that downward +--- searches are recursive and may search through many directories! If {stop} +--- is non-nil, then the search stops when the directory given in {stop} is +--- reached. The search terminates when {limit} (default 1) matches are found. +--- The search can be narrowed to find only files or or only directories by +--- specifying {type} to be "file" or "directory", respectively. +--- +---@param names (string|table) Names of the files and directories to find. Must +--- be base names, paths and globs are not supported. +---@param opts (table) Optional keyword arguments: +--- - path (string): Path to begin searching from. If +--- omitted, the current working directory is used. +--- - upward (boolean, default false): If true, search +--- upward through parent directories. Otherwise, +--- search through child directories +--- (recursively). +--- - stop (string): Stop searching when this directory is +--- reached. The directory itself is not searched. +--- - type (string): Find only files ("file") or +--- directories ("directory"). If omitted, both +--- files and directories that match {name} are +--- included. +--- - limit (number, default 1): Stop the search after +--- finding this many matches. Use `math.huge` to +--- place no limit on the number of matches. +---@return (table) The paths of all matching files or directories +function M.find(names, opts) + opts = opts or {} + vim.validate({ + names = { names, { 's', 't' } }, + path = { opts.path, 's', true }, + upward = { opts.upward, 'b', true }, + stop = { opts.stop, 's', true }, + type = { opts.type, 's', true }, + limit = { opts.limit, 'n', true }, + }) + + names = type(names) == 'string' and { names } or names + + local path = opts.path or vim.loop.cwd() + local stop = opts.stop + local limit = opts.limit or 1 + + local matches = {} + + ---@private + local function add(match) + matches[#matches + 1] = match + if #matches == limit then + return true + end + end + + if opts.upward then + ---@private + local function test(p) + local t = {} + for _, name in ipairs(names) do + local f = p .. '/' .. name + local stat = vim.loop.fs_stat(f) + if stat and (not opts.type or opts.type == stat.type) then + t[#t + 1] = f + end + end + + return t + end + + for _, match in ipairs(test(path)) do + if add(match) then + return matches + end + end + + for parent in M.parents(path) do + if stop and parent == stop then + break + end + + for _, match in ipairs(test(parent)) do + if add(match) then + return matches + end + end + end + else + local dirs = { path } + while #dirs > 0 do + local dir = table.remove(dirs, 1) + if stop and dir == stop then + break + end + + for other, type in M.dir(dir) do + local f = dir .. '/' .. other + for _, name in ipairs(names) do + if name == other and (not opts.type or opts.type == type) then + if add(f) then + return matches + end + end + end + + if type == 'directory' then + dirs[#dirs + 1] = f + end + end + end + end + + return matches +end + +--- Normalize a path to a standard format. A tilde (~) character at the +--- beginning of the path is expanded to the user's home directory and any +--- backslash (\\) characters are converted to forward slashes (/). Environment +--- variables are also expanded. +--- +--- Example: +--- <pre> +--- vim.fs.normalize('C:\\Users\\jdoe') +--- => 'C:/Users/jdoe' +--- +--- vim.fs.normalize('~/src/neovim') +--- => '/home/jdoe/src/neovim' +--- +--- vim.fs.normalize('$XDG_CONFIG_HOME/nvim/init.vim') +--- => '/Users/jdoe/.config/nvim/init.vim' +--- </pre> +--- +---@param path (string) Path to normalize +---@return (string) Normalized path +function M.normalize(path) + vim.validate({ path = { path, 's' } }) + return (path:gsub('^~/', vim.env.HOME .. '/'):gsub('%$([%w_]+)', vim.env):gsub('\\', '/')) +end + +return M diff --git a/runtime/lua/vim/health.lua b/runtime/lua/vim/health.lua new file mode 100644 index 0000000000..b875da0abc --- /dev/null +++ b/runtime/lua/vim/health.lua @@ -0,0 +1,49 @@ +local M = {} + +function M.report_start(msg) + vim.fn['health#report_start'](msg) +end + +function M.report_info(msg) + vim.fn['health#report_info'](msg) +end + +function M.report_ok(msg) + vim.fn['health#report_ok'](msg) +end + +function M.report_warn(msg, ...) + vim.fn['health#report_warn'](msg, ...) +end + +function M.report_error(msg, ...) + vim.fn['health#report_error'](msg, ...) +end + +local path2name = function(path) + if path:match('%.lua$') then + -- Lua: transform "../lua/vim/lsp/health.lua" into "vim.lsp" + return path:gsub('.-lua[%\\%/]', '', 1):gsub('[%\\%/]', '.'):gsub('%.health.-$', '') + else + -- Vim: transform "../autoload/health/provider.vim" into "provider" + return vim.fn.fnamemodify(path, ':t:r') + end +end + +local PATTERNS = { '/autoload/health/*.vim', '/lua/**/**/health.lua', '/lua/**/**/health/init.lua' } +-- :checkhealth completion function used by ex_getln.c get_healthcheck_names() +M._complete = function() + local names = vim.tbl_flatten(vim.tbl_map(function(pattern) + return vim.tbl_map(path2name, vim.api.nvim_get_runtime_file(pattern, true)) + end, PATTERNS)) + -- Remove duplicates + local unique = {} + vim.tbl_map(function(f) + unique[f] = true + end, names) + -- vim.health is this file, which is not a healthcheck + unique['vim'] = nil + return vim.tbl_keys(unique) +end + +return M diff --git a/runtime/lua/vim/highlight.lua b/runtime/lua/vim/highlight.lua index 236f3165f2..ddd504a0e0 100644 --- a/runtime/lua/vim/highlight.lua +++ b/runtime/lua/vim/highlight.lua @@ -1,45 +1,79 @@ 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) + vim.deprecate('vim.highlight.create', 'vim.api.nvim_set_hl', '0.9') local options = {} -- TODO: Add validation for k, v in pairs(hi_info) do - table.insert(options, string.format("%s=%s", k, v)) + table.insert(options, string.format('%s=%s', k, v)) end - vim.cmd(string.format([[highlight %s %s %s]], default and "default" or "", higroup, table.concat(options, " "))) + vim.cmd( + string.format( + [[highlight %s %s %s]], + default and 'default' or '', + higroup, + table.concat(options, ' ') + ) + ) end ---@private -function highlight.link(higroup, link_to, force) - vim.cmd(string.format([[highlight%s link %s %s]], force and "!" or " default", higroup, link_to)) +function M.link(higroup, link_to, force) + vim.deprecate('vim.highlight.link', 'vim.api.nvim_set_hl', '0.9') + 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_timer --- Highlight the yanked region --- --- use from init.vim via @@ -49,49 +83,71 @@ 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 higroup = opts.higroup or 'IncSearch' local timeout = opts.timeout or 150 local bufnr = api.nvim_get_current_buf() api.nvim_buf_clear_namespace(bufnr, yank_ns, 0, -1) + if yank_timer then + yank_timer:close() + end 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]} - - highlight.range(bufnr, yank_ns, higroup, pos1, pos2, event.regtype, event.inclusive) + pos1 = { pos1[2] - 1, pos1[3] - 1 + pos1[4] } + pos2 = { pos2[2] - 1, pos2[3] - 1 + pos2[4] } - 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 } ) + + yank_timer = vim.defer_fn(function() + yank_timer = nil + 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/inspect.lua b/runtime/lua/vim/inspect.lua index 0448ea487f..0a53fb203b 100644 --- a/runtime/lua/vim/inspect.lua +++ b/runtime/lua/vim/inspect.lua @@ -1,6 +1,6 @@ -local inspect ={ +local inspect = { _VERSION = 'inspect.lua 3.1.0', - _URL = 'http://github.com/kikito/inspect.lua', + _URL = 'http://github.com/kikito/inspect.lua', _DESCRIPTION = 'human-readable representations of tables', _LICENSE = [[ MIT LICENSE @@ -25,13 +25,26 @@ local inspect ={ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ]] + ]], } -local tostring = tostring +inspect.KEY = setmetatable({}, { + __tostring = function() + return 'inspect.KEY' + end, +}) +inspect.METATABLE = setmetatable({}, { + __tostring = function() + return 'inspect.METATABLE' + end, +}) -inspect.KEY = setmetatable({}, {__tostring = function() return 'inspect.KEY' end}) -inspect.METATABLE = setmetatable({}, {__tostring = function() return 'inspect.METATABLE' end}) +local tostring = tostring +local rep = string.rep +local match = string.match +local char = string.char +local gsub = string.gsub +local fmt = string.format local function rawpairs(t) return next, t, nil @@ -40,127 +53,130 @@ end -- Apostrophizes the string if it has quotes, but not aphostrophes -- Otherwise, it returns a regular quoted string local function smartQuote(str) - if str:match('"') and not str:match("'") then + if match(str, '"') and not match(str, "'") then return "'" .. str .. "'" end - return '"' .. str:gsub('"', '\\"') .. '"' + return '"' .. gsub(str, '"', '\\"') .. '"' end -- \a => '\\a', \0 => '\\0', 31 => '\31' local shortControlCharEscapes = { - ["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n", - ["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v" + ['\a'] = '\\a', + ['\b'] = '\\b', + ['\f'] = '\\f', + ['\n'] = '\\n', + ['\r'] = '\\r', + ['\t'] = '\\t', + ['\v'] = '\\v', + ['\127'] = '\\127', } -local longControlCharEscapes = {} -- \a => nil, \0 => \000, 31 => \031 -for i=0, 31 do - local ch = string.char(i) +local longControlCharEscapes = { ['\127'] = '\127' } +for i = 0, 31 do + local ch = char(i) if not shortControlCharEscapes[ch] then - shortControlCharEscapes[ch] = "\\"..i - longControlCharEscapes[ch] = string.format("\\%03d", i) + shortControlCharEscapes[ch] = '\\' .. i + longControlCharEscapes[ch] = fmt('\\%03d', i) end end local function escape(str) - return (str:gsub("\\", "\\\\") - :gsub("(%c)%f[0-9]", longControlCharEscapes) - :gsub("%c", shortControlCharEscapes)) + return ( + gsub( + gsub(gsub(str, '\\', '\\\\'), '(%c)%f[0-9]', longControlCharEscapes), + '%c', + shortControlCharEscapes + ) + ) end local function isIdentifier(str) - return type(str) == 'string' and str:match( "^[_%a][_%a%d]*$" ) + return type(str) == 'string' and not not str:match('^[_%a][_%a%d]*$') end +local flr = math.floor local function isSequenceKey(k, sequenceLength) - return type(k) == 'number' - and 1 <= k - and k <= sequenceLength - and math.floor(k) == k + return type(k) == 'number' and flr(k) == k and 1 <= k and k <= sequenceLength end local defaultTypeOrders = { - ['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4, - ['function'] = 5, ['userdata'] = 6, ['thread'] = 7 + ['number'] = 1, + ['boolean'] = 2, + ['string'] = 3, + ['table'] = 4, + ['function'] = 5, + ['userdata'] = 6, + ['thread'] = 7, } local function sortKeys(a, b) local ta, tb = type(a), type(b) -- strings and numbers are sorted numerically/alphabetically - if ta == tb and (ta == 'string' or ta == 'number') then return a < b end + if ta == tb and (ta == 'string' or ta == 'number') then + return a < b + end - local dta, dtb = defaultTypeOrders[ta], defaultTypeOrders[tb] + local dta = defaultTypeOrders[ta] or 100 + local dtb = defaultTypeOrders[tb] or 100 -- Two default types are compared according to the defaultTypeOrders table - if dta and dtb then return defaultTypeOrders[ta] < defaultTypeOrders[tb] - elseif dta then return true -- default types before custom ones - elseif dtb then return false -- custom types after default ones - end -- custom types are sorted out alphabetically - return ta < tb + return dta == dtb and ta < tb or dta < dtb end --- For implementation reasons, the behavior of rawlen & # is "undefined" when --- tables aren't pure sequences. So we implement our own # operator. -local function getSequenceLength(t) - local len = 1 - local v = rawget(t,len) - while v ~= nil do - len = len + 1 - v = rawget(t,len) +local function getKeys(t) + local seqLen = 1 + while rawget(t, seqLen) ~= nil do + seqLen = seqLen + 1 end - return len - 1 -end + seqLen = seqLen - 1 -local function getNonSequentialKeys(t) - local keys, keysLength = {}, 0 - local sequenceLength = getSequenceLength(t) - for k,_ in rawpairs(t) do - if not isSequenceKey(k, sequenceLength) then - keysLength = keysLength + 1 - keys[keysLength] = k + local keys, keysLen = {}, 0 + for k in rawpairs(t) do + if not isSequenceKey(k, seqLen) then + keysLen = keysLen + 1 + keys[keysLen] = k end end table.sort(keys, sortKeys) - return keys, keysLength, sequenceLength + return keys, keysLen, seqLen end -local function countTableAppearances(t, tableAppearances) - tableAppearances = tableAppearances or {} - - if type(t) == 'table' then - if not tableAppearances[t] then - tableAppearances[t] = 1 - for k,v in rawpairs(t) do - countTableAppearances(k, tableAppearances) - countTableAppearances(v, tableAppearances) - end - countTableAppearances(getmetatable(t), tableAppearances) +local function countCycles(x, cycles) + if type(x) == 'table' then + if cycles[x] then + cycles[x] = cycles[x] + 1 else - tableAppearances[t] = tableAppearances[t] + 1 + cycles[x] = 1 + for k, v in rawpairs(x) do + countCycles(k, cycles) + countCycles(v, cycles) + end + countCycles(getmetatable(x), cycles) end end - - return tableAppearances -end - -local copySequence = function(s) - local copy, len = {}, #s - for i=1, len do copy[i] = s[i] end - return copy, len end -local function makePath(path, ...) - local keys = {...} - local newPath, len = copySequence(path) - for i=1, #keys do - newPath[len + i] = keys[i] +local function makePath(path, a, b) + local newPath = {} + local len = #path + for i = 1, len do + newPath[i] = path[i] end + + newPath[len + 1] = a + newPath[len + 2] = b + return newPath end local function processRecursive(process, item, path, visited) - if item == nil then return nil end - if visited[item] then return visited[item] end + if item == nil then + return nil + end + if visited[item] then + return visited[item] + end local processed = process(item, path) if type(processed) == 'table' then @@ -168,171 +184,166 @@ local function processRecursive(process, item, path, visited) visited[item] = processedCopy local processedKey - for k,v in rawpairs(processed) do + for k, v in rawpairs(processed) do processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY), visited) if processedKey ~= nil then - processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey), visited) + processedCopy[processedKey] = + processRecursive(process, v, makePath(path, processedKey), visited) end end - local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited) - if type(mt) ~= 'table' then mt = nil end -- ignore not nil/table __metatable field + local mt = + processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited) + if type(mt) ~= 'table' then + mt = nil + end setmetatable(processedCopy, mt) processed = processedCopy end return processed end - - -------------------------------------------------------------------- - -local Inspector = {} -local Inspector_mt = {__index = Inspector} - -function Inspector:puts(...) - local args = {...} - local buffer = self.buffer - local len = #buffer - for i=1, #args do - len = len + 1 - buffer[len] = args[i] - end +local function puts(buf, str) + buf.n = buf.n + 1 + buf[buf.n] = str end -function Inspector:down(f) - self.level = self.level + 1 - f() - self.level = self.level - 1 -end +local Inspector = {} -function Inspector:tabify() - self:puts(self.newline, string.rep(self.indent, self.level)) -end +local Inspector_mt = { __index = Inspector } -function Inspector:alreadyVisited(v) - return self.ids[v] ~= nil +local function tabify(inspector) + puts(inspector.buf, inspector.newline .. rep(inspector.indent, inspector.level)) end function Inspector:getId(v) local id = self.ids[v] + local ids = self.ids if not id then local tv = type(v) - id = (self.maxIds[tv] or 0) + 1 - self.maxIds[tv] = id - self.ids[v] = id + id = (ids[tv] or 0) + 1 + ids[v], ids[tv] = id, id end return tostring(id) end -function Inspector:putKey(k) - if isIdentifier(k) then return self:puts(k) end - self:puts("[") - self:putValue(k) - self:puts("]") -end +function Inspector:putValue(v) + local buf = self.buf + local tv = type(v) + if tv == 'string' then + puts(buf, smartQuote(escape(v))) + elseif + tv == 'number' + or tv == 'boolean' + or tv == 'nil' + or tv == 'cdata' + or tv == 'ctype' + or (vim and v == vim.NIL) + then + puts(buf, tostring(v)) + elseif tv == 'table' and not self.ids[v] then + local t = v + + if t == inspect.KEY or t == inspect.METATABLE then + puts(buf, tostring(t)) + elseif self.level >= self.depth then + puts(buf, '{...}') + else + if self.cycles[t] > 1 then + puts(buf, fmt('<%d>', self:getId(t))) + end -function Inspector:putTable(t) - if t == inspect.KEY or t == inspect.METATABLE then - self:puts(tostring(t)) - elseif self:alreadyVisited(t) then - self:puts('<table ', self:getId(t), '>') - elseif self.level >= self.depth then - self:puts('{...}') - else - if self.tableAppearances[t] > 1 then self:puts('<', self:getId(t), '>') end - - local nonSequentialKeys, nonSequentialKeysLength, sequenceLength = getNonSequentialKeys(t) - local mt = getmetatable(t) - if (vim and sequenceLength == 0 and nonSequentialKeysLength == 0 - and mt == vim._empty_dict_mt) then - self:puts(tostring(t)) - return - end + local keys, keysLen, seqLen = getKeys(t) + local mt = getmetatable(t) - self:puts('{') - self:down(function() - local count = 0 - for i=1, sequenceLength do - if count > 0 then self:puts(',') end - self:puts(' ') - self:putValue(t[i]) - count = count + 1 + if vim and seqLen == 0 and keysLen == 0 and mt == vim._empty_dict_mt then + puts(buf, tostring(t)) + return end - for i=1, nonSequentialKeysLength do - local k = nonSequentialKeys[i] - if count > 0 then self:puts(',') end - self:tabify() - self:putKey(k) - self:puts(' = ') - self:putValue(t[k]) - count = count + 1 + puts(buf, '{') + self.level = self.level + 1 + + for i = 1, seqLen + keysLen do + if i > 1 then + puts(buf, ',') + end + if i <= seqLen then + puts(buf, ' ') + self:putValue(t[i]) + else + local k = keys[i - seqLen] + tabify(self) + if isIdentifier(k) then + puts(buf, k) + else + puts(buf, '[') + self:putValue(k) + puts(buf, ']') + end + puts(buf, ' = ') + self:putValue(t[k]) + end end if type(mt) == 'table' then - if count > 0 then self:puts(',') end - self:tabify() - self:puts('<metatable> = ') + if seqLen + keysLen > 0 then + puts(buf, ',') + end + tabify(self) + puts(buf, '<metatable> = ') self:putValue(mt) end - end) - if nonSequentialKeysLength > 0 or type(mt) == 'table' then -- result is multi-lined. Justify closing } - self:tabify() - elseif sequenceLength > 0 then -- array tables have one extra space before closing } - self:puts(' ') - end + self.level = self.level - 1 - self:puts('}') - end -end - -function Inspector:putValue(v) - local tv = type(v) + if keysLen > 0 or type(mt) == 'table' then + tabify(self) + elseif seqLen > 0 then + puts(buf, ' ') + end - if tv == 'string' then - self:puts(smartQuote(escape(v))) - elseif tv == 'number' or tv == 'boolean' or tv == 'nil' or - tv == 'cdata' or tv == 'ctype' or (vim and v == vim.NIL) then - self:puts(tostring(v)) - elseif tv == 'table' then - self:putTable(v) + puts(buf, '}') + end else - self:puts('<', tv, ' ', self:getId(v), '>') + puts(buf, fmt('<%s %d>', tv, self:getId(v))) end end -------------------------------------------------------------------- - function inspect.inspect(root, options) - options = options or {} + options = options or {} - local depth = options.depth or math.huge + local depth = options.depth or math.huge local newline = options.newline or '\n' - local indent = options.indent or ' ' + local indent = options.indent or ' ' local process = options.process if process then root = processRecursive(process, root, {}, {}) end + local cycles = {} + countCycles(root, cycles) + local inspector = setmetatable({ - depth = depth, - level = 0, - buffer = {}, - ids = {}, - maxIds = {}, - newline = newline, - indent = indent, - tableAppearances = countTableAppearances(root) + buf = { n = 0 }, + ids = {}, + cycles = cycles, + depth = depth, + level = 0, + newline = newline, + indent = indent, }, Inspector_mt) inspector:putValue(root) - return table.concat(inspector.buffer) + return table.concat(inspector.buf) end -setmetatable(inspect, { __call = function(_, ...) return inspect.inspect(...) end }) +setmetatable(inspect, { + __call = function(_, root, options) + return inspect.inspect(root, options) + end, +}) return inspect diff --git a/runtime/lua/vim/keymap.lua b/runtime/lua/vim/keymap.lua index d53b790746..f4c2b507a9 100644 --- a/runtime/lua/vim/keymap.lua +++ b/runtime/lua/vim/keymap.lua @@ -25,8 +25,8 @@ local keymap = {} --- 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: +--- 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> @@ -35,39 +35,51 @@ local 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: +--- - buffer: (number or boolean) Add a mapping to the given buffer. When "true" +--- or 0, use the current buffer. --- - 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 `true` if `lhs` is a string starting with `<plug>` (case-insensitive), `false` otherwise. +--- 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} - } + 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 + local is_rhs_luaref = type(rhs) == 'function' + mode = type(mode) == 'string' and { mode } or mode - if is_rhs_luaref and opts.expr and opts.replace_keycodes ~= false then + if is_rhs_luaref and opts.expr then local user_rhs = rhs - rhs = function () - return vim.api.nvim_replace_termcodes(user_rhs(), true, true, true) + 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 - -- remap by default on <plug> mappings and don't otherwise. - opts.noremap = is_rhs_luaref or rhs:lower():match("^<plug>") == nil + -- default remap value is false + opts.noremap = true else -- remaps behavior is opposite of noremap option. opts.noremap = not opts.remap @@ -106,19 +118,18 @@ end ---@see |vim.keymap.set()| --- function keymap.del(modes, lhs, opts) - vim.validate { - mode = {modes, {'s', 't'}}, - lhs = {lhs, 's'}, - opts = {opts, 't', true} - } + vim.validate({ + mode = { modes, { 's', 't' } }, + lhs = { lhs, 's' }, + opts = { opts, 't', true }, + }) opts = opts or {} - modes = type(modes) == 'string' and {modes} or modes + 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 diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 8b7eb4ac90..61586ca44f 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -1,58 +1,62 @@ -local if_nil = vim.F.if_nil - -local default_handlers = require 'vim.lsp.handlers' -local log = require 'vim.lsp.log' -local lsp_rpc = require 'vim.lsp.rpc' -local protocol = require 'vim.lsp.protocol' -local util = require 'vim.lsp.util' -local sync = require 'vim.lsp.sync' +local default_handlers = require('vim.lsp.handlers') +local log = require('vim.lsp.log') +local lsp_rpc = require('vim.lsp.rpc') +local protocol = require('vim.lsp.protocol') +local util = require('vim.lsp.util') +local sync = require('vim.lsp.sync') local vim = vim -local nvim_err_writeln, nvim_buf_get_lines, nvim_command, nvim_buf_get_option - = vim.api.nvim_err_writeln, vim.api.nvim_buf_get_lines, vim.api.nvim_command, vim.api.nvim_buf_get_option +local api = vim.api +local nvim_err_writeln, nvim_buf_get_lines, nvim_command, nvim_buf_get_option, nvim_exec_autocmds = + api.nvim_err_writeln, + api.nvim_buf_get_lines, + api.nvim_command, + api.nvim_buf_get_option, + api.nvim_exec_autocmds local uv = vim.loop local tbl_isempty, tbl_extend = vim.tbl_isempty, vim.tbl_extend local validate = vim.validate +local if_nil = vim.F.if_nil local lsp = { - protocol = protocol; + protocol = protocol, - handlers = default_handlers; + handlers = default_handlers, - buf = require'vim.lsp.buf'; - diagnostic = require'vim.lsp.diagnostic'; - codelens = require'vim.lsp.codelens'; - util = util; + buf = require('vim.lsp.buf'), + diagnostic = require('vim.lsp.diagnostic'), + codelens = require('vim.lsp.codelens'), + util = util, -- Allow raw RPC access. - rpc = lsp_rpc; + rpc = lsp_rpc, -- Export these directly from rpc. - rpc_response_error = lsp_rpc.rpc_response_error; + rpc_response_error = lsp_rpc.rpc_response_error, } --- maps request name to the required resolved_capability in the client. +-- maps request name to the required server_capability in the client. lsp._request_name_to_capability = { - ['textDocument/hover'] = 'hover'; - ['textDocument/signatureHelp'] = 'signature_help'; - ['textDocument/definition'] = 'goto_definition'; - ['textDocument/implementation'] = 'implementation'; - ['textDocument/declaration'] = 'declaration'; - ['textDocument/typeDefinition'] = 'type_definition'; - ['textDocument/documentSymbol'] = 'document_symbol'; - ['textDocument/prepareCallHierarchy'] = 'call_hierarchy'; - ['textDocument/rename'] = 'rename'; - ['textDocument/prepareRename'] = 'rename'; - ['textDocument/codeAction'] = 'code_action'; - ['textDocument/codeLens'] = 'code_lens'; - ['codeLens/resolve'] = 'code_lens_resolve'; - ['workspace/executeCommand'] = 'execute_command'; - ['workspace/symbol'] = 'workspace_symbol'; - ['textDocument/references'] = 'find_references'; - ['textDocument/rangeFormatting'] = 'document_range_formatting'; - ['textDocument/formatting'] = 'document_formatting'; - ['textDocument/completion'] = 'completion'; - ['textDocument/documentHighlight'] = 'document_highlight'; + ['textDocument/hover'] = { 'hoverProvider' }, + ['textDocument/signatureHelp'] = { 'signatureHelpProvider' }, + ['textDocument/definition'] = { 'definitionProvider' }, + ['textDocument/implementation'] = { 'implementationProvider' }, + ['textDocument/declaration'] = { 'declarationProvider' }, + ['textDocument/typeDefinition'] = { 'typeDefinitionProvider' }, + ['textDocument/documentSymbol'] = { 'documentSymbolProvider' }, + ['textDocument/prepareCallHierarchy'] = { 'callHierarchyProvider' }, + ['textDocument/rename'] = { 'renameProvider' }, + ['textDocument/prepareRename'] = { 'renameProvider', 'prepareProvider' }, + ['textDocument/codeAction'] = { 'codeActionProvider' }, + ['textDocument/codeLens'] = { 'codeLensProvider' }, + ['codeLens/resolve'] = { 'codeLensProvider', 'resolveProvider' }, + ['workspace/executeCommand'] = { 'executeCommandProvider' }, + ['workspace/symbol'] = { 'workspaceSymbolProvider' }, + ['textDocument/references'] = { 'referencesProvider' }, + ['textDocument/rangeFormatting'] = { 'documentRangeFormattingProvider' }, + ['textDocument/formatting'] = { 'documentFormattingProvider' }, + ['textDocument/completion'] = { 'completionProvider' }, + ['textDocument/documentHighlight'] = { 'documentHighlightProvider' }, } -- TODO improve handling of scratch buffers with LSP attached. @@ -62,8 +66,8 @@ lsp._request_name_to_capability = { --- ---@param {...} (List of strings) List to write to the buffer local function err_message(...) - nvim_err_writeln(table.concat(vim.tbl_flatten{...})) - nvim_command("redraw") + nvim_err_writeln(table.concat(vim.tbl_flatten({ ... }))) + nvim_command('redraw') end ---@private @@ -73,9 +77,9 @@ end ---buffer if not given. ---@returns bufnr (number) Number of requested buffer local function resolve_bufnr(bufnr) - validate { bufnr = { bufnr, 'n', true } } + validate({ bufnr = { bufnr, 'n', true } }) if bufnr == nil or bufnr == 0 then - return vim.api.nvim_get_current_buf() + return api.nvim_get_current_buf() end return bufnr end @@ -85,7 +89,10 @@ end --- supported in any of the servers registered for the current buffer. ---@param method (string) name of the method function lsp._unsupported_method(method) - local msg = string.format("method %s is not supported by any of the servers registered for the current buffer", method) + local msg = string.format( + 'method %s is not supported by any of the servers registered for the current buffer', + method + ) log.warn(msg) return msg end @@ -96,23 +103,29 @@ end ---@param filename (string) path to check ---@returns true if {filename} exists and is a directory, false otherwise local function is_dir(filename) - validate{filename={filename,'s'}} + validate({ filename = { filename, 's' } }) local stat = uv.fs_stat(filename) return stat and stat.type == 'directory' or false end -local wait_result_reason = { [-1] = "timeout"; [-2] = "interrupted"; [-3] = "error" } +local wait_result_reason = { [-1] = 'timeout', [-2] = 'interrupted', [-3] = 'error' } local valid_encodings = { - ["utf-8"] = 'utf-8'; ["utf-16"] = 'utf-16'; ["utf-32"] = 'utf-32'; - ["utf8"] = 'utf-8'; ["utf16"] = 'utf-16'; ["utf32"] = 'utf-32'; - UTF8 = 'utf-8'; UTF16 = 'utf-16'; UTF32 = 'utf-32'; + ['utf-8'] = 'utf-8', + ['utf-16'] = 'utf-16', + ['utf-32'] = 'utf-32', + ['utf8'] = 'utf-8', + ['utf16'] = 'utf-16', + ['utf32'] = 'utf-32', + UTF8 = 'utf-8', + UTF16 = 'utf-16', + UTF32 = 'utf-32', } local format_line_ending = { - ["unix"] = '\n', - ["dos"] = '\r\n', - ["mac"] = '\r', + ['unix'] = '\n', + ['dos'] = '\r\n', + ['mac'] = '\r', } ---@private @@ -138,10 +151,10 @@ local uninitialized_clients = {} ---@private local function for_each_buffer_client(bufnr, fn, restrict_client_ids) - validate { - fn = { fn, 'f' }; - restrict_client_ids = { restrict_client_ids, 't' , true}; - } + validate({ + fn = { fn, 'f' }, + restrict_client_ids = { restrict_client_ids, 't', true }, + }) bufnr = resolve_bufnr(bufnr) local client_ids = all_buffer_active_clients[bufnr] if not client_ids or tbl_isempty(client_ids) then @@ -169,9 +182,13 @@ end -- Error codes to be used with `on_error` from |vim.lsp.start_client|. -- Can be used to look up the string from a the number or the number -- from the string. -lsp.client_errors = tbl_extend("error", lsp_rpc.client_errors, vim.tbl_add_reverse_lookup { - ON_INIT_CALLBACK_ERROR = table.maxn(lsp_rpc.client_errors) + 1; -}) +lsp.client_errors = tbl_extend( + 'error', + lsp_rpc.client_errors, + vim.tbl_add_reverse_lookup({ + ON_INIT_CALLBACK_ERROR = table.maxn(lsp_rpc.client_errors) + 1, + }) +) ---@private --- Normalizes {encoding} to valid LSP encoding names. @@ -179,11 +196,16 @@ lsp.client_errors = tbl_extend("error", lsp_rpc.client_errors, vim.tbl_add_rever ---@param encoding (string) Encoding to normalize ---@returns (string) normalized encoding name local function validate_encoding(encoding) - validate { - encoding = { encoding, 's' }; - } + validate({ + encoding = { encoding, 's' }, + }) return valid_encodings[encoding:lower()] - or error(string.format("Invalid offset encoding %q. Must be one of: 'utf-8', 'utf-16', 'utf-32'", encoding)) + or error( + string.format( + "Invalid offset encoding %q. Must be one of: 'utf-8', 'utf-16', 'utf-32'", + encoding + ) + ) end ---@internal @@ -194,16 +216,21 @@ end ---@returns (string) the command ---@returns (list of strings) its arguments function lsp._cmd_parts(input) - vim.validate{cmd={ - input, - function() return vim.tbl_islist(input) end, - "list"}} + validate({ + cmd = { + input, + function() + return vim.tbl_islist(input) + end, + 'list', + }, + }) local cmd = input[1] local cmd_args = {} -- Don't mutate our input. for i, v in ipairs(input) do - vim.validate{["cmd argument"]={v, "s"}} + validate({ ['cmd argument'] = { v, 's' } }) if i > 1 then table.insert(cmd_args, v) end @@ -233,30 +260,33 @@ end --- ---@see |vim.lsp.start_client()| local function validate_client_config(config) - validate { - config = { config, 't' }; - } - validate { - handlers = { config.handlers, "t", true }; - capabilities = { config.capabilities, "t", true }; - cmd_cwd = { config.cmd_cwd, optional_validator(is_dir), "directory" }; - cmd_env = { config.cmd_env, "t", true }; - name = { config.name, 's', true }; - on_error = { config.on_error, "f", true }; - on_exit = { config.on_exit, "f", true }; - on_init = { config.on_init, "f", true }; - settings = { config.settings, "t", true }; - commands = { config.commands, 't', true }; - before_init = { config.before_init, "f", true }; - offset_encoding = { config.offset_encoding, "s", true }; - flags = { config.flags, "t", true }; - get_language_id = { config.get_language_id, "f", true }; - } + validate({ + config = { config, 't' }, + }) + validate({ + handlers = { config.handlers, 't', true }, + capabilities = { config.capabilities, 't', true }, + cmd_cwd = { config.cmd_cwd, optional_validator(is_dir), 'directory' }, + cmd_env = { config.cmd_env, 't', true }, + detached = { config.detached, 'b', true }, + name = { config.name, 's', true }, + on_error = { config.on_error, 'f', true }, + on_exit = { config.on_exit, 'f', true }, + on_init = { config.on_init, 'f', true }, + settings = { config.settings, 't', true }, + commands = { config.commands, 't', true }, + before_init = { config.before_init, 'f', true }, + offset_encoding = { config.offset_encoding, 's', true }, + flags = { config.flags, 't', true }, + get_language_id = { config.get_language_id, 'f', true }, + }) assert( - (not config.flags + ( + not config.flags or not config.flags.debounce_text_changes - or type(config.flags.debounce_text_changes) == 'number'), - "flags.debounce_text_changes must be a number with the debounce time in milliseconds" + or type(config.flags.debounce_text_changes) == 'number' + ), + 'flags.debounce_text_changes must be a number with the debounce time in milliseconds' ) local cmd, cmd_args = lsp._cmd_parts(config.cmd) @@ -266,9 +296,9 @@ local function validate_client_config(config) end return { - cmd = cmd; - cmd_args = cmd_args; - offset_encoding = offset_encoding; + cmd = cmd, + cmd_args = cmd_args, + offset_encoding = offset_encoding, } end @@ -328,14 +358,15 @@ do 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 + and vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'change') + == protocol.TextDocumentSyncKind.Incremental ) local state = state_by_client[client.id] if not state then state = { - buffers = {}; + buffers = {}, debounce = client.config.flags.debounce_text_changes or 150, - use_incremental_sync = use_incremental_sync; + use_incremental_sync = use_incremental_sync, } state_by_client[client.id] = state end @@ -344,6 +375,7 @@ do 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 @@ -403,21 +435,59 @@ do ---@private function changetracking.prepare(bufnr, firstline, lastline, new_lastline) local incremental_changes = function(client, buf_state) - local curr_lines = nvim_buf_get_lines(bufnr, 0, -1, true) + 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( - buf_state.lines, curr_lines, firstline, lastline, new_lastline, client.offset_encoding or 'utf-16', line_ending) + 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() return { - text = buf_get_full_text(bufnr); - }; + text = buf_get_full_text(bufnr), + } end) local uri = vim.uri_from_bufnr(bufnr) return function(client) - if client.resolved_capabilities.text_document_did_change == protocol.TextDocumentSyncKind.None then + if + vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'change') + == protocol.TextDocumentSyncKind.None + then return end local state = state_by_client[client.id] @@ -430,13 +500,17 @@ do table.insert(buf_state.pending_changes, incremental_changes(client, buf_state)) end buf_state.pending_change = function() + if buf_state.pending_change == nil then + return + end 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 + if client.is_stopped() or not api.nvim_buf_is_valid(bufnr) then return end - local changes = state.use_incremental_sync and buf_state.pending_changes or { full_changes() } - client.notify("textDocument/didChange", { + local changes = state.use_incremental_sync and buf_state.pending_changes + or { full_changes() } + client.notify('textDocument/didChange', { textDocument = { uri = uri, version = util.buf_versions[bufnr], @@ -448,7 +522,7 @@ do if debounce == 0 then buf_state.pending_change() else - local timer = vim.loop.new_timer() + local timer = uv.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)) @@ -488,29 +562,28 @@ do end end - ---@private --- Default handler for the 'textDocument/didOpen' LSP notification. --- ----@param bufnr (Number) Number of the buffer, or 0 for current +---@param bufnr number Number of the buffer, or 0 for current ---@param client Client object local function text_document_did_open_handler(bufnr, client) changetracking.init(client, bufnr) - if not client.resolved_capabilities.text_document_open_close then + if not vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then return end - if not vim.api.nvim_buf_is_loaded(bufnr) then + if not api.nvim_buf_is_loaded(bufnr) then return end local filetype = nvim_buf_get_option(bufnr, 'filetype') local params = { textDocument = { - version = 0; - uri = vim.uri_from_bufnr(bufnr); - languageId = client.config.get_language_id(bufnr, filetype); - text = buf_get_full_text(bufnr); - } + version = 0, + uri = vim.uri_from_bufnr(bufnr), + languageId = client.config.get_language_id(bufnr, filetype), + text = buf_get_full_text(bufnr), + }, } client.notify('textDocument/didOpen', params) util.buf_versions[bufnr] = params.textDocument.version @@ -519,7 +592,7 @@ local function text_document_did_open_handler(bufnr, client) vim.schedule(function() -- 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 + if api.nvim_buf_is_valid(bufnr) then local namespace = vim.lsp.diagnostic.get_namespace(client.id) vim.diagnostic.show(namespace, bufnr) end @@ -602,14 +675,86 @@ end --- --- - {server_capabilities} (table): Response from the server sent on --- `initialize` describing the server's capabilities. ---- ---- - {resolved_capabilities} (table): Normalized table of ---- capabilities that we have detected based on the initialize ---- response from the server in `server_capabilities`. function lsp.client() error() end +--- Create a new LSP client and start a language server or reuses an already +--- running client if one is found matching `name` and `root_dir`. +--- Attaches the current buffer to the client. +--- +--- Example: +--- +--- <pre> +--- vim.lsp.start({ +--- name = 'my-server-name', +--- cmd = {'name-of-language-server-executable'}, +--- root_dir = vim.fs.dirname(vim.fs.find({'pyproject.toml', 'setup.py'}, { upward = true })[1]), +--- }) +--- </pre> +--- +--- See |lsp.start_client| for all available options. The most important are: +--- +--- `name` is an arbitrary name for the LSP client. It should be unique per +--- language server. +--- +--- `cmd` the command as list - used to start the language server. +--- The command must be present in the `$PATH` environment variable or an +--- absolute path to the executable. Shell constructs like `~` are *NOT* expanded. +--- +--- `root_dir` path to the project root. +--- By default this is used to decide if an existing client should be re-used. +--- The example above uses |vim.fs.find| and |vim.fs.dirname| to detect the +--- root by traversing the file system upwards starting +--- from the current directory until either a `pyproject.toml` or `setup.py` +--- file is found. +--- +--- `workspace_folders` a list of { uri:string, name: string } tables. +--- The project root folders used by the language server. +--- If `nil` the property is derived from the `root_dir` for convenience. +--- +--- Language servers use this information to discover metadata like the +--- dependencies of your project and they tend to index the contents within the +--- project folder. +--- +--- +--- To ensure a language server is only started for languages it can handle, +--- make sure to call |vim.lsp.start| within a |FileType| autocmd. +--- Either use |:au|, |nvim_create_autocmd()| or put the call in a +--- `ftplugin/<filetype_name>.lua` (See |ftplugin-name|) +--- +---@param config table Same configuration as documented in |lsp.start_client()| +---@param opts nil|table Optional keyword arguments: +--- - reuse_client (fun(client: client, config: table): boolean) +--- Predicate used to decide if a client should be re-used. +--- Used on all running clients. +--- The default implementation re-uses a client if name +--- and root_dir matches. +---@return number client_id +function lsp.start(config, opts) + opts = opts or {} + local reuse_client = opts.reuse_client + or function(client, conf) + return client.config.root_dir == conf.root_dir and client.name == conf.name + end + config.name = config.name or (config.cmd[1] and vim.fs.basename(config.cmd[1])) or nil + local bufnr = api.nvim_get_current_buf() + for _, clients in ipairs({ uninitialized_clients, lsp.get_active_clients() }) do + for _, client in pairs(clients) do + if reuse_client(client, config) then + lsp.buf_attach_client(bufnr, client.id) + return client.id + end + end + end + local client_id = lsp.start_client(config) + if client_id == nil then + return nil -- lsp.start_client will have printed an error + end + lsp.buf_attach_client(bufnr, client_id) + return client_id +end + -- FIXME: DOC: Currently all methods on the `vim.lsp.client` object are -- documented twice: Here, and on the methods themselves (e.g. -- `client.request()`). This is a workaround for the vimdoc generator script @@ -637,6 +782,10 @@ end --- { "PRODUCTION=true"; "TEST=123"; PORT = 8080; HOST = "0.0.0.0"; } --- </pre> --- +---@param detached: (boolean, default true) Daemonize the server process so that it runs in a +--- separate process group from Nvim. Nvim will shutdown the process on exit, but if Nvim fails to +--- exit cleanly this could leave behind orphaned server processes. +--- ---@param workspace_folders (table) List of workspace folders passed to the --- language server. For backwards compatibility rootUri and rootPath will be --- derived from the first workspace folder in this list. See `workspaceFolders` in @@ -708,7 +857,7 @@ end --- server in the initialize request. Invalid/empty values will default to "off" ---@param flags: A table with flags for the client. The current (experimental) flags are: --- - allow_incremental_sync (bool, default true): Allow using incremental sync for buffer edits ---- - debounce_text_changes (number, default nil): Debounce didChange +--- - debounce_text_changes (number, default 150): Debounce didChange --- notifications to the server by the given number in milliseconds. No debounce --- occurs if nil --- - exit_timeout (number, default 500): Milliseconds to wait for server to @@ -724,7 +873,8 @@ end --- the client has been initialized. function lsp.start_client(config) local cleaned_config = validate_client_config(config) - local cmd, cmd_args, offset_encoding = cleaned_config.cmd, cleaned_config.cmd_args, cleaned_config.offset_encoding + local cmd, cmd_args, offset_encoding = + cleaned_config.cmd, cleaned_config.cmd_args, cleaned_config.offset_encoding config.flags = config.flags or {} config.settings = config.settings or {} @@ -732,13 +882,15 @@ function lsp.start_client(config) -- By default, get_language_id just returns the exact filetype it is passed. -- It is possible to pass in something that will calculate a different filetype, -- to be sent by the client. - config.get_language_id = config.get_language_id or function(_, filetype) return filetype end + config.get_language_id = config.get_language_id or function(_, filetype) + return filetype + end local client_id = next_client_id() local handlers = config.handlers or {} local name = config.name or tostring(client_id) - local log_prefix = string.format("LSP[%s]", name) + local log_prefix = string.format('LSP[%s]', name) local dispatch = {} @@ -763,7 +915,7 @@ function lsp.start_client(config) local handler = resolve_handler(method) if handler then -- Method name is provided here for convenience. - handler(nil, params, {method=method, client_id=client_id}) + handler(nil, params, { method = method, client_id = client_id }) end end @@ -776,10 +928,10 @@ function lsp.start_client(config) local _ = log.trace() and log.trace('server_request', method, params) local handler = resolve_handler(method) if handler then - local _ = log.trace() and log.trace("server_request: found handler for", method) - return handler(nil, params, {method=method, client_id=client_id}) + local _ = log.trace() and log.trace('server_request: found handler for', method) + return handler(nil, params, { method = method, client_id = client_id }) end - local _ = log.warn() and log.warn("server_request: no handler found for", method) + local _ = log.warn() and log.warn('server_request: no handler found for', method) return nil, lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound) end @@ -791,18 +943,41 @@ function lsp.start_client(config) ---@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 }) + 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)) if config.on_error then local status, usererr = pcall(config.on_error, code, err) if not status then - local _ = log.error() and log.error(log_prefix, "user on_error failed", { err = usererr }) + local _ = log.error() and log.error(log_prefix, 'user on_error failed', { err = usererr }) err_message(log_prefix, ' user on_error failed: ', tostring(usererr)) end end end ---@private + local function set_defaults(client, bufnr) + if client.server_capabilities.definitionProvider and vim.bo[bufnr].tagfunc == '' then + vim.bo[bufnr].tagfunc = 'v:lua.vim.lsp.tagfunc' + end + if client.server_capabilities.completionProvider and vim.bo[bufnr].omnifunc == '' then + vim.bo[bufnr].omnifunc = 'v:lua.vim.lsp.omnifunc' + end + end + + ---@private + --- Reset defaults set by `set_defaults`. + --- Must only be called if the last client attached to a buffer exits. + local function unset_defaults(bufnr) + if vim.bo[bufnr].tagfunc == 'v:lua.vim.lsp.tagfunc' then + vim.bo[bufnr].tagfunc = nil + end + if vim.bo[bufnr].omnifunc == 'v:lua.vim.lsp.omnifunc' then + vim.bo[bufnr].omnifunc = nil + end + end + + ---@private --- Invoked on client exit. --- ---@param code (number) exit code of the process @@ -812,17 +987,35 @@ function lsp.start_client(config) pcall(config.on_exit, code, signal, client_id) end + for bufnr, client_ids in pairs(all_buffer_active_clients) do + if client_ids[client_id] then + vim.schedule(function() + nvim_exec_autocmds('LspDetach', { + buffer = bufnr, + modeline = false, + data = { client_id = client_id }, + }) + + local namespace = vim.lsp.diagnostic.get_namespace(client_id) + vim.diagnostic.reset(namespace, bufnr) + end) + + client_ids[client_id] = nil + end + if vim.tbl_isempty(client_ids) then + vim.schedule(function() + unset_defaults(bufnr) + end) + end + end + active_clients[client_id] = nil uninitialized_clients[client_id] = nil - lsp.diagnostic.reset(client_id, all_buffer_active_clients) changetracking.reset(client_id) - for _, client_ids in pairs(all_buffer_active_clients) do - client_ids[client_id] = nil - end - if code ~= 0 or (signal ~= 0 and signal ~= 15) then - local msg = string.format("Client %s quit with exit code %s and signal %s", client_id, code, signal) + local msg = + string.format('Client %s quit with exit code %s and signal %s', client_id, code, signal) vim.schedule(function() vim.notify(msg, vim.log.levels.WARN) end) @@ -831,36 +1024,41 @@ function lsp.start_client(config) -- Start the RPC client. local rpc = lsp_rpc.start(cmd, cmd_args, dispatch, { - cwd = config.cmd_cwd; - env = config.cmd_env; + cwd = config.cmd_cwd, + env = config.cmd_env, + detached = config.detached, }) -- Return nil if client fails to start - if not rpc then return end + if not rpc then + return + end local client = { - id = client_id; - name = name; - rpc = rpc; - offset_encoding = offset_encoding; - config = config; - attached_buffers = {}; + id = client_id, + name = name, + rpc = rpc, + offset_encoding = offset_encoding, + config = config, + attached_buffers = {}, - handlers = handlers; - commands = config.commands or {}; + handlers = handlers, + commands = config.commands or {}, - requests = {}; + requests = {}, -- for $/progress report - messages = { name = name, messages = {}, progress = {}, status = {} }; + messages = { name = name, messages = {}, progress = {}, status = {} }, } -- Store the uninitialized_clients for cleanup in case we exit before initialize finishes. - uninitialized_clients[client_id] = client; + uninitialized_clients[client_id] = client ---@private local function initialize() local valid_traces = { - off = 'off'; messages = 'messages'; verbose = 'verbose'; + off = 'off', + messages = 'messages', + verbose = 'verbose', } local version = vim.version() @@ -869,10 +1067,12 @@ function lsp.start_client(config) 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); - }}; + workspace_folders = { + { + uri = vim.uri_from_fname(config.root_dir), + name = string.format('%s', config.root_dir), + }, + } else workspace_folders = config.workspace_folders end @@ -889,68 +1089,102 @@ function lsp.start_client(config) -- the process has not been started by another process. If the parent -- process is not alive then the server should exit (see exit notification) -- its process. - processId = uv.getpid(); + processId = uv.getpid(), -- Information about the client -- since 3.15.0 clientInfo = { - name = "Neovim", - version = string.format("%s.%s.%s", version.major, version.minor, version.patch) - }; + name = 'Neovim', + version = string.format('%s.%s.%s', version.major, version.minor, version.patch), + }, -- The rootPath of the workspace. Is null if no folder is open. -- -- @deprecated in favour of rootUri. - rootPath = root_path or vim.NIL; + 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 = root_uri or vim.NIL; + 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; + workspaceFolders = workspace_folders or vim.NIL, -- User provided initialization options. - initializationOptions = config.init_options; + initializationOptions = config.init_options, -- The capabilities provided by the client (editor or tool) - capabilities = config.capabilities or protocol.make_client_capabilities(); + capabilities = config.capabilities or protocol.make_client_capabilities(), -- The initial trace setting. If omitted trace is disabled ("off"). -- trace = "off" | "messages" | "verbose"; - trace = valid_traces[config.trace] or 'off'; + trace = valid_traces[config.trace] or 'off', } if config.before_init then -- TODO(ashkan) handle errors here. pcall(config.before_init, initialize_params, config) end - local _ = log.trace() and log.trace(log_prefix, "initialize_params", initialize_params) + local _ = log.trace() and log.trace(log_prefix, 'initialize_params', initialize_params) rpc.request('initialize', initialize_params, function(init_err, result) assert(not init_err, tostring(init_err)) - assert(result, "server sent empty result") + assert(result, 'server sent empty result') rpc.notify('initialized', vim.empty_dict()) client.initialized = true uninitialized_clients[client_id] = nil client.workspace_folders = workspace_folders -- TODO(mjlbach): Backwards compatibility, to be removed in 0.7 client.workspaceFolders = client.workspace_folders - 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. - client.resolved_capabilities = protocol.resolve_capabilities(client.server_capabilities) + client.server_capabilities = + assert(result.capabilities, "initialize result doesn't contain capabilities") + client.server_capabilities = protocol.resolve_capabilities(client.server_capabilities) + + -- Deprecation wrapper: this will be removed in 0.8 + local mt = {} + mt.__index = function(table, key) + if key == 'resolved_capabilities' then + vim.notify_once( + '[LSP] Accessing client.resolved_capabilities is deprecated, ' + .. 'update your plugins or configuration to access client.server_capabilities instead.' + .. 'The new key/value pairs in server_capabilities directly match those ' + .. 'defined in the language server protocol', + vim.log.levels.WARN + ) + rawset(table, key, protocol._resolve_capabilities_compat(client.server_capabilities)) + return rawget(table, key) + else + return rawget(table, key) + end + end + setmetatable(client, mt) + client.supports_method = function(method) local required_capability = lsp._request_name_to_capability[method] -- if we don't know about the method, assume that the client supports it. if not required_capability then return true end + if vim.tbl_get(client.server_capabilities, unpack(required_capability)) then + return true + else + return false + end + end - return client.resolved_capabilities[required_capability] + if next(config.settings) then + client.notify('workspace/didChangeConfiguration', { settings = config.settings }) end + if config.on_init then local status, err = pcall(config.on_init, client, result) if not status then pcall(handlers.on_error, lsp.client_errors.ON_INIT_CALLBACK_ERROR, err) end end - local _ = log.debug() and log.debug(log_prefix, "server_capabilities", client.server_capabilities) - local _ = log.info() and log.info(log_prefix, "initialized", { resolved_capabilities = client.resolved_capabilities }) + local _ = log.info() + and log.info( + log_prefix, + 'server_capabilities', + { server_capabilities = client.server_capabilities } + ) -- Only assign after initialized. active_clients[client_id] = client @@ -985,22 +1219,27 @@ function lsp.start_client(config) function client.request(method, params, handler, bufnr) if not handler then handler = resolve_handler(method) - or error(string.format("not found: %q request handler for client %q.", method, client.name)) + 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, bufnr) bufnr = resolve_bufnr(bufnr) - local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, handler, 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) - handler(err, result, {method=method, client_id=client_id, bufnr=bufnr, params=params}) + handler( + err, + result, + { method = method, client_id = client_id, bufnr = bufnr, params = params } + ) end, function(request_id) client.requests[request_id] = nil - nvim_command("doautocmd <nomodeline> User LspRequest") + nvim_exec_autocmds('User', { pattern = 'LspRequest', modeline = false }) end) if success then - client.requests[request_id] = { type='pending', bufnr=bufnr, method=method } - nvim_command("doautocmd <nomodeline> User LspRequest") + client.requests[request_id] = { type = 'pending', bufnr = bufnr, method = method } + nvim_exec_autocmds('User', { pattern = 'LspRequest', modeline = false }) end return success, request_id @@ -1027,9 +1266,10 @@ function lsp.start_client(config) request_result = { err = err, result = result } end - local success, request_id = client.request(method, params, _sync_handler, - bufnr) - if not success then return nil end + local success, request_id = client.request(method, params, _sync_handler, bufnr) + if not success then + return nil + end local wait_result, reason = vim.wait(timeout_ms or 1000, function() return request_result ~= nil @@ -1064,13 +1304,13 @@ function lsp.start_client(config) ---@returns true if any client returns true; false otherwise ---@see |vim.lsp.client.notify()| function client.cancel_request(id) - validate{id = {id, 'n'}} + validate({ id = { id, 'n' } }) local request = client.requests[id] if request and request.type == 'pending' then request.type = 'cancel' - nvim_command("doautocmd <nomodeline> User LspRequest") + nvim_exec_autocmds('User', { pattern = 'LspRequest', modeline = false }) end - return rpc.notify("$/cancelRequest", { id = id }) + return rpc.notify('$/cancelRequest', { id = id }) end -- Track this so that we can escalate automatically if we've already tried a @@ -1085,18 +1325,11 @@ function lsp.start_client(config) --- ---@param force (bool, optional) function client.stop(force) - - lsp.diagnostic.reset(client_id, all_buffer_active_clients) - changetracking.reset(client_id) - for _, client_ids in pairs(all_buffer_active_clients) do - client_ids[client_id] = nil - end - local handle = rpc.handle if handle:is_closing() then return end - if force or (not client.initialized) or graceful_shutdown_failed then + if force or not client.initialized or graceful_shutdown_failed then handle:kill(15) return end @@ -1126,6 +1359,15 @@ function lsp.start_client(config) ---@param bufnr (number) Buffer number function client._on_attach(bufnr) text_document_did_open_handler(bufnr, client) + + set_defaults(client, bufnr) + + nvim_exec_autocmds('LspAttach', { + buffer = bufnr, + modeline = false, + data = { client_id = client.id }, + }) + if config.on_attach then -- TODO(ashkan) handle errors. pcall(config.on_attach, client, bufnr) @@ -1143,34 +1385,37 @@ end --- Notify all attached clients that a buffer has changed. local text_document_did_change_handler do - text_document_did_change_handler = function(_, bufnr, changedtick, firstline, lastline, new_lastline) - - -- Detach (nvim_buf_attach) via returning True to on_lines if no clients are attached - if tbl_isempty(all_buffer_active_clients[bufnr] or {}) then - return true + text_document_did_change_handler = + function(_, bufnr, changedtick, firstline, lastline, new_lastline) + -- Detach (nvim_buf_attach) via returning True to on_lines if no clients are attached + if tbl_isempty(all_buffer_active_clients[bufnr] or {}) then + return true + end + util.buf_versions[bufnr] = changedtick + local compute_change_and_notify = + changetracking.prepare(bufnr, firstline, lastline, new_lastline) + for_each_buffer_client(bufnr, compute_change_and_notify) end - util.buf_versions[bufnr] = changedtick - local compute_change_and_notify = changetracking.prepare(bufnr, firstline, lastline, new_lastline) - for_each_buffer_client(bufnr, compute_change_and_notify) - end end --- Buffer lifecycle handler for textDocument/didSave -function lsp._text_document_did_save_handler(bufnr) +---@private +---Buffer lifecycle handler for textDocument/didSave +local function text_document_did_save_handler(bufnr) bufnr = resolve_bufnr(bufnr) local uri = vim.uri_from_bufnr(bufnr) local text = once(buf_get_full_text) - for_each_buffer_client(bufnr, function(client, _client_id) - if client.resolved_capabilities.text_document_save then + for_each_buffer_client(bufnr, function(client) + local save_capability = vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'save') + if save_capability then local included_text - if client.resolved_capabilities.text_document_save_include_text then + if type(save_capability) == 'table' and save_capability.includeText then included_text = text(bufnr) end client.notify('textDocument/didSave', { textDocument = { - uri = uri; - }; - text = included_text; + uri = uri, + }, + text = included_text, }) end end) @@ -1184,15 +1429,14 @@ end ---@param bufnr (number) Buffer handle, or 0 for current ---@param client_id (number) Client id function lsp.buf_attach_client(bufnr, client_id) - validate { - bufnr = {bufnr, 'n', true}; - client_id = {client_id, 'n'}; - } + validate({ + bufnr = { bufnr, 'n', true }, + 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) - ) + if not 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] @@ -1202,45 +1446,49 @@ function lsp.buf_attach_client(bufnr, client_id) all_buffer_active_clients[bufnr] = buffer_client_ids local uri = vim.uri_from_bufnr(bufnr) - local buf_did_save_autocommand = [=[ - augroup lsp_c_%d_b_%d_did_save - au! - au BufWritePost <buffer=%d> lua vim.lsp._text_document_did_save_handler(0) - augroup END - ]=] - vim.api.nvim_exec(string.format(buf_did_save_autocommand, client_id, bufnr, bufnr), false) + local augroup = ('lsp_c_%d_b_%d_did_save'):format(client_id, bufnr) + api.nvim_create_autocmd('BufWritePost', { + group = api.nvim_create_augroup(augroup, { clear = true }), + buffer = bufnr, + desc = 'vim.lsp: textDocument/didSave handler', + callback = function(ctx) + text_document_did_save_handler(ctx.buf) + end, + }) -- First time, so attach and set up stuff. - vim.api.nvim_buf_attach(bufnr, false, { - on_lines = text_document_did_change_handler; + api.nvim_buf_attach(bufnr, false, { + on_lines = text_document_did_change_handler, on_reload = function() - local params = { textDocument = { uri = uri; } } + 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 + if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then client.notify('textDocument/didClose', params) end text_document_did_open_handler(bufnr, client) end) - end; + end, on_detach = function() - local params = { textDocument = { uri = uri; } } + 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 + if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then client.notify('textDocument/didClose', params) end end) util.buf_versions[bufnr] = nil all_buffer_active_clients[bufnr] = nil - end; + end, -- TODO if we know all of the potential clients ahead of time, then we -- could conditionally set this. -- utf_sizes = size_index > 1; - utf_sizes = true; + utf_sizes = true, }) end - if buffer_client_ids[client_id] then return end + if buffer_client_ids[client_id] then + return + end -- This is our first time attaching this client to this buffer. buffer_client_ids[client_id] = true @@ -1260,25 +1508,35 @@ end ---@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'}; - } + 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) + string.format( + 'Buffer (id: %d) is not attached to client (id: %d). Cannot detach.', + client_id, + bufnr + ) ) return end + nvim_exec_autocmds('LspDetach', { + buffer = bufnr, + modeline = false, + data = { client_id = client_id }, + }) + changetracking.reset_buf(client, bufnr) - if client.resolved_capabilities.text_document_open_close then + if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then local uri = vim.uri_from_bufnr(bufnr) - local params = { textDocument = { uri = uri; } } + local params = { textDocument = { uri = uri } } client.notify('textDocument/didClose', params) end @@ -1294,7 +1552,6 @@ function lsp.buf_detach_client(bufnr, 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. @@ -1339,7 +1596,7 @@ end ---@param client_id client id or |vim.lsp.client| object, or list thereof ---@param force boolean (optional) shutdown forcefully function lsp.stop_client(client_id, force) - local ids = type(client_id) == 'table' and client_id or {client_id} + local ids = type(client_id) == 'table' and client_id or { client_id } for _, id in ipairs(ids) do if type(id) == 'table' and id.stop ~= nil then id.stop(force) @@ -1351,68 +1608,90 @@ function lsp.stop_client(client_id, force) end end ---- Gets all active clients. +--- Get active clients. --- ----@returns Table of |vim.lsp.client| objects -function lsp.get_active_clients() - return vim.tbl_values(active_clients) -end +---@param filter (table|nil) A table with key-value pairs used to filter the +--- returned clients. The available keys are: +--- - id (number): Only return clients with the given id +--- - bufnr (number): Only return clients attached to this buffer +--- - name (string): Only return clients with the given name +---@returns (table) List of |vim.lsp.client| objects +function lsp.get_active_clients(filter) + validate({ filter = { filter, 't', true } }) -function lsp._vim_exit_handler() - log.info("exit_handler", active_clients) - for _, client in pairs(uninitialized_clients) do - client.stop(true) - end - -- TODO handle v:dying differently? - if tbl_isempty(active_clients) then - return - end - for _, client in pairs(active_clients) do - client.stop() - end + filter = filter or {} - local timeouts = {} - local max_timeout = 0 - local send_kill = false + local clients = {} - for client_id, client in pairs(active_clients) do - local timeout = if_nil(client.config.flags.exit_timeout, 500) - if timeout then - send_kill = true - timeouts[client_id] = timeout - max_timeout = math.max(timeout, max_timeout) + local t = filter.bufnr and (all_buffer_active_clients[resolve_bufnr(filter.bufnr)] or {}) + or active_clients + for client_id in pairs(t) do + local client = active_clients[client_id] + if + (filter.id == nil or client.id == filter.id) + and (filter.name == nil or client.name == filter.name) + then + clients[#clients + 1] = client end end + return clients +end - local poll_time = 50 - - ---@private - local function check_clients_closed() - for client_id, timeout in pairs(timeouts) do - timeouts[client_id] = timeout - poll_time +api.nvim_create_autocmd('VimLeavePre', { + desc = 'vim.lsp: exit handler', + callback = function() + log.info('exit_handler', active_clients) + for _, client in pairs(uninitialized_clients) do + client.stop(true) + end + -- TODO handle v:dying differently? + if tbl_isempty(active_clients) then + return end + for _, client in pairs(active_clients) do + client.stop() + end + + local timeouts = {} + local max_timeout = 0 + local send_kill = false - for client_id, _ in pairs(active_clients) do - if timeouts[client_id] ~= nil and timeouts[client_id] > 0 then - return false + for client_id, client in pairs(active_clients) do + local timeout = if_nil(client.config.flags.exit_timeout, 500) + if timeout then + send_kill = true + timeouts[client_id] = timeout + max_timeout = math.max(timeout, max_timeout) end end - return true - end - if send_kill then - if not vim.wait(max_timeout, check_clients_closed, poll_time) then - for client_id, client in pairs(active_clients) do - if timeouts[client_id] ~= nil then - client.stop(true) + local poll_time = 50 + + ---@private + local function check_clients_closed() + for client_id, timeout in pairs(timeouts) do + timeouts[client_id] = timeout - poll_time + end + + for client_id, _ in pairs(active_clients) do + if timeouts[client_id] ~= nil and timeouts[client_id] > 0 then + return false end end + return true end - end -end - -nvim_command("autocmd VimLeavePre * lua vim.lsp._vim_exit_handler()") + if send_kill then + if not vim.wait(max_timeout, check_clients_closed, poll_time) then + for client_id, client in pairs(active_clients) do + if timeouts[client_id] ~= nil then + client.stop(true) + end + end + end + end + end, +}) --- Sends an async request for all active clients attached to the --- buffer. @@ -1428,11 +1707,11 @@ nvim_command("autocmd VimLeavePre * lua vim.lsp._vim_exit_handler()") --- - Function which can be used to cancel all the requests. You could instead --- iterate all clients and call their `cancel_request()` methods. function lsp.buf_request(bufnr, method, params, handler) - validate { - bufnr = { bufnr, 'n', true }; - method = { method, 's' }; - handler = { handler, 'f', true }; - } + validate({ + bufnr = { bufnr, 'n', true }, + method = { method, 's' }, + handler = { handler, 'f', true }, + }) local supported_clients = {} local method_supported = false @@ -1444,20 +1723,22 @@ function lsp.buf_request(bufnr, method, params, handler) end) -- if has client but no clients support the given method, notify the user - if not tbl_isempty(all_buffer_active_clients[resolve_bufnr(bufnr)] or {}) and not method_supported then + if + not tbl_isempty(all_buffer_active_clients[resolve_bufnr(bufnr)] or {}) and not method_supported + then vim.notify(lsp._unsupported_method(method), vim.log.levels.ERROR) - vim.api.nvim_command("redraw") + nvim_command('redraw') return {}, function() end end local client_request_ids = {} for_each_buffer_client(bufnr, function(client, client_id, resolved_bufnr) - local request_success, request_id = client.request(method, params, handler, resolved_bufnr) - -- This could only fail if the client shut down in the time since we looked - -- it up and we did the request, which should be rare. - if request_success then - client_request_ids[client_id] = request_id - end + local request_success, request_id = client.request(method, params, handler, resolved_bufnr) + -- This could only fail if the client shut down in the time since we looked + -- it up and we did the request, which should be rare. + if request_success then + client_request_ids[client_id] = request_id + end end, supported_clients) local function _cancel_all_requests() @@ -1488,7 +1769,7 @@ function lsp.buf_request_all(bufnr, method, params, callback) local result_count = 0 local expected_result_count = 0 - local set_expected_result_count = once(function () + local set_expected_result_count = once(function() for_each_buffer_client(bufnr, function(client) if client.supports_method(method) then expected_result_count = expected_result_count + 1 @@ -1552,23 +1833,24 @@ end --- ---@returns true if any client returns true; false otherwise function lsp.buf_notify(bufnr, method, params) - validate { - bufnr = { bufnr, 'n', true }; - method = { method, 's' }; - } + validate({ + bufnr = { bufnr, 'n', true }, + method = { method, 's' }, + }) local resp = false for_each_buffer_client(bufnr, function(client, _client_id, _resolved_bufnr) - if client.rpc.notify(method, params) then resp = true end + if client.rpc.notify(method, params) then + resp = true + end end) return resp end - ---@private 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 @@ -1595,7 +1877,7 @@ end --- - findstart=0: column where the completion starts, or -2 or -3 --- - findstart=1: list of matches (actually just calls |complete()|) function lsp.omnifunc(findstart, base) - local _ = log.debug() and log.debug("omnifunc.findstart", { findstart = findstart, base = base }) + local _ = log.debug() and log.debug('omnifunc.findstart', { findstart = findstart, base = base }) local bufnr = resolve_bufnr() local has_buffer_clients = not tbl_isempty(all_buffer_active_clients[bufnr] or {}) @@ -1608,12 +1890,12 @@ function lsp.omnifunc(findstart, base) end -- Then, perform standard completion request - local _ = log.info() and log.info("base ", base) + local _ = log.info() and log.info('base ', base) - local pos = vim.api.nvim_win_get_cursor(0) - local line = vim.api.nvim_get_current_line() + local pos = api.nvim_win_get_cursor(0) + local line = api.nvim_get_current_line() local line_to_cursor = line:sub(1, pos[2]) - local _ = log.trace() and log.trace("omnifunc.line", pos, line) + local _ = log.trace() and log.trace('omnifunc.line', pos, line) -- Get the start position of the current keyword local textMatch = vim.fn.match(line_to_cursor, '\\k*$') @@ -1622,7 +1904,9 @@ function lsp.omnifunc(findstart, base) local items = {} lsp.buf_request(bufnr, 'textDocument/completion', params, function(err, result, ctx) - if err or not result or vim.fn.mode() ~= "i" then return end + if err or not result or vim.fn.mode() ~= 'i' then + return + end -- Completion response items may be relative to a position different than `textMatch`. -- Concrete example, with sumneko/lua-language-server: @@ -1659,7 +1943,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: @@ -1668,7 +1952,7 @@ function lsp.formatexpr(opts) opts = opts or {} local timeout_ms = opts.timeout_ms or 500 - if vim.tbl_contains({'i', 'R', 'ic', 'ix'}, vim.fn.mode()) then + if vim.tbl_contains({ 'i', 'R', 'ic', 'ix' }, vim.fn.mode()) then -- `formatexpr` is also called when exceeding `textwidth` in insert mode -- fall back to internal formatting return 1 @@ -1679,19 +1963,24 @@ function lsp.formatexpr(opts) if start_line > 0 and end_line > 0 then local params = { - textDocument = util.make_text_document_params(); + textDocument = util.make_text_document_params(), range = { - start = { line = start_line - 1; character = 0; }; - ["end"] = { line = end_line - 1; character = 0; }; - }; - }; + start = { line = start_line - 1, character = 0 }, + ['end'] = { line = end_line - 1, character = 0 }, + }, + } params.options = util.make_formatting_params().options - local client_results = vim.lsp.buf_request_sync(0, "textDocument/rangeFormatting", params, timeout_ms) + 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 @@ -1728,26 +2017,28 @@ end --- is a |vim.lsp.client| object. --- ---@param bufnr (optional, number): Buffer handle, or 0 for current +---@returns (table) Table of (client_id, client) pairs +---@deprecated Use |vim.lsp.get_active_clients()| instead. function lsp.buf_get_clients(bufnr) - bufnr = resolve_bufnr(bufnr) - local result = {} - for_each_buffer_client(bufnr, function(client, client_id) - result[client_id] = client - end) - return result + local result = {} + for _, client in ipairs(lsp.get_active_clients({ bufnr = resolve_bufnr(bufnr) })) do + result[client.id] = client + end + return result end -- 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" +-- Levels by name: "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "OFF" -- 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" +--- Levels by name: "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "OFF" +--- --- Level numbers begin with "TRACE" at 0 --- --- Use `lsp.log_levels` for reverse lookup. @@ -1759,7 +2050,7 @@ function lsp.set_log_level(level) if type(level) == 'string' or type(level) == 'number' then log.set_level(level) else - error(string.format("Invalid log level: %q", level)) + error(string.format('Invalid log level: %q', level)) end end @@ -1789,7 +2080,7 @@ end ---@param override_config (table) Table containing the keys to override behavior of the {handler} function lsp.with(handler, override_config) return function(err, result, ctx, config) - return handler(err, result, ctx, vim.tbl_deep_extend("force", config or {}, override_config)) + return handler(err, result, ctx, vim.tbl_deep_extend('force', config or {}, override_config)) end end @@ -1804,12 +2095,16 @@ function lsp._with_extend(name, options, user_config) local resulting_config = {} for k, v in pairs(user_config) do if options[k] == nil then - error(debug.traceback(string.format( - "Invalid option for `%s`: %s. Valid options are:\n%s", - name, - k, - vim.inspect(vim.tbl_keys(options)) - ))) + error( + debug.traceback( + string.format( + 'Invalid option for `%s`: %s. Valid options are:\n%s', + name, + k, + vim.inspect(vim.tbl_keys(options)) + ) + ) + ) end resulting_config[k] = v @@ -1824,7 +2119,6 @@ function lsp._with_extend(name, options, user_config) return resulting_config end - --- Registry for client side commands. --- This is an extension point for plugins to handle custom commands which are --- not part of the core language server protocol specification. @@ -1846,12 +2140,11 @@ end --- The second argument is the `ctx` of |lsp-handler| lsp.commands = setmetatable({}, { __newindex = function(tbl, key, value) - assert(type(key) == 'string', "The key for commands in `vim.lsp.commands` must be a string") - assert(type(value) == 'function', "Command added to `vim.lsp.commands` must be a function") + assert(type(key) == 'string', 'The key for commands in `vim.lsp.commands` must be a string') + assert(type(value) == 'function', 'Command added to `vim.lsp.commands` must be a function') rawset(tbl, key, value) - end; + end, }) - return lsp -- vim:sw=2 ts=2 et diff --git a/runtime/lua/vim/lsp/_snippet.lua b/runtime/lua/vim/lsp/_snippet.lua index 0140b0aee3..3488639fb4 100644 --- a/runtime/lua/vim/lsp/_snippet.lua +++ b/runtime/lua/vim/lsp/_snippet.lua @@ -41,7 +41,7 @@ P.take_until = function(targets, specials) parsed = true, value = { raw = table.concat(raw, ''), - esc = table.concat(esc, '') + esc = table.concat(esc, ''), }, pos = new_pos, } @@ -156,10 +156,10 @@ P.seq = function(...) return function(input, pos) local values = {} local new_pos = pos - for _, parser in ipairs(parsers) do + for i, parser in ipairs(parsers) do local result = parser(input, new_pos) if result.parsed then - table.insert(values, result.value) + values[i] = result.value new_pos = result.pos else return P.unmatch(pos) @@ -248,49 +248,122 @@ S.format = P.any( capture_index = values[3], }, Node) end), - P.map(P.seq(S.dollar, S.open, S.int, S.colon, S.slash, P.any( - P.token('upcase'), - P.token('downcase'), - P.token('capitalize'), - P.token('camelcase'), - P.token('pascalcase') - ), S.close), function(values) - return setmetatable({ - type = Node.Type.FORMAT, - capture_index = values[3], - modifier = values[6], - }, Node) - end), - P.map(P.seq(S.dollar, S.open, S.int, S.colon, P.any( - P.seq(S.question, P.take_until({ ':' }, { '\\' }), S.colon, P.take_until({ '}' }, { '\\' })), - P.seq(S.plus, P.take_until({ '}' }, { '\\' })), - P.seq(S.minus, P.take_until({ '}' }, { '\\' })) - ), S.close), function(values) + P.map( + P.seq( + S.dollar, + S.open, + S.int, + S.colon, + S.slash, + P.any( + P.token('upcase'), + P.token('downcase'), + P.token('capitalize'), + P.token('camelcase'), + P.token('pascalcase') + ), + S.close + ), + function(values) + return setmetatable({ + type = Node.Type.FORMAT, + capture_index = values[3], + modifier = values[6], + }, Node) + end + ), + P.map( + P.seq( + S.dollar, + S.open, + S.int, + S.colon, + P.seq( + S.question, + P.opt(P.take_until({ ':' }, { '\\' })), + S.colon, + P.opt(P.take_until({ '}' }, { '\\' })) + ), + S.close + ), + function(values) + return setmetatable({ + type = Node.Type.FORMAT, + capture_index = values[3], + if_text = values[5][2] and values[5][2].esc or '', + else_text = values[5][4] and values[5][4].esc or '', + }, Node) + end + ), + P.map( + P.seq( + S.dollar, + S.open, + S.int, + S.colon, + P.seq(S.plus, P.opt(P.take_until({ '}' }, { '\\' }))), + S.close + ), + function(values) + return setmetatable({ + type = Node.Type.FORMAT, + capture_index = values[3], + if_text = values[5][2] and values[5][2].esc or '', + else_text = '', + }, Node) + end + ), + P.map( + P.seq( + S.dollar, + S.open, + S.int, + S.colon, + S.minus, + P.opt(P.take_until({ '}' }, { '\\' })), + S.close + ), + function(values) + return setmetatable({ + type = Node.Type.FORMAT, + capture_index = values[3], + if_text = '', + else_text = values[6] and values[6].esc or '', + }, Node) + end + ), + P.map( + P.seq(S.dollar, S.open, S.int, S.colon, P.opt(P.take_until({ '}' }, { '\\' })), S.close), + function(values) + return setmetatable({ + type = Node.Type.FORMAT, + capture_index = values[3], + if_text = '', + else_text = values[5] and values[5].esc or '', + }, Node) + end + ) +) + +S.transform = P.map( + P.seq( + S.slash, + P.take_until({ '/' }, { '\\' }), + S.slash, + P.many(P.any(S.format, S.text({ '$', '/' }, { '\\' }))), + S.slash, + P.opt(P.pattern('[ig]+')) + ), + function(values) return setmetatable({ - type = Node.Type.FORMAT, - capture_index = values[3], - if_text = values[5][2].esc, - else_text = (values[5][4] or {}).esc, + type = Node.Type.TRANSFORM, + pattern = values[2].raw, + format = values[4], + option = values[6], }, Node) - end) + end ) -S.transform = P.map(P.seq( - S.slash, - P.take_until({ '/' }, { '\\' }), - S.slash, - P.many(P.any(S.format, S.text({ '$', '/' }, { '\\' }))), - S.slash, - P.opt(P.pattern('[ig]+')) -), function(values) - return setmetatable({ - type = Node.Type.TRANSFORM, - pattern = values[2].raw, - format = values[4], - option = values[6], - }, Node) -end) - S.tabstop = P.any( P.map(P.seq(S.dollar, S.int), function(values) return setmetatable({ @@ -314,34 +387,52 @@ S.tabstop = P.any( ) S.placeholder = P.any( - P.map(P.seq(S.dollar, S.open, S.int, S.colon, P.many(P.any(S.toplevel, S.text({ '$', '}' }, { '\\' }))), S.close), function(values) - return setmetatable({ - type = Node.Type.PLACEHOLDER, - tabstop = values[3], - children = values[5], - }, Node) - end) + P.map( + P.seq( + S.dollar, + S.open, + S.int, + S.colon, + P.opt(P.many(P.any(S.toplevel, S.text({ '$', '}' }, { '\\' })))), + S.close + ), + function(values) + return setmetatable({ + type = Node.Type.PLACEHOLDER, + tabstop = values[3], + -- insert empty text if opt did not match. + children = values[5] or { + setmetatable({ + type = Node.Type.TEXT, + raw = '', + esc = '', + }, Node), + }, + }, Node) + end + ) ) -S.choice = P.map(P.seq( - S.dollar, - S.open, - S.int, - S.pipe, - P.many( - P.map(P.seq(S.text({ ',', '|' }), P.opt(S.comma)), function(values) +S.choice = P.map( + P.seq( + S.dollar, + S.open, + S.int, + S.pipe, + P.many(P.map(P.seq(S.text({ ',', '|' }), P.opt(S.comma)), function(values) return values[1].esc - end) + end)), + S.pipe, + S.close ), - S.pipe, - S.close -), function(values) - return setmetatable({ - type = Node.Type.CHOICE, - tabstop = values[3], - items = values[5], - }, Node) -end) + function(values) + return setmetatable({ + type = Node.Type.CHOICE, + tabstop = values[3], + items = values[5], + }, Node) + end +) S.variable = P.any( P.map(P.seq(S.dollar, S.var), function(values) @@ -363,13 +454,23 @@ S.variable = P.any( transform = values[4], }, Node) end), - P.map(P.seq(S.dollar, S.open, S.var, S.colon, P.many(P.any(S.toplevel, S.text({ '$', '}' }, { '\\' }))), S.close), function(values) - return setmetatable({ - type = Node.Type.VARIABLE, - name = values[3], - children = values[5], - }, Node) - end) + P.map( + P.seq( + S.dollar, + S.open, + S.var, + S.colon, + P.many(P.any(S.toplevel, S.text({ '$', '}' }, { '\\' }))), + S.close + ), + function(values) + return setmetatable({ + type = Node.Type.VARIABLE, + name = values[3], + children = values[5], + }, Node) + end + ) ) S.snippet = P.map(P.many(P.any(S.toplevel, S.text({ '$' }, { '}', '\\' }))), function(values) diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index c1d777ae6c..50a51e897c 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -1,30 +1,12 @@ local vim = vim +local api = vim.api local validate = vim.validate -local vfn = vim.fn -local util = require 'vim.lsp.util' +local util = require('vim.lsp.util') +local npcall = vim.F.npcall local M = {} ---@private ---- Returns nil if {status} is false or nil, otherwise returns the rest of the ---- arguments. -local function ok_or_nil(status, ...) - if not status then return end - return ... -end - ----@private ---- Swallows errors. ---- ----@param fn Function to run ----@param ... Function arguments ----@returns Result of `fn(...)` if there are no errors, otherwise nil. ---- Returns nil if errors occur during {fn}, otherwise returns -local function npcall(fn, ...) - return ok_or_nil(pcall(fn, ...)) -end - ----@private --- Sends an async request to all active clients attached to the current --- buffer. --- @@ -39,10 +21,10 @@ end --- ---@see |vim.lsp.buf_request()| local function request(method, params, handler) - validate { - method = {method, 's'}; - handler = {handler, 'f', true}; - } + validate({ + method = { method, 's' }, + handler = { handler, 'f', true }, + }) return vim.lsp.buf_request(0, method, params, handler) end @@ -51,7 +33,7 @@ end --- ---@returns `true` if server responds. function M.server_ready() - return not not vim.lsp.buf_notify(0, "window/progress", {}) + return not not vim.lsp.buf_notify(0, 'window/progress', {}) end --- Displays hover information about the symbol under the cursor in a floating @@ -61,26 +43,45 @@ function M.hover() request('textDocument/hover', params) end +---@private +local function request_with_options(name, params, options) + local req_handler + if options then + req_handler = function(err, result, ctx, config) + local client = vim.lsp.get_client_by_id(ctx.client_id) + local handler = client.handlers[name] or vim.lsp.handlers[name] + handler(err, result, ctx, vim.tbl_extend('force', config or {}, options)) + end + end + request(name, params, req_handler) +end + --- Jumps to the declaration of the symbol under the cursor. ---@note Many servers do not implement this method. Generally, see |vim.lsp.buf.definition()| instead. --- -function M.declaration() +---@param options table|nil additional options +--- - reuse_win: (boolean) Jump to existing window if buffer is already open. +function M.declaration(options) local params = util.make_position_params() - request('textDocument/declaration', params) + request_with_options('textDocument/declaration', params, options) end --- Jumps to the definition of the symbol under the cursor. --- -function M.definition() +---@param options table|nil additional options +--- - reuse_win: (boolean) Jump to existing window if buffer is already open. +function M.definition(options) local params = util.make_position_params() - request('textDocument/definition', params) + request_with_options('textDocument/definition', params, options) end --- Jumps to the definition of the type of the symbol under the cursor. --- -function M.type_definition() +---@param options table|nil additional options +--- - reuse_win: (boolean) Jump to existing window if buffer is already open. +function M.type_definition(options) local params = util.make_position_params() - request('textDocument/typeDefinition', params) + request_with_options('textDocument/typeDefinition', params, options) end --- Lists all the implementations for the symbol under the cursor in the @@ -117,9 +118,9 @@ end -- ---@returns The client that the user selected or nil local function select_client(method, on_choice) - validate { + 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) @@ -143,16 +144,105 @@ local function select_client(method, on_choice) end end +--- Formats a buffer using the attached (and optionally filtered) language +--- server clients. +--- +--- @param options table|nil Optional table which holds the following optional fields: +--- - formatting_options (table|nil): +--- Can be used to specify FormattingOptions. Some unspecified options will be +--- automatically derived from the current Neovim options. +--- @see https://microsoft.github.io/language-server-protocol/specification#textDocument_formatting +--- - timeout_ms (integer|nil, default 1000): +--- Time in milliseconds to block for formatting requests. No effect if async=true +--- - bufnr (number|nil): +--- Restrict formatting to the clients attached to the given buffer, defaults to the current +--- buffer (0). +--- +--- - filter (function|nil): +--- Predicate used to filter clients. Receives a client as argument and must return a +--- boolean. Clients matching the predicate are included. Example: +--- +--- <pre> +--- -- Never request typescript-language-server for formatting +--- vim.lsp.buf.format { +--- filter = function(client) return client.name ~= "tsserver" end +--- } +--- </pre> +--- +--- - async boolean|nil +--- If true the method won't block. Defaults to false. +--- Editing the buffer while formatting asynchronous can lead to unexpected +--- changes. +--- +--- - id (number|nil): +--- Restrict formatting to the client with ID (client.id) matching this field. +--- - name (string|nil): +--- Restrict formatting to the client with name (client.name) matching this field. + +function M.format(options) + options = options or {} + local bufnr = options.bufnr or api.nvim_get_current_buf() + local clients = vim.lsp.get_active_clients({ + id = options.id, + bufnr = bufnr, + name = options.name, + }) + + if options.filter then + clients = vim.tbl_filter(options.filter, clients) + end + + clients = vim.tbl_filter(function(client) + return client.supports_method('textDocument/formatting') + end, clients) + + if #clients == 0 then + vim.notify('[LSP] Format request failed, no matching language servers.') + end + + if options.async then + local do_format + do_format = function(idx, client) + if not client then + return + end + local params = util.make_formatting_params(options.formatting_options) + client.request('textDocument/formatting', params, function(...) + local handler = client.handlers['textDocument/formatting'] + or vim.lsp.handlers['textDocument/formatting'] + handler(...) + do_format(next(clients, idx)) + end, bufnr) + end + do_format(next(clients)) + else + local timeout_ms = options.timeout_ms or 1000 + for _, client in pairs(clients) do + local params = util.make_formatting_params(options.formatting_options) + 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(string.format('[LSP][%s] %s', client.name, err), vim.log.levels.WARN) + end + end + end +end + --- Formats the current buffer. --- ----@param options (optional, table) Can be used to specify FormattingOptions. +---@param options (table|nil) Can be used to specify FormattingOptions. --- Some unspecified options will be automatically derived from the current --- Neovim options. -- ---@see https://microsoft.github.io/language-server-protocol/specification#textDocument_formatting function M.formatting(options) + vim.notify_once( + 'vim.lsp.buf.formatting is deprecated. Use vim.lsp.buf.format { async = true } instead', + vim.log.levels.WARN + ) local params = util.make_formatting_params(options) - local bufnr = vim.api.nvim_get_current_buf() + local bufnr = api.nvim_get_current_buf() select_client('textDocument/formatting', function(client) if client == nil then return @@ -171,12 +261,16 @@ end --- autocmd BufWritePre <buffer> lua vim.lsp.buf.formatting_sync() --- </pre> --- ----@param options Table with valid `FormattingOptions` entries +---@param options table|nil with valid `FormattingOptions` entries ---@param timeout_ms (number) Request timeout ---@see |vim.lsp.buf.formatting_seq_sync| function M.formatting_sync(options, timeout_ms) + vim.notify_once( + 'vim.lsp.buf.formatting_sync is deprecated. Use vim.lsp.buf.format instead', + vim.log.levels.WARN + ) local params = util.make_formatting_params(options) - local bufnr = vim.api.nvim_get_current_buf() + local bufnr = api.nvim_get_current_buf() select_client('textDocument/formatting', function(client) if client == nil then return @@ -184,7 +278,7 @@ function M.formatting_sync(options, timeout_ms) 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) + 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 @@ -202,14 +296,18 @@ end --- vim.api.nvim_command[[autocmd BufWritePre <buffer> lua vim.lsp.buf.formatting_seq_sync()]] --- </pre> --- ----@param options (optional, table) `FormattingOptions` entries ----@param timeout_ms (optional, number) Request timeout ----@param order (optional, table) List of client names. Formatting is requested from clients +---@param options (table|nil) `FormattingOptions` entries +---@param timeout_ms (number|nil) Request timeout +---@param order (table|nil) List of client names. Formatting is requested from clients ---in the following order: first all clients that are not in the `order` list, then ---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() + vim.notify_once( + 'vim.lsp.buf.formatting_seq_sync is deprecated. Use vim.lsp.buf.format instead', + vim.log.levels.WARN + ) + local clients = vim.tbl_values(vim.lsp.buf_get_clients()) + local bufnr = api.nvim_get_current_buf() -- sort the clients according to `order` for _, client_name in pairs(order or {}) do @@ -224,13 +322,21 @@ function M.formatting_seq_sync(options, timeout_ms, order) -- loop through the clients and make synchronous formatting requests for _, client in pairs(clients) do - if client.resolved_capabilities.document_formatting then + if vim.tbl_get(client.server_capabilities, 'documentFormattingProvider') then local params = util.make_formatting_params(options) - local result, err = client.request_sync("textDocument/formatting", params, timeout_ms, vim.api.nvim_get_current_buf()) + local result, err = client.request_sync( + 'textDocument/formatting', + params, + timeout_ms, + api.nvim_get_current_buf() + ) if result and result.result then - util.apply_text_edits(result.result, bufnr) + 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) + vim.notify( + string.format('vim.lsp.buf.formatting_seq_sync: (%s) %s', client.name, err), + vim.log.levels.WARN + ) end end end @@ -257,50 +363,133 @@ end --- Renames all references to the symbol under the cursor. --- ----@param new_name (string) If not provided, the user will be prompted for a new ----name using |vim.ui.input()|. -function M.rename(new_name) - local opts = { - prompt = "New Name: " - } +---@param new_name string|nil If not provided, the user will be prompted for a new +--- name using |vim.ui.input()|. +---@param options table|nil additional options +--- - filter (function|nil): +--- Predicate used to filter clients. Receives a client as argument and +--- must return a boolean. Clients matching the predicate are included. +--- - name (string|nil): +--- Restrict clients used for rename to ones where client.name matches +--- this field. +function M.rename(new_name, options) + options = options or {} + local bufnr = options.bufnr or api.nvim_get_current_buf() + local clients = vim.lsp.get_active_clients({ + bufnr = bufnr, + name = options.name, + }) + if options.filter then + clients = vim.tbl_filter(options.filter, clients) + end - ---@private - local function on_confirm(input) - if not (input and #input > 0) then return end - local params = util.make_position_params() - params.newName = input - request('textDocument/rename', params) + -- Clients must at least support rename, prepareRename is optional + clients = vim.tbl_filter(function(client) + return client.supports_method('textDocument/rename') + end, clients) + + if #clients == 0 then + vim.notify('[LSP] Rename, no matching language servers with rename capability.') end + local win = api.nvim_get_current_win() + + -- Compute early to account for cursor movements after going async + local cword = vim.fn.expand('<cword>') + ---@private - local function prepare_rename(err, result) - if err == nil and result == nil then - vim.notify('nothing to rename', vim.log.levels.INFO) + local function get_text_at_range(range, offset_encoding) + return api.nvim_buf_get_text( + bufnr, + range.start.line, + util._get_line_byte_from_position(bufnr, range.start, offset_encoding), + range['end'].line, + util._get_line_byte_from_position(bufnr, range['end'], offset_encoding), + {} + )[1] + end + + local try_use_client + try_use_client = function(idx, client) + if not client then return end - if result and result.placeholder then - opts.default = result.placeholder - if not new_name then npcall(vim.ui.input, opts, on_confirm) end - elseif result and result.start and result['end'] and - result.start.line == result['end'].line then - local line = vfn.getline(result.start.line+1) - local start_char = result.start.character+1 - local end_char = result['end'].character - opts.default = string.sub(line, start_char, end_char) - if not new_name then npcall(vim.ui.input, opts, on_confirm) end + + ---@private + local function rename(name) + local params = util.make_position_params(win, client.offset_encoding) + params.newName = name + local handler = client.handlers['textDocument/rename'] + or vim.lsp.handlers['textDocument/rename'] + client.request('textDocument/rename', params, function(...) + handler(...) + try_use_client(next(clients, idx)) + end, bufnr) + end + + if client.supports_method('textDocument/prepareRename') then + local params = util.make_position_params(win, client.offset_encoding) + client.request('textDocument/prepareRename', params, function(err, result) + if err or result == nil then + if next(clients, idx) then + try_use_client(next(clients, idx)) + else + local msg = err and ('Error on prepareRename: ' .. (err.message or '')) + or 'Nothing to rename' + vim.notify(msg, vim.log.levels.INFO) + end + return + end + + if new_name then + rename(new_name) + return + end + + local prompt_opts = { + prompt = 'New Name: ', + } + -- result: Range | { range: Range, placeholder: string } + if result.placeholder then + prompt_opts.default = result.placeholder + elseif result.start then + prompt_opts.default = get_text_at_range(result, client.offset_encoding) + elseif result.range then + prompt_opts.default = get_text_at_range(result.range, client.offset_encoding) + else + prompt_opts.default = cword + end + vim.ui.input(prompt_opts, function(input) + if not input or #input == 0 then + return + end + rename(input) + end) + end, bufnr) else - -- fallback to guessing symbol using <cword> - -- - -- this can happen if the language server does not support prepareRename, - -- returns an unexpected response, or requests for "default behavior" - -- - -- see https://microsoft.github.io/language-server-protocol/specification#textDocument_prepareRename - opts.default = vfn.expand('<cword>') - if not new_name then npcall(vim.ui.input, opts, on_confirm) end + assert( + client.supports_method('textDocument/rename'), + 'Client must support textDocument/rename' + ) + if new_name then + rename(new_name) + return + end + + local prompt_opts = { + prompt = 'New Name: ', + default = cword, + } + vim.ui.input(prompt_opts, function(input) + if not input or #input == 0 then + return + end + rename(input) + end) end - if new_name then on_confirm(new_name) end end - request('textDocument/prepareRename', util.make_position_params(), prepare_rename) + + try_use_client(next(clients)) end --- Lists all the references to the symbol under the cursor in the quickfix window. @@ -308,10 +497,10 @@ end ---@param context (table) Context for the request ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references function M.references(context) - validate { context = { context, 't', true } } + validate({ context = { context, 't', true } }) local params = util.make_position_params() params.context = context or { - includeDeclaration = true; + includeDeclaration = true, } request('textDocument/references', params) end @@ -325,14 +514,16 @@ end ---@private local function pick_call_hierarchy_item(call_hierarchy_items) - if not call_hierarchy_items then return end + if not call_hierarchy_items then + return + end if #call_hierarchy_items == 1 then return call_hierarchy_items[1] end local items = {} for i, item in pairs(call_hierarchy_items) do local entry = item.detail or item.name - table.insert(items, string.format("%d. %s", i, entry)) + table.insert(items, string.format('%d. %s', i, entry)) end local choice = vim.fn.inputlist(items) if choice < 1 or choice > #items then @@ -354,8 +545,8 @@ local function call_hierarchy(method) if client then client.request(method, { item = call_hierarchy_item }, nil, ctx.bufnr) else - vim.notify(string.format( - 'Client with id=%d disappeared during call hierarchy request', ctx.client_id), + vim.notify( + string.format('Client with id=%d disappeared during call hierarchy request', ctx.client_id), vim.log.levels.WARN ) end @@ -391,20 +582,26 @@ end --- Add the folder at path to the workspace folders. If {path} is --- not provided, the user will be prompted for a path using |input()|. function M.add_workspace_folder(workspace_folder) - workspace_folder = workspace_folder or npcall(vfn.input, "Workspace Folder: ", vfn.expand('%:p:h'), 'dir') - vim.api.nvim_command("redraw") - if not (workspace_folder and #workspace_folder > 0) then return end + workspace_folder = workspace_folder + or npcall(vim.fn.input, 'Workspace Folder: ', vim.fn.expand('%:p:h'), 'dir') + api.nvim_command('redraw') + if not (workspace_folder and #workspace_folder > 0) then + return + end if vim.fn.isdirectory(workspace_folder) == 0 then - print(workspace_folder, " is not a valid directory") + print(workspace_folder, ' is not a valid directory') return end - local params = util.make_workspace_params({{uri = vim.uri_from_fname(workspace_folder); name = 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.workspace_folders or {}) do if folder.name == workspace_folder then found = true - print(workspace_folder, "is already part of this workspace") + print(workspace_folder, 'is already part of this workspace') break end end @@ -422,10 +619,16 @@ end --- {path} is not provided, the user will be prompted for --- a path using |input()|. function M.remove_workspace_folder(workspace_folder) - workspace_folder = workspace_folder or npcall(vfn.input, "Workspace Folder: ", vfn.expand('%:p:h')) - vim.api.nvim_command("redraw") - 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}}) + workspace_folder = workspace_folder + or npcall(vim.fn.input, 'Workspace Folder: ', vim.fn.expand('%:p:h')) + api.nvim_command('redraw') + 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.workspace_folders) do if folder.name == workspace_folder then @@ -435,7 +638,7 @@ function M.remove_workspace_folder(workspace_folder) end end end - print(workspace_folder, "is not currently part of the workspace") + print(workspace_folder, 'is not currently part of the workspace') end --- Lists all symbols in the current workspace in the quickfix window. @@ -446,8 +649,11 @@ end --- ---@param query (string, optional) function M.workspace_symbol(query) - query = query or npcall(vfn.input, "Query: ") - local params = {query = query} + query = query or npcall(vim.fn.input, 'Query: ') + if query == nil then + return + end + local params = { query = query } request('workspace/symbol', params) end @@ -477,7 +683,6 @@ function M.clear_references() util.buf_clear_references() end - ---@private -- --- This is not public because the main extension point is @@ -488,11 +693,42 @@ end --- from multiple clients to have 1 single UI prompt for the user, yet we still --- need to be able to link a `CodeAction|Command` to the right client for --- `codeAction/resolve` -local function on_code_action_results(results, ctx) +local function on_code_action_results(results, ctx, options) local action_tuples = {} + + ---@private + local function action_filter(a) + -- filter by specified action kind + if options and options.context and options.context.only then + if not a.kind then + return false + end + local found = false + for _, o in ipairs(options.context.only) do + -- action kinds are hierarchical with . as a separator: when requesting only + -- 'quickfix' this filter allows both 'quickfix' and 'quickfix.foo', for example + if a.kind:find('^' .. o .. '$') or a.kind:find('^' .. o .. '%.') then + found = true + break + end + end + if not found then + return false + end + end + -- filter by user function + if options and options.filter and not options.filter(a) then + return false + end + -- no filter removed this action + return true + end + for client_id, result in pairs(results) do for _, action in pairs(result.result or {}) do - table.insert(action_tuples, { client_id, action }) + if action_filter(action) then + table.insert(action_tuples, { client_id, action }) + end end end if #action_tuples == 0 then @@ -503,7 +739,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 @@ -513,7 +749,14 @@ local function on_code_action_results(results, ctx) enriched_ctx.client_id = client.id fn(command, enriched_ctx) else - M.execute_command(command) + -- Not using command directly to exclude extra properties, + -- see https://github.com/python-lsp/python-lsp-server/issues/146 + local params = { + command = command.command, + arguments = command.arguments, + workDoneToken = command.workDoneToken, + } + client.request('workspace/executeCommand', params, nil, ctx.bufnr) end end end @@ -537,11 +780,11 @@ local function on_code_action_results(results, ctx) -- local client = vim.lsp.get_client_by_id(action_tuple[1]) local action = action_tuple[2] - if not action.edit - and client - and type(client.resolved_capabilities.code_action) == 'table' - and client.resolved_capabilities.code_action.resolveProvider then - + if + not action.edit + and client + and vim.tbl_get(client.server_capabilities, 'codeActionProvider', 'resolveProvider') + then client.request('codeAction/resolve', action, function(err, resolved_action) if err then vim.notify(err.code .. ': ' .. err.message, vim.log.levels.ERROR) @@ -554,6 +797,13 @@ local function on_code_action_results(results, ctx) end end + -- If options.apply is given, and there are just one remaining code action, + -- apply it directly without querying the user. + if options and options.apply and #action_tuples == 1 then + on_user_choice(action_tuples[1]) + return + end + vim.ui.select(action_tuples, { prompt = 'Code actions:', kind = 'codeaction', @@ -564,39 +814,53 @@ local function on_code_action_results(results, ctx) }, on_user_choice) end - --- Requests code actions from all clients and calls the handler exactly once --- with all aggregated results ---@private -local function code_action_request(params) - local bufnr = vim.api.nvim_get_current_buf() +local function code_action_request(params, options) + local bufnr = api.nvim_get_current_buf() local method = 'textDocument/codeAction' vim.lsp.buf_request_all(bufnr, method, params, function(results) - on_code_action_results(results, { bufnr = bufnr, method = method, params = params }) + local ctx = { bufnr = bufnr, method = method, params = params } + on_code_action_results(results, ctx, options) end) end --- Selects a code action available at the current --- cursor position. --- ----@param context table|nil `CodeActionContext` of the LSP specification: ---- - diagnostics: (table|nil) ---- LSP `Diagnostic[]`. Inferred from the current ---- position if not provided. ---- - only: (string|nil) ---- LSP `CodeActionKind` used to filter the code actions. ---- Most language servers support values like `refactor` ---- or `quickfix`. +---@param options table|nil Optional table which holds the following optional fields: +--- - context (table|nil): +--- Corresponds to `CodeActionContext` of the LSP specification: +--- - diagnostics (table|nil): +--- LSP `Diagnostic[]`. Inferred from the current +--- position if not provided. +--- - only (table|nil): +--- List of LSP `CodeActionKind`s used to filter the code actions. +--- Most language servers support values like `refactor` +--- or `quickfix`. +--- - filter (function|nil): +--- Predicate function taking an `CodeAction` and returning a boolean. +--- - apply (boolean|nil): +--- When set to `true`, and there is just one remaining action +--- (after filtering), the action is applied without user query. ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction -function M.code_action(context) - validate { context = { context, 't', true } } - context = context or {} +function M.code_action(options) + validate({ options = { options, 't', true } }) + options = options or {} + -- Detect old API call code_action(context) which should now be + -- code_action({ context = context} ) + if options.diagnostics or options.only then + options = { options = options } + end + local context = options.context or {} if not context.diagnostics then - context.diagnostics = vim.lsp.diagnostic.get_line_diagnostics() + local bufnr = api.nvim_get_current_buf() + context.diagnostics = vim.lsp.diagnostic.get_line_diagnostics(bufnr) end local params = util.make_range_params() params.context = context - code_action_request(params) + code_action_request(params, options) end --- Performs |vim.lsp.buf.code_action()| for a given range. @@ -606,8 +870,8 @@ end --- - diagnostics: (table|nil) --- LSP `Diagnostic[]`. Inferred from the current --- position if not provided. ---- - only: (string|nil) ---- LSP `CodeActionKind` used to filter the code actions. +--- - only: (table|nil) +--- List of LSP `CodeActionKind`s used to filter the code actions. --- Most language servers support values like `refactor` --- or `quickfix`. ---@param start_pos ({number, number}, optional) mark-indexed position. @@ -615,10 +879,11 @@ end ---@param end_pos ({number, number}, optional) mark-indexed position. ---Defaults to the end of the last visual selection. function M.range_code_action(context, start_pos, end_pos) - validate { context = { context, 't', true } } + validate({ context = { context, 't', true } }) context = context or {} if not context.diagnostics then - context.diagnostics = vim.lsp.diagnostic.get_line_diagnostics() + local bufnr = api.nvim_get_current_buf() + context.diagnostics = vim.lsp.diagnostic.get_line_diagnostics(bufnr) end local params = util.make_given_range_params(start_pos, end_pos) params.context = context @@ -630,16 +895,16 @@ end ---@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_params) - validate { + validate({ command = { command_params.command, 's' }, - arguments = { command_params.arguments, 't', true } - } + arguments = { command_params.arguments, 't', true }, + }) command_params = { - command=command_params.command, - arguments=command_params.arguments, - workDoneToken=command_params.workDoneToken, + command = command_params.command, + arguments = command_params.arguments, + workDoneToken = command_params.workDoneToken, } - request('workspace/executeCommand', command_params ) + request('workspace/executeCommand', command_params) end return M diff --git a/runtime/lua/vim/lsp/codelens.lua b/runtime/lua/vim/lsp/codelens.lua index 9eb64c9a2e..4fa02c8db2 100644 --- a/runtime/lua/vim/lsp/codelens.lua +++ b/runtime/lua/vim/lsp/codelens.lua @@ -1,4 +1,5 @@ local util = require('vim.lsp.util') +local log = require('vim.lsp.log') local api = vim.api local M = {} @@ -11,7 +12,7 @@ local lens_cache_by_buf = setmetatable({}, { __index = function(t, b) local key = b > 0 and b or api.nvim_get_current_buf() return rawget(t, key) - end + end, }) local namespaces = setmetatable({}, { @@ -19,13 +20,12 @@ local namespaces = setmetatable({}, { local value = api.nvim_create_namespace('vim_lsp_codelens:' .. key) rawset(t, key, value) return value - end; + end, }) ---@private M.__namespaces = namespaces - ---@private local function execute_lens(lens, bufnr, client_id) local line = lens.range.start.line @@ -43,10 +43,14 @@ local function execute_lens(lens, bufnr, client_id) local command_provider = client.server_capabilities.executeCommandProvider local commands = type(command_provider) == 'table' and command_provider.commands or {} if not vim.tbl_contains(commands, command.command) then - vim.notify(string.format( - "Language server does not support command `%s`. This command may require a client extension.", command.command), - vim.log.levels.WARN) - return + vim.notify( + string.format( + 'Language server does not support command `%s`. This command may require a client extension.', + command.command + ), + vim.log.levels.WARN + ) + return end client.request('workspace/executeCommand', command, function(...) local result = vim.lsp.handlers['workspace/executeCommand'](...) @@ -55,14 +59,15 @@ local function execute_lens(lens, bufnr, client_id) end, bufnr) end - --- Return all lenses for the given buffer --- ---@param bufnr number Buffer number. 0 can be used for the current buffer. ---@return table (`CodeLens[]`) function M.get(bufnr) local lenses_by_client = lens_cache_by_buf[bufnr or 0] - if not lenses_by_client then return {} end + if not lenses_by_client then + return {} + end local lenses = {} for _, client_lenses in pairs(lenses_by_client) do vim.list_extend(lenses, client_lenses) @@ -70,7 +75,6 @@ function M.get(bufnr) return lenses end - --- Run the code lens in the current line --- function M.run() @@ -81,7 +85,7 @@ function M.run() for client, lenses in pairs(lenses_by_client) do for _, lens in pairs(lenses) do if lens.range.start.line == (line - 1) then - table.insert(options, {client=client, lens=lens}) + table.insert(options, { client = client, lens = lens }) end end end @@ -104,7 +108,6 @@ function M.run() end end - --- Display the lenses using virtual text --- ---@param lenses table of lenses to display (`CodeLens[] | null`) @@ -130,21 +133,25 @@ function M.display(lenses, bufnr, client_id) api.nvim_buf_clear_namespace(bufnr, ns, i, i + 1) local chunks = {} local num_line_lenses = #line_lenses + table.sort(line_lenses, function(a, b) + return a.range.start.character < b.range.start.character + end) for j, lens in ipairs(line_lenses) do local text = lens.command and lens.command.title or 'Unresolved lens ...' - table.insert(chunks, {text, 'LspCodeLens' }) + table.insert(chunks, { text, 'LspCodeLens' }) if j < num_line_lenses then - table.insert(chunks, {' | ', 'LspCodeLensSeparator' }) + table.insert(chunks, { ' | ', 'LspCodeLensSeparator' }) end end if #chunks > 0 then - api.nvim_buf_set_extmark(bufnr, ns, i, 0, { virt_text = chunks, - hl_mode="combine" }) + api.nvim_buf_set_extmark(bufnr, ns, i, 0, { + virt_text = chunks, + hl_mode = 'combine', + }) end end end - --- Store lenses for a specific buffer and client --- ---@param lenses table of lenses to store (`CodeLens[] | null`) @@ -157,16 +164,17 @@ function M.save(lenses, bufnr, client_id) lens_cache_by_buf[bufnr] = lenses_by_client local ns = namespaces[client_id] api.nvim_buf_attach(bufnr, false, { - on_detach = function(b) lens_cache_by_buf[b] = nil end, + on_detach = function(b) + lens_cache_by_buf[b] = nil + end, on_lines = function(_, b, _, first_lnum, last_lnum) api.nvim_buf_clear_namespace(b, ns, first_lnum, last_lnum) - end + end, }) end lenses_by_client[client_id] = lenses end - ---@private local function resolve_lenses(lenses, bufnr, client_id, callback) lenses = lenses or {} @@ -200,8 +208,7 @@ local function resolve_lenses(lenses, bufnr, client_id, callback) ns, lens.range.start.line, 0, - { virt_text = {{ lens.command.title, 'LspCodeLens' }}, - hl_mode="combine" } + { virt_text = { { lens.command.title, 'LspCodeLens' } }, hl_mode = 'combine' } ) end countdown() @@ -210,11 +217,14 @@ local function resolve_lenses(lenses, bufnr, client_id, callback) end end - --- |lsp-handler| for the method `textDocument/codeLens` --- function M.on_codelens(err, result, ctx, _) - assert(not err, vim.inspect(err)) + if err then + active_refreshes[ctx.bufnr] = nil + local _ = log.error() and log.error('codelens', err) + return + end M.save(result, ctx.bufnr, ctx.client_id) @@ -222,12 +232,11 @@ function M.on_codelens(err, result, ctx, _) -- once resolved. M.display(result, ctx.bufnr, ctx.client_id) resolve_lenses(result, ctx.bufnr, ctx.client_id, function() - M.display(result, ctx.bufnr, ctx.client_id) active_refreshes[ctx.bufnr] = nil + M.display(result, ctx.bufnr, ctx.client_id) end) end - --- Refresh the codelens for the current buffer --- --- It is recommended to trigger this using an autocmd or via keymap. @@ -238,15 +247,14 @@ end --- function M.refresh() local params = { - textDocument = util.make_text_document_params() + textDocument = util.make_text_document_params(), } local bufnr = api.nvim_get_current_buf() if active_refreshes[bufnr] then return end active_refreshes[bufnr] = true - vim.lsp.buf_request(0, 'textDocument/codeLens', params) + vim.lsp.buf_request(0, 'textDocument/codeLens', params, M.on_codelens) end - return M diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua index f38b469f3c..1f9d084e2b 100644 --- a/runtime/lua/vim/lsp/diagnostic.lua +++ b/runtime/lua/vim/lsp/diagnostic.lua @@ -22,17 +22,6 @@ local function get_client_id(client_id) end ---@private -local function get_bufnr(bufnr) - if not bufnr then - return vim.api.nvim_get_current_buf() - elseif bufnr == 0 then - return vim.api.nvim_get_current_buf() - end - - return bufnr -end - ----@private local function severity_lsp_to_vim(severity) if type(severity) == 'string' then severity = vim.lsp.protocol.DiagnosticSeverity[severity] @@ -50,12 +39,12 @@ end ---@private local function line_byte_from_position(lines, lnum, col, offset_encoding) - if not lines or offset_encoding == "utf-8" then + if not lines or offset_encoding == 'utf-8' then return col end local line = lines[lnum + 1] - local ok, result = pcall(vim.str_byteindex, line, col, offset_encoding == "utf-16") + local ok, result = pcall(vim.str_byteindex, line, col, offset_encoding == 'utf-16') if ok then return result end @@ -75,7 +64,7 @@ local function get_buf_lines(bufnr) return end - local content = f:read("*a") + local content = f:read('*a') if not content then -- Some LSP servers report diagnostics at a directory level, in which case -- io.read() returns nil @@ -83,7 +72,7 @@ local function get_buf_lines(bufnr) return end - local lines = vim.split(content, "\n") + local lines = vim.split(content, '\n') f:close() return lines end @@ -92,10 +81,10 @@ end local function diagnostic_lsp_to_vim(diagnostics, bufnr, client_id) local buf_lines = get_buf_lines(bufnr) local client = vim.lsp.get_client_by_id(client_id) - local offset_encoding = client and client.offset_encoding or "utf-16" + local offset_encoding = client and client.offset_encoding or 'utf-16' return vim.tbl_map(function(diagnostic) local start = diagnostic.range.start - local _end = diagnostic.range["end"] + local _end = diagnostic.range['end'] return { lnum = start.line, col = line_byte_from_position(buf_lines, start.line, start.character, offset_encoding), @@ -104,8 +93,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,13 +111,14 @@ 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, character = diagnostic.col, }, - ["end"] = { + ['end'] = { line = diagnostic.end_lnum, character = diagnostic.end_col, }, @@ -134,6 +126,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 @@ -144,10 +137,10 @@ local _client_namespaces = {} --- ---@param client_id number The id of the LSP client function M.get_namespace(client_id) - vim.validate { client_id = { client_id, 'n' } } + vim.validate({ client_id = { client_id, 'n' } }) if not _client_namespaces[client_id] then local client = vim.lsp.get_client_by_id(client_id) - local name = string.format("vim.lsp.%s.%d", client and client.name or "unknown", client_id) + local name = string.format('vim.lsp.%s.%d', client and client.name or 'unknown', client_id) _client_namespaces[client_id] = vim.api.nvim_create_namespace(name) end return _client_namespaces[client_id] @@ -168,8 +161,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, @@ -181,7 +174,12 @@ end function M.on_publish_diagnostics(_, result, ctx, config) local client_id = ctx.client_id local uri = result.uri - local bufnr = vim.uri_to_bufnr(uri) + local fname = vim.uri_to_fname(uri) + local diagnostics = result.diagnostics + if #diagnostics == 0 and vim.fn.bufexists(fname) == 0 then + return + end + local bufnr = vim.fn.bufadd(fname) if not bufnr then return @@ -189,13 +187,12 @@ function M.on_publish_diagnostics(_, result, ctx, config) client_id = get_client_id(client_id) local namespace = M.get_namespace(client_id) - local diagnostics = result.diagnostics if config then for _, opt in pairs(config) do if type(opt) == 'table' then if not opt.severity and opt.severity_limit then - opt.severity = {min=severity_lsp_to_vim(opt.severity_limit)} + opt.severity = { min = severity_lsp_to_vim(opt.severity_limit) } end end end @@ -230,72 +227,6 @@ function M.reset(client_id, buffer_client_map) end) 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()| ---- ----@param client_id number Restrict included diagnostics to the client ---- 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 - namespace = M.get_namespace(client_id) - end - for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do - local diagnostics = diagnostic_vim_to_lsp(vim.diagnostic.get(bufnr, {namespace = namespace})) - result[bufnr] = diagnostics - end - return result -end - ---- Return associated diagnostics for bufnr ---- ----@deprecated Prefer |vim.diagnostic.get()| ---- ----@param bufnr number ----@param client_id number|nil If nil, then return all of the diagnostics. ---- 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 = {} - vim.lsp.for_each_buffer_client(bufnr, function(_, iter_client_id, _) - local iter_diagnostics = vim.tbl_filter(predicate, M.get(bufnr, iter_client_id)) - for _, diagnostic in ipairs(iter_diagnostics) do - table.insert(all_diagnostics, diagnostic) - end - end) - return all_diagnostics - end - - local namespace = M.get_namespace(client_id) - return diagnostic_vim_to_lsp(vim.tbl_filter(predicate, vim.diagnostic.get(bufnr, {namespace=namespace}))) -end - --- Get the diagnostics by line --- --- Marked private as this is used internally by the LSP subsystem, but @@ -317,7 +248,7 @@ function M.get_line_diagnostics(bufnr, line_nr, opts, client_id) if opts.severity then opts.severity = severity_lsp_to_vim(opts.severity) elseif opts.severity_limit then - opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)} + opts.severity = { min = severity_lsp_to_vim(opts.severity_limit) } end if client_id then @@ -333,390 +264,4 @@ function M.get_line_diagnostics(bufnr, line_nr, opts, client_id) return diagnostic_vim_to_lsp(vim.diagnostic.get(bufnr, opts)) end ---- Get the counts for a particular severity ---- ----@deprecated Prefer |vim.diagnostic.get_count()| ---- ----@param bufnr number The buffer number ----@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 - opts.namespace = M.get_namespace(client_id) - end - - return #vim.diagnostic.get(bufnr, opts) -end - ---- Get the previous diagnostic closest to the cursor_position ---- ----@deprecated Prefer |vim.diagnostic.get_prev()| ---- ----@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) - elseif opts.severity_limit then - opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)} - end - end - return diagnostic_vim_to_lsp({vim.diagnostic.get_prev(opts)})[1] -end - ---- Return the pos, {row, col}, for the prev diagnostic in the current buffer. ---- ----@deprecated Prefer |vim.diagnostic.get_prev_pos()| ---- ----@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) - elseif opts.severity_limit then - opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)} - end - end - return vim.diagnostic.get_prev_pos(opts) -end - ---- Move to the previous diagnostic ---- ----@deprecated Prefer |vim.diagnostic.goto_prev()| ---- ----@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) - elseif opts.severity_limit then - opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)} - end - end - return vim.diagnostic.goto_prev(opts) -end - ---- Get the next diagnostic closest to the cursor_position ---- ----@deprecated Prefer |vim.diagnostic.get_next()| ---- ----@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) - elseif opts.severity_limit then - opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)} - end - end - return diagnostic_vim_to_lsp({vim.diagnostic.get_next(opts)})[1] -end - ---- Return the pos, {row, col}, for the next diagnostic in the current buffer. ---- ----@deprecated Prefer |vim.diagnostic.get_next_pos()| ---- ----@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) - elseif opts.severity_limit then - opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)} - end - end - return vim.diagnostic.get_next_pos(opts) -end - ---- Move to the next diagnostic ---- ----@deprecated Prefer |vim.diagnostic.goto_next()| -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) - elseif opts.severity_limit then - opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)} - end - end - return vim.diagnostic.goto_next(opts) -end - ---- Set signs for given diagnostics ---- ----@deprecated Prefer |vim.diagnostic._set_signs()| ---- ----@param diagnostics Diagnostic[] ----@param bufnr number The buffer number ----@param client_id number the client id ----@param sign_ns number|nil ----@param opts table Configuration for signs. Keys: ---- - priority: Set the priority of the signs. ---- - 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)} - end - - vim.diagnostic._set_signs(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id), opts) -end - ---- Set underline for given diagnostics ---- ----@deprecated Prefer |vim.diagnostic._set_underline()| ---- ----@param diagnostics Diagnostic[] ----@param bufnr number: The buffer number ----@param client_id number: The client id ----@param diagnostic_ns number|nil: The namespace ----@param opts table: Configuration table: ---- - 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)} - end - return vim.diagnostic._set_underline(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id), opts) -end - ---- Set virtual text given diagnostics ---- ----@deprecated Prefer |vim.diagnostic._set_virtual_text()| ---- ----@param diagnostics Diagnostic[] ----@param bufnr number ----@param client_id number ----@param diagnostic_ns number ----@param opts table Options on how to display virtual text. Keys: ---- - prefix (string): Prefix to display before virtual text on line ---- - spacing (number): Number of spaces to insert before virtual text ---- - 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)} - end - return vim.diagnostic._set_virtual_text(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id), opts) -end - ---- Default function to get text chunks to display using |nvim_buf_set_extmark()|. ---- ----@deprecated Prefer |vim.diagnostic.get_virt_text_chunks()| ---- ----@param bufnr number The buffer to display the virtual text in ----@param line number The line number to display the virtual text on ----@param line_diags Diagnostic[] The diagnostics associated with the line ----@param opts table See {opts} from |vim.lsp.diagnostic.set_virtual_text()| ----@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 - ---- Open a floating window with the diagnostics from {position} ---- ----@deprecated Prefer |vim.diagnostic.show_position_diagnostics()| ---- ----@param opts table|nil Configuration keys ---- - severity: (DiagnosticSeverity, default nil) ---- - Only return diagnostics with this severity. Overrides severity_limit ---- - severity_limit: (DiagnosticSeverity, default nil) ---- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid. ---- - all opts for |show_diagnostics()| can be used here ----@param buf_nr number|nil The buffer number ----@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 - if opts.severity then - opts.severity = severity_lsp_to_vim(opts.severity) - elseif opts.severity_limit then - opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)} - end - return vim.diagnostic.open_float(buf_nr, opts) -end - ---- Open a floating window with the diagnostics from {line_nr} ---- ----@deprecated Prefer |vim.diagnostic.open_float()| ---- ----@param opts table Configuration table ---- - all opts for |vim.lsp.diagnostic.get_line_diagnostics()| and ---- |show_diagnostics()| can be used here ----@param buf_nr number|nil The buffer number ----@param line_nr number|nil The line number ----@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 - if client_id then - opts.namespace = M.get_namespace(client_id) - end - return vim.diagnostic.open_float(buf_nr, opts) -end - ---- Redraw diagnostics for the given buffer and client ---- ----@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 ---- for redrawing diagnostics after making changes in diagnostics ---- configuration. |lsp-handler-configuration| ---- ----@param bufnr (optional, number): Buffer handle, defaults to current ----@param client_id (optional, number): Redraw diagnostics for the given ---- 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) - M.redraw(bufnr, client.id) - end) - end - - local namespace = M.get_namespace(client_id) - return vim.diagnostic.show(namespace, bufnr) -end - ---- Sets the quickfix list ---- ----@deprecated Prefer |vim.diagnostic.setqflist()| ---- ----@param opts table|nil Configuration table. Keys: ---- - {open}: (boolean, default true) ---- - Open quickfix list after set ---- - {client_id}: (number) ---- - If nil, will consider all clients attached to buffer. ---- - {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. ---- - {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) - elseif opts.severity_limit then - opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)} - end - if opts.client_id then - opts.client_id = nil - opts.namespace = M.get_namespace(opts.client_id) - end - local workspace = vim.F.if_nil(opts.workspace, true) - opts.bufnr = not workspace and 0 - return vim.diagnostic.setqflist(opts) -end - ---- Sets the location list ---- ----@deprecated Prefer |vim.diagnostic.setloclist()| ---- ----@param opts table|nil Configuration table. Keys: ---- - {open}: (boolean, default true) ---- - Open loclist after set ---- - {client_id}: (number) ---- - If nil, will consider all clients attached to buffer. ---- - {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. ---- - {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) - elseif opts.severity_limit then - opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)} - end - if opts.client_id then - opts.client_id = nil - opts.namespace = M.get_namespace(opts.client_id) - end - local workspace = vim.F.if_nil(opts.workspace, false) - opts.bufnr = not workspace and 0 - return vim.diagnostic.setloclist(opts) -end - ---- Disable diagnostics for the given buffer and client ---- ----@deprecated Prefer |vim.diagnostic.disable()| ---- ----@param bufnr (optional, number): Buffer handle, defaults to current ----@param client_id (optional, number): Disable diagnostics for the given ---- client. The default is to disable diagnostics for all attached ---- clients. --- Note that when diagnostics are disabled for a buffer, the server will still --- 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) - end) - end - - bufnr = get_bufnr(bufnr) - local namespace = M.get_namespace(client_id) - return vim.diagnostic.disable(bufnr, namespace) -end - ---- Enable diagnostics for the given buffer and client ---- ----@deprecated Prefer |vim.diagnostic.enable()| ---- ----@param bufnr (optional, number): Buffer handle, defaults to current ----@param client_id (optional, number): Enable diagnostics for the given ---- 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) - end) - end - - bufnr = get_bufnr(bufnr) - local namespace = M.get_namespace(client_id) - return vim.diagnostic.enable(bufnr, namespace) -end - --- }}} - return M diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua index a48302cc4b..3b869d8f5c 100644 --- a/runtime/lua/vim/lsp/handlers.lua +++ b/runtime/lua/vim/lsp/handlers.lua @@ -1,6 +1,6 @@ -local log = require 'vim.lsp.log' -local protocol = require 'vim.lsp.protocol' -local util = require 'vim.lsp.util' +local log = require('vim.lsp.log') +local protocol = require('vim.lsp.protocol') +local util = require('vim.lsp.util') local vim = vim local api = vim.api @@ -12,8 +12,8 @@ local M = {} --- Writes to error buffer. ---@param ... (table of strings) Will be concatenated before being written local function err_message(...) - vim.notify(table.concat(vim.tbl_flatten{...}), vim.log.levels.ERROR) - api.nvim_command("redraw") + vim.notify(table.concat(vim.tbl_flatten({ ... })), vim.log.levels.ERROR) + api.nvim_command('redraw') end --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand @@ -25,51 +25,56 @@ end local function progress_handler(_, result, ctx, _) local client_id = ctx.client_id local client = vim.lsp.get_client_by_id(client_id) - local client_name = client and client.name or string.format("id=%d", client_id) + 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") + err_message('LSP[', client_name, '] client has shut down during progress update') return vim.NIL end - local val = result.value -- unspecified yet - local token = result.token -- string or number - + local val = result.value -- unspecified yet + local token = result.token -- string or number + if type(val) ~= 'table' then + val = { content = val } + end if val.kind then if val.kind == 'begin' then client.messages.progress[token] = { title = val.title, + cancellable = val.cancellable, message = val.message, percentage = val.percentage, } elseif val.kind == 'report' then - client.messages.progress[token].message = val.message; - client.messages.progress[token].percentage = val.percentage; + client.messages.progress[token].cancellable = val.cancellable + client.messages.progress[token].message = val.message + client.messages.progress[token].percentage = val.percentage elseif val.kind == 'end' then if client.messages.progress[token] == nil then - err_message("LSP[", client_name, "] received `end` message with no corresponding `begin`") + err_message('LSP[', client_name, '] received `end` message with no corresponding `begin`') else client.messages.progress[token].message = val.message client.messages.progress[token].done = true end end else - table.insert(client.messages, {content = val, show_once = true, shown = 0}) + client.messages.progress[token] = val + client.messages.progress[token].done = true end - vim.api.nvim_command("doautocmd <nomodeline> User LspProgressUpdate") + api.nvim_exec_autocmds('User', { pattern = 'LspProgressUpdate', modeline = false }) end --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#progress M['$/progress'] = progress_handler --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_workDoneProgress_create -M['window/workDoneProgress/create'] = function(_, result, ctx) +M['window/workDoneProgress/create'] = function(_, result, ctx) local client_id = ctx.client_id local client = vim.lsp.get_client_by_id(client_id) - local token = result.token -- string or number - local client_name = client and client.name or string.format("id=%d", client_id) + local token = result.token -- string or number + 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") + err_message('LSP[', client_name, '] client has shut down while creating progress report') return vim.NIL end client.messages.progress[token] = {} @@ -78,20 +83,19 @@ end --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_showMessageRequest M['window/showMessageRequest'] = function(_, result) - local actions = result.actions print(result.message) - local option_strings = {result.message, "\nRequest Actions:"} + local option_strings = { result.message, '\nRequest Actions:' } for i, action in ipairs(actions) do local title = action.title:gsub('\r\n', '\\r\\n') title = title:gsub('\n', '\\n') - table.insert(option_strings, string.format("%d. %s", i, title)) + table.insert(option_strings, string.format('%d. %s', i, title)) end -- window/showMessageRequest can return either MessageActionItem[] or null. local choice = vim.fn.inputlist(option_strings) if choice < 1 or choice > #actions then - return vim.NIL + return vim.NIL else return actions[choice] end @@ -100,27 +104,32 @@ end --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#client_registerCapability M['client/registerCapability'] = function(_, _, ctx) local client_id = ctx.client_id - local warning_tpl = "The language server %s triggers a registerCapability ".. - "handler despite dynamicRegistration set to false. ".. - "Report upstream, this warning is harmless" + local warning_tpl = 'The language server %s triggers a registerCapability ' + .. 'handler despite dynamicRegistration set to false. ' + .. 'Report upstream, this warning is harmless' local client = vim.lsp.get_client_by_id(client_id) - local client_name = client and client.name or string.format("id=%d", client_id) + local client_name = client and client.name or string.format('id=%d', client_id) local warning = string.format(warning_tpl, client_name) log.warn(warning) return vim.NIL end --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit -M['workspace/applyEdit'] = function(_, workspace_edit) - if not workspace_edit then return end +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) + 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; + applied = status, + failureReason = result, } end @@ -129,7 +138,11 @@ M['workspace/configuration'] = function(_, result, 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") + err_message( + 'LSP[', + client_id, + '] client has shut down after sending a workspace/configuration request' + ) return end if not result.items then @@ -139,7 +152,7 @@ M['workspace/configuration'] = function(_, result, ctx) local response = {} for _, item in ipairs(result.items) do if item.section then - local value = util.lookup_section(client.config.settings, item.section) or vim.NIL + local value = util.lookup_section(client.config.settings, item.section) -- For empty sections with no explicit '' key, return settings as is if value == vim.NIL and item.section == '' then value = client.config.settings or vim.NIL @@ -150,6 +163,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,7 +182,30 @@ M['textDocument/codeLens'] = function(...) return require('vim.lsp.codelens').on_codelens(...) 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 @@ -169,69 +216,88 @@ 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'; - items = map_result(result, ctx.bufnr); + title = title_fn(ctx), + items = map_result(result, ctx.bufnr), + context = ctx, }) - api.nvim_command("lopen") + api.nvim_command('lopen') else vim.fn.setqflist({}, ' ', { - title = 'Language Server'; - items = map_result(result, ctx.bufnr); + title = title_fn(ctx), + items = map_result(result, ctx.bufnr), + context = ctx, }) - api.nvim_command("botright copen") + api.nvim_command('botright copen') end end end 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, _) - if not result then return end - util.apply_workspace_edit(result) +M['textDocument/rename'] = function(_, result, ctx, _) + if not result then + return + end + 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) + if not result then + return + end + 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) + if not result then + return + end + 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 M['textDocument/completion'] = function(_, result, _, _) - if vim.tbl_isempty(result or {}) then return end + if vim.tbl_isempty(result or {}) then + return + end local row, col = unpack(api.nvim_win_get_cursor(0)) - local line = assert(api.nvim_buf_get_lines(0, row-1, row, false)[1]) - local line_to_cursor = line:sub(col+1) + local line = assert(api.nvim_buf_get_lines(0, row - 1, row, false)[1]) + local line_to_cursor = line:sub(col + 1) local textMatch = vim.fn.match(line_to_cursor, '\\k*$') - local prefix = line_to_cursor:sub(textMatch+1) + local prefix = line_to_cursor:sub(textMatch + 1) local matches = util.text_document_completion_list_to_complete_items(result, prefix) - vim.fn.complete(textMatch+1, matches) + vim.fn.complete(textMatch + 1, matches) end --- |lsp-handler| for the method "textDocument/hover" @@ -251,16 +317,16 @@ function M.hover(_, result, ctx, config) config = config or {} config.focus_id = ctx.method if not (result and result.contents) then - -- return { 'No information available' } + vim.notify('No information available') return end local markdown_lines = util.convert_input_to_markdown_lines(result.contents) markdown_lines = util.trim_empty_lines(markdown_lines) if vim.tbl_isempty(markdown_lines) then - -- return { 'No information available' } + vim.notify('No information available') return end - return util.open_floating_preview(markdown_lines, "markdown", config) + return util.open_floating_preview(markdown_lines, 'markdown', config) end --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover @@ -272,24 +338,30 @@ M['textDocument/hover'] = M.hover ---@param result (table) result of LSP method; a location or a list of locations. ---@param ctx (table) table containing the context of the request, including the method ---(`textDocument/definition` can return `Location` or `Location[]` -local function location_handler(_, result, ctx, _) +local function location_handler(_, result, ctx, config) if result == nil or vim.tbl_isempty(result) then 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) + + config = config or {} -- 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, config.reuse_win) if #result > 1 then - vim.fn.setqflist({}, ' ', {title = 'LSP locations', items = 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, config.reuse_win) end end @@ -328,7 +400,8 @@ function M.signature_help(_, result, ctx, config) return end local client = vim.lsp.get_client_by_id(ctx.client_id) - local triggers = client.resolved_capabilities.signature_help_trigger_characters + local triggers = + vim.tbl_get(client.server_capabilities, 'signatureHelpProvider', 'triggerCharacters') local ft = api.nvim_buf_get_option(ctx.bufnr, 'filetype') local lines, hl = util.convert_signature_help_to_markdown_lines(result, ft, triggers) lines = util.trim_empty_lines(lines) @@ -338,9 +411,9 @@ function M.signature_help(_, result, ctx, config) end return end - local fbuf, fwin = util.open_floating_preview(lines, "markdown", config) + local fbuf, fwin = util.open_floating_preview(lines, 'markdown', config) if hl then - api.nvim_buf_add_highlight(fbuf, -1, "LspSignatureActiveParameter", 0, unpack(hl)) + api.nvim_buf_add_highlight(fbuf, -1, 'LspSignatureActiveParameter', 0, unpack(hl)) end return fbuf, fwin end @@ -350,10 +423,14 @@ M['textDocument/signatureHelp'] = M.signature_help --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentHighlight M['textDocument/documentHighlight'] = function(_, result, ctx, _) - if not result then return end + if not result then + return + end local client_id = ctx.client_id local client = vim.lsp.get_client_by_id(client_id) - if not client then return end + if not client then + return + end util.buf_highlight_references(ctx.bufnr, result, client.offset_encoding) end @@ -366,7 +443,9 @@ end ---@returns `CallHierarchyOutgoingCall[]` if {direction} is `"to"`, local make_call_hierarchy_handler = function(direction) return function(_, result) - if not result then return end + if not result then + return + end local items = {} for _, call_hierarchy_call in pairs(result) do local call_hierarchy_item = call_hierarchy_call[direction] @@ -379,8 +458,8 @@ local make_call_hierarchy_handler = function(direction) }) end end - vim.fn.setqflist({}, ' ', {title = 'LSP call hierarchy', items = items}) - api.nvim_command("copen") + vim.fn.setqflist({}, ' ', { title = 'LSP call hierarchy', items = items }) + api.nvim_command('botright copen') end end @@ -396,15 +475,15 @@ M['window/logMessage'] = function(_, result, ctx, _) local message = result.message local client_id = ctx.client_id local client = vim.lsp.get_client_by_id(client_id) - local client_name = client and client.name or string.format("id=%d", client_id) + 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") + err_message('LSP[', client_name, '] client has shut down after sending ', message) end if message_type == protocol.MessageType.Error then log.error(message) elseif message_type == protocol.MessageType.Warning then log.warn(message) - elseif message_type == protocol.MessageType.Info or message_type == protocol.MessageType.Log then + elseif message_type == protocol.MessageType.Info or message_type == protocol.MessageType.Log then log.info(message) else log.debug(message) @@ -418,15 +497,15 @@ M['window/showMessage'] = function(_, result, ctx, _) local message = result.message local client_id = ctx.client_id local client = vim.lsp.get_client_by_id(client_id) - local client_name = client and client.name or string.format("id=%d", client_id) + 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") + err_message('LSP[', client_name, '] client has shut down after sending ', message) end if message_type == protocol.MessageType.Error then - err_message("LSP[", client_name, "] ", message) + err_message('LSP[', client_name, '] ', message) else local message_type_name = protocol.MessageType[message_type] - api.nvim_out_write(string.format("LSP[%s][%s] %s\n", client_name, message_type_name, message)) + api.nvim_out_write(string.format('LSP[%s][%s] %s\n', client_name, message_type_name, message)) end return result end @@ -434,9 +513,13 @@ end -- Add boilerplate error validation and logging for all of these. for k, fn in pairs(M) do M[k] = function(err, result, ctx, config) - local _ = log.trace() and log.trace('default_handler', ctx.method, { - err = err, result = result, ctx=vim.inspect(ctx), config = config - }) + local _ = log.trace() + and log.trace('default_handler', ctx.method, { + err = err, + result = result, + ctx = vim.inspect(ctx), + config = config, + }) if err then -- LSP spec: @@ -448,7 +531,7 @@ for k, fn in pairs(M) do -- 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) + 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 diff --git a/runtime/lua/vim/lsp/health.lua b/runtime/lua/vim/lsp/health.lua index ed3eea59df..ba730e3d6d 100644 --- a/runtime/lua/vim/lsp/health.lua +++ b/runtime/lua/vim/lsp/health.lua @@ -8,20 +8,25 @@ function M.check() local log = require('vim.lsp.log') local current_log_level = log.get_level() local log_level_string = log.levels[current_log_level] - report_info(string.format("LSP log level : %s", log_level_string)) + report_info(string.format('LSP log level : %s', log_level_string)) if current_log_level < log.levels.WARN then - report_warn(string.format("Log level %s will cause degraded performance and high disk usage", log_level_string)) + report_warn( + string.format( + 'Log level %s will cause degraded performance and high disk usage', + log_level_string + ) + ) end local log_path = vim.lsp.get_log_path() - report_info(string.format("Log path: %s", log_path)) + report_info(string.format('Log path: %s', log_path)) - local log_size = vim.loop.fs_stat(log_path).size + local log_file = vim.loop.fs_stat(log_path) + local log_size = log_file and log_file.size or 0 local report_fn = (log_size / 1000000 > 100 and report_warn or report_info) - report_fn(string.format("Log size: %d KB", log_size / 1000 )) + report_fn(string.format('Log size: %d KB', log_size / 1000)) end return M - diff --git a/runtime/lua/vim/lsp/log.lua b/runtime/lua/vim/lsp/log.lua index e0b5653587..6c6ba0f206 100644 --- a/runtime/lua/vim/lsp/log.lua +++ b/runtime/lua/vim/lsp/log.lua @@ -8,22 +8,29 @@ 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" +-- Levels by name: "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "OFF" -- Level numbers begin with "TRACE" at 0 log.levels = vim.deepcopy(vim.log.levels) -- Default log level is warn. local current_log_level = log.levels.WARN -local log_date_format = "%F %H:%M:%S" -local format_func = function(arg) return vim.inspect(arg, {newline=''}) end +local log_date_format = '%F %H:%M:%S' +local format_func = function(arg) + return vim.inspect(arg, { newline = '' }) +end do - local path_sep = vim.loop.os_uname().version:match("Windows") and "\\" or "/" + local path_sep = vim.loop.os_uname().version:match('Windows') and '\\' or '/' ---@private local function path_join(...) - return table.concat(vim.tbl_flatten{...}, path_sep) + return table.concat(vim.tbl_flatten({ ... }), path_sep) end - local logfilename = path_join(vim.fn.stdpath('cache'), 'lsp.log') + local logfilename = path_join(vim.fn.stdpath('log'), 'lsp.log') + + -- TODO: Ideally the directory should be created in open_logfile(), right + -- before opening the log file, but open_logfile() can be called from libuv + -- callbacks, where using fn.mkdir() is not allowed. + vim.fn.mkdir(vim.fn.stdpath('log'), 'p') --- Returns the log filename. ---@returns (string) log filename @@ -31,21 +38,40 @@ do return logfilename end - vim.fn.mkdir(vim.fn.stdpath('cache'), "p") - local logfile = assert(io.open(logfilename, "a+")) - - local log_info = vim.loop.fs_stat(logfilename) - if log_info and log_info.size > 1e9 then - local warn_msg = string.format( - "LSP client log is large (%d MB): %s", - log_info.size / (1000 * 1000), - logfilename - ) - vim.notify(warn_msg) + local logfile, openerr + ---@private + --- Opens log file. Returns true if file is open, false on error + local function open_logfile() + -- Try to open file only once + if logfile then + return true + end + if openerr then + return false + end + + logfile, openerr = io.open(logfilename, 'a+') + if not logfile then + local err_msg = string.format('Failed to open LSP client log file: %s', openerr) + vim.notify(err_msg, vim.log.levels.ERROR) + return false + end + + local log_info = vim.loop.fs_stat(logfilename) + if log_info and log_info.size > 1e9 then + local warn_msg = string.format( + 'LSP client log is large (%d MB): %s', + log_info.size / (1000 * 1000), + logfilename + ) + vim.notify(warn_msg) + end + + -- Start message for logging + logfile:write(string.format('[START][%s] LSP logging initiated\n', os.date(log_date_format))) + return true end - -- Start message for logging - logfile:write(string.format("[START][%s] LSP logging initiated\n", os.date(log_date_format))) for level, levelnr in pairs(log.levels) do -- Also export the log level on the root object. log[level] = levelnr @@ -63,23 +89,38 @@ do -- ``` -- -- This way you can avoid string allocations if the log level isn't high enough. - log[level:lower()] = function(...) - local argc = select("#", ...) - if levelnr < current_log_level then return false end - if argc == 0 then return true end - local info = debug.getinfo(2, "Sl") - local header = string.format("[%s][%s] ...%s:%s", level, os.date(log_date_format), string.sub(info.short_src, #info.short_src - 15), info.currentline) - local parts = { header } - for i = 1, argc do - local arg = select(i, ...) - if arg == nil then - table.insert(parts, "nil") - else - table.insert(parts, format_func(arg)) + if level ~= 'OFF' then + log[level:lower()] = function(...) + local argc = select('#', ...) + if levelnr < current_log_level then + return false + end + if argc == 0 then + return true + end + if not open_logfile() then + return false + end + local info = debug.getinfo(2, 'Sl') + local header = string.format( + '[%s][%s] ...%s:%s', + level, + os.date(log_date_format), + string.sub(info.short_src, #info.short_src - 15), + info.currentline + ) + local parts = { header } + for i = 1, argc do + local arg = select(i, ...) + if arg == nil then + table.insert(parts, 'nil') + else + table.insert(parts, format_func(arg)) + end end + logfile:write(table.concat(parts, '\t'), '\n') + logfile:flush() end - logfile:write(table.concat(parts, '\t'), "\n") - logfile:flush() end end end @@ -92,10 +133,11 @@ vim.tbl_add_reverse_lookup(log.levels) ---@param level (string or number) One of `vim.lsp.log.levels` function log.set_level(level) if type(level) == 'string' then - current_log_level = assert(log.levels[level:upper()], string.format("Invalid log level: %q", level)) + current_log_level = + assert(log.levels[level:upper()], string.format('Invalid log level: %q', level)) else - assert(type(level) == 'number', "level must be a number or string") - assert(log.levels[level], string.format("Invalid log level: %d", level)) + assert(type(level) == 'number', 'level must be a number or string') + assert(log.levels[level], string.format('Invalid log level: %d', level)) current_log_level = level end end @@ -109,7 +151,7 @@ end --- Sets formatting function used to format logs ---@param handle function function to apply to logging arguments, pass vim.inspect for multi-line formatting function log.set_format_func(handle) - assert(handle == vim.inspect or type(handle) == 'function', "handle must be a function") + assert(handle == vim.inspect or type(handle) == 'function', 'handle must be a function') format_func = handle end diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 86c9e2fd58..6ecb9959d5 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -1,7 +1,5 @@ -- Protocol for the Microsoft Language Server Protocol (mslsp) -local if_nil = vim.F.if_nil - local protocol = {} --[=[ @@ -25,150 +23,150 @@ end local constants = { DiagnosticSeverity = { -- Reports an error. - Error = 1; + Error = 1, -- Reports a warning. - Warning = 2; + Warning = 2, -- Reports an information. - Information = 3; + Information = 3, -- Reports a hint. - Hint = 4; - }; + Hint = 4, + }, DiagnosticTag = { -- Unused or unnecessary code - Unnecessary = 1; + Unnecessary = 1, -- Deprecated or obsolete code - Deprecated = 2; - }; + Deprecated = 2, + }, MessageType = { -- An error message. - Error = 1; + Error = 1, -- A warning message. - Warning = 2; + Warning = 2, -- An information message. - Info = 3; + Info = 3, -- A log message. - Log = 4; - }; + Log = 4, + }, -- The file event type. FileChangeType = { -- The file got created. - Created = 1; + Created = 1, -- The file got changed. - Changed = 2; + Changed = 2, -- The file got deleted. - Deleted = 3; - }; + Deleted = 3, + }, -- The kind of a completion entry. CompletionItemKind = { - Text = 1; - Method = 2; - Function = 3; - Constructor = 4; - Field = 5; - Variable = 6; - Class = 7; - Interface = 8; - Module = 9; - Property = 10; - Unit = 11; - Value = 12; - Enum = 13; - Keyword = 14; - Snippet = 15; - Color = 16; - File = 17; - Reference = 18; - Folder = 19; - EnumMember = 20; - Constant = 21; - Struct = 22; - Event = 23; - Operator = 24; - TypeParameter = 25; - }; + Text = 1, + Method = 2, + Function = 3, + Constructor = 4, + Field = 5, + Variable = 6, + Class = 7, + Interface = 8, + Module = 9, + Property = 10, + Unit = 11, + Value = 12, + Enum = 13, + Keyword = 14, + Snippet = 15, + Color = 16, + File = 17, + Reference = 18, + Folder = 19, + EnumMember = 20, + Constant = 21, + Struct = 22, + Event = 23, + Operator = 24, + TypeParameter = 25, + }, -- How a completion was triggered CompletionTriggerKind = { -- Completion was triggered by typing an identifier (24x7 code -- complete), manual invocation (e.g Ctrl+Space) or via API. - Invoked = 1; + Invoked = 1, -- Completion was triggered by a trigger character specified by -- the `triggerCharacters` properties of the `CompletionRegistrationOptions`. - TriggerCharacter = 2; + TriggerCharacter = 2, -- Completion was re-triggered as the current completion list is incomplete. - TriggerForIncompleteCompletions = 3; - }; + TriggerForIncompleteCompletions = 3, + }, -- A document highlight kind. DocumentHighlightKind = { -- A textual occurrence. - Text = 1; + Text = 1, -- Read-access of a symbol, like reading a variable. - Read = 2; + Read = 2, -- Write-access of a symbol, like writing to a variable. - Write = 3; - }; + Write = 3, + }, -- A symbol kind. SymbolKind = { - File = 1; - Module = 2; - Namespace = 3; - Package = 4; - Class = 5; - Method = 6; - Property = 7; - Field = 8; - Constructor = 9; - Enum = 10; - Interface = 11; - Function = 12; - Variable = 13; - Constant = 14; - String = 15; - Number = 16; - Boolean = 17; - Array = 18; - Object = 19; - Key = 20; - Null = 21; - EnumMember = 22; - Struct = 23; - Event = 24; - Operator = 25; - TypeParameter = 26; - }; + File = 1, + Module = 2, + Namespace = 3, + Package = 4, + Class = 5, + Method = 6, + Property = 7, + Field = 8, + Constructor = 9, + Enum = 10, + Interface = 11, + Function = 12, + Variable = 13, + Constant = 14, + String = 15, + Number = 16, + Boolean = 17, + Array = 18, + Object = 19, + Key = 20, + Null = 21, + EnumMember = 22, + Struct = 23, + Event = 24, + Operator = 25, + TypeParameter = 26, + }, -- Represents reasons why a text document is saved. TextDocumentSaveReason = { -- Manually triggered, e.g. by the user pressing save, by starting debugging, -- or by an API call. - Manual = 1; + Manual = 1, -- Automatic after a delay. - AfterDelay = 2; + AfterDelay = 2, -- When the editor lost focus. - FocusOut = 3; - }; + FocusOut = 3, + }, ErrorCodes = { -- Defined by JSON RPC - ParseError = -32700; - InvalidRequest = -32600; - MethodNotFound = -32601; - InvalidParams = -32602; - InternalError = -32603; - serverErrorStart = -32099; - serverErrorEnd = -32000; - ServerNotInitialized = -32002; - UnknownErrorCode = -32001; + ParseError = -32700, + InvalidRequest = -32600, + MethodNotFound = -32601, + InvalidParams = -32602, + InternalError = -32603, + serverErrorStart = -32099, + serverErrorEnd = -32000, + ServerNotInitialized = -32002, + UnknownErrorCode = -32001, -- Defined by the protocol. - RequestCancelled = -32800; - ContentModified = -32801; - }; + RequestCancelled = -32800, + ContentModified = -32801, + }, -- Describes the content type that a client supports in various -- result literals like `Hover`, `ParameterInfo` or `CompletionItem`. @@ -177,88 +175,88 @@ local constants = { -- are reserved for internal usage. MarkupKind = { -- Plain text is supported as a content format - PlainText = 'plaintext'; + PlainText = 'plaintext', -- Markdown is supported as a content format - Markdown = 'markdown'; - }; + Markdown = 'markdown', + }, ResourceOperationKind = { -- Supports creating new files and folders. - Create = 'create'; + Create = 'create', -- Supports renaming existing files and folders. - Rename = 'rename'; + Rename = 'rename', -- Supports deleting existing files and folders. - Delete = 'delete'; - }; + Delete = 'delete', + }, FailureHandlingKind = { -- Applying the workspace change is simply aborted if one of the changes provided -- fails. All operations executed before the failing operation stay executed. - Abort = 'abort'; + Abort = 'abort', -- All operations are executed transactionally. That means they either all -- succeed or no changes at all are applied to the workspace. - Transactional = 'transactional'; + Transactional = 'transactional', -- If the workspace edit contains only textual file changes they are executed transactionally. -- If resource changes (create, rename or delete file) are part of the change the failure -- handling strategy is abort. - TextOnlyTransactional = 'textOnlyTransactional'; + TextOnlyTransactional = 'textOnlyTransactional', -- The client tries to undo the operations already executed. But there is no -- guarantee that this succeeds. - Undo = 'undo'; - }; + Undo = 'undo', + }, -- Known error codes for an `InitializeError`; InitializeError = { -- If the protocol version provided by the client can't be handled by the server. -- @deprecated This initialize error got replaced by client capabilities. There is -- no version handshake in version 3.0x - unknownProtocolVersion = 1; - }; + unknownProtocolVersion = 1, + }, -- Defines how the host (editor) should sync document changes to the language server. TextDocumentSyncKind = { -- Documents should not be synced at all. - None = 0; + None = 0, -- Documents are synced by always sending the full content -- of the document. - Full = 1; + Full = 1, -- Documents are synced by sending the full content on open. -- After that only incremental updates to the document are -- send. - Incremental = 2; - }; + Incremental = 2, + }, WatchKind = { -- Interested in create events. - Create = 1; + Create = 1, -- Interested in change events - Change = 2; + Change = 2, -- Interested in delete events - Delete = 4; - }; + Delete = 4, + }, -- Defines whether the insert text in a completion item should be interpreted as -- plain text or a snippet. InsertTextFormat = { -- The primary text to be inserted is treated as a plain string. - PlainText = 1; + PlainText = 1, -- The primary text to be inserted is treated as a snippet. -- -- A snippet can define tab stops and placeholders with `$1`, `$2` -- and `${3:foo};`. `$0` defines the final tab stop, it defaults to -- the end of the snippet. Placeholders with equal identifiers are linked, -- that is typing in one will update others too. - Snippet = 2; - }; + Snippet = 2, + }, -- A set of predefined code action kinds CodeActionKind = { -- Empty kind. - Empty = ''; + Empty = '', -- Base kind for quickfix actions - QuickFix = 'quickfix'; + QuickFix = 'quickfix', -- Base kind for refactoring actions - Refactor = 'refactor'; + Refactor = 'refactor', -- Base kind for refactoring extraction actions -- -- Example extract actions: @@ -268,7 +266,7 @@ local constants = { -- - Extract variable -- - Extract interface from class -- - ... - RefactorExtract = 'refactor.extract'; + RefactorExtract = 'refactor.extract', -- Base kind for refactoring inline actions -- -- Example inline actions: @@ -277,7 +275,7 @@ local constants = { -- - Inline variable -- - Inline constant -- - ... - RefactorInline = 'refactor.inline'; + RefactorInline = 'refactor.inline', -- Base kind for refactoring rewrite actions -- -- Example rewrite actions: @@ -288,14 +286,14 @@ local constants = { -- - Make method static -- - Move method to base class -- - ... - RefactorRewrite = 'refactor.rewrite'; + RefactorRewrite = 'refactor.rewrite', -- Base kind for source actions -- -- Source code actions apply to the entire file. - Source = 'source'; + Source = 'source', -- Base kind for an organize imports source action - SourceOrganizeImports = 'source.organizeImports'; - }; + SourceOrganizeImports = 'source.organizeImports', + }, } for k, v in pairs(constants) do @@ -622,19 +620,19 @@ function protocol.make_client_capabilities() return { textDocument = { synchronization = { - dynamicRegistration = false; + dynamicRegistration = false, -- TODO(ashkan) Send textDocument/willSave before saving (BufWritePre) - willSave = false; + willSave = false, -- TODO(ashkan) Implement textDocument/willSaveWaitUntil - willSaveWaitUntil = false; + willSaveWaitUntil = false, -- Send textDocument/didSave after saving (BufWritePost) - didSave = true; - }; + didSave = true, + }, codeAction = { - dynamicRegistration = false; + dynamicRegistration = false, codeActionLiteralSupport = { codeActionKind = { @@ -642,144 +640,193 @@ function protocol.make_client_capabilities() local res = vim.tbl_values(protocol.CodeActionKind) table.sort(res) return res - end)(); - }; - }; - dataSupport = true; + end)(), + }, + }, + isPreferredSupport = true, + dataSupport = true, resolveSupport = { - properties = { 'edit', } - }; - }; + properties = { 'edit' }, + }, + }, completion = { - dynamicRegistration = false; + dynamicRegistration = false, completionItem = { -- Until we can actually expand snippet, move cursor and allow for true snippet experience, -- this should be disabled out of the box. -- However, users can turn this back on if they have a snippet plugin. - snippetSupport = false; + snippetSupport = false, - commitCharactersSupport = false; - preselectSupport = false; - deprecatedSupport = false; - documentationFormat = { protocol.MarkupKind.Markdown; protocol.MarkupKind.PlainText }; - }; + commitCharactersSupport = false, + preselectSupport = false, + deprecatedSupport = false, + documentationFormat = { protocol.MarkupKind.Markdown, protocol.MarkupKind.PlainText }, + }, completionItemKind = { valueSet = (function() local res = {} for k in ipairs(protocol.CompletionItemKind) do - if type(k) == 'number' then table.insert(res, k) end + if type(k) == 'number' then + table.insert(res, k) + end end return res - end)(); - }; + end)(), + }, -- TODO(tjdevries): Implement this - contextSupport = false; - }; + contextSupport = false, + }, declaration = { - linkSupport = true; - }; + linkSupport = true, + }, definition = { - linkSupport = true; - }; + linkSupport = true, + }, implementation = { - linkSupport = true; - }; + linkSupport = true, + }, typeDefinition = { - linkSupport = true; - }; + linkSupport = true, + }, hover = { - dynamicRegistration = false; - contentFormat = { protocol.MarkupKind.Markdown; protocol.MarkupKind.PlainText }; - }; + dynamicRegistration = false, + contentFormat = { protocol.MarkupKind.Markdown, protocol.MarkupKind.PlainText }, + }, signatureHelp = { - dynamicRegistration = false; + dynamicRegistration = false, signatureInformation = { - activeParameterSupport = true; - documentationFormat = { protocol.MarkupKind.Markdown; protocol.MarkupKind.PlainText }; + activeParameterSupport = true, + documentationFormat = { protocol.MarkupKind.Markdown, protocol.MarkupKind.PlainText }, parameterInformation = { - labelOffsetSupport = true; - }; - }; - }; + labelOffsetSupport = true, + }, + }, + }, references = { - dynamicRegistration = false; - }; + dynamicRegistration = false, + }, documentHighlight = { - dynamicRegistration = false - }; + dynamicRegistration = false, + }, documentSymbol = { - dynamicRegistration = false; + dynamicRegistration = false, symbolKind = { valueSet = (function() local res = {} for k in ipairs(protocol.SymbolKind) do - if type(k) == 'number' then table.insert(res, k) end + if type(k) == 'number' then + table.insert(res, k) + end end return res - end)(); - }; - hierarchicalDocumentSymbolSupport = true; - }; + end)(), + }, + hierarchicalDocumentSymbolSupport = true, + }, rename = { - dynamicRegistration = false; - prepareSupport = true; - }; + dynamicRegistration = false, + prepareSupport = true, + }, publishDiagnostics = { - relatedInformation = true; + relatedInformation = true, tagSupport = { valueSet = (function() local res = {} for k in ipairs(protocol.DiagnosticTag) do - if type(k) == 'number' then table.insert(res, k) end + if type(k) == 'number' then + table.insert(res, k) + end end return res - end)(); - }; - }; - }; + end)(), + }, + }, + }, workspace = { symbol = { - dynamicRegistration = false; + dynamicRegistration = false, symbolKind = { valueSet = (function() local res = {} for k in ipairs(protocol.SymbolKind) do - if type(k) == 'number' then table.insert(res, k) end + if type(k) == 'number' then + table.insert(res, k) + end end return res - end)(); - }; - hierarchicalWorkspaceSymbolSupport = true; - }; - workspaceFolders = true; - applyEdit = true; + end)(), + }, + hierarchicalWorkspaceSymbolSupport = true, + }, + workspaceFolders = true, + applyEdit = true, workspaceEdit = { - resourceOperations = {'rename', 'create', 'delete',}, - }; - }; + resourceOperations = { 'rename', 'create', 'delete' }, + }, + }, callHierarchy = { - dynamicRegistration = false; - }; - experimental = nil; + dynamicRegistration = false, + }, + experimental = nil, window = { - workDoneProgress = true; + workDoneProgress = true, showMessage = { messageActionItem = { - additionalPropertiesSupport = false; - }; - }; + additionalPropertiesSupport = false, + }, + }, showDocument = { - support = false; - }; - }; + support = false, + }, + }, } end +local if_nil = vim.F.if_nil --- 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 TextDocumentSyncKind = protocol.TextDocumentSyncKind + local textDocumentSync = server_capabilities.textDocumentSync + if textDocumentSync == nil then + -- Defaults if omitted. + server_capabilities.textDocumentSync = { + openClose = false, + change = TextDocumentSyncKind.None, + willSave = false, + willSaveWaitUntil = false, + save = { + includeText = false, + }, + } + elseif type(textDocumentSync) == 'number' then + -- Backwards compatibility + if not TextDocumentSyncKind[textDocumentSync] then + return nil, 'Invalid server TextDocumentSyncKind for textDocumentSync' + end + server_capabilities.textDocumentSync = { + openClose = true, + change = textDocumentSync, + willSave = false, + willSaveWaitUntil = false, + save = { + includeText = false, + }, + } + elseif type(textDocumentSync) ~= 'table' then + return nil, string.format('Invalid type for textDocumentSync: %q', type(textDocumentSync)) + end + return server_capabilities +end + +---@private +--- Creates a normalized object describing LSP server capabilities. +-- @deprecated access resolved_capabilities instead +---@param server_capabilities table Table of capabilities supported by the server +---@return table Normalized table of capabilities +function protocol._resolve_capabilities_compat(server_capabilities) local general_properties = {} local text_document_sync_properties do @@ -788,39 +835,41 @@ function protocol.resolve_capabilities(server_capabilities) if textDocumentSync == nil then -- Defaults if omitted. text_document_sync_properties = { - text_document_open_close = false; - text_document_did_change = TextDocumentSyncKind.None; --- text_document_did_change = false; - text_document_will_save = false; - text_document_will_save_wait_until = false; - text_document_save = false; - text_document_save_include_text = false; + text_document_open_close = false, + text_document_did_change = TextDocumentSyncKind.None, + -- text_document_did_change = false; + text_document_will_save = false, + text_document_will_save_wait_until = false, + text_document_save = false, + text_document_save_include_text = false, } elseif type(textDocumentSync) == 'number' then -- Backwards compatibility if not TextDocumentSyncKind[textDocumentSync] then - return nil, "Invalid server TextDocumentSyncKind for textDocumentSync" + return nil, 'Invalid server TextDocumentSyncKind for textDocumentSync' end text_document_sync_properties = { - text_document_open_close = true; - text_document_did_change = textDocumentSync; - text_document_will_save = false; - text_document_will_save_wait_until = false; - text_document_save = true; - text_document_save_include_text = false; + text_document_open_close = true, + text_document_did_change = textDocumentSync, + text_document_will_save = false, + text_document_will_save_wait_until = false, + text_document_save = true, + text_document_save_include_text = false, } elseif type(textDocumentSync) == 'table' then text_document_sync_properties = { - text_document_open_close = if_nil(textDocumentSync.openClose, false); - text_document_did_change = if_nil(textDocumentSync.change, TextDocumentSyncKind.None); - text_document_will_save = if_nil(textDocumentSync.willSave, false); - text_document_will_save_wait_until = if_nil(textDocumentSync.willSaveWaitUntil, false); - text_document_save = if_nil(textDocumentSync.save, false); - text_document_save_include_text = if_nil(type(textDocumentSync.save) == 'table' - and textDocumentSync.save.includeText, false); + text_document_open_close = if_nil(textDocumentSync.openClose, false), + text_document_did_change = if_nil(textDocumentSync.change, TextDocumentSyncKind.None), + text_document_will_save = if_nil(textDocumentSync.willSave, false), + text_document_will_save_wait_until = if_nil(textDocumentSync.willSaveWaitUntil, false), + text_document_save = if_nil(textDocumentSync.save, false), + text_document_save_include_text = if_nil( + type(textDocumentSync.save) == 'table' and textDocumentSync.save.includeText, + false + ), } else - return nil, string.format("Invalid type for textDocumentSync: %q", type(textDocumentSync)) + return nil, string.format('Invalid type for textDocumentSync: %q', type(textDocumentSync)) end end general_properties.completion = server_capabilities.completionProvider ~= nil @@ -831,7 +880,8 @@ function protocol.resolve_capabilities(server_capabilities) general_properties.document_symbol = server_capabilities.documentSymbolProvider or false general_properties.workspace_symbol = server_capabilities.workspaceSymbolProvider or false general_properties.document_formatting = server_capabilities.documentFormattingProvider or false - general_properties.document_range_formatting = server_capabilities.documentRangeFormattingProvider or false + general_properties.document_range_formatting = server_capabilities.documentRangeFormattingProvider + or false general_properties.call_hierarchy = server_capabilities.callHierarchyProvider or false general_properties.execute_command = server_capabilities.executeCommandProvider ~= nil @@ -848,18 +898,21 @@ function protocol.resolve_capabilities(server_capabilities) general_properties.code_lens_resolve = false elseif type(server_capabilities.codeLensProvider) == 'table' then general_properties.code_lens = true - general_properties.code_lens_resolve = server_capabilities.codeLensProvider.resolveProvider or false + general_properties.code_lens_resolve = server_capabilities.codeLensProvider.resolveProvider + or false else - error("The server sent invalid codeLensProvider") + error('The server sent invalid codeLensProvider') end if server_capabilities.codeActionProvider == nil then general_properties.code_action = false - elseif type(server_capabilities.codeActionProvider) == 'boolean' - or type(server_capabilities.codeActionProvider) == 'table' then + elseif + type(server_capabilities.codeActionProvider) == 'boolean' + or type(server_capabilities.codeActionProvider) == 'table' + then general_properties.code_action = server_capabilities.codeActionProvider else - error("The server sent invalid codeActionProvider") + error('The server sent invalid codeActionProvider') end if server_capabilities.declarationProvider == nil then @@ -869,7 +922,7 @@ function protocol.resolve_capabilities(server_capabilities) elseif type(server_capabilities.declarationProvider) == 'table' then general_properties.declaration = server_capabilities.declarationProvider else - error("The server sent invalid declarationProvider") + error('The server sent invalid declarationProvider') end if server_capabilities.typeDefinitionProvider == nil then @@ -879,7 +932,7 @@ function protocol.resolve_capabilities(server_capabilities) elseif type(server_capabilities.typeDefinitionProvider) == 'table' then general_properties.type_definition = server_capabilities.typeDefinitionProvider else - error("The server sent invalid typeDefinitionProvider") + error('The server sent invalid typeDefinitionProvider') end if server_capabilities.implementationProvider == nil then @@ -889,7 +942,7 @@ function protocol.resolve_capabilities(server_capabilities) elseif type(server_capabilities.implementationProvider) == 'table' then general_properties.implementation = server_capabilities.implementationProvider else - error("The server sent invalid implementationProvider") + error('The server sent invalid implementationProvider') end local workspace = server_capabilities.workspace @@ -897,45 +950,48 @@ function protocol.resolve_capabilities(server_capabilities) if workspace == nil or workspace.workspaceFolders == nil then -- Defaults if omitted. workspace_properties = { - workspace_folder_properties = { - supported = false; - changeNotifications=false; - } + workspace_folder_properties = { + supported = false, + changeNotifications = false, + }, } elseif type(workspace.workspaceFolders) == 'table' then workspace_properties = { workspace_folder_properties = { - supported = if_nil(workspace.workspaceFolders.supported, false); - changeNotifications = if_nil(workspace.workspaceFolders.changeNotifications, false); - - } + supported = if_nil(workspace.workspaceFolders.supported, false), + changeNotifications = if_nil(workspace.workspaceFolders.changeNotifications, false), + }, } else - error("The server sent invalid workspace") + error('The server sent invalid workspace') end local signature_help_properties if server_capabilities.signatureHelpProvider == nil then signature_help_properties = { - signature_help = false; - signature_help_trigger_characters = {}; + signature_help = false, + signature_help_trigger_characters = {}, } elseif type(server_capabilities.signatureHelpProvider) == 'table' then signature_help_properties = { - signature_help = true; + signature_help = true, -- The characters that trigger signature help automatically. - signature_help_trigger_characters = server_capabilities.signatureHelpProvider.triggerCharacters or {}; + signature_help_trigger_characters = server_capabilities.signatureHelpProvider.triggerCharacters + or {}, } else - error("The server sent invalid signatureHelpProvider") + error('The server sent invalid signatureHelpProvider') end - return vim.tbl_extend("error" - , text_document_sync_properties - , signature_help_properties - , workspace_properties - , general_properties - ) + local capabilities = vim.tbl_extend( + 'error', + text_document_sync_properties, + signature_help_properties, + workspace_properties, + general_properties + ) + + return capabilities end return protocol diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua index 1ecac50df4..913eee19a2 100644 --- a/runtime/lua/vim/lsp/rpc.lua +++ b/runtime/lua/vim/lsp/rpc.lua @@ -4,12 +4,14 @@ local log = require('vim.lsp.log') local protocol = require('vim.lsp.protocol') local validate, schedule, schedule_wrap = vim.validate, vim.schedule, vim.schedule_wrap +local is_win = uv.os_uname().version:find('Windows') + ---@private --- Checks whether a given path exists and is a directory. ---@param filename (string) path to check ---@returns (bool) local function is_dir(filename) - local stat = vim.loop.fs_stat(filename) + local stat = uv.fs_stat(filename) return stat and stat.type == 'directory' or false end @@ -32,9 +34,9 @@ local function env_merge(env) -- Merge. env = vim.tbl_extend('force', vim.fn.environ(), env) local final_env = {} - for k,v in pairs(env) do + for k, v in pairs(env) do assert(type(k) == 'string', 'env must be a dict') - table.insert(final_env, k..'='..tostring(v)) + table.insert(final_env, k .. '=' .. tostring(v)) end return final_env end @@ -45,10 +47,12 @@ end ---@param encoded_message (string) ---@returns (table) table containing encoded message and `Content-Length` attribute local function format_message_with_content_length(encoded_message) - return table.concat { - 'Content-Length: '; tostring(#encoded_message); '\r\n\r\n'; - encoded_message; - } + return table.concat({ + 'Content-Length: ', + tostring(#encoded_message), + '\r\n\r\n', + encoded_message, + }) end ---@private @@ -65,23 +69,25 @@ local function parse_headers(header) if line == '' then break end - local key, value = line:match("^%s*(%S+)%s*:%s*(.+)%s*$") + local key, value = line:match('^%s*(%S+)%s*:%s*(.+)%s*$') if key then key = key:lower():gsub('%-', '_') headers[key] = value else - local _ = log.error() and log.error("invalid header line %q", line) - error(string.format("invalid header line %q", line)) + local _ = log.error() and log.error('invalid header line %q', line) + error(string.format('invalid header line %q', line)) end end headers.content_length = tonumber(headers.content_length) - or error(string.format("Content-Length not found in headers. %q", header)) + or error(string.format('Content-Length not found in headers. %q', header)) return headers end -- This is the start of any possible header patterns. The gsub converts it to a -- case insensitive pattern. -local header_start_pattern = ("content"):gsub("%w", function(c) return "["..c..c:upper().."]" end) +local header_start_pattern = ('content'):gsub('%w', function(c) + return '[' .. c .. c:upper() .. ']' +end) ---@private --- The actual workhorse. @@ -100,17 +106,17 @@ local function request_parser_loop() -- be searching for. -- TODO(ashkan) I'd like to remove this, but it seems permanent :( local buffer_start = buffer:find(header_start_pattern) - local headers = parse_headers(buffer:sub(buffer_start, start-1)) + local headers = parse_headers(buffer:sub(buffer_start, start - 1)) local content_length = headers.content_length -- Use table instead of just string to buffer the message. It prevents -- a ton of strings allocating. -- ref. http://www.lua.org/pil/11.6.html - local body_chunks = {buffer:sub(finish+1)} + local body_chunks = { buffer:sub(finish + 1) } local body_length = #body_chunks[1] -- Keep waiting for data until we have enough. while body_length < content_length do local chunk = coroutine.yield() - or error("Expected more data for the body. The server may have died.") -- TODO hmm. + or error('Expected more data for the body. The server may have died.') -- TODO hmm. table.insert(body_chunks, chunk) body_length = body_length + #chunk end @@ -123,25 +129,30 @@ local function request_parser_loop() end local body = table.concat(body_chunks) -- Yield our data. - buffer = rest..(coroutine.yield(headers, body) - or error("Expected more data for the body. The server may have died.")) -- TODO hmm. + buffer = rest + .. ( + coroutine.yield(headers, body) + or error('Expected more data for the body. The server may have died.') + ) -- TODO hmm. else -- Get more data since we don't have enough. - buffer = buffer..(coroutine.yield() - or error("Expected more data for the header. The server may have died.")) -- TODO hmm. + buffer = buffer + .. ( + coroutine.yield() or error('Expected more data for the header. The server may have died.') + ) -- TODO hmm. end end end --- Mapping of error codes used by the client local client_errors = { - INVALID_SERVER_MESSAGE = 1; - INVALID_SERVER_JSON = 2; - NO_RESULT_CALLBACK_FOUND = 3; - READ_ERROR = 4; - NOTIFICATION_HANDLER_ERROR = 5; - SERVER_REQUEST_HANDLER_ERROR = 6; - SERVER_RESULT_CALLBACK_ERROR = 7; + INVALID_SERVER_MESSAGE = 1, + INVALID_SERVER_JSON = 2, + NO_RESULT_CALLBACK_FOUND = 3, + READ_ERROR = 4, + NOTIFICATION_HANDLER_ERROR = 5, + SERVER_REQUEST_HANDLER_ERROR = 6, + SERVER_RESULT_CALLBACK_ERROR = 7, } client_errors = vim.tbl_add_reverse_lookup(client_errors) @@ -151,26 +162,26 @@ client_errors = vim.tbl_add_reverse_lookup(client_errors) ---@param err (table) The error object ---@returns (string) The formatted error message local function format_rpc_error(err) - validate { - err = { err, 't' }; - } + validate({ + err = { err, 't' }, + }) -- There is ErrorCodes in the LSP specification, -- but in ResponseError.code it is not used and the actual type is number. local code if protocol.ErrorCodes[err.code] then - code = string.format("code_name = %s,", protocol.ErrorCodes[err.code]) + code = string.format('code_name = %s,', protocol.ErrorCodes[err.code]) else - code = string.format("code_name = unknown, code = %s,", err.code) + code = string.format('code_name = unknown, code = %s,', err.code) end - local message_parts = {"RPC[Error]", code} + local message_parts = { 'RPC[Error]', code } if err.message then - table.insert(message_parts, "message =") - table.insert(message_parts, string.format("%q", err.message)) + table.insert(message_parts, 'message =') + table.insert(message_parts, string.format('%q', err.message)) end if err.data then - table.insert(message_parts, "data =") + table.insert(message_parts, 'data =') table.insert(message_parts, vim.inspect(err.data)) end return table.concat(message_parts, ' ') @@ -185,11 +196,11 @@ local function rpc_response_error(code, message, data) -- TODO should this error or just pick a sane error (like InternalError)? local code_name = assert(protocol.ErrorCodes[code], 'Invalid RPC error code') return setmetatable({ - code = code; - message = message or code_name; - data = data; + code = code, + message = message or code_name, + data = data, }, { - __tostring = format_rpc_error; + __tostring = format_rpc_error, }) end @@ -220,7 +231,7 @@ end ---@param signal (number): Number describing the signal used to terminate (if ---any) function default_dispatchers.on_exit(code, signal) - local _ = log.info() and log.info("client_exit", { code = code, signal = signal }) + local _ = log.info() and log.info('client_exit', { code = code, signal = signal }) end ---@private --- Default dispatcher for client errors. @@ -258,15 +269,16 @@ end --- - {handle} A handle for low-level interaction with the LSP server process --- |vim.loop|. local function start(cmd, cmd_args, dispatchers, extra_spawn_params) - local _ = log.info() and log.info("Starting RPC client", {cmd = cmd, args = cmd_args, extra = extra_spawn_params}) - validate { - cmd = { cmd, 's' }; - cmd_args = { cmd_args, 't' }; - dispatchers = { dispatchers, 't', true }; - } + local _ = log.info() + and log.info('Starting RPC client', { cmd = cmd, args = cmd_args, extra = extra_spawn_params }) + validate({ + cmd = { cmd, 's' }, + cmd_args = { cmd_args, 't' }, + dispatchers = { dispatchers, 't', true }, + }) if extra_spawn_params and extra_spawn_params.cwd then - assert(is_dir(extra_spawn_params.cwd), "cwd must be a directory") + assert(is_dir(extra_spawn_params.cwd), 'cwd must be a directory') end if dispatchers then local user_dispatchers = dispatchers @@ -275,11 +287,11 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params) local user_dispatcher = user_dispatchers[dispatch_name] if user_dispatcher then if type(user_dispatcher) ~= 'function' then - error(string.format("dispatcher.%s must be a function", dispatch_name)) + error(string.format('dispatcher.%s must be a function', dispatch_name)) end -- server_request is wrapped elsewhere. - if not (dispatch_name == 'server_request' - or dispatch_name == 'on_exit') -- TODO this blocks the loop exiting for some reason. + if + not (dispatch_name == 'server_request' or dispatch_name == 'on_exit') -- TODO this blocks the loop exiting for some reason. then user_dispatcher = schedule_wrap(user_dispatcher) end @@ -317,20 +329,25 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params) dispatchers.on_exit(code, signal) end local spawn_params = { - args = cmd_args; - stdio = {stdin, stdout, stderr}; + args = cmd_args, + stdio = { stdin, stdout, stderr }, + detached = not is_win, } if extra_spawn_params then spawn_params.cwd = extra_spawn_params.cwd spawn_params.env = env_merge(extra_spawn_params.env) + if extra_spawn_params.detached ~= nil then + spawn_params.detached = extra_spawn_params.detached + end end handle, pid = uv.spawn(cmd, spawn_params, onexit) if handle == nil then - 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." + 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) + msg = msg .. string.format(' with error message: %s', pid) end vim.notify(msg, vim.log.levels.WARN) return @@ -344,8 +361,10 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params) ---@param payload table ---@returns true if the payload could be scheduled, false if the main event-loop is in the process of closing. local function encode_and_send(payload) - local _ = log.debug() and log.debug("rpc.send", payload) - if handle == nil or handle:is_closing() then return false end + local _ = log.debug() and log.debug('rpc.send', payload) + if handle == nil or handle:is_closing() then + return false + end local encoded = vim.json.encode(payload) stdin:write(format_message_with_content_length(encoded)) return true @@ -359,22 +378,22 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params) ---@param params (table): Parameters for the invoked LSP method ---@returns (bool) `true` if notification could be sent, `false` if not local function notify(method, params) - return encode_and_send { - jsonrpc = "2.0"; - method = method; - params = params; - } + return encode_and_send({ + jsonrpc = '2.0', + method = method, + params = params, + }) end ---@private --- sends an error object to the remote LSP process. local function send_response(request_id, err, result) - return encode_and_send { - id = request_id; - jsonrpc = "2.0"; - error = err; - result = result; - } + return encode_and_send({ + id = request_id, + jsonrpc = '2.0', + error = err, + result = result, + }) end -- FIXME: DOC: Should be placed on the RPC client object returned by @@ -385,21 +404,21 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params) ---@param method (string) The invoked LSP method ---@param params (table) Parameters for the invoked LSP method ---@param callback (function) Callback to invoke - ---@param notify_reply_callback (function) Callback to invoke as soon as a request is no longer pending + ---@param notify_reply_callback (function|nil) Callback to invoke as soon as a request is no longer pending ---@returns (bool, number) `(true, message_id)` if request could be sent, `false` if not local function request(method, params, callback, notify_reply_callback) - validate { - callback = { callback, 'f' }; - notify_reply_callback = { notify_reply_callback, 'f', true }; - } + validate({ + callback = { callback, 'f' }, + notify_reply_callback = { notify_reply_callback, 'f', true }, + }) message_index = message_index + 1 local message_id = message_index - local result = encode_and_send { - id = message_id; - jsonrpc = "2.0"; - method = method; - params = params; - } + local result = encode_and_send({ + id = message_id, + jsonrpc = '2.0', + method = method, + params = params, + }) if result then if message_callbacks then message_callbacks[message_id] = schedule_wrap(callback) @@ -417,7 +436,7 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params) stderr:read_start(function(_err, chunk) if chunk then - local _ = log.error() and log.error("rpc", cmd, "stderr", chunk) + local _ = log.error() and log.error('rpc', cmd, 'stderr', chunk) end end) @@ -451,7 +470,7 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params) on_error(client_errors.INVALID_SERVER_JSON, decoded) return end - local _ = log.debug() and log.debug("rpc.receive", decoded) + local _ = log.debug() and log.debug('rpc.receive', decoded) if type(decoded.method) == 'string' and decoded.id then local err @@ -459,17 +478,36 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params) -- we can still use the result. schedule(function() local status, result - status, result, err = try_call(client_errors.SERVER_REQUEST_HANDLER_ERROR, - dispatchers.server_request, decoded.method, decoded.params) - local _ = log.debug() and log.debug("server_request: callback result", { status = status, result = result, err = err }) + status, result, err = try_call( + client_errors.SERVER_REQUEST_HANDLER_ERROR, + dispatchers.server_request, + decoded.method, + decoded.params + ) + local _ = log.debug() + and log.debug( + 'server_request: callback result', + { status = status, result = result, err = err } + ) if status then if not (result or err) then -- TODO this can be a problem if `null` is sent for result. needs vim.NIL - error(string.format("method %q: either a result or an error must be sent to the server in response", decoded.method)) + error( + string.format( + 'method %q: either a result or an error must be sent to the server in response', + decoded.method + ) + ) end if err then - assert(type(err) == 'table', "err must be a table. Use rpc_response_error to help format errors.") - local code_name = assert(protocol.ErrorCodes[err.code], "Errors must use protocol.ErrorCodes. Use rpc_response_error to help format errors.") + assert( + type(err) == 'table', + 'err must be a table. Use rpc_response_error to help format errors.' + ) + local code_name = assert( + protocol.ErrorCodes[err.code], + 'Errors must use protocol.ErrorCodes. Use rpc_response_error to help format errors.' + ) err.message = err.message or code_name end else @@ -479,18 +517,17 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params) end send_response(decoded.id, err, result) end) - -- This works because we are expecting vim.NIL here + -- This works because we are expecting vim.NIL here elseif decoded.id and (decoded.result ~= vim.NIL or decoded.error ~= vim.NIL) then - -- We sent a number, so we expect a number. local result_id = tonumber(decoded.id) -- Notify the user that a response was received for the request local notify_reply_callback = notify_reply_callbacks and notify_reply_callbacks[result_id] if notify_reply_callback then - validate { - notify_reply_callback = { notify_reply_callback, 'f' }; - } + validate({ + notify_reply_callback = { notify_reply_callback, 'f' }, + }) notify_reply_callback(result_id) notify_reply_callbacks[result_id] = nil end @@ -499,7 +536,7 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params) if decoded.error then local mute_error = false if decoded.error.code == protocol.ErrorCodes.RequestCancelled then - local _ = log.debug() and log.debug("Received cancellation ack", decoded) + local _ = log.debug() and log.debug('Received cancellation ack', decoded) mute_error = true end @@ -519,24 +556,33 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params) local callback = message_callbacks and message_callbacks[result_id] if callback then message_callbacks[result_id] = nil - validate { - callback = { callback, 'f' }; - } + validate({ + callback = { callback, 'f' }, + }) if decoded.error then decoded.error = setmetatable(decoded.error, { - __tostring = format_rpc_error; + __tostring = format_rpc_error, }) end - try_call(client_errors.SERVER_RESULT_CALLBACK_ERROR, - callback, decoded.error, decoded.result) + try_call( + client_errors.SERVER_RESULT_CALLBACK_ERROR, + callback, + decoded.error, + decoded.result + ) else on_error(client_errors.NO_RESULT_CALLBACK_FOUND, decoded) - local _ = log.error() and log.error("No callback found for server response id "..result_id) + local _ = log.error() + and log.error('No callback found for server response id ' .. result_id) end elseif type(decoded.method) == 'string' then -- Notification - try_call(client_errors.NOTIFICATION_HANDLER_ERROR, - dispatchers.notification, decoded.method, decoded.params) + try_call( + client_errors.NOTIFICATION_HANDLER_ERROR, + dispatchers.notification, + decoded.method, + decoded.params + ) else -- Invalid server message on_error(client_errors.INVALID_SERVER_MESSAGE, decoded) @@ -552,7 +598,9 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params) return end -- This should signal that we are done reading from the client. - if not chunk then return end + if not chunk then + return + end -- Flush anything in the parser by looping until we don't get a result -- anymore. while true do @@ -570,17 +618,17 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params) end) return { - pid = pid; - handle = handle; - request = request; - notify = notify + pid = pid, + handle = handle, + request = request, + notify = notify, } end return { - start = start; - rpc_response_error = rpc_response_error; - format_rpc_error = format_rpc_error; - client_errors = client_errors; + start = start, + rpc_response_error = rpc_response_error, + format_rpc_error = format_rpc_error, + client_errors = client_errors, } -- vim:sw=2 ts=2 et diff --git a/runtime/lua/vim/lsp/sync.lua b/runtime/lua/vim/lsp/sync.lua index 0f4e5b572b..0d65e86b55 100644 --- a/runtime/lua/vim/lsp/sync.lua +++ b/runtime/lua/vim/lsp/sync.lua @@ -79,7 +79,7 @@ local function compute_line_length(line, offset_encoding) local length local _ if offset_encoding == 'utf-16' then - _, length = str_utfindex(line) + _, length = str_utfindex(line) elseif offset_encoding == 'utf-32' then length, _ = str_utfindex(line) else @@ -100,7 +100,7 @@ local function align_end_position(line, byte, offset_encoding) -- If on the first byte, or an empty string: the trivial case if byte == 1 or #line == 0 then char = byte - -- Called in the case of extending an empty line "" -> "a" + -- Called in the case of extending an empty line "" -> "a" elseif byte == #line + 1 then char = compute_line_length(line, offset_encoding) + 1 else @@ -130,14 +130,38 @@ end ---@param new_lastline integer new_lastline from on_lines, adjusted to 1-index ---@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 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 == 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 @@ -158,14 +182,12 @@ 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 + if start_byte_idx == 1 or (#prev_line == 0 and start_byte_idx == 1) then byte_idx = start_byte_idx char_idx = 1 elseif start_byte_idx == #prev_line + 1 then byte_idx = start_byte_idx - char_idx = compute_line_length(prev_line, offset_encoding) + 1 + 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, byte_idx, offset_encoding) @@ -188,14 +210,30 @@ end ---@param new_lastline integer ---@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) +local function compute_end_range( + prev_lines, + curr_lines, + start_range, + firstline, + lastline, + new_lastline, + offset_encoding +) -- 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 } + return { line_idx = (lastline - new_lastline + firstline), byte_idx = 1, char_idx = 1 }, { + line_idx = firstline, + byte_idx = 1, + char_idx = 1, + } end if firstline == lastline then - return { line_idx = firstline, byte_idx = 1, char_idx = 1 }, { line_idx = new_lastline - lastline + firstline, byte_idx = 1, char_idx = 1 } + return { line_idx = firstline, byte_idx = 1, char_idx = 1 }, { + line_idx = new_lastline - lastline + firstline, + byte_idx = 1, + char_idx = 1, + } end -- Compare on last line, at minimum will be the start range local start_line_idx = start_range.line_idx @@ -218,14 +256,18 @@ local function compute_end_range(prev_lines, curr_lines, start_range, firstline, local max_length if start_line_idx == prev_line_idx then -- Search until beginning of difference - max_length = min(prev_line_length - start_range.byte_idx, curr_line_length - start_range.byte_idx) + 1 + max_length = min( + prev_line_length - start_range.byte_idx, + curr_line_length - start_range.byte_idx + ) + 1 else max_length = min(prev_line_length, curr_line_length) + 1 end for idx = 0, max_length do byte_offset = idx if - str_byte(prev_line, prev_line_length - byte_offset) ~= str_byte(curr_line, curr_line_length - byte_offset) + str_byte(prev_line, prev_line_length - byte_offset) + ~= str_byte(curr_line, curr_line_length - byte_offset) then break end @@ -239,8 +281,10 @@ local function compute_end_range(prev_lines, curr_lines, start_range, firstline, if prev_end_byte_idx == 0 then prev_end_byte_idx = 1 end - local prev_byte_idx, prev_char_idx = align_end_position(prev_line, prev_end_byte_idx, offset_encoding) - local prev_end_range = { line_idx = prev_line_idx, byte_idx = prev_byte_idx, char_idx = prev_char_idx } + local prev_byte_idx, prev_char_idx = + align_end_position(prev_line, prev_end_byte_idx, offset_encoding) + local prev_end_range = + { line_idx = prev_line_idx, byte_idx = prev_byte_idx, char_idx = prev_char_idx } local curr_end_range -- Deletion event, new_range cannot be before start @@ -252,8 +296,10 @@ local function compute_end_range(prev_lines, curr_lines, start_range, firstline, if curr_end_byte_idx == 0 then curr_end_byte_idx = 1 end - local curr_byte_idx, curr_char_idx = align_end_position(curr_line, curr_end_byte_idx, offset_encoding) - curr_end_range = { line_idx = curr_line_idx, byte_idx = curr_byte_idx, char_idx = curr_char_idx } + local curr_byte_idx, curr_char_idx = + align_end_position(curr_line, curr_end_byte_idx, offset_encoding) + curr_end_range = + { line_idx = curr_line_idx, byte_idx = curr_byte_idx, char_idx = curr_char_idx } end return prev_end_range, curr_end_range @@ -266,14 +312,13 @@ end ---@param end_range table new_end_range returned by last_difference ---@returns string text extracted from defined region local function extract_text(lines, start_range, end_range, line_ending) - if not lines[start_range.line_idx] then - return "" - end + if not lines[start_range.line_idx] then + return '' + end -- Trivial case: start and end range are the same line, directly grab changed text if start_range.line_idx == end_range.line_idx then -- string.sub is inclusive, end_range is not return string.sub(lines[start_range.line_idx], start_range.byte_idx, end_range.byte_idx - 1) - else -- Handle deletion case -- Collect the changed portion of the first changed line @@ -288,7 +333,7 @@ local function extract_text(lines, start_range, end_range, line_ending) -- Collect the changed portion of the last changed line. table.insert(result, string.sub(lines[end_range.line_idx], 1, end_range.byte_idx - 1)) else - table.insert(result, "") + table.insert(result, '') end -- Add line ending between all lines @@ -313,7 +358,10 @@ local function compute_range_length(lines, start_range, end_range, offset_encodi local start_line = lines[start_range.line_idx] local range_length if start_line and #start_line > 0 then - range_length = compute_line_length(start_line, offset_encoding) - start_range.char_idx + 1 + line_ending_length + range_length = compute_line_length(start_line, offset_encoding) + - start_range.char_idx + + 1 + + line_ending_length else -- Length of newline character range_length = line_ending_length @@ -345,7 +393,15 @@ end ---@param new_lastline number line to begin search in new_lines for last difference ---@param offset_encoding string encoding requested by language server ---@returns table TextDocumentContentChangeEvent see https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocumentContentChangeEvent -function M.compute_diff(prev_lines, curr_lines, firstline, lastline, new_lastline, offset_encoding, line_ending) +function M.compute_diff( + prev_lines, + curr_lines, + firstline, + lastline, + new_lastline, + offset_encoding, + line_ending +) -- Find the start of changes between the previous and current buffer. Common between both. -- Sent to the server as the start of the changed range. -- Used to grab the changed text from the latest buffer. @@ -375,7 +431,8 @@ function M.compute_diff(prev_lines, curr_lines, firstline, lastline, new_lastlin local text = extract_text(curr_lines, start_range, curr_end_range, line_ending) -- Compute the range of the replaced text. Deprecated but still required for certain language servers - local range_length = compute_range_length(prev_lines, start_range, prev_end_range, offset_encoding, line_ending) + local range_length = + compute_range_length(prev_lines, start_range, prev_end_range, offset_encoding, line_ending) -- convert to 0 based indexing local result = { diff --git a/runtime/lua/vim/lsp/tagfunc.lua b/runtime/lua/vim/lsp/tagfunc.lua index 5c55e8559f..49029f8599 100644 --- a/runtime/lua/vim/lsp/tagfunc.lua +++ b/runtime/lua/vim/lsp/tagfunc.lua @@ -1,5 +1,5 @@ local lsp = vim.lsp -local util = vim.lsp.util +local util = lsp.util ---@private local function mk_tag_item(name, range, uri, offset_encoding) @@ -15,7 +15,7 @@ end ---@private local function query_definition(pattern) - local params = lsp.util.make_position_params() + local params = util.make_position_params() local results_by_client, err = lsp.buf_request_sync(0, 'textDocument/definition', params, 1000) if err then return {} @@ -44,7 +44,8 @@ end ---@private local function query_workspace_symbols(pattern) - local results_by_client, err = lsp.buf_request_sync(0, 'workspace/symbol', { query = pattern }, 1000) + local results_by_client, err = + lsp.buf_request_sync(0, 'workspace/symbol', { query = pattern }, 1000) if err then return {} end diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 89c5ebe8f7..70f5010256 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -1,10 +1,10 @@ -local protocol = require 'vim.lsp.protocol' -local snippet = require 'vim.lsp._snippet' +local protocol = require('vim.lsp.protocol') +local snippet = require('vim.lsp._snippet') local vim = vim local validate = vim.validate local api = vim.api local list_extend = vim.list_extend -local highlight = require 'vim.highlight' +local highlight = require('vim.highlight') local uv = vim.loop local npcall = vim.F.npcall @@ -13,14 +13,14 @@ local split = vim.split local M = {} local default_border = { - {"", "NormalFloat"}, - {"", "NormalFloat"}, - {"", "NormalFloat"}, - {" ", "NormalFloat"}, - {"", "NormalFloat"}, - {"", "NormalFloat"}, - {"", "NormalFloat"}, - {" ", "NormalFloat"}, + { '', 'NormalFloat' }, + { '', 'NormalFloat' }, + { '', 'NormalFloat' }, + { ' ', 'NormalFloat' }, + { '', 'NormalFloat' }, + { '', 'NormalFloat' }, + { '', 'NormalFloat' }, + { ' ', 'NormalFloat' }, } ---@private @@ -35,43 +35,70 @@ local function get_border_size(opts) local width = 0 if type(border) == 'string' then - local border_size = {none = {0, 0}, single = {2, 2}, double = {2, 2}, rounded = {2, 2}, solid = {2, 2}, shadow = {1, 1}} + local border_size = { + none = { 0, 0 }, + single = { 2, 2 }, + double = { 2, 2 }, + rounded = { 2, 2 }, + solid = { 2, 2 }, + shadow = { 1, 1 }, + } if border_size[border] == nil then - error(string.format("invalid floating preview border: %s. :help vim.api.nvim_open_win()", vim.inspect(border))) + error( + string.format( + 'invalid floating preview border: %s. :help vim.api.nvim_open_win()', + vim.inspect(border) + ) + ) end height, width = unpack(border_size[border]) else if 8 % #border ~= 0 then - error(string.format("invalid floating preview border: %s. :help vim.api.nvim_open_win()", vim.inspect(border))) + error( + string.format( + 'invalid floating preview border: %s. :help vim.api.nvim_open_win()', + vim.inspect(border) + ) + ) end ---@private local function border_width(id) id = (id - 1) % #border + 1 - if type(border[id]) == "table" then + if type(border[id]) == 'table' then -- border specified as a table of <character, highlight group> return vim.fn.strdisplaywidth(border[id][1]) - elseif type(border[id]) == "string" then + elseif type(border[id]) == 'string' then -- border specified as a list of border characters return vim.fn.strdisplaywidth(border[id]) end - error(string.format("invalid floating preview border: %s. :help vim.api.nvim_open_win()", vim.inspect(border))) + error( + string.format( + 'invalid floating preview border: %s. :help vim.api.nvim_open_win()', + vim.inspect(border) + ) + ) end ---@private local function border_height(id) id = (id - 1) % #border + 1 - if type(border[id]) == "table" then + if type(border[id]) == 'table' then -- border specified as a table of <character, highlight group> return #border[id][1] > 0 and 1 or 0 - elseif type(border[id]) == "string" then + elseif type(border[id]) == 'string' then -- border specified as a list of border characters return #border[id] > 0 and 1 or 0 end - error(string.format("invalid floating preview border: %s. :help vim.api.nvim_open_win()", vim.inspect(border))) + error( + string.format( + 'invalid floating preview border: %s. :help vim.api.nvim_open_win()', + vim.inspect(border) + ) + ) end - height = height + border_height(2) -- top - height = height + border_height(6) -- bottom - width = width + border_width(4) -- right - width = width + border_width(8) -- left + height = height + border_height(2) -- top + height = height + border_height(6) -- bottom + width = width + border_width(4) -- right + width = width + border_width(8) -- left end return { height = height, width = width } @@ -89,9 +116,15 @@ end ---@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 not encoding then + encoding = 'utf-16' + end if encoding == 'utf-8' then - if index then return index else return #line end + if index then + return index + else + return #line + end elseif encoding == 'utf-16' then local _, col16 = vim.str_utfindex(line, index) return col16 @@ -99,7 +132,7 @@ function M._str_utfindex_enc(line, index, encoding) local col32, _ = vim.str_utfindex(line, index) return col32 else - error("Invalid encoding: " .. vim.inspect(encoding)) + error('Invalid encoding: ' .. vim.inspect(encoding)) end end @@ -111,15 +144,21 @@ end ---@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 not encoding then + encoding = 'utf-16' + end if encoding == 'utf-8' then - if index then return index else return #line end + 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)) + error('Invalid encoding: ' .. vim.inspect(encoding)) end end @@ -142,34 +181,38 @@ function M.set_lines(lines, A, B, new_lines) -- specifying a line number after what we would call the last line. local i_n = math.min(B[1] + 1, #lines) if not (i_0 >= 1 and i_0 <= #lines + 1 and i_n >= 1 and i_n <= #lines) then - error("Invalid range: "..vim.inspect{A = A; B = B; #lines, new_lines}) + error('Invalid range: ' .. vim.inspect({ A = A, B = B, #lines, new_lines })) end - local prefix = "" - local suffix = lines[i_n]:sub(B[2]+1) + local prefix = '' + local suffix = lines[i_n]:sub(B[2] + 1) if A[2] > 0 then prefix = lines[i_0]:sub(1, A[2]) end local n = i_n - i_0 + 1 if n ~= #new_lines then - for _ = 1, n - #new_lines do table.remove(lines, i_0) end - for _ = 1, #new_lines - n do table.insert(lines, i_0, '') end + for _ = 1, n - #new_lines do + table.remove(lines, i_0) + end + for _ = 1, #new_lines - n do + table.insert(lines, i_0, '') + end end for i = 1, #new_lines do lines[i - 1 + i_0] = new_lines[i] end if #suffix > 0 then local i = i_0 + #new_lines - 1 - lines[i] = lines[i]..suffix + lines[i] = lines[i] .. suffix end if #prefix > 0 then - lines[i_0] = prefix..lines[i_0] + lines[i_0] = prefix .. lines[i_0] end return lines end ---@private local function sort_by_key(fn) - return function(a,b) + return function(a, b) local ka, kb = fn(a), fn(b) assert(#ka == #kb) for i = 1, #ka do @@ -191,18 +234,18 @@ end ---@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 } + 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() + bufnr = 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] + lines[row] = (api.nvim_buf_get_lines(bufnr, row, row + 1, false) or { '' })[1] end return lines end @@ -211,7 +254,7 @@ local function get_lines(bufnr, rows) -- 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 + if uri:sub(1, 4) ~= 'file' then vim.fn.bufload(bufnr) return buf_lines() end @@ -224,8 +267,10 @@ local function get_lines(bufnr, rows) 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 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) @@ -242,11 +287,13 @@ local function get_lines(bufnr, rows) local found = 0 local lnum = 0 - for line in string.gmatch(data, "([^\n]*)\n?") do + 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 + if found == need then + break + end end lnum = lnum + 1 end @@ -254,13 +301,12 @@ local function get_lines(bufnr, rows) -- change any lines we didn't find to the empty string for i, line in pairs(lines) do if line == true then - lines[i] = "" + 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. @@ -273,11 +319,10 @@ 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|nil defaults to utf-16 +---@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 @@ -286,7 +331,7 @@ 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 - local line = get_line(bufnr, position.line) + local line = get_line(bufnr, position.line) or '' local ok, result ok, result = pcall(_str_byteindex_enc, line, col, offset_encoding) if ok then @@ -300,54 +345,27 @@ end --- Process and return progress reports from lsp server ---@private function M.get_progress_messages() - local new_messages = {} - local msg_remove = {} local progress_remove = {} for _, client in ipairs(vim.lsp.get_active_clients()) do - local messages = client.messages - local data = messages - for token, ctx in pairs(data.progress) do - - local new_report = { - name = data.name, - title = ctx.title or "empty title", - message = ctx.message, - percentage = ctx.percentage, - done = ctx.done, - progress = true, - } - table.insert(new_messages, new_report) - - if ctx.done then - table.insert(progress_remove, {client = client, token = token}) - end - end - - for i, msg in ipairs(data.messages) do - if msg.show_once then - msg.shown = msg.shown + 1 - if msg.shown > 1 then - table.insert(msg_remove, {client = client, idx = i}) - end - end - - table.insert(new_messages, {name = data.name, content = msg.content}) - end + local messages = client.messages + local data = messages + for token, ctx in pairs(data.progress) do + local new_report = { + name = data.name, + title = ctx.title or 'empty title', + message = ctx.message, + percentage = ctx.percentage, + done = ctx.done, + progress = true, + } + table.insert(new_messages, new_report) - if next(data.status) ~= nil then - table.insert(new_messages, { - name = data.name, - content = data.status.content, - uri = data.status.uri, - status = true - }) + if ctx.done then + table.insert(progress_remove, { client = client, token = token }) end - for _, item in ipairs(msg_remove) do - table.remove(client.messages, item.idx) end - end for _, item in ipairs(progress_remove) do @@ -360,16 +378,17 @@ 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|nil defaults to encoding of first client of `bufnr` +---@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, offset_encoding) - validate { - text_edits = { text_edits, 't', false }; - bufnr = { bufnr, 'number', false }; - offset_encoding = { offset_encoding, 'string', true }; - } - offset_encoding = offset_encoding or M._get_offset_encoding(bufnr) - if not next(text_edits) then return end + 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) end @@ -381,7 +400,11 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) index = index + 1 text_edit._index = index - if text_edit.range.start.line > text_edit.range['end'].line or text_edit.range.start.line == text_edit.range['end'].line and text_edit.range.start.character > text_edit.range['end'].character then + if + text_edit.range.start.line > text_edit.range['end'].line + or text_edit.range.start.line == text_edit.range['end'].line + and text_edit.range.start.character > text_edit.range['end'].character + then local start = text_edit.range.start text_edit.range.start = text_edit.range['end'] text_edit.range['end'] = start @@ -402,28 +425,9 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) 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) - local len = _str_utfindex_enc(vim.api.nvim_buf_get_lines(bufnr, -2, -1, false)[1] or '', nil, offset_encoding) - 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 + local is_current_buf = api.nvim_get_current_buf() == bufnr local cursor = (function() if not is_current_buf then return { @@ -431,7 +435,7 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) col = -1, } end - local cursor = vim.api.nvim_win_get_cursor(0) + local cursor = api.nvim_win_get_cursor(0) return { row = cursor[1] - 1, col = cursor[2], @@ -440,16 +444,38 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) -- 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']), - text = vim.split(text_edit.newText, '\n', true), + end_col = get_line_byte_from_position(bufnr, text_edit.range['end'], offset_encoding), + text = split(text_edit.newText, '\n', true), } - vim.api.nvim_buf_set_text(bufnr, e.start_row, e.start_col, e.end_row, e.end_col, e.text) + -- 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 = 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 + 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) @@ -464,21 +490,28 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) end end + local max = 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 }) + api.nvim_win_set_cursor(0, { cursor.row + 1, cursor.col }) end end -- 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(bufnr, '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, {}) + api.nvim_buf_set_lines(bufnr, -2, -1, false, {}) end end @@ -514,9 +547,15 @@ 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. @@ -527,15 +566,20 @@ function M.apply_text_document_edit(text_document_edit, index) -- `VersionedTextDocumentIdentifier`s version may be null -- https://microsoft.github.io/language-server-protocol/specification#versionedTextDocumentIdentifier - if should_check_version and (text_document.version + if + should_check_version + and ( + text_document.version and text_document.version > 0 and M.buf_versions[bufnr] - and M.buf_versions[bufnr] > text_document.version) then - print("Buffer ", text_document.uri, " newer than edits.") + and M.buf_versions[bufnr] > text_document.version + ) + then + print('Buffer ', text_document.uri, ' newer than edits.') 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. @@ -567,16 +611,16 @@ end --- precedence is as follows: textEdit.newText > insertText > label --see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion local function get_completion_word(item) - if item.textEdit ~= nil and item.textEdit.newText ~= nil and item.textEdit.newText ~= "" then + if item.textEdit ~= nil and item.textEdit.newText ~= nil and item.textEdit.newText ~= '' then local insert_text_format = protocol.InsertTextFormat[item.insertTextFormat] - if insert_text_format == "PlainText" or insert_text_format == nil then + if insert_text_format == 'PlainText' or insert_text_format == nil then return item.textEdit.newText else return M.parse_snippet(item.textEdit.newText) end - elseif item.insertText ~= nil and item.insertText ~= "" then + elseif item.insertText ~= nil and item.insertText ~= '' then local insert_text_format = protocol.InsertTextFormat[item.insertTextFormat] - if insert_text_format == "PlainText" or insert_text_format == nil then + if insert_text_format == 'PlainText' or insert_text_format == nil then return item.insertText else return M.parse_snippet(item.insertText) @@ -604,7 +648,7 @@ end ---@returns (`vim.lsp.protocol.completionItemKind`) ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion function M._get_completion_item_kind_name(completion_item_kind) - return protocol.CompletionItemKind[completion_item_kind] or "Unknown" + return protocol.CompletionItemKind[completion_item_kind] or 'Unknown' end --- Turns the result of a `textDocument/completion` request into vim-compatible @@ -635,7 +679,7 @@ function M.text_document_completion_list_to_complete_items(result, prefix) info = documentation elseif type(documentation) == 'table' and type(documentation.value) == 'string' then info = documentation.value - -- else + -- else -- TODO(ashkan) Validation handling here? end end @@ -653,9 +697,9 @@ function M.text_document_completion_list_to_complete_items(result, prefix) user_data = { nvim = { lsp = { - completion_item = completion_item - } - } + completion_item = completion_item, + }, + }, }, }) end @@ -663,6 +707,15 @@ function M.text_document_completion_list_to_complete_items(result, prefix) return matches end +---@private +--- Like vim.fn.bufwinid except it works across tabpages. +local function bufwinid(bufnr) + for _, win in ipairs(api.nvim_list_wins()) do + if api.nvim_win_get_buf(win) == bufnr then + return win + end + end +end --- Rename old_fname to new_fname --- @@ -671,7 +724,7 @@ end -- ignoreIfExists? bool function M.rename(old_fname, new_fname, opts) opts = opts or {} - local target_exists = vim.loop.fs_stat(new_fname) ~= nil + local target_exists = uv.fs_stat(new_fname) ~= nil if target_exists and not opts.overwrite or opts.ignoreIfExists then vim.notify('Rename target already exists. Skipping rename.') return @@ -688,10 +741,9 @@ function M.rename(old_fname, new_fname, opts) assert(ok, err) local newbuf = vim.fn.bufadd(new_fname) - for _, win in pairs(api.nvim_list_wins()) do - if api.nvim_win_get_buf(win) == oldbuf then - api.nvim_win_set_buf(win, newbuf) - end + local win = bufwinid(oldbuf) + if win then + api.nvim_win_set_buf(win, newbuf) end api.nvim_buf_delete(oldbuf, { force = true }) end @@ -712,11 +764,11 @@ end local function delete_file(change) local opts = change.options or {} local fname = vim.uri_to_fname(change.uri) - local stat = vim.loop.fs_stat(fname) + local stat = uv.fs_stat(fname) if opts.ignoreIfNotExists and not stat then return end - assert(stat, "Cannot delete not existing file or folder " .. fname) + assert(stat, 'Cannot delete not existing file or folder ' .. fname) local flags if stat and stat.type == 'directory' then flags = opts.recursive and 'rf' or 'd' @@ -729,28 +781,30 @@ local function delete_file(change) api.nvim_buf_delete(bufnr, { force = true }) 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 - M.rename( - vim.uri_to_fname(change.oldUri), - vim.uri_to_fname(change.newUri), - change.options - ) + if change.kind == 'rename' then + M.rename(vim.uri_to_fname(change.oldUri), vim.uri_to_fname(change.newUri), change.options) elseif change.kind == 'create' then create_file(change) elseif change.kind == 'delete' then delete_file(change) elseif change.kind then - error(string.format("Unsupported change: %q", vim.inspect(change))) + 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 @@ -763,7 +817,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 @@ -782,7 +836,7 @@ function M.convert_input_to_markdown_lines(input, contents) if type(input) == 'string' then list_extend(contents, split_lines(input)) else - assert(type(input) == 'table', "Expected a table for Hover.contents") + assert(type(input) == 'table', 'Expected a table for Hover.contents') -- MarkupContent if input.kind then -- The kind can be either plaintext or markdown. @@ -791,22 +845,22 @@ function M.convert_input_to_markdown_lines(input, contents) -- Some servers send input.value as empty, so let's ignore this :( local value = input.value or '' - if input.kind == "plaintext" then + if input.kind == 'plaintext' then -- wrap this in a <text></text> block so that stylize_markdown -- can properly process it as plaintext - value = string.format("<text>\n%s\n</text>", value) + value = string.format('<text>\n%s\n</text>', value) end -- assert(type(value) == 'string') list_extend(contents, split_lines(value)) - -- MarkupString variation 2 + -- MarkupString variation 2 elseif input.language then -- Some servers send input.value as empty, so let's ignore this :( -- assert(type(input.value) == 'string') - table.insert(contents, "```"..input.language) + table.insert(contents, '```' .. input.language) list_extend(contents, split_lines(input.value or '')) - table.insert(contents, "```") - -- By deduction, this must be MarkedString[] + table.insert(contents, '```') + -- By deduction, this must be MarkedString[] else -- Use our existing logic to handle MarkedString for _, marked_string in ipairs(input) do @@ -839,7 +893,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] @@ -849,16 +904,16 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers local label = signature.label if ft then -- wrap inside a code block so stylize_markdown can render it properly - label = ("```%s\n%s\n```"):format(ft, label) + label = ('```%s\n%s\n```'):format(ft, label) end - vim.list_extend(contents, vim.split(label, '\n', true)) + list_extend(contents, split(label, '\n', true)) if signature.documentation then M.convert_input_to_markdown_lines(signature.documentation, contents) end if signature.parameters and #signature.parameters > 0 then local active_parameter = (signature.activeParameter or signature_help.activeParameter or 0) - if active_parameter < 0 - then active_parameter = 0 + if active_parameter < 0 then + active_parameter = 0 end -- If the activeParameter is > #parameters, then set it to the last @@ -888,7 +943,7 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers } --]=] if parameter.label then - if type(parameter.label) == "table" then + if type(parameter.label) == 'table' then active_hl = parameter.label else local offset = 1 @@ -901,9 +956,11 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers end for p, param in pairs(signature.parameters) do offset = signature.label:find(param.label, offset, true) - if not offset then break end + if not offset then + break + end if p == active_parameter + 1 then - active_hl = {offset - 1, offset + #parameter.label - 1} + active_hl = { offset - 1, offset + #parameter.label - 1 } break end offset = offset + #param.label + 1 @@ -931,14 +988,14 @@ end --- - zindex (string or table) override `zindex`, defaults to 50 ---@returns (table) Options function M.make_floating_popup_options(width, height, opts) - validate { - opts = { opts, 't', true }; - } + validate({ + opts = { opts, 't', true }, + }) opts = opts or {} - validate { - ["opts.offset_x"] = { opts.offset_x, 'n', true }; - ["opts.offset_y"] = { opts.offset_y, 'n', true }; - } + validate({ + ['opts.offset_x'] = { opts.offset_x, 'n', true }, + ['opts.offset_y'] = { opts.offset_y, 'n', true }, + }) local anchor = '' local row, col @@ -947,20 +1004,20 @@ function M.make_floating_popup_options(width, height, opts) local lines_below = vim.fn.winheight(0) - lines_above if lines_above < lines_below then - anchor = anchor..'N' + anchor = anchor .. 'N' height = math.min(lines_below, height) row = 1 else - anchor = anchor..'S' + anchor = anchor .. 'S' height = math.min(lines_above, height) row = 0 end if vim.fn.wincol() + width + (opts.offset_x or 0) <= api.nvim_get_option('columns') then - anchor = anchor..'W' + anchor = anchor .. 'W' col = 0 else - anchor = anchor..'E' + anchor = anchor .. 'E' col = 1 end @@ -980,30 +1037,45 @@ 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) +---@param reuse_win boolean Jump to existing window if buffer is already opened. ---@returns `true` if the jump succeeded -function M.jump_to_location(location) +function M.jump_to_location(location, offset_encoding, reuse_win) -- location may be Location or LocationLink local uri = location.uri or location.targetUri - if uri == nil then return end + 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'" + vim.cmd("normal! m'") -- Push a new item into tagstack - local from = {vim.fn.bufnr('%'), vim.fn.line('.'), vim.fn.col('.'), 0} - local items = {{tagname=vim.fn.expand('<cword>'), from=from}} - vim.fn.settagstack(vim.fn.win_getid(), {items=items}, 't') + local from = { vim.fn.bufnr('%'), vim.fn.line('.'), vim.fn.col('.'), 0 } + local items = { { tagname = vim.fn.expand('<cword>'), from = from } } + vim.fn.settagstack(vim.fn.win_getid(), { items = items }, 't') --- Jump to new location (adjusting for UTF-16 encoding of characters) - api.nvim_set_current_buf(bufnr) - api.nvim_buf_set_option(bufnr, 'buflisted', true) + local win = reuse_win and bufwinid(bufnr) + if win then + api.nvim_set_current_win(win) + else + api.nvim_buf_set_option(bufnr, 'buflisted', true) + api.nvim_set_current_buf(bufnr) + end local range = location.range or location.targetSelectionRange local row = range.start.line - local col = get_line_byte_from_position(bufnr, range.start) - api.nvim_win_set_cursor(0, {row + 1, col}) + 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") + vim.cmd('normal! zv') return true end @@ -1018,22 +1090,24 @@ end function M.preview_location(location, opts) -- location may be LocationLink or Location (more useful for the former) local uri = location.targetUri or location.uri - if uri == nil then return end + if uri == nil then + return + end local bufnr = vim.uri_to_bufnr(uri) if not api.nvim_buf_is_loaded(bufnr) then vim.fn.bufload(bufnr) end local range = location.targetRange or location.range - local contents = api.nvim_buf_get_lines(bufnr, range.start.line, range["end"].line+1, false) + local contents = api.nvim_buf_get_lines(bufnr, range.start.line, range['end'].line + 1, false) local syntax = api.nvim_buf_get_option(bufnr, 'syntax') - if syntax == "" then + if syntax == '' then -- When no syntax is set, we use filetype as fallback. This might not result -- in a valid syntax definition. See also ft detection in stylize_markdown. -- An empty syntax is more common now with TreeSitter, since TS disables syntax. syntax = api.nvim_buf_get_option(bufnr, 'filetype') end opts = opts or {} - opts.focus_id = "location" + opts.focus_id = 'location' return M.open_floating_preview(contents, syntax, opts) end @@ -1054,20 +1128,20 @@ end --- - pad_bottom number of lines to pad contents at bottom (default 0) ---@return contents table of trimmed and padded lines function M._trim(contents, opts) - validate { - contents = { contents, 't' }; - opts = { opts, 't', true }; - } + validate({ + contents = { contents, 't' }, + opts = { opts, 't', true }, + }) opts = opts or {} contents = M.trim_empty_lines(contents) if opts.pad_top then for _ = 1, opts.pad_top do - table.insert(contents, 1, "") + table.insert(contents, 1, '') end end if opts.pad_bottom then for _ = 1, opts.pad_bottom do - table.insert(contents, "") + table.insert(contents, '') end end return contents @@ -1080,7 +1154,7 @@ end local function get_markdown_fences() local fences = {} for _, fence in pairs(vim.g.markdown_fenced_languages or {}) do - local lang, syntax = fence:match("^(.*)=(.*)$") + local lang, syntax = fence:match('^(.*)=(.*)$') if lang then fences[lang] = syntax end @@ -1109,28 +1183,28 @@ end --- - separator insert separator after code block ---@returns width,height size of float function M.stylize_markdown(bufnr, contents, opts) - validate { - contents = { contents, 't' }; - opts = { opts, 't', true }; - } + validate({ + contents = { contents, 't' }, + opts = { opts, 't', true }, + }) opts = opts or {} -- table of fence types to {ft, begin, end} -- when ft is nil, we get the ft from the regex match local matchers = { - block = {nil, "```+([a-zA-Z0-9_]*)", "```+"}, - pre = {"", "<pre>", "</pre>"}, - code = {"", "<code>", "</code>"}, - text = {"plaintex", "<text>", "</text>"}, + block = { nil, '```+([a-zA-Z0-9_]*)', '```+' }, + pre = { '', '<pre>', '</pre>' }, + code = { '', '<code>', '</code>' }, + text = { 'text', '<text>', '</text>' }, } local match_begin = function(line) for type, pattern in pairs(matchers) do - local ret = line:match(string.format("^%%s*%s%%s*$", pattern[2])) + local ret = line:match(string.format('^%%s*%s%%s*$', pattern[2])) if ret then return { type = type, - ft = pattern[1] or ret + ft = pattern[1] or ret, } end end @@ -1138,7 +1212,7 @@ function M.stylize_markdown(bufnr, contents, opts) local match_end = function(line, match) local pattern = matchers[match.type] - return line:match(string.format("^%%s*%s%%s*$", pattern[3])) + return line:match(string.format('^%%s*%s%%s*$', pattern[3])) end -- Clean up @@ -1168,25 +1242,34 @@ function M.stylize_markdown(bufnr, contents, opts) i = i + 1 end table.insert(highlights, { - ft = match.ft; - start = start + 1; - finish = #stripped; + ft = match.ft, + start = start + 1, + finish = #stripped, }) -- add a separator, but not on the last line if add_sep and i < #contents then - table.insert(stripped, "---") + table.insert(stripped, '---') markdown_lines[#stripped] = true end else -- 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 + if line:match('^---+$') then + while + markdown_lines[#stripped] + and (stripped[#stripped]:match('^%s*$') or stripped[#stripped]:match('^---+$')) + do markdown_lines[#stripped] = false table.remove(stripped, #stripped) end end -- add the line if its not an empty line following a separator - if not (line:match("^%s*$") and markdown_lines[#stripped] and stripped[#stripped]:match("^---+$")) then + if + not ( + line:match('^%s*$') + and markdown_lines[#stripped] + and stripped[#stripped]:match('^---+$') + ) + then table.insert(stripped, line) markdown_lines[#stripped] = true end @@ -1196,18 +1279,18 @@ function M.stylize_markdown(bufnr, contents, opts) end -- Compute size of float needed to show (wrapped) lines - opts.wrap_at = opts.wrap_at or (vim.wo["wrap"] and api.nvim_win_get_width(0)) + opts.wrap_at = opts.wrap_at or (vim.wo['wrap'] and api.nvim_win_get_width(0)) local width = M._make_floating_popup_size(stripped, opts) - local sep_line = string.rep("─", math.min(width, opts.wrap_at or width)) + local sep_line = string.rep('─', math.min(width, opts.wrap_at or width)) for l in pairs(markdown_lines) do - if stripped[l]:match("^---+$") then + if stripped[l]:match('^---+$') then stripped[l] = sep_line end end - vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, stripped) + api.nvim_buf_set_lines(bufnr, 0, -1, false, stripped) local idx = 1 ---@private @@ -1216,24 +1299,38 @@ function M.stylize_markdown(bufnr, contents, opts) local langs = {} local fences = get_markdown_fences() local function apply_syntax_to_region(ft, start, finish) - if ft == "" then - vim.cmd(string.format("syntax region markdownCode start=+\\%%%dl+ end=+\\%%%dl+ keepend extend", start, finish + 1)) + if ft == '' then + vim.cmd( + string.format( + 'syntax region markdownCode start=+\\%%%dl+ end=+\\%%%dl+ keepend extend', + start, + finish + 1 + ) + ) return end ft = fences[ft] or ft - local name = ft..idx + local name = ft .. idx idx = idx + 1 - local lang = "@"..ft:upper() + local lang = '@' .. ft:upper() if not langs[lang] then -- HACK: reset current_syntax, since some syntax files like markdown won't load if it is already set - pcall(vim.api.nvim_buf_del_var, bufnr, "current_syntax") + pcall(api.nvim_buf_del_var, bufnr, 'current_syntax') -- TODO(ashkan): better validation before this. - if not pcall(vim.cmd, string.format("syntax include %s syntax/%s.vim", lang, ft)) then + if not pcall(vim.cmd, string.format('syntax include %s syntax/%s.vim', lang, ft)) then return end langs[lang] = true end - vim.cmd(string.format("syntax region %s start=+\\%%%dl+ end=+\\%%%dl+ contains=%s keepend", name, start, finish + 1, lang)) + vim.cmd( + string.format( + 'syntax region %s start=+\\%%%dl+ end=+\\%%%dl+ contains=%s keepend', + name, + start, + finish + 1, + lang + ) + ) end -- needs to run in the buffer for the regions to work @@ -1244,13 +1341,13 @@ function M.stylize_markdown(bufnr, contents, opts) local last = 1 for _, h in ipairs(highlights) do if last < h.start then - apply_syntax_to_region("lsp_markdown", last, h.start - 1) + apply_syntax_to_region('lsp_markdown', last, h.start - 1) end apply_syntax_to_region(h.ft, h.start, h.finish) last = h.finish + 1 end if last <= #stripped then - apply_syntax_to_region("lsp_markdown", last, #stripped) + apply_syntax_to_region('lsp_markdown', last, #stripped) end end) @@ -1258,6 +1355,24 @@ function M.stylize_markdown(bufnr, contents, opts) end ---@private +--- Closes the preview window +--- +---@param winnr number window id of preview window +---@param bufnrs table|nil optional list of ignored buffers +local function 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 + pcall(api.nvim_del_augroup_by_name, augroup) + pcall(api.nvim_win_close, winnr, true) + end) +end + +---@private --- Creates autocommands to close a preview window when events happen. --- ---@param events table list of events @@ -1265,49 +1380,30 @@ end ---@param bufnrs table list of buffers where the preview window will remain visible ---@see |autocmd-events| local function close_preview_autocmd(events, winnr, bufnrs) - local augroup = 'preview_window_'..winnr + local augroup = api.nvim_create_augroup('preview_window_' .. winnr, { + clear = true, + }) -- 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, ','))) + api.nvim_create_autocmd('BufEnter', { + group = augroup, + callback = function() + close_preview_window(winnr, bufnrs) + end, + }) if #events > 0 then - vim.cmd(string.format([[ - augroup %s - autocmd %s <buffer> lua vim.lsp.util._close_preview_window(%d) - augroup end - ]], augroup, table.concat(events, ','), winnr)) + api.nvim_create_autocmd(events, { + group = augroup, + buffer = bufnrs[2], + callback = function() + close_preview_window(winnr) + end, + }) 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) --- @@ -1320,10 +1416,10 @@ end --- - max_height maximal height of floating window ---@returns width,height size of float function M._make_floating_popup_size(contents, opts) - validate { - contents = { contents, 't' }; - opts = { opts, 't', true }; - } + validate({ + contents = { contents, 't' }, + opts = { opts, 't', true }, + }) opts = opts or {} local width = opts.width @@ -1367,11 +1463,11 @@ function M._make_floating_popup_size(contents, opts) if vim.tbl_isempty(line_widths) then for _, line in ipairs(contents) do local line_width = vim.fn.strdisplaywidth(line) - height = height + math.ceil(line_width/wrap_at) + height = height + math.ceil(line_width / wrap_at) end else for i = 1, #contents do - height = height + math.max(1, math.ceil(line_widths[i]/wrap_at)) + height = height + math.max(1, math.ceil(line_widths[i] / wrap_at)) end end end @@ -1391,7 +1487,7 @@ end --- - 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 +--- - wrap_at: (number) 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 @@ -1405,16 +1501,16 @@ end ---@returns bufnr,winnr buffer and window number of the newly created floating ---preview window function M.open_floating_preview(contents, syntax, opts) - validate { - contents = { contents, 't' }; - syntax = { syntax, 's', true }; - opts = { opts, 't', true }; - } + validate({ + contents = { contents, 't' }, + syntax = { syntax, 's', true }, + opts = { opts, 't', true }, + }) opts = opts or {} opts.wrap = opts.wrap ~= false -- wrapping by default - opts.stylize_markdown = opts.stylize_markdown ~= false + opts.stylize_markdown = opts.stylize_markdown ~= false and vim.g.syntax_on ~= nil opts.focus = opts.focus ~= false - opts.close_events = opts.close_events or {"CursorMoved", "CursorMovedI", "InsertCharPre"} + opts.close_events = opts.close_events or { 'CursorMoved', 'CursorMovedI', 'InsertCharPre' } local bufnr = api.nvim_get_current_buf() @@ -1423,7 +1519,7 @@ function M.open_floating_preview(contents, syntax, opts) -- 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 - api.nvim_command("wincmd p") + api.nvim_command('wincmd p') return bufnr, current_winnr end do @@ -1431,7 +1527,7 @@ function M.open_floating_preview(contents, syntax, opts) if win and api.nvim_win_is_valid(win) and vim.fn.pumvisible() == 0 then -- focus and return the existing buf, win api.nvim_set_current_win(win) - api.nvim_command("stopinsert") + api.nvim_command('stopinsert') return api.nvim_win_get_buf(win), win end end @@ -1439,14 +1535,13 @@ function M.open_floating_preview(contents, syntax, opts) -- check if another floating preview already exists for this buffer -- and close it if needed - local existing_float = npcall(api.nvim_buf_get_var, bufnr, "lsp_floating_preview") + local existing_float = npcall(api.nvim_buf_get_var, bufnr, 'lsp_floating_preview') if existing_float and api.nvim_win_is_valid(existing_float) then api.nvim_win_close(existing_float, true) end local floating_bufnr = api.nvim_create_buf(false, true) - local do_stylize = syntax == "markdown" and opts.stylize_markdown - + local do_stylize = syntax == 'markdown' and opts.stylize_markdown -- Clean up input: trim empty lines from the end, pad contents = M._trim(contents, opts) @@ -1482,26 +1577,32 @@ 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, nowait = true}) - close_preview_autocmd(opts.close_events, floating_winnr, {floating_bufnr, bufnr}) + 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 api.nvim_win_set_var(floating_winnr, opts.focus_id, bufnr) end - api.nvim_buf_set_var(bufnr, "lsp_floating_preview", floating_winnr) + api.nvim_buf_set_var(bufnr, 'lsp_floating_preview', floating_winnr) return floating_bufnr, floating_winnr end do --[[ References ]] - local reference_ns = api.nvim_create_namespace("vim_lsp_references") + local reference_ns = api.nvim_create_namespace('vim_lsp_references') --- Removes document highlights from a buffer. --- ---@param bufnr number Buffer id function M.buf_clear_references(bufnr) - validate { bufnr = {bufnr, 'n', true} } + validate({ bufnr = { bufnr, 'n', true } }) api.nvim_buf_clear_namespace(bufnr or 0, reference_ns, 0, -1) end @@ -1509,35 +1610,50 @@ do --[[ References ]] --- ---@param bufnr number Buffer id ---@param references table List of `DocumentHighlight` objects to highlight - ---@param offset_encoding string One of "utf-8", "utf-16", "utf-32", or nil. Defaults to `offset_encoding` of first client of `bufnr` + ---@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 M._get_offset_encoding(bufnr) + 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"] - - local start_idx = get_line_byte_from_position(bufnr, { line = start_line, character = start_char }, offset_encoding) - local end_idx = get_line_byte_from_position(bufnr, { line = start_line, character = end_char }, offset_encoding) + 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'] + + local start_idx = get_line_byte_from_position( + bufnr, + { line = start_line, character = start_char }, + offset_encoding + ) + local end_idx = get_line_byte_from_position( + bufnr, + { line = start_line, character = end_char }, + offset_encoding + ) local document_highlight_kind = { - [protocol.DocumentHighlightKind.Text] = "LspReferenceText"; - [protocol.DocumentHighlightKind.Read] = "LspReferenceRead"; - [protocol.DocumentHighlightKind.Write] = "LspReferenceWrite"; + [protocol.DocumentHighlightKind.Text] = 'LspReferenceText', + [protocol.DocumentHighlightKind.Read] = 'LspReferenceRead', + [protocol.DocumentHighlightKind.Write] = 'LspReferenceWrite', } - local kind = reference["kind"] or protocol.DocumentHighlightKind.Text - highlight.range(bufnr, - reference_ns, - document_highlight_kind[kind], - { start_line, start_idx }, - { end_line, end_idx }) + local kind = reference['kind'] or protocol.DocumentHighlightKind.Text + highlight.range( + bufnr, + reference_ns, + document_highlight_kind[kind], + { start_line, start_idx }, + { end_line, end_idx }, + { priority = vim.highlight.priorities.user } + ) end end end local position_sort = sort_by_key(function(v) - return {v.start.line, v.start.character} + return { v.start.line, v.start.character } end) --- Returns the items with the byte position calculated correctly and in sorted @@ -1546,25 +1662,32 @@ end) --- 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) local v = {} rawset(t, k, v) return v - end; + end, }) for _, d in ipairs(locations) do -- locations may be Location or LocationLink local uri = d.uri or d.targetUri local range = d.range or d.targetSelectionRange - table.insert(grouped[uri], {start = range.start}) + table.insert(grouped[uri], { start = range.start }) end - local keys = vim.tbl_keys(grouped) table.sort(keys) -- TODO(ashkan) I wish we could do this lazily. @@ -1587,53 +1710,24 @@ function M.locations_to_items(locations) for _, temp in ipairs(rows) do local pos = temp.start local row = pos.line - local line = lines[row] or "" - local col = pos.character + local line = lines[row] or '' + local col = M._str_byteindex_enc(line, pos.character, offset_encoding) table.insert(items, { filename = filename, lnum = row + 1, - col = col + 1; - text = line; + col = col + 1, + text = line, }) end end return items end ---- Fills target window's location list with given list of items. ---- Can be obtained with e.g. |vim.lsp.util.locations_to_items()|. ---- Defaults to current window. ---- ----@deprecated Use |setloclist()| ---- ----@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; - }) -end - ---- Fills quickfix list with given list of items. ---- Can be obtained with e.g. |vim.lsp.util.locations_to_items()|. ---- ----@deprecated Use |setqflist()| ---- ----@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 - -- 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) - return protocol.SymbolKind[symbol_kind] or "Unknown" + return protocol.SymbolKind[symbol_kind] or 'Unknown' end --- Converts symbols to quickfix list items. @@ -1651,17 +1745,17 @@ function M.symbols_to_items(symbols, bufnr) lnum = range.start.line + 1, col = range.start.character + 1, kind = kind, - text = '['..kind..'] '..symbol.name, + text = '[' .. kind .. '] ' .. symbol.name, }) elseif symbol.selectionRange then -- DocumentSymbole type local kind = M._get_symbol_kind_name(symbol.kind) table.insert(_items, { -- bufnr = _bufnr, - filename = vim.api.nvim_buf_get_name(_bufnr), + filename = api.nvim_buf_get_name(_bufnr), lnum = symbol.selectionRange.start.line + 1, col = symbol.selectionRange.start.character + 1, kind = kind, - text = '['..kind..'] '..symbol.name + text = '[' .. kind .. '] ' .. symbol.name, }) if symbol.children then for _, v in ipairs(_symbols_to_items(symbol.children, _items, _bufnr)) do @@ -1695,7 +1789,7 @@ function M.trim_empty_lines(lines) break end end - return vim.list_extend({}, lines, start, finish) + return list_extend({}, lines, start, finish) end --- Accepts markdown lines and tries to reduce them to a filetype if they @@ -1706,12 +1800,12 @@ end ---@param lines (table) list of lines ---@returns (string) filetype or 'markdown' if it was unchanged. function M.try_trim_markdown_code_blocks(lines) - local language_id = lines[1]:match("^```(.*)") + local language_id = lines[1]:match('^```(.*)') if language_id then local has_inner_code_fence = false for i = 2, (#lines - 1) do local line = lines[i] - if line:sub(1,3) == '```' then + if line:sub(1, 3) == '```' then has_inner_code_fence = true break end @@ -1731,18 +1825,18 @@ end ---@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 buf = 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(buf, 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; } + return { line = 0, character = 0 } end col = _str_utfindex_enc(line, col, offset_encoding) - return { line = row; character = col; } + return { line = row, character = col } end --- Creates a `TextDocumentPositionParams` object for the current buffer and cursor position. @@ -1753,11 +1847,11 @@ end ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentPositionParams function M.make_position_params(window, offset_encoding) window = window or 0 - local buf = vim.api.nvim_win_get_buf(window) + local buf = api.nvim_win_get_buf(window) offset_encoding = offset_encoding or M._get_offset_encoding(buf) return { - textDocument = M.make_text_document_params(buf); - position = make_position_param(window, offset_encoding) + textDocument = M.make_text_document_params(buf), + position = make_position_param(window, offset_encoding), } end @@ -1765,18 +1859,30 @@ end ---@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}; - } + validate({ + bufnr = { bufnr, 'n', true }, + }) local offset_encoding for _, client in pairs(vim.lsp.buf_get_clients(bufnr)) do - local this_offset_encoding = client.offset_encoding or "utf-16" + 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) + vim.notify( + 'warning: multiple different client offset_encodings detected for buffer, this is not supported yet', + vim.log.levels.WARN + ) end end @@ -1793,12 +1899,12 @@ end ---@returns { textDocument = { uri = `current_file_uri` }, range = { start = ---`current_position`, end = `current_position` } } function M.make_range_params(window, offset_encoding) - local buf = vim.api.nvim_win_get_buf(window or 0) + local buf = 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(buf), - range = { start = position; ["end"] = position; } + range = { start = position, ['end'] = position }, } end @@ -1814,12 +1920,12 @@ end ---@returns { textDocument = { uri = `current_file_uri` }, range = { start = ---`start_position`, end = `end_position` } } 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}; - } - bufnr = bufnr or vim.api.nvim_get_current_buf() + validate({ + start_pos = { start_pos, 't', true }, + end_pos = { end_pos, 't', true }, + offset_encoding = { offset_encoding, 's', true }, + }) + bufnr = bufnr or 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, '>')) @@ -1828,10 +1934,10 @@ function M.make_given_range_params(start_pos, end_pos, bufnr, offset_encoding) B[1] = B[1] - 1 -- account for offset_encoding. if A[2] > 0 then - A = {A[1], M.character_offset(bufnr, A[1], A[2], offset_encoding)} + A = { A[1], M.character_offset(bufnr, A[1], A[2], offset_encoding) } end if B[2] > 0 then - B = {B[1], M.character_offset(bufnr, B[1], B[2], offset_encoding)} + 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 @@ -1842,9 +1948,9 @@ function M.make_given_range_params(start_pos, end_pos, bufnr, offset_encoding) return { textDocument = M.make_text_document_params(bufnr), range = { - start = {line = A[1], character = A[2]}, - ['end'] = {line = B[1], character = B[2]} - } + start = { line = A[1], character = A[2] }, + ['end'] = { line = B[1], character = B[2] }, + }, } end @@ -1861,34 +1967,34 @@ end ---@param added ---@param removed function M.make_workspace_params(added, removed) - return { event = { added = added; removed = removed; } } + return { event = { added = added, removed = removed } } end ---- Returns visual width of tabstop. +--- Returns indentation size. --- ----@see |softtabstop| ----@param bufnr (optional, number): Buffer handle, defaults to current ----@returns (number) tabstop visual width +---@see |shiftwidth| +---@param bufnr (number|nil): Buffer handle, defaults to current +---@returns (number) indentation size function M.get_effective_tabstop(bufnr) - validate { bufnr = {bufnr, 'n', true} } + 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. --- ----@param options Table with valid `FormattingOptions` entries +---@param options table|nil with valid `FormattingOptions` entries ---@returns `DocumentFormattingParams` object ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting function M.make_formatting_params(options) - validate { options = {options, 't', true} } + validate({ options = { options, 't', true } }) options = vim.tbl_extend('keep', options or {}, { - tabSize = M.get_effective_tabstop(); - insertSpaces = vim.bo.expandtab; + tabSize = M.get_effective_tabstop(), + insertSpaces = vim.bo.expandtab, }) return { - textDocument = { uri = vim.uri_from_bufnr(0) }; - options = options; + textDocument = { uri = vim.uri_from_bufnr(0) }, + options = options, } end @@ -1901,7 +2007,12 @@ end ---@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) - offset_encoding = offset_encoding or M._get_offset_encoding(buf) + 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_enc(line, nil, offset_encoding) @@ -1917,8 +2028,8 @@ end function M.lookup_section(settings, section) for part in vim.gsplit(section, '.', true) do settings = settings[part] - if not settings then - return + if settings == nil then + return vim.NIL end end return settings diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index e170befa4c..d6c3e25b3b 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 {} @@ -13,8 +15,8 @@ local vim = vim or {} --- copied and will throw an error. --- ---@param orig table Table to copy ----@returns New table of copied keys and (nested) values. -function vim.deepcopy(orig) end -- luacheck: no unused +---@return table Table of copied keys and (nested) values. +function vim.deepcopy(orig) end -- luacheck: no unused vim.deepcopy = (function() local function _id(v) return v @@ -22,7 +24,9 @@ vim.deepcopy = (function() local deepcopy_funcs = { table = function(orig, cache) - if cache[orig] then return cache[orig] end + if cache[orig] then + return cache[orig] + end local copy = {} cache[orig] = copy @@ -44,7 +48,7 @@ vim.deepcopy = (function() if f then return f(orig, cache or {}) else - error("Cannot deepcopy object of type "..type(orig)) + error('Cannot deepcopy object of type ' .. type(orig)) end end end)() @@ -55,19 +59,19 @@ end)() ---@see https://www.lua.org/pil/20.2.html ---@see http://lua-users.org/wiki/StringLibraryTutorial --- ----@param s String to split ----@param sep Separator string or pattern ----@param plain If `true` use `sep` literally (passed to String.find) ----@returns Iterator over the split components +---@param s string String to split +---@param sep string Separator or pattern +---@param plain boolean If `true` use `sep` literally (passed to string.find) +---@return function Iterator over the split components function vim.gsplit(s, sep, plain) - vim.validate{s={s,'s'},sep={sep,'s'},plain={plain,'b',true}} + vim.validate({ s = { s, 's' }, sep = { sep, 's' }, plain = { plain, 'b', true } }) local start = 1 local done = false local function _pass(i, j, ...) if i then - assert(j+1 > start, "Infinite loop detected") + assert(j + 1 > start, 'Infinite loop detected') local seg = s:sub(start, i - 1) start = j + 1 return seg, ... @@ -85,7 +89,7 @@ function vim.gsplit(s, sep, plain) if start == #s then done = true end - return _pass(start+1, start) + return _pass(start + 1, start) end return _pass(s:find(sep, start, plain)) end @@ -103,13 +107,13 @@ end --- ---@see |vim.gsplit()| --- ----@param s String to split ----@param sep Separator string or pattern ----@param kwargs Keyword arguments: +---@param s string String to split +---@param sep string Separator or pattern +---@param kwargs table Keyword arguments: --- - plain: (boolean) If `true` use `sep` literally (passed to string.find) --- - trimempty: (boolean) If `true` remove empty items from the front --- and back of the list ----@returns List-like table of the split components. +---@return table List of split components function vim.split(s, sep, kwargs) local plain local trimempty = false @@ -117,7 +121,7 @@ function vim.split(s, sep, kwargs) -- Support old signature for backward compatibility plain = kwargs else - vim.validate { kwargs = {kwargs, 't', true} } + vim.validate({ kwargs = { kwargs, 't', true } }) kwargs = kwargs or {} plain = kwargs.plain trimempty = kwargs.trimempty @@ -126,7 +130,7 @@ function vim.split(s, sep, kwargs) local t = {} local skip = trimempty for c in vim.gsplit(s, sep, plain) do - if c ~= "" then + if c ~= '' then skip = false end @@ -137,7 +141,7 @@ function vim.split(s, sep, kwargs) if trimempty then for i = #t, 1, -1 do - if t[i] ~= "" then + if t[i] ~= '' then break end table.remove(t, i) @@ -152,10 +156,10 @@ end --- ---@see From https://github.com/premake/premake-core/blob/master/src/base/table.lua --- ----@param t Table ----@returns list of keys +---@param t table Table +---@return table List of keys function vim.tbl_keys(t) - assert(type(t) == 'table', string.format("Expected table, got %s", type(t))) + assert(type(t) == 'table', string.format('Expected table, got %s', type(t))) local keys = {} for k, _ in pairs(t) do @@ -167,10 +171,10 @@ end --- Return a list of all values used in a table. --- However, the order of the return table of values is not guaranteed. --- ----@param t Table ----@returns list of values +---@param t table Table +---@return table List of values function vim.tbl_values(t) - assert(type(t) == 'table', string.format("Expected table, got %s", type(t))) + assert(type(t) == 'table', string.format('Expected table, got %s', type(t))) local values = {} for _, v in pairs(t) do @@ -181,10 +185,11 @@ end --- Apply a function to all values of a table. --- ----@param func function or callable table ----@param t table +---@param func function|table Function or callable table +---@param t table Table +---@return table Table of transformed values function vim.tbl_map(func, t) - vim.validate{func={func,'c'},t={t,'t'}} + vim.validate({ func = { func, 'c' }, t = { t, 't' } }) local rettab = {} for k, v in pairs(t) do @@ -195,10 +200,11 @@ end --- Filter a table using a predicate function --- ----@param func function or callable table ----@param t table +---@param func function|table Function or callable table +---@param t table Table +---@return table Table of filtered values function vim.tbl_filter(func, t) - vim.validate{func={func,'c'},t={t,'t'}} + vim.validate({ func = { func, 'c' }, t = { t, 't' } }) local rettab = {} for _, entry in pairs(t) do @@ -211,13 +217,13 @@ end --- Checks if a list-like (vector) table contains `value`. --- ----@param t Table to check ----@param value Value to compare ----@returns true if `t` contains `value` +---@param t table Table to check +---@param value any Value to compare +---@return boolean `true` if `t` contains `value` function vim.tbl_contains(t, value) - vim.validate{t={t,'t'}} + vim.validate({ t = { t, 't' } }) - for _,v in ipairs(t) do + for _, v in ipairs(t) do if v == value then return true end @@ -229,25 +235,30 @@ end --- ---@see https://github.com/premake/premake-core/blob/master/src/base/table.lua --- ----@param t Table to check +---@param t table Table to check +---@return boolean `true` if `t` is empty function vim.tbl_isempty(t) - assert(type(t) == 'table', string.format("Expected table, got %s", type(t))) + assert(type(t) == 'table', string.format('Expected table, got %s', type(t))) return next(t) == nil end ---- we only merge empty tables or tables that are not a list +--- We only merge empty tables or tables that are not a list ---@private local function can_merge(v) - return type(v) == "table" and (vim.tbl_isempty(v) or not vim.tbl_islist(v)) + return type(v) == 'table' and (vim.tbl_isempty(v) or not vim.tbl_islist(v)) end local function tbl_extend(behavior, deep_extend, ...) - if (behavior ~= 'error' and behavior ~= 'keep' and behavior ~= 'force') then - error('invalid "behavior": '..tostring(behavior)) + if behavior ~= 'error' and behavior ~= 'keep' and behavior ~= 'force' then + error('invalid "behavior": ' .. tostring(behavior)) end if select('#', ...) < 2 then - error('wrong number of arguments (given '..tostring(1 + select('#', ...))..', expected at least 3)') + error( + 'wrong number of arguments (given ' + .. tostring(1 + select('#', ...)) + .. ', expected at least 3)' + ) end local ret = {} @@ -257,15 +268,15 @@ local function tbl_extend(behavior, deep_extend, ...) for i = 1, select('#', ...) do local tbl = select(i, ...) - vim.validate{["after the second argument"] = {tbl,'t'}} + vim.validate({ ['after the second argument'] = { tbl, 't' } }) if tbl then for k, v in pairs(tbl) do if deep_extend and can_merge(v) and can_merge(ret[k]) then ret[k] = tbl_extend(behavior, true, ret[k], v) elseif behavior ~= 'force' and ret[k] ~= nil then if behavior == 'error' then - error('key found in more than one map: '..k) - end -- Else behavior is "keep". + error('key found in more than one map: ' .. k) + end -- Else behavior is "keep". else ret[k] = v end @@ -279,11 +290,12 @@ end --- ---@see |extend()| --- ----@param behavior Decides what to do if a key is found in more than one map: +---@param behavior string Decides what to do if a key is found in more than one map: --- - "error": raise an error --- - "keep": use value from the leftmost map --- - "force": use value from the rightmost map ----@param ... Two or more map-like tables. +---@param ... table Two or more map-like tables +---@return table Merged table function vim.tbl_extend(behavior, ...) return tbl_extend(behavior, false, ...) end @@ -292,25 +304,30 @@ end --- ---@see |tbl_extend()| --- ----@param behavior Decides what to do if a key is found in more than one map: +---@param behavior string Decides what to do if a key is found in more than one map: --- - "error": raise an error --- - "keep": use value from the leftmost map --- - "force": use value from the rightmost map ----@param ... Two or more map-like tables. +---@param ... table Two or more map-like tables +---@return table Merged table function vim.tbl_deep_extend(behavior, ...) return tbl_extend(behavior, true, ...) end --- Deep compare values for equality --- ---- Tables are compared recursively unless they both provide the `eq` methamethod. +--- Tables are compared recursively unless they both provide the `eq` metamethod. --- All other types are compared using the equality `==` operator. ----@param a first value ----@param b second value ----@returns `true` if values are equals, else `false`. +---@param a any First value +---@param b any Second value +---@return boolean `true` if values are equals, else `false` function vim.deep_equal(a, b) - if a == b then return true end - if type(a) ~= type(b) then return false end + if a == b then + return true + end + if type(a) ~= type(b) then + return false + end if type(a) == 'table' then for k, v in pairs(a) do if not vim.deep_equal(v, b[k]) then @@ -329,40 +346,77 @@ end --- Add the reverse lookup values to an existing table. --- For example: ---- `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. +--- ``tbl_add_reverse_lookup { A = 1 } == { [1] = 'A', A = 1 }`` +--- +--- Note that this *modifies* the input. +---@param o table Table to add the reverse to +---@return table o function vim.tbl_add_reverse_lookup(o) local keys = vim.tbl_keys(o) for _, k in ipairs(keys) do local v = o[k] if o[v] then - error(string.format("The reverse lookup found an existing value for %q while processing key %q", tostring(v), tostring(k))) + error( + string.format( + 'The reverse lookup found an existing value for %q while processing key %q', + tostring(v), + tostring(k) + ) + ) end o[v] = k end 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 Table to index +---@param ... string Optional strings (0 or more, variadic) via which to index the table +--- +---@return any Nested value indexed by key (if it exists), else nil +function vim.tbl_get(o, ...) + local keys = { ... } + if #keys == 0 then + return + end + for i, k in ipairs(keys) do + if type(o[k]) ~= 'table' and next(keys, i) then + return nil + end + o = o[k] + if o == nil then + return + end + end + return o +end + --- Extends a list-like table with the values of another list-like table. --- --- NOTE: This mutates dst! --- ---@see |vim.tbl_extend()| --- ----@param dst list which will be modified and appended to. ----@param src list from which values will be inserted. ----@param start Start index on src. defaults to 1 ----@param finish Final index on src. defaults to #src ----@returns dst +---@param dst table List which will be modified and appended to +---@param src table List from which values will be inserted +---@param start number Start index on src. Defaults to 1 +---@param finish number Final index on src. Defaults to `#src` +---@return table dst function vim.list_extend(dst, src, start, finish) - vim.validate { - dst = {dst, 't'}; - src = {src, 't'}; - start = {start, 'n', true}; - finish = {finish, 'n', true}; - } + vim.validate({ + dst = { dst, 't' }, + src = { src, 't' }, + start = { start, 'n', true }, + finish = { finish, 'n', true }, + }) for i = start or 1, finish or #src do table.insert(dst, src[i]) end @@ -374,15 +428,15 @@ end --- ---@see From https://github.com/premake/premake-core/blob/master/src/base/table.lua --- ----@param t List-like table ----@returns Flattened copy of the given list-like table. +---@param t table List-like table +---@return table Flattened copy of the given list-like table function vim.tbl_flatten(t) local result = {} local function _tbl_flatten(_t) local n = #_t for i = 1, n do local v = _t[i] - if type(v) == "table" then + if type(v) == 'table' then _tbl_flatten(v) elseif v then table.insert(result, v) @@ -399,8 +453,8 @@ end --- |vim.empty_dict()| or returned as a dict-like |API| or Vimscript result, --- for example from |rpcrequest()| or |vim.fn|. --- ----@param t Table ----@returns `true` if array-like table, else `false`. +---@param t table Table +---@return boolean `true` if array-like table, else `false` function vim.tbl_islist(t) if type(t) ~= 'table' then return false @@ -409,7 +463,7 @@ function vim.tbl_islist(t) local count = 0 for k, _ in pairs(t) do - if type(k) == "number" then + if type(k) == 'number' then count = count + 1 else return false @@ -436,26 +490,28 @@ end --- </pre> --- ---@see https://github.com/Tieske/Penlight/blob/master/lua/pl/tablex.lua ----@param t Table ----@returns Number that is the number of the value in table +---@param t table Table +---@return number Number of non-nil values in table function vim.tbl_count(t) - vim.validate{t={t,'t'}} + vim.validate({ t = { t, 't' } }) local count = 0 - for _ in pairs(t) do count = count + 1 end + for _ in pairs(t) do + count = count + 1 + end return count end --- Creates a copy of a table containing only elements from start to end (inclusive) --- ----@param list table table ----@param start integer Start range of slice ----@param finish integer End range of slice ----@returns Copy of table sliced from start to finish (inclusive) +---@param list table Table +---@param start number Start range of slice +---@param finish number End range of slice +---@return table Copy of table sliced from start to finish (inclusive) function vim.list_slice(list, start, finish) local new_list = {} for i = start or 1, finish or #list do - new_list[#new_list+1] = list[i] + new_list[#new_list + 1] = list[i] end return new_list end @@ -463,40 +519,40 @@ end --- Trim whitespace (Lua pattern "%s") from both sides of a string. --- ---@see https://www.lua.org/pil/20.2.html ----@param s String to trim ----@returns String with whitespace removed from its beginning and end +---@param s string String to trim +---@return string String with whitespace removed from its beginning and end function vim.trim(s) - vim.validate{s={s,'s'}} + vim.validate({ s = { s, 's' } }) return s:match('^%s*(.*%S)') or '' end --- Escapes magic chars in a Lua pattern. --- ---@see https://github.com/rxi/lume ----@param s String to escape ----@returns %-escaped pattern string +---@param s string String to escape +---@return string %-escaped pattern string function vim.pesc(s) - vim.validate{s={s,'s'}} + vim.validate({ s = { s, 's' } }) return s:gsub('[%(%)%.%%%+%-%*%?%[%]%^%$]', '%%%1') end --- Tests if `s` starts with `prefix`. --- ----@param s (string) a string ----@param prefix (string) a prefix ----@return (boolean) true if `prefix` is a prefix of s +---@param s string String +---@param prefix string Prefix to match +---@return boolean `true` if `prefix` is a prefix of `s` function vim.startswith(s, prefix) - vim.validate { s = {s, 's'}; prefix = {prefix, 's'}; } + vim.validate({ s = { s, 's' }, prefix = { prefix, 's' } }) return s:sub(1, #prefix) == prefix end --- Tests if `s` ends with `suffix`. --- ----@param s (string) a string ----@param suffix (string) a suffix ----@return (boolean) true if `suffix` is a suffix of s +---@param s string String +---@param suffix string Suffix to match +---@return boolean `true` if `suffix` is a suffix of `s` function vim.endswith(s, suffix) - vim.validate { s = {s, 's'}; suffix = {suffix, 's'}; } + vim.validate({ s = { s, 's' }, suffix = { suffix, 's' } }) return #suffix == 0 or s:sub(-#suffix) == suffix end @@ -536,7 +592,7 @@ end --- --- </pre> --- ----@param opt table of parameter names to validations. Each key is a parameter +---@param opt table Names of parameters to validate. 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 @@ -550,18 +606,24 @@ end --- only if the argument is valid. Can optionally return an additional --- informative error message as the second returned value. --- - msg: (optional) error string if validation fails -function vim.validate(opt) end -- luacheck: no unused +function vim.validate(opt) end -- luacheck: no unused do local type_names = { - ['table'] = 'table', t = 'table', - ['string'] = 'string', s = 'string', - ['number'] = 'number', n = 'number', - ['boolean'] = 'boolean', b = 'boolean', - ['function'] = 'function', f = 'function', - ['callable'] = 'callable', c = 'callable', - ['nil'] = 'nil', - ['thread'] = 'thread', + ['table'] = 'table', + t = 'table', + ['string'] = 'string', + s = 'string', + ['number'] = 'number', + n = 'number', + ['boolean'] = 'boolean', + b = 'boolean', + ['function'] = 'function', + f = 'function', + ['callable'] = 'callable', + c = 'callable', + ['nil'] = 'nil', + ['thread'] = 'thread', ['userdata'] = 'userdata', } @@ -580,21 +642,22 @@ do return false, string.format('opt[%s]: expected table, got %s', param_name, type(spec)) end - local val = spec[1] -- Argument value. - local types = spec[2] -- Type name, or callable. + local val = spec[1] -- Argument value + local types = spec[2] -- Type name, or callable local optional = (true == spec[3]) if type(types) == 'string' then - types = {types} + types = { types } end if vim.is_callable(types) then - -- Check user-provided validation function. + -- Check user-provided validation function 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 '?'), tostring(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) + error_message = error_message .. string.format('. Info: %s', optional_message) end return false, error_message @@ -614,10 +677,16 @@ do end end if not success then - return false, string.format("%s: expected %s, got %s", param_name, table.concat(types, '|'), type(val)) + 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(types)) + return false, string.format('invalid type name: %s', tostring(types)) end end @@ -633,12 +702,16 @@ do end --- Returns true if object `f` can be called as a function. --- ----@param f Any object ----@return true if `f` is callable, else false +---@param f any Any object +---@return boolean `true` if `f` is callable, else `false` function vim.is_callable(f) - if type(f) == 'function' then return true end + if type(f) == 'function' then + return true + end local m = getmetatable(f) - if m == nil then return false end + if m == nil then + return false + end return type(m.__call) == 'function' end diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index 07f6418c0c..70f2c425ed 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -1,31 +1,32 @@ local a = vim.api -local query = require'vim.treesitter.query' -local language = require'vim.treesitter.language' -local LanguageTree = require'vim.treesitter.languagetree' +local query = require('vim.treesitter.query') +local language = require('vim.treesitter.language') +local LanguageTree = require('vim.treesitter.languagetree') -- TODO(bfredl): currently we retain parsers for the lifetime of the buffer. -- Consider use weak references to release parser if all plugins are done with -- it. local parsers = {} -local M = vim.tbl_extend("error", query, language) +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) - if k == "highlighter" then - t[k] = require'vim.treesitter.highlighter' - return t[k] - elseif k == "language" then - t[k] = require"vim.treesitter.language" - return t[k] - elseif k == "query" then - t[k] = require"vim.treesitter.query" - return t[k] - end - end - }) + __index = function(t, k) + if k == 'highlighter' then + t[k] = require('vim.treesitter.highlighter') + return t[k] + elseif k == 'language' then + t[k] = require('vim.treesitter.language') + return t[k] + elseif k == 'query' then + t[k] = require('vim.treesitter.query') + return t[k] + end + end, +}) --- Creates a new parser. --- @@ -62,7 +63,11 @@ function M._create_parser(bufnr, lang, opts) self:_on_reload(...) end - a.nvim_buf_attach(self:source(), false, {on_bytes=bytes_cb, on_detach=detach_cb, on_reload=reload_cb, preview=true}) + a.nvim_buf_attach( + self:source(), + false, + { on_bytes = bytes_cb, on_detach = detach_cb, on_reload = reload_cb, preview = true } + ) self:parse() @@ -86,10 +91,10 @@ function M.get_parser(bufnr, lang, opts) bufnr = a.nvim_get_current_buf() end if lang == nil then - lang = a.nvim_buf_get_option(bufnr, "filetype") + lang = a.nvim_buf_get_option(bufnr, 'filetype') end - if parsers[bufnr] == nil then + if parsers[bufnr] == nil or parsers[bufnr]:lang() ~= lang then parsers[bufnr] = M._create_parser(bufnr, lang, opts) end @@ -104,10 +109,10 @@ end ---@param lang The language of this string ---@param opts Options to pass to the created language tree function M.get_string_parser(str, lang, opts) - vim.validate { + vim.validate({ str = { str, 'string' }, - lang = { lang, 'string' } - } + lang = { lang, 'string' }, + }) language.require_language(lang) return LanguageTree.new(str, lang, opts) diff --git a/runtime/lua/vim/treesitter/health.lua b/runtime/lua/vim/treesitter/health.lua index 53ccc6e88d..3bd59ca282 100644 --- a/runtime/lua/vim/treesitter/health.lua +++ b/runtime/lua/vim/treesitter/health.lua @@ -15,24 +15,24 @@ function M.check() local report_error = vim.fn['health#report_error'] local parsers = M.list_parsers() - report_info(string.format("Runtime ABI version : %d", ts.language_version)) + report_info(string.format('Runtime ABI version : %d', ts.language_version)) for _, parser in pairs(parsers) do - local parsername = vim.fn.fnamemodify(parser, ":t:r") + local parsername = vim.fn.fnamemodify(parser, ':t:r') local is_loadable, ret = pcall(ts.language.require_language, parsername) if not is_loadable then - report_error(string.format("Impossible to load parser for %s: %s", parsername, ret)) + report_error(string.format('Impossible to load parser for %s: %s', parsername, ret)) elseif ret then local lang = ts.language.inspect_language(parsername) - report_ok(string.format("Loaded parser for %s: ABI version %d", - parsername, lang._abi_version)) + report_ok( + string.format('Loaded parser for %s: ABI version %d', parsername, lang._abi_version) + ) else - report_error(string.format("Unable to load parser for %s", parsername)) + report_error(string.format('Unable to load parser for %s', parsername)) end end end return M - diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index 22b528838c..e27a5fa9c3 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -1,5 +1,5 @@ local a = vim.api -local query = require"vim.treesitter.query" +local query = require('vim.treesitter.query') -- support reload for quick experimentation local TSHighlighter = rawget(vim.treesitter, 'TSHighlighter') or {} @@ -10,64 +10,88 @@ TSHighlighter.active = TSHighlighter.active or {} local TSHighlighterQuery = {} TSHighlighterQuery.__index = TSHighlighterQuery -local ns = a.nvim_create_namespace("treesitter/highlighter") +local ns = a.nvim_create_namespace('treesitter/highlighter') local _default_highlights = {} local _link_default_highlight_once = function(from, to) if not _default_highlights[from] then _default_highlights[from] = true - vim.cmd(string.format("highlight default link %s %s", from, to)) + a.nvim_set_hl(0, from, { link = to, default = true }) end return from end -TSHighlighter.hl_map = { - ["error"] = "Error", - --- Miscs - ["comment"] = "Comment", - ["punctuation.delimiter"] = "Delimiter", - ["punctuation.bracket"] = "Delimiter", - ["punctuation.special"] = "Delimiter", - --- Constants - ["constant"] = "Constant", - ["constant.builtin"] = "Special", - ["constant.macro"] = "Define", - ["string"] = "String", - ["string.regex"] = "String", - ["string.escape"] = "SpecialChar", - ["character"] = "Character", - ["number"] = "Number", - ["boolean"] = "Boolean", - ["float"] = "Float", - --- Functions - ["function"] = "Function", - ["function.special"] = "Function", - ["function.builtin"] = "Special", - ["function.macro"] = "Macro", - ["parameter"] = "Identifier", - ["method"] = "Function", - ["field"] = "Identifier", - ["property"] = "Identifier", - ["constructor"] = "Special", - --- Keywords - ["conditional"] = "Conditional", - ["repeat"] = "Repeat", - ["label"] = "Label", - ["operator"] = "Operator", - ["keyword"] = "Keyword", - ["exception"] = "Exception", - - ["type"] = "Type", - ["type.builtin"] = "Type", - ["structure"] = "Structure", - ["include"] = "Include", +-- 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', + ['punctuation.delimiter'] = 'Delimiter', + ['punctuation.bracket'] = 'Delimiter', + ['punctuation.special'] = 'Delimiter', + + -- Constants + ['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', + + -- Functions + ['function'] = 'Function', + ['function.special'] = 'Function', + ['function.builtin'] = 'Special', + ['function.macro'] = 'Macro', + ['parameter'] = 'Identifier', + ['method'] = 'Function', + ['field'] = 'Identifier', + ['property'] = 'Identifier', + ['constructor'] = 'Special', + + -- Keywords + ['conditional'] = 'Conditional', + ['repeat'] = 'Repeat', + ['label'] = 'Label', + ['operator'] = 'Operator', + ['keyword'] = 'Keyword', + ['exception'] = 'Exception', + + ['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) local firstc = string.sub(capture_name, 1, 1) @@ -89,13 +113,13 @@ function TSHighlighterQuery.new(lang, query_string) rawset(table, capture, id) return id - end + end, }) if query_string then self._query = query.parse_query(lang, query_string) else - self._query = query.get_query(lang, "highlights") + self._query = query.get_query(lang, 'highlights') end return self @@ -128,17 +152,23 @@ end function TSHighlighter.new(tree, opts) local self = setmetatable({}, TSHighlighter) - if type(tree:source()) ~= "number" then - error("TSHighlighter can not be used with a string parser source.") + if type(tree:source()) ~= 'number' then + error('TSHighlighter can not be used with a string parser source.') end opts = opts or {} self.tree = tree - tree:register_cbs { - on_changedtree = function(...) self:on_changedtree(...) end; - on_bytes = function(...) self:on_bytes(...) end; - on_detach = function(...) self:on_detach(...) end; - } + tree:register_cbs({ + on_changedtree = function(...) + self:on_changedtree(...) + end, + on_bytes = function(...) + self:on_bytes(...) + end, + on_detach = function(...) + self:on_detach(...) + end, + }) self.bufnr = tree:source() self.edit_count = 0 @@ -157,7 +187,7 @@ function TSHighlighter.new(tree, opts) end end - a.nvim_buf_set_option(self.bufnr, "syntax", "") + a.nvim_buf_set_option(self.bufnr, 'syntax', '') TSHighlighter.active[self.bufnr] = self @@ -166,7 +196,7 @@ function TSHighlighter.new(tree, opts) -- syntax FileType autocmds. Later on we should integrate with the -- `:syntax` and `set syntax=...` machinery properly. if vim.g.syntax_on ~= 1 then - vim.api.nvim_command("runtime! syntax/synload.vim") + vim.api.nvim_command('runtime! syntax/synload.vim') end self.tree:parse() @@ -186,7 +216,7 @@ function TSHighlighter:get_highlight_state(tstree) if not self._highlight_states[tstree] then self._highlight_states[tstree] = { next_row = 0, - iter = nil + iter = nil, } end @@ -211,7 +241,7 @@ end ---@private function TSHighlighter:on_changedtree(changes) for _, ch in ipairs(changes or {}) do - a.nvim__buf_redraw_range(self.bufnr, ch[1], ch[3]+1) + a.nvim__buf_redraw_range(self.bufnr, ch[1], ch[3] + 1) end end @@ -229,39 +259,50 @@ end ---@private local function on_line_impl(self, buf, line) self.tree:for_each_tree(function(tstree, tree) - if not tstree then return end + if not tstree then + return + end local root_node = tstree:root() local root_start_row, _, root_end_row, _ = root_node:range() -- Only worry about trees within the line range - if root_start_row > line or root_end_row < line then return end + if root_start_row > line or root_end_row < line then + return + end local state = self:get_highlight_state(tstree) local highlighter_query = self:get_query(tree:lang()) -- Some injected languages may not have highlight queries. - if not highlighter_query:query() then return end + if not highlighter_query:query() then + return + end - if state.iter == nil then - state.iter = highlighter_query:query():iter_captures(root_node, self.bufnr, line, root_end_row + 1) + if state.iter == nil or state.next_row < line then + state.iter = + highlighter_query:query():iter_captures(root_node, self.bufnr, line, root_end_row + 1) end while line >= state.next_row do local capture, node, metadata = state.iter() - if capture == nil then break end + if capture == nil then + break + end local start_row, start_col, end_row, end_col = node:range() local hl = highlighter_query.hl_cache[capture] if hl and end_row >= line then - a.nvim_buf_set_extmark(buf, ns, start_row, start_col, - { end_line = end_row, end_col = end_col, - hl_group = hl, - ephemeral = true, - priority = tonumber(metadata.priority) or 100 -- Low but leaves room below - }) + a.nvim_buf_set_extmark(buf, ns, start_row, start_col, { + end_line = end_row, + end_col = end_col, + hl_group = hl, + ephemeral = true, + priority = tonumber(metadata.priority) or 100, -- Low but leaves room below + conceal = metadata.conceal, + }) end if start_row > line then state.next_row = start_row @@ -273,7 +314,9 @@ end ---@private function TSHighlighter._on_line(_, _win, buf, line, _) local self = TSHighlighter.active[buf] - if not self then return end + if not self then + return + end on_line_impl(self, buf, line) end @@ -299,9 +342,9 @@ function TSHighlighter._on_win(_, _win, buf, _topline) end a.nvim_set_decoration_provider(ns, { - on_buf = TSHighlighter._on_buf; - on_win = TSHighlighter._on_win; - on_line = TSHighlighter._on_line; + on_buf = TSHighlighter._on_buf, + on_win = TSHighlighter._on_win, + on_line = TSHighlighter._on_line, }) return TSHighlighter diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua index 6f347ff25f..dfb6f5be84 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 @@ -22,13 +22,15 @@ function M.require_language(lang, path, silent) end -- TODO(bfredl): help tag? - error("no parser for '"..lang.."' language, see :help treesitter-parsers") + error("no parser for '" .. lang .. "' language, see :help treesitter-parsers") end path = paths[1] end if silent then - return pcall(function() vim._ts_add_language(path, lang) end) + return pcall(function() + vim._ts_add_language(path, lang) + end) else vim._ts_add_language(path, lang) end diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 85fd5cd8e0..4d3b0631a2 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -1,6 +1,6 @@ local a = vim.api -local query = require'vim.treesitter.query' -local language = require'vim.treesitter.language' +local query = require('vim.treesitter.query') +local language = require('vim.treesitter.language') local LanguageTree = {} LanguageTree.__index = LanguageTree @@ -32,9 +32,8 @@ function LanguageTree.new(source, lang, opts) _regions = {}, _trees = {}, _opts = opts, - _injection_query = injections[lang] - and query.parse_query(lang, injections[lang]) - or query.get_query(lang, "injections"), + _injection_query = injections[lang] and query.parse_query(lang, injections[lang]) + or query.get_query(lang, 'injections'), _valid = false, _parser = vim._create_ts_parser(lang), _callbacks = { @@ -42,11 +41,10 @@ function LanguageTree.new(source, lang, opts) bytes = {}, detach = {}, child_added = {}, - child_removed = {} + child_removed = {}, }, }, LanguageTree) - return self end @@ -76,8 +74,8 @@ function LanguageTree:lang() end --- Determines whether this tree is valid. ---- If the tree is invalid, `parse()` must be called ---- to get the 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 +232,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 @@ -259,22 +259,27 @@ end --- --- Note, this call invalidates the tree and requires it to be parsed again. --- ----@param regions A list of regions this tree should manage and parse. +---@param regions (table) list of regions this tree should manage and parse. function LanguageTree:set_included_regions(regions) - -- TODO(vigoux): I don't think string parsers are useful for now - if type(self._source) == "number" then - -- Transform the tables from 4 element long to 6 element long (with byte offset) - for _, region in ipairs(regions) do - for i, range in ipairs(region) do - if type(range) == "table" and #range == 4 then - local start_row, start_col, end_row, end_col = unpack(range) + -- Transform the tables from 4 element long to 6 element long (with byte offset) + for _, region in ipairs(regions) do + for i, range in ipairs(region) do + if type(range) == 'table' and #range == 4 then + local start_row, start_col, end_row, end_col = unpack(range) + local start_byte = 0 + local end_byte = 0 + -- TODO(vigoux): proper byte computation here, and account for EOL ? + if type(self._source) == 'number' then -- Easy case, this is a buffer parser - -- TODO(vigoux): proper byte computation here, and account for EOL ? - local start_byte = a.nvim_buf_get_offset(self._source, start_row) + start_col - local end_byte = a.nvim_buf_get_offset(self._source, end_row) + end_col - - region[i] = { start_row, start_col, start_byte, end_row, end_col, end_byte } + start_byte = a.nvim_buf_get_offset(self._source, start_row) + start_col + end_byte = a.nvim_buf_get_offset(self._source, end_row) + end_col + elseif type(self._source) == 'string' then + -- string parser, single `\n` delimited string + start_byte = vim.fn.byteidx(self._source, start_col) + end_byte = vim.fn.byteidx(self._source, end_col) end + + region[i] = { start_row, start_col, start_byte, end_row, end_col, end_byte } end end end @@ -293,6 +298,14 @@ function LanguageTree:included_regions() return self._regions end +---@private +local function get_node_range(node, id, metadata) + if metadata[id] and metadata[id].range then + return metadata[id].range + end + return { node:range() } +end + --- Gets language injection points by language. --- --- This is where most of the injection processing occurs. @@ -301,7 +314,9 @@ end --- instead of using the entire nodes range. ---@private function LanguageTree:_get_injections() - if not self._injection_query then return {} end + if not self._injection_query then + return {} + end local injections = {} @@ -309,7 +324,9 @@ function LanguageTree:_get_injections() local root_node = tree:root() local start_line, _, end_line, _ = root_node:range() - for pattern, match, metadata in self._injection_query:iter_matches(root_node, self._source, start_line, end_line+1) do + for pattern, match, metadata in + self._injection_query:iter_matches(root_node, self._source, start_line, end_line + 1) + do local lang = nil local ranges = {} local combined = metadata.combined @@ -320,11 +337,11 @@ function LanguageTree:_get_injections() local content = metadata.content -- Allow for captured nodes to be used - if type(content) == "number" then - content = {match[content]} + if type(content) == 'number' then + content = { match[content]:range() } end - if content then + if type(content) == 'table' and #content >= 4 then vim.list_extend(ranges, content) end end @@ -340,21 +357,21 @@ function LanguageTree:_get_injections() local name = self._injection_query.captures[id] -- Lang should override any other language tag - if name == "language" and not lang then + if name == 'language' and not lang then lang = query.get_node_text(node, self._source) - elseif name == "combined" then + elseif name == 'combined' then combined = true - elseif name == "content" and #ranges == 0 then - table.insert(ranges, node) - -- Ignore any tags that start with "_" - -- Allows for other tags to be used in matches - elseif string.sub(name, 1, 1) ~= "_" then + elseif name == 'content' and #ranges == 0 then + table.insert(ranges, get_node_range(node, id, metadata)) + -- Ignore any tags that start with "_" + -- Allows for other tags to be used in matches + elseif string.sub(name, 1, 1) ~= '_' then if not lang then lang = name end if #ranges == 0 then - table.insert(ranges, node) + table.insert(ranges, get_node_range(node, id, metadata)) end end end @@ -391,7 +408,10 @@ function LanguageTree:_get_injections() for _, entry in pairs(patterns) do if entry.combined then - table.insert(result[lang], vim.tbl_flatten(entry.regions)) + local regions = vim.tbl_map(function(e) + return vim.tbl_flatten(e) + end, entry.regions) + table.insert(result[lang], regions) else for _, ranges in ipairs(entry.regions) do table.insert(result[lang], ranges) @@ -412,10 +432,19 @@ function LanguageTree:_do_callback(cb_name, ...) end ---@private -function LanguageTree:_on_bytes(bufnr, changed_tick, - start_row, start_col, start_byte, - old_row, old_col, old_byte, - new_row, new_col, new_byte) +function LanguageTree:_on_bytes( + bufnr, + changed_tick, + start_row, + start_col, + start_byte, + old_row, + old_col, + old_byte, + new_row, + new_col, + new_byte +) self:invalidate() local old_end_col = old_col + ((old_row == 0) and start_col or 0) @@ -424,16 +453,33 @@ function LanguageTree:_on_bytes(bufnr, changed_tick, -- Edit all trees recursively, together BEFORE emitting a bytes callback. -- In most cases this callback should only be called from the root tree. self:for_each_tree(function(tree) - tree:edit(start_byte,start_byte+old_byte,start_byte+new_byte, - start_row, start_col, - start_row+old_row, old_end_col, - start_row+new_row, new_end_col) + tree:edit( + start_byte, + start_byte + old_byte, + start_byte + new_byte, + start_row, + start_col, + start_row + old_row, + old_end_col, + start_row + new_row, + new_end_col + ) end) - self:_do_callback('bytes', bufnr, changed_tick, - start_row, start_col, start_byte, - old_row, old_col, old_byte, - new_row, new_col, new_byte) + self:_do_callback( + 'bytes', + bufnr, + changed_tick, + start_row, + start_col, + start_byte, + old_row, + old_col, + old_byte, + new_row, + new_col, + new_byte + ) end ---@private @@ -441,23 +487,24 @@ function LanguageTree:_on_reload() self:invalidate(true) end - ---@private function LanguageTree:_on_detach(...) self:invalidate(true) 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 + if not cbs then + return + end if cbs.on_changedtree then table.insert(self._callbacks.changedtree, cbs.on_changedtree) @@ -486,16 +533,10 @@ local function tree_contains(tree, range) local start_fits = start_row < range[1] or (start_row == range[1] and start_col <= range[2]) local end_fits = end_row > range[3] or (end_row == range[3] and end_col >= range[4]) - if start_fits and end_fits then - return true - end - - return false + return start_fits and end_fits end ---- Determines whether @param range is contained in this language tree ---- ---- This goes down the tree to recursively check children. +--- Determines whether {range} is contained in this language tree --- ---@param range A range, that is a `{ start_line, start_col, end_line, end_col }` table. function LanguageTree:contains(range) @@ -508,7 +549,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 ebed502c92..103e85abfd 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -1,5 +1,5 @@ local a = vim.api -local language = require'vim.treesitter.language' +local language = require('vim.treesitter.language') -- query: pattern matching on trees -- predicate matching is implemented in lua @@ -43,7 +43,9 @@ function M.get_query_files(lang, query_name, is_included) local query_path = string.format('queries/%s/%s.scm', lang, query_name) local lang_files = dedupe_files(a.nvim_get_runtime_file(query_path, true)) - if #lang_files == 0 then return {} end + if #lang_files == 0 then + return {} + end local base_langs = {} @@ -52,7 +54,7 @@ function M.get_query_files(lang, query_name, is_included) -- ;+ inherits: ({language},)*{language} -- -- {language} ::= {lang} | ({lang}) - local MODELINE_FORMAT = "^;+%s*inherits%s*:?%s*([a-z_,()]+)%s*$" + local MODELINE_FORMAT = '^;+%s*inherits%s*:?%s*([a-z_,()]+)%s*$' for _, file in ipairs(lang_files) do local modeline = safe_read(file, '*l') @@ -62,7 +64,7 @@ function M.get_query_files(lang, query_name, is_included) if langlist then for _, incllang in ipairs(vim.split(langlist, ',', true)) do - local is_optional = incllang:match("%(.*%)") + local is_optional = incllang:match('%(.*%)') if is_optional then if not is_included then @@ -90,7 +92,7 @@ end local function read_query_files(filenames) local contents = {} - for _,filename in ipairs(filenames) do + for _, filename in ipairs(filenames) do table.insert(contents, safe_read(filename, '*a')) end @@ -138,30 +140,43 @@ 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 --- Gets the text corresponding to a given node @@ -172,7 +187,7 @@ 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 type(source) == 'number' then local lines local eof_row = a.nvim_buf_line_count(source) if start_row >= eof_row then @@ -186,56 +201,64 @@ function M.get_node_text(node, source) lines = a.nvim_buf_get_lines(source, start_row, end_row + 1, true) end - 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) + 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) + return table.concat(lines, '\n') + elseif type(source) == 'string' then + return source:sub(start_byte + 1, end_byte) end end -- Predicate handler receive the following arguments -- (match, pattern, bufnr, predicate) local predicate_handlers = { - ["eq?"] = function(match, _, source, predicate) - local node = match[predicate[2]] - local node_text = M.get_node_text(node, source) - - local str - if type(predicate[3]) == "string" then - -- (#eq? @aa "foo") - str = predicate[3] - else - -- (#eq? @aa @bb) - str = M.get_node_text(match[predicate[3]], source) - end + ['eq?'] = function(match, _, source, predicate) + local node = match[predicate[2]] + if not node then + return true + end + local node_text = M.get_node_text(node, source) - if node_text ~= str or str == nil then - return false - end + local str + if type(predicate[3]) == 'string' then + -- (#eq? @aa "foo") + str = predicate[3] + else + -- (#eq? @aa @bb) + str = M.get_node_text(match[predicate[3]], source) + end - return true + if node_text ~= str or str == nil then + return false + end + + return true end, - ["lua-match?"] = function(match, _, source, predicate) - local node = match[predicate[2]] - local regex = predicate[3] - return string.find(M.get_node_text(node, source), regex) + ['lua-match?'] = function(match, _, source, predicate) + local node = match[predicate[2]] + if not node then + return true + end + local regex = predicate[3] + return string.find(M.get_node_text(node, source), regex) end, - ["match?"] = (function() - local magic_prefixes = {['\\v']=true, ['\\m']=true, ['\\M']=true, ['\\V']=true} + ['match?'] = (function() + local magic_prefixes = { ['\\v'] = true, ['\\m'] = true, ['\\M'] = true, ['\\V'] = true } ---@private local function check_magic(str) - if string.len(str) < 2 or magic_prefixes[string.sub(str,1,2)] then + if string.len(str) < 2 or magic_prefixes[string.sub(str, 1, 2)] then return str end - return '\\v'..str + return '\\v' .. str end local compiled_vim_regexes = setmetatable({}, { @@ -243,21 +266,27 @@ local predicate_handlers = { local res = vim.regex(check_magic(pattern)) rawset(t, pattern, res) return res - end + end, }) return function(match, _, source, pred) local node = match[pred[2]] + if not node then + return true + end local regex = compiled_vim_regexes[pred[3]] return regex:match_str(M.get_node_text(node, source)) end end)(), - ["contains?"] = function(match, _, source, predicate) + ['contains?'] = function(match, _, source, predicate) local node = match[predicate[2]] + if not node then + return true + end local node_text = M.get_node_text(node, source) - for i=3,#predicate do + for i = 3, #predicate do if string.find(node_text, predicate[i], 1, true) then return true end @@ -266,19 +295,22 @@ local predicate_handlers = { return false end, - ["any-of?"] = function(match, _, source, predicate) + ['any-of?'] = function(match, _, source, predicate) local node = match[predicate[2]] + if not node then + return true + end local node_text = M.get_node_text(node, source) -- Since 'predicate' will not be used by callers of this function, use it -- to store a string set built from the list of words to check against. - local string_set = predicate["string_set"] + local string_set = predicate['string_set'] if not string_set then string_set = {} - for i=3,#predicate do + for i = 3, #predicate do string_set[predicate[i]] = true end - predicate["string_set"] = string_set + predicate['string_set'] = string_set end return string_set[node_text] @@ -286,32 +318,33 @@ local predicate_handlers = { } -- As we provide lua-match? also expose vim-match? -predicate_handlers["vim-match?"] = predicate_handlers["match?"] - +predicate_handlers['vim-match?'] = predicate_handlers['match?'] -- Directives store metadata or perform side effects against a match. -- Directives should always end with a `!`. -- Directive handler receive the following arguments -- (match, pattern, bufnr, predicate, metadata) local directive_handlers = { - ["set!"] = function(_, _, _, pred, metadata) + ['set!'] = function(_, _, _, pred, metadata) if #pred == 4 then -- (#set! @capture "key" "value") - local capture = pred[2] - if not metadata[capture] then - metadata[capture] = {} + local _, capture_id, key, value = unpack(pred) + if not metadata[capture_id] then + metadata[capture_id] = {} end - metadata[capture][pred[3]] = pred[4] + metadata[capture_id][key] = value else + local _, key, value = unpack(pred) -- (#set! "key" "value") - metadata[pred[2]] = pred[3] + metadata[key] = value end end, -- Shifts the range of a node. -- Example: (#offset! @_node 0 1 0 -1) - ["offset!"] = function(match, _, _, pred, metadata) - local offset_node = match[pred[2]] - local range = {offset_node:range()} + ['offset!'] = function(match, _, _, pred, metadata) + local capture_id = pred[2] + local offset_node = match[capture_id] + local range = { offset_node:range() } local start_row_offset = pred[3] or 0 local start_col_offset = pred[4] or 0 local end_row_offset = pred[5] or 0 @@ -324,9 +357,12 @@ local directive_handlers = { -- If this produces an invalid range, we just skip it. if range[1] < range[3] or (range[1] == range[3] and range[2] <= range[4]) then - metadata.content = {range} + if not metadata[capture_id] then + metadata[capture_id] = {} + end + metadata[capture_id].range = range end - end + end, } --- Adds a new predicate to be used in queries @@ -336,7 +372,7 @@ local directive_handlers = { --- signature will be (match, pattern, bufnr, predicate) function M.add_predicate(name, handler, force) if predicate_handlers[name] and not force then - error(string.format("Overriding %s", name)) + error(string.format('Overriding %s', name)) end predicate_handlers[name] = handler @@ -344,17 +380,23 @@ end --- Adds a new directive to be used in queries --- +--- Handlers can set match level data by setting directly on the +--- metadata object `metadata.key = value`, additionally, handlers +--- can set node level data by using the capture id on the +--- metadata table `metadata[capture_id].key = value` +--- ---@param name the name of the directive, without leading # ---@param handler the handler function to be used ---- signature will be (match, pattern, bufnr, predicate) +--- signature will be (match, pattern, bufnr, predicate, metadata) function M.add_directive(name, handler, force) if directive_handlers[name] and not force then - error(string.format("Overriding %s", name)) + error(string.format('Overriding %s', name)) end directive_handlers[name] = handler end +--- Lists the currently available directives to use in queries. ---@return The list of supported directives. function M.list_directives() return vim.tbl_keys(directive_handlers) @@ -372,7 +414,7 @@ end ---@private local function is_directive(name) - return string.sub(name, -1) == "!" + return string.sub(name, -1) == '!' end ---@private @@ -389,7 +431,7 @@ function Query:match_preds(match, pattern, source) -- Skip over directives... they will get processed after all the predicates. if not is_directive(pred[1]) then - if string.sub(pred[1], 1, 4) == "not-" then + if string.sub(pred[1], 1, 4) == 'not-' then pred_name = string.sub(pred[1], 5) is_not = true else @@ -400,7 +442,7 @@ function Query:match_preds(match, pattern, source) local handler = predicate_handlers[pred_name] if not handler then - error(string.format("No handler for %s", pred[1])) + error(string.format('No handler for %s', pred[1])) return false end @@ -423,7 +465,7 @@ function Query:apply_directives(match, pattern, source, metadata) local handler = directive_handlers[pred[1]] if not handler then - error(string.format("No handler for %s", pred[1])) + error(string.format('No handler for %s', pred[1])) return end @@ -432,7 +474,6 @@ function Query:apply_directives(match, pattern, source, metadata) end end - --- Returns the start and stop value if set else the node's range. -- When the node's range is used, the stop is incremented by 1 -- to make the search inclusive. @@ -477,7 +518,7 @@ end ---@returns The matching capture id ---@returns The captured node function Query:iter_captures(node, source, start, stop) - if type(source) == "number" and source == 0 then + if type(source) == 'number' and source == 0 then source = vim.api.nvim_get_current_buf() end @@ -534,7 +575,7 @@ end ---@returns The matching pattern id ---@returns The matching match function Query:iter_matches(node, source, start, stop) - if type(source) == "number" and source == 0 then + if type(source) == 'number' and source == 0 then source = vim.api.nvim_get_current_buf() end diff --git a/runtime/lua/vim/ui.lua b/runtime/lua/vim/ui.lua index 9568b60fd0..6f1ce3089d 100644 --- a/runtime/lua/vim/ui.lua +++ b/runtime/lua/vim/ui.lua @@ -16,15 +16,33 @@ local M = {} --- `items`, or the context in which select() was called. ---@param on_choice function ((item|nil, idx|nil) -> ()) --- Called once the user made a choice. ---- `idx` is the 1-based index of `item` within `item`. +--- `idx` is the 1-based index of `item` within `items`. --- `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 { + vim.validate({ items = { items, 'table', false }, on_choice = { on_choice, 'function', false }, - } + }) opts = opts or {} - local choices = {opts.prompt or 'Select one of:'} + local choices = { opts.prompt or 'Select one of:' } local format_item = opts.format_item or tostring for i, item in pairs(items) do table.insert(choices, string.format('%d: %s', i, format_item(item))) @@ -41,7 +59,7 @@ end --- ---@param opts table Additional options. See |input()| --- - prompt (string|nil) ---- Text of the prompt. Defaults to `Input: `. +--- Text of the prompt --- - default (string|nil) --- Default reply to the input --- - completion (string|nil) @@ -57,12 +75,19 @@ 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 { + vim.validate({ on_confirm = { on_confirm, 'function', false }, - } + }) - opts = opts or {} + opts = (opts and not vim.tbl_isempty(opts)) and opts or vim.empty_dict() local input = vim.fn.input(opts) if #input > 0 then on_confirm(input) diff --git a/runtime/lua/vim/uri.lua b/runtime/lua/vim/uri.lua index 11b661cd1a..d6b0b7410e 100644 --- a/runtime/lua/vim/uri.lua +++ b/runtime/lua/vim/uri.lua @@ -3,7 +3,6 @@ -- https://tools.ietf.org/html/rfc2732 -- https://tools.ietf.org/html/rfc2396 - local uri_decode do local schar = string.char @@ -14,7 +13,7 @@ do return schar(tonumber(hex, 16)) end uri_decode = function(str) - return str:gsub("%%([a-fA-F0-9][a-fA-F0-9])", hex_to_char) + return str:gsub('%%([a-fA-F0-9][a-fA-F0-9])', hex_to_char) end end @@ -23,33 +22,36 @@ do local PATTERNS = { --- RFC 2396 -- https://tools.ietf.org/html/rfc2396#section-2.2 - rfc2396 = "^A-Za-z0-9%-_.!~*'()"; + rfc2396 = "^A-Za-z0-9%-_.!~*'()", --- RFC 2732 -- https://tools.ietf.org/html/rfc2732 - rfc2732 = "^A-Za-z0-9%-_.!~*'()[]"; + rfc2732 = "^A-Za-z0-9%-_.!~*'()[]", --- RFC 3986 -- https://tools.ietf.org/html/rfc3986#section-2.2 - rfc3986 = "^A-Za-z0-9%-._~!$&'()*+,;=:@/"; + rfc3986 = "^A-Za-z0-9%-._~!$&'()*+,;=:@/", } local sbyte, tohex = string.byte if jit then - tohex = require'bit'.tohex + tohex = require('bit').tohex else - tohex = function(b) return string.format("%02x", b) end + tohex = function(b) + return string.format('%02x', b) + end end ---@private local function percent_encode_char(char) - return "%"..tohex(sbyte(char), 2) + return '%' .. tohex(sbyte(char), 2) end uri_encode = function(text, rfc) - if not text then return end + if not text then + return + end local pattern = PATTERNS[rfc] or PATTERNS.rfc3986 - return text:gsub("(["..pattern.."])", percent_encode_char) + return text:gsub('([' .. pattern .. '])', percent_encode_char) end end - ---@private local function is_windows_file_uri(uri) return uri:match('^file:/+[a-zA-Z]:') ~= nil @@ -59,16 +61,16 @@ end ---@param path string Path to file ---@return string URI local function uri_from_fname(path) - local volume_path, fname = path:match("^([a-zA-Z]:)(.*)") + local volume_path, fname = path:match('^([a-zA-Z]:)(.*)') local is_windows = volume_path ~= nil if is_windows then - path = volume_path..uri_encode(fname:gsub("\\", "/")) + path = volume_path .. uri_encode(fname:gsub('\\', '/')) else path = uri_encode(path) end - local uri_parts = {"file://"} + local uri_parts = { 'file://' } if is_windows then - table.insert(uri_parts, "/") + table.insert(uri_parts, '/') end table.insert(uri_parts, path) return table.concat(uri_parts) @@ -82,11 +84,11 @@ local WINDOWS_URI_SCHEME_PATTERN = '^([a-zA-Z]+[a-zA-Z0-9.+-]*):[a-zA-Z]:.*' ---@return string URI local function uri_from_bufnr(bufnr) local fname = vim.api.nvim_buf_get_name(bufnr) - local volume_path = fname:match("^([a-zA-Z]:).*") + local volume_path = fname:match('^([a-zA-Z]:).*') local is_windows = volume_path ~= nil local scheme if is_windows then - fname = fname:gsub("\\", "/") + fname = fname:gsub('\\', '/') scheme = fname:match(WINDOWS_URI_SCHEME_PATTERN) else scheme = fname:match(URI_SCHEME_PATTERN) |