aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua/vim')
-rw-r--r--runtime/lua/vim/F.lua8
-rw-r--r--runtime/lua/vim/_editor.lua50
-rw-r--r--runtime/lua/vim/_init_packages.lua3
-rw-r--r--runtime/lua/vim/_meta.lua869
-rw-r--r--runtime/lua/vim/compat.lua12
-rw-r--r--runtime/lua/vim/diagnostic.lua45
-rw-r--r--runtime/lua/vim/filetype.lua95
-rw-r--r--runtime/lua/vim/filetype/detect.lua13
-rw-r--r--runtime/lua/vim/fs.lua56
-rw-r--r--runtime/lua/vim/highlight.lua2
-rw-r--r--runtime/lua/vim/inspect.lua32
-rw-r--r--runtime/lua/vim/keymap.lua31
-rw-r--r--runtime/lua/vim/lsp.lua118
-rw-r--r--runtime/lua/vim/lsp/buf.lua280
-rw-r--r--runtime/lua/vim/lsp/handlers.lua49
-rw-r--r--runtime/lua/vim/lsp/protocol.lua2
-rw-r--r--runtime/lua/vim/lsp/rpc.lua781
-rw-r--r--runtime/lua/vim/lsp/util.lua174
-rw-r--r--runtime/lua/vim/shared.lua94
-rw-r--r--runtime/lua/vim/treesitter.lua269
-rw-r--r--runtime/lua/vim/treesitter/health.lua2
-rw-r--r--runtime/lua/vim/treesitter/highlighter.lua155
-rw-r--r--runtime/lua/vim/treesitter/language.lua22
-rw-r--r--runtime/lua/vim/treesitter/languagetree.lua143
-rw-r--r--runtime/lua/vim/treesitter/query.lua170
25 files changed, 1890 insertions, 1585 deletions
diff --git a/runtime/lua/vim/F.lua b/runtime/lua/vim/F.lua
index bca5ddf68b..3e370c0a84 100644
--- a/runtime/lua/vim/F.lua
+++ b/runtime/lua/vim/F.lua
@@ -2,8 +2,12 @@ local F = {}
--- Returns {a} if it is not nil, otherwise returns {b}.
---
----@param a
----@param b
+---@generic A
+---@generic B
+---
+---@param a A
+---@param b B
+---@return A | B
function F.if_nil(a, b)
if a == nil then
return b
diff --git a/runtime/lua/vim/_editor.lua b/runtime/lua/vim/_editor.lua
index b8a7f71145..28e5897aa9 100644
--- a/runtime/lua/vim/_editor.lua
+++ b/runtime/lua/vim/_editor.lua
@@ -12,21 +12,8 @@
-- 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
+--
+-- Compatibility with Vim's `if_lua` is explicitly a non-goal.
--
-- Reference (#6580):
-- - https://github.com/luafun/luafun
@@ -36,8 +23,6 @@
-- - 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({
@@ -122,9 +107,7 @@ function vim._os_proc_children(ppid)
return children
end
--- TODO(ZyX-I): Create compatibility layer.
-
---- Return a human-readable representation of the given object.
+--- Gets a human-readable representation of the given object.
---
---@see https://github.com/kikito/inspect.lua
---@see https://github.com/mpeterv/vinspect
@@ -152,14 +135,15 @@ do
--- </pre>
---
---@see |paste|
+ ---@alias paste_phase -1 | 1 | 2 | 3
---
- ---@param lines |readfile()|-style list of lines to paste. |channel-lines|
- ---@param phase -1: "non-streaming" paste: the call contains all lines.
+ ---@param lines string[] # |readfile()|-style list of lines to paste. |channel-lines|
+ ---@param phase paste_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.
+ ---@returns boolean # false if client should cancel the paste.
function vim.paste(lines, phase)
local now = vim.loop.now()
local is_first_chunk = phase < 2
@@ -255,6 +239,8 @@ end
---@see |lua-loop-callbacks|
---@see |vim.schedule()|
---@see |vim.in_fast_event()|
+---@param cb function
+---@return function
function vim.schedule_wrap(cb)
return function(...)
local args = vim.F.pack_len(...)
@@ -399,11 +385,11 @@ 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 pos1 integer[] (line, column) tuple marking beginning of region
+---@param pos2 integer[] (line, column) tuple marking end of region
+---@param regtype string type of selection, see |setreg()|
---@param inclusive boolean indicating whether the selection is end-inclusive
----@return region lua table of the form {linenr = {startcol,endcol}}
+---@return table<integer, {}> 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)
@@ -448,11 +434,11 @@ 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
+--- Note: The {fn} is |vim.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
+---@param fn function Callback to call once `timeout` expires
+---@param timeout integer Number of milliseconds to wait before calling `fn`
+---@return table timer luv timer object
function vim.defer_fn(fn, timeout)
vim.validate({ fn = { fn, 'c', true } })
local timer = vim.loop.new_timer()
@@ -758,7 +744,7 @@ end
--- local hl_normal = vim.pretty_print(vim.api.nvim_get_hl_by_name("Normal", true))
---</pre>
---@see |vim.inspect()|
----@return given arguments.
+---@return any # given arguments.
function vim.pretty_print(...)
local objects = {}
for i = 1, select('#', ...) do
diff --git a/runtime/lua/vim/_init_packages.lua b/runtime/lua/vim/_init_packages.lua
index 7e3c73667e..19c8608732 100644
--- a/runtime/lua/vim/_init_packages.lua
+++ b/runtime/lua/vim/_init_packages.lua
@@ -1,6 +1,3 @@
--- 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
diff --git a/runtime/lua/vim/_meta.lua b/runtime/lua/vim/_meta.lua
index f1652718ee..9c7972873e 100644
--- a/runtime/lua/vim/_meta.lua
+++ b/runtime/lua/vim/_meta.lua
@@ -1,173 +1,113 @@
--- prevents luacheck from making lints for setting things on vim
-local vim = assert(vim)
-
local a = vim.api
-local validate = vim.validate
-
-local SET_TYPES = setmetatable({
- SET = 0,
- LOCAL = 1,
- GLOBAL = 2,
-}, { __index = error })
-
-local options_info = nil
-local buf_options = nil
-local glb_options = nil
-local win_options = nil
-
-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
- 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
+-- TODO(tjdevries): Improve option metadata so that this doesn't have to be hardcoded.
+-- Can be done in a separate PR.
+local key_value_options = {
+ fillchars = true,
+ fcs = true,
+ listchars = true,
+ lcs = true,
+ winhighlight = true,
+ winhl = true,
+}
+
+--- Convert a vimoption_T style dictionary to the correct OptionType associated with it.
+---@return string
+local function get_option_metatype(name, info)
+ if info.type == 'string' then
+ if info.flaglist then
+ return 'set'
+ elseif info.commalist then
+ if key_value_options[name] then
+ return 'map'
end
+ return 'array'
end
-
- return result
+ return 'string'
end
-
- buf_options = get_scoped_options('buf')
- glb_options = get_scoped_options('global')
- win_options = get_scoped_options('win')
+ return info.type
end
-local function make_meta_accessor(get, set, del, validator)
- validator = validator or function()
- return true
- end
-
- validate({
- get = { get, 'f' },
- set = { set, 'f' },
- del = { del, 'f', true },
- validator = { validator, 'f' },
- })
+local options_info = setmetatable({}, {
+ __index = function(t, k)
+ local info = a.nvim_get_option_info(k)
+ info.metatype = get_option_metatype(k, info)
+ rawset(t, k, info)
+ return rawget(t, k)
+ end,
+})
- local mt = {}
- function mt:__newindex(k, v)
- if not validator(k) then
- return
+vim.env = setmetatable({}, {
+ __index = function(_, k)
+ local v = vim.fn.getenv(k)
+ if v == vim.NIL then
+ return nil
end
+ return v
+ end,
- if del and v == nil then
- return del(k)
- end
- return set(k, v)
- end
- function mt:__index(k)
- if not validator(k) then
- return
- end
+ __newindex = function(_, k, v)
+ vim.fn.setenv(k, v)
+ end,
+})
- return get(k)
+local function opt_validate(option_name, target_scope)
+ local scope = options_info[option_name].scope
+ if scope ~= target_scope then
+ local scope_to_string = { buf = 'buffer', win = 'window' }
+ error(
+ string.format(
+ [['%s' is a %s option, not a %s option. See ":help %s"]],
+ option_name,
+ scope_to_string[scope] or scope,
+ scope_to_string[target_scope] or target_scope,
+ option_name
+ )
+ )
end
- return setmetatable({}, mt)
end
-vim.env = make_meta_accessor(function(k)
- local v = vim.fn.getenv(k)
- if v == vim.NIL then
- return nil
- end
- return v
-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
- return new_buf_opt_accessor(k)
- end
-
- return a.nvim_get_option_value(k, { buf = bufnr or 0 })
- end
-
- local function set(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)
- )
- elseif glb_options[k] then
- error(
- string.format([['%s' is a global option, not a buffer option. See ":help %s"]], k, k)
- )
- end
+local function new_opt_accessor(handle, scope)
+ return setmetatable({}, {
+ __index = function(_, k)
+ if handle == nil and type(k) == 'number' then
+ return new_opt_accessor(k, scope)
end
+ opt_validate(k, scope)
+ return a.nvim_get_option_value(k, { [scope] = handle or 0 })
+ end,
- return true
- end)
- end
-
- vim.bo = new_buf_opt_accessor(nil)
+ __newindex = function(_, k, v)
+ opt_validate(k, scope)
+ return a.nvim_set_option_value(k, v, { [scope] = handle or 0 })
+ end,
+ })
end
-do -- window option accessor
- local function new_win_opt_accessor(winnr)
- local function get(k)
- if winnr == nil and type(k) == 'number' then
- return new_win_opt_accessor(k)
- end
- return a.nvim_get_option_value(k, { win = winnr or 0 })
- end
-
- local function set(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)
- )
- elseif glb_options[k] then
- error(
- string.format([['%s' is a global option, not a window option. See ":help %s"]], k, k)
- )
- end
- end
-
- return true
- end)
- end
-
- vim.wo = new_win_opt_accessor(nil)
-end
+vim.bo = new_opt_accessor(nil, 'buf')
+vim.wo = new_opt_accessor(nil, 'win')
-- 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 = setmetatable({}, {
+ __index = function(_, k)
+ return a.nvim_get_option_value(k, { scope = 'global' })
+ end,
+ __newindex = 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 = setmetatable({}, {
+ __index = function(_, k)
+ return a.nvim_get_option_value(k, {})
+ end,
+ __newindex = function(_, k, v)
+ return a.nvim_set_option_value(k, v, {})
+ end,
+})
---@brief [[
--- vim.opt, vim.opt_local and vim.opt_global implementation
@@ -178,11 +118,8 @@ end)
---@brief ]]
--- Preserves the order and does not mutate the original list
-local remove_duplicate_values = function(t)
+local function remove_duplicate_values(t)
local result, seen = {}, {}
- 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)
@@ -194,61 +131,6 @@ local remove_duplicate_values = function(t)
return result
end
--- TODO(tjdevries): Improve option metadata so that this doesn't have to be hardcoded.
--- Can be done in a separate PR.
-local key_value_options = {
- fillchars = true,
- listchars = true,
- winhl = true,
-}
-
----@class OptionTypes
---- Option Type Enum
-local OptionTypes = setmetatable({
- BOOLEAN = 0,
- 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,
-})
-
---- 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
- return OptionTypes.BOOLEAN
- elseif info.type == 'number' then
- return OptionTypes.NUMBER
- 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')
- return OptionTypes.MAP
- end
-
- if info.flaglist then
- return OptionTypes.SET
- elseif info.commalist then
- return OptionTypes.ARRAY
- end
-
- error('Fallthrough in OptionTypes')
- else
- 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)
@@ -269,373 +151,323 @@ local function assert_valid_value(name, value, types)
)
end
+local function passthrough(_, x)
+ return x
+end
+
+local function tbl_merge(left, right)
+ return vim.tbl_extend('force', left, right)
+end
+
+local function tbl_remove(t, value)
+ if type(value) == 'string' then
+ t[value] = nil
+ else
+ for _, v in ipairs(value) do
+ t[v] = nil
+ end
+ end
+
+ return t
+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' },
+ boolean = { 'boolean' },
+ number = { 'number' },
+ string = { 'string' },
+ set = { 'string', 'table' },
+ array = { 'string', 'table' },
+ map = { 'string', 'table' },
}
---- Convert a lua value to a vimoption_T value
-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.SET] = function(info, value)
- if type(value) == 'string' then
- return value
- end
+-- 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 = {
+ boolean = passthrough,
+ number = passthrough,
+ string = passthrough,
- if info.flaglist and info.commalist then
- local keys = {}
- for k, v in pairs(value) do
- if v then
- table.insert(keys, k)
- end
- end
+ set = function(info, value)
+ if type(value) == 'string' then
+ return value
+ end
- table.sort(keys)
- return table.concat(keys, ',')
- else
- local result = ''
- for k, v in pairs(value) do
- if v then
- result = result .. k
- end
+ if info.flaglist and info.commalist then
+ local keys = {}
+ for k, v in pairs(value) do
+ if v then
+ table.insert(keys, k)
end
-
- return result
end
- end,
- [OptionTypes.ARRAY] = function(info, value)
- if type(value) == 'string' then
- return value
- end
- if not info.allows_duplicates then
- value = remove_duplicate_values(value)
+ table.sort(keys)
+ return table.concat(keys, ',')
+ else
+ local result = ''
+ for k, v in pairs(value) do
+ if v then
+ result = result .. k
+ end
end
- return table.concat(value, ',')
- end,
- [OptionTypes.MAP] = function(_, value)
- if type(value) == 'string' then
- return value
- end
+ return result
+ end
+ end,
- local result = {}
- for opt_key, opt_value in pairs(value) do
- table.insert(result, string.format('%s:%s', opt_key, opt_value))
- end
+ array = function(info, value)
+ if type(value) == 'string' then
+ return value
+ end
+ if not info.allows_duplicates then
+ value = remove_duplicate_values(value)
+ end
+ return table.concat(value, ',')
+ end,
- table.sort(result)
- return table.concat(result, ',')
- end,
- }
+ map = function(_, value)
+ if type(value) == 'string' then
+ return value
+ end
- return function(name, info, value)
- if value == nil then
- return vim.NIL
+ local result = {}
+ for opt_key, opt_value in pairs(value) do
+ table.insert(result, string.format('%s:%s', opt_key, opt_value))
end
- local option_type = get_option_type(name, info)
- assert_valid_value(name, value, valid_types[option_type])
+ table.sort(result)
+ return table.concat(result, ',')
+ end,
+}
- return to_vim_value[option_type](info, value)
+--- Convert a lua value to a vimoption_T value
+local function convert_value_to_vim(name, info, value)
+ if value == nil then
+ return vim.NIL
end
-end)()
---- Converts a vimoption_T style value to a Lua value
-local convert_value_to_lua = (function()
- -- Map of OptionType to functions that take vimoption_T values and 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,
+ assert_valid_value(name, value, valid_types[info.metatype])
- [OptionTypes.ARRAY] = function(info, value)
- if type(value) == 'table' then
- if not info.allows_duplicates then
- value = remove_duplicate_values(value)
- end
+ return to_vim_value[info.metatype](info, value)
+end
- return value
- end
+-- 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 = {
+ boolean = passthrough,
+ number = passthrough,
+ string = passthrough,
- -- Empty strings mean that there is nothing there,
- -- so empty table should be returned.
- if value == '' then
- return {}
+ array = function(info, value)
+ if type(value) == 'table' then
+ if not info.allows_duplicates then
+ value = remove_duplicate_values(value)
end
- -- Handles unescaped commas in a list.
- if string.find(value, ',,,') then
- local comma_split = vim.split(value, ',,,')
- local left = comma_split[1]
- local right = comma_split[2]
+ return value
+ end
- local result = {}
- vim.list_extend(result, vim.split(left, ','))
- table.insert(result, ',')
- vim.list_extend(result, vim.split(right, ','))
+ -- Empty strings mean that there is nothing there,
+ -- so empty table should be returned.
+ if value == '' then
+ return {}
+ end
- table.sort(result)
+ -- Handles unescaped commas in a list.
+ if string.find(value, ',,,') then
+ local left, right = unpack(vim.split(value, ',,,'))
- return result
- end
+ local result = {}
+ vim.list_extend(result, vim.split(left, ','))
+ table.insert(result, ',')
+ vim.list_extend(result, vim.split(right, ','))
- if string.find(value, ',^,,', 1, true) then
- local comma_split = vim.split(value, ',^,,', true)
- local left = comma_split[1]
- local right = comma_split[2]
+ table.sort(result)
- local result = {}
- vim.list_extend(result, vim.split(left, ','))
- table.insert(result, '^,')
- vim.list_extend(result, vim.split(right, ','))
+ return result
+ end
- table.sort(result)
+ if string.find(value, ',^,,', 1, true) then
+ local left, right = unpack(vim.split(value, ',^,,', true))
- return result
- end
+ local result = {}
+ vim.list_extend(result, vim.split(left, ','))
+ table.insert(result, '^,')
+ vim.list_extend(result, vim.split(right, ','))
- return vim.split(value, ',')
- end,
+ table.sort(result)
- [OptionTypes.SET] = function(info, value)
- if type(value) == 'table' then
- return value
- end
+ return result
+ end
- -- Empty strings mean that there is nothing there,
- -- so empty table should be returned.
- if value == '' then
- return {}
- end
+ return vim.split(value, ',')
+ end,
- assert(info.flaglist, 'That is the only one I know how to handle')
+ set = function(info, value)
+ if type(value) == 'table' then
+ return value
+ end
- if info.flaglist and info.commalist then
- local split_value = vim.split(value, ',')
- local result = {}
- for _, v in ipairs(split_value) do
- result[v] = true
- end
+ -- Empty strings mean that there is nothing there,
+ -- so empty table should be returned.
+ if value == '' then
+ return {}
+ end
- return result
- else
- local result = {}
- for i = 1, #value do
- result[value:sub(i, i)] = true
- end
+ assert(info.flaglist, 'That is the only one I know how to handle')
- return result
+ if info.flaglist and info.commalist then
+ local split_value = vim.split(value, ',')
+ local result = {}
+ for _, v in ipairs(split_value) do
+ result[v] = true
end
- end,
- [OptionTypes.MAP] = function(info, raw_value)
- if type(raw_value) == 'table' then
- return raw_value
+ return result
+ else
+ local result = {}
+ for i = 1, #value do
+ result[value:sub(i, i)] = true
end
- assert(info.commalist, 'Only commas are supported currently')
-
- local result = {}
+ return result
+ end
+ end,
- 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, ':'))
- key = vim.trim(key)
+ map = function(info, raw_value)
+ if type(raw_value) == 'table' then
+ return raw_value
+ end
- result[key] = value
- end
+ assert(info.commalist, 'Only commas are supported currently')
- return result
- end,
- }
+ local result = {}
- return function(name, info, option_value)
- return to_lua_value[get_option_type(name, info)](info, option_value)
- end
-end)()
+ 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, ':'))
+ key = vim.trim(key)
---- Handles the mutation of various different values.
-local value_mutator = function(name, info, current, new, mutator)
- return mutator[get_option_type(name, info)](current, new)
-end
+ result[key] = value
+ end
---- Handles the '^' operator
-local prepend_value = (function()
- local methods = {
- [OptionTypes.NUMBER] = function()
- error("The '^' operator is not currently supported for")
- end,
+ return result
+ end,
+}
- [OptionTypes.STRING] = function(left, right)
- return right .. left
- end,
+--- Converts a vimoption_T style value to a Lua value
+local function convert_value_to_lua(info, option_value)
+ return to_lua_value[info.metatype](info, option_value)
+end
- [OptionTypes.ARRAY] = function(left, right)
- for i = #right, 1, -1 do
- table.insert(left, 1, right[i])
- end
+local prepend_methods = {
+ number = function()
+ error("The '^' operator is not currently supported for")
+ end,
- return left
- end,
+ string = function(left, right)
+ return right .. left
+ end,
- [OptionTypes.MAP] = function(left, right)
- return vim.tbl_extend('force', left, right)
- end,
+ array = function(left, right)
+ for i = #right, 1, -1 do
+ table.insert(left, 1, right[i])
+ end
- [OptionTypes.SET] = function(left, right)
- return vim.tbl_extend('force', left, right)
- end,
- }
+ return left
+ 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
- )
- end
-end)()
+ map = tbl_merge,
+ set = tbl_merge,
+}
---- Handles the '+' operator
-local add_value = (function()
- local methods = {
- [OptionTypes.NUMBER] = function(left, right)
- return left + right
- end,
+--- Handles the '^' operator
+local function prepend_value(info, current, new)
+ return prepend_methods[info.metatype](
+ convert_value_to_lua(info, current),
+ convert_value_to_lua(info, new)
+ )
+end
- [OptionTypes.STRING] = function(left, right)
- return left .. right
- end,
+local add_methods = {
+ number = function(left, right)
+ return left + right
+ end,
- [OptionTypes.ARRAY] = function(left, right)
- for _, v in ipairs(right) do
- table.insert(left, v)
- end
+ string = function(left, right)
+ return left .. right
+ end,
- return left
- end,
+ array = function(left, right)
+ for _, v in ipairs(right) do
+ table.insert(left, v)
+ end
- [OptionTypes.MAP] = function(left, right)
- return vim.tbl_extend('force', left, right)
- end,
+ return left
+ end,
- [OptionTypes.SET] = function(left, right)
- return vim.tbl_extend('force', left, right)
- end,
- }
+ map = tbl_merge,
+ set = tbl_merge,
+}
- 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
- )
- end
-end)()
+--- Handles the '+' operator
+local function add_value(info, current, new)
+ return add_methods[info.metatype](
+ convert_value_to_lua(info, current),
+ convert_value_to_lua(info, new)
+ )
+end
---- Handles the '-' operator
-local remove_value = (function()
- local remove_one_item = function(t, val)
- if vim.tbl_islist(t) then
- local remove_index = nil
- for i, v in ipairs(t) do
- if v == val then
- remove_index = i
- end
+local function remove_one_item(t, val)
+ if vim.tbl_islist(t) then
+ local remove_index = nil
+ for i, v in ipairs(t) do
+ if v == val then
+ remove_index = i
end
+ end
- if remove_index then
- table.remove(t, remove_index)
- end
- else
- t[val] = nil
+ if remove_index then
+ table.remove(t, remove_index)
end
+ else
+ t[val] = nil
end
+end
- local methods = {
- [OptionTypes.NUMBER] = function(left, right)
- return left - right
- end,
-
- [OptionTypes.STRING] = function()
- error('Subtraction not supported for strings.')
- end,
-
- [OptionTypes.ARRAY] = function(left, right)
- if type(right) == 'string' then
- remove_one_item(left, right)
- else
- for _, v in ipairs(right) do
- remove_one_item(left, v)
- end
- end
+local remove_methods = {
+ number = function(left, right)
+ return left - right
+ end,
- return left
- end,
+ string = function()
+ error('Subtraction not supported for strings.')
+ end,
- [OptionTypes.MAP] = function(left, right)
- if type(right) == 'string' then
- left[right] = nil
- else
- for _, v in ipairs(right) do
- left[v] = nil
- end
+ array = function(left, right)
+ if type(right) == 'string' then
+ remove_one_item(left, right)
+ else
+ for _, v in ipairs(right) do
+ remove_one_item(left, v)
end
+ end
- return left
- end,
-
- [OptionTypes.SET] = function(left, right)
- if type(right) == 'string' then
- left[right] = nil
- else
- for _, v in ipairs(right) do
- left[v] = nil
- end
- end
+ return left
+ end,
- return left
- end,
- }
+ map = tbl_remove,
+ set = tbl_remove,
+}
- return function(name, info, current, new)
- return value_mutator(name, info, convert_value_to_lua(name, info, current), new, methods)
- end
-end)()
+--- Handles the '-' operator
+local function remove_value(info, current, new)
+ return remove_methods[info.metatype](convert_value_to_lua(info, current), new)
+end
-local create_option_metatable = function(set_type)
- local set_mt, option_mt
+local function create_option_accessor(scope)
+ local option_mt
- local make_option = function(name, value)
- _setup()
+ local function make_option(name, value)
local info = assert(options_info[name], 'Not a valid option name: ' .. name)
if type(value) == 'table' and getmetatable(value) == option_mt then
@@ -651,67 +483,58 @@ local create_option_metatable = function(set_type)
}, option_mt)
end
- local scope
- if set_type == SET_TYPES.GLOBAL then
- scope = 'global'
- elseif set_type == SET_TYPES.LOCAL then
- scope = 'local'
- end
-
option_mt = {
-- To set a value, instead use:
-- opt[my_option] = value
_set = function(self)
local value = convert_value_to_vim(self._name, self._info, self._value)
a.nvim_set_option_value(self._name, value, { scope = scope })
-
- return self
end,
get = function(self)
- return convert_value_to_lua(self._name, self._info, self._value)
+ return convert_value_to_lua(self._info, self._value)
end,
append = function(self, right)
- return self:__add(right):_set()
+ self._value = add_value(self._info, self._value, right)
+ self:_set()
end,
__add = function(self, right)
- return make_option(self._name, add_value(self._name, self._info, self._value, right))
+ return make_option(self._name, add_value(self._info, self._value, right))
end,
prepend = function(self, right)
- return self:__pow(right):_set()
+ self._value = prepend_value(self._info, self._value, right)
+ self:_set()
end,
__pow = function(self, right)
- return make_option(self._name, prepend_value(self._name, self._info, self._value, right))
+ return make_option(self._name, prepend_value(self._info, self._value, right))
end,
remove = function(self, right)
- return self:__sub(right):_set()
+ self._value = remove_value(self._info, self._value, right)
+ self:_set()
end,
__sub = function(self, right)
- return make_option(self._name, remove_value(self._name, self._info, self._value, right))
+ return make_option(self._name, remove_value(self._info, self._value, right))
end,
}
option_mt.__index = option_mt
- set_mt = {
+ return setmetatable({}, {
__index = function(_, k)
return make_option(k, a.nvim_get_option_value(k, { scope = scope }))
end,
__newindex = function(_, k, v)
- local opt = make_option(k, v)
- opt:_set()
+ make_option(k, v):_set()
end,
- }
-
- return set_mt
+ })
end
-vim.opt = setmetatable({}, create_option_metatable(SET_TYPES.SET))
-vim.opt_local = setmetatable({}, create_option_metatable(SET_TYPES.LOCAL))
-vim.opt_global = setmetatable({}, create_option_metatable(SET_TYPES.GLOBAL))
+vim.opt = create_option_accessor()
+vim.opt_local = create_option_accessor('local')
+vim.opt_global = create_option_accessor('global')
diff --git a/runtime/lua/vim/compat.lua b/runtime/lua/vim/compat.lua
deleted file mode 100644
index 2c9786d491..0000000000
--- a/runtime/lua/vim/compat.lua
+++ /dev/null
@@ -1,12 +0,0 @@
--- Lua 5.1 forward-compatibility layer.
--- For background see https://github.com/neovim/neovim/pull/9280
---
--- Reference the lua-compat-5.2 project for hints:
--- https://github.com/keplerproject/lua-compat-5.2/blob/c164c8f339b95451b572d6b4b4d11e944dc7169d/compat52/mstrict.lua
--- https://github.com/keplerproject/lua-compat-5.2/blob/c164c8f339b95451b572d6b4b4d11e944dc7169d/tests/test.lua
-
-local lua_version = _VERSION:sub(-3)
-
-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 3f71d4f70d..84091fbf0e 100644
--- a/runtime/lua/vim/diagnostic.lua
+++ b/runtime/lua/vim/diagnostic.lua
@@ -45,18 +45,24 @@ local bufnr_and_namespace_cacher_mt = {
end,
}
-local diagnostic_cache = setmetatable({}, {
- __index = function(t, bufnr)
- assert(bufnr > 0, 'Invalid buffer number')
- vim.api.nvim_buf_attach(bufnr, false, {
- on_detach = function()
- rawset(t, bufnr, nil) -- clear cache
- end,
- })
- t[bufnr] = {}
- return t[bufnr]
- end,
-})
+local diagnostic_cache
+do
+ local group = vim.api.nvim_create_augroup('DiagnosticBufWipeout', {})
+ diagnostic_cache = setmetatable({}, {
+ __index = function(t, bufnr)
+ assert(bufnr > 0, 'Invalid buffer number')
+ vim.api.nvim_create_autocmd('BufWipeout', {
+ group = group,
+ buffer = bufnr,
+ callback = function()
+ rawset(t, bufnr, nil)
+ end,
+ })
+ t[bufnr] = {}
+ return t[bufnr]
+ end,
+ })
+end
local diagnostic_cache_extmarks = setmetatable({}, bufnr_and_namespace_cacher_mt)
local diagnostic_attached_buffers = {}
@@ -351,19 +357,6 @@ local function execute_scheduled_display(namespace, bufnr)
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' }
@@ -721,6 +714,7 @@ function M.set(namespace, bufnr, diagnostics, opts)
vim.api.nvim_exec_autocmds('DiagnosticChanged', {
modeline = false,
buffer = bufnr,
+ data = { diagnostics = diagnostics },
})
end
@@ -1419,6 +1413,7 @@ function M.reset(namespace, bufnr)
vim.api.nvim_exec_autocmds('DiagnosticChanged', {
modeline = false,
buffer = iter_bufnr,
+ data = { diagnostics = {} },
})
end
end
diff --git a/runtime/lua/vim/filetype.lua b/runtime/lua/vim/filetype.lua
index 99c98764dd..e204fe8ae2 100644
--- a/runtime/lua/vim/filetype.lua
+++ b/runtime/lua/vim/filetype.lua
@@ -177,6 +177,7 @@ local extension = {
bbappend = 'bitbake',
bbclass = 'bitbake',
bl = 'blank',
+ blp = 'blueprint',
bsd = 'bsdl',
bsdl = 'bsdl',
bst = 'bst',
@@ -201,6 +202,7 @@ local extension = {
return require('vim.filetype.detect').change(bufnr)
end,
chs = 'chaskell',
+ chatito = 'chatito',
chopro = 'chordpro',
crd = 'chordpro',
crdpro = 'chordpro',
@@ -422,9 +424,11 @@ local extension = {
gdb = 'gdb',
gdmo = 'gdmo',
mo = 'gdmo',
- tres = 'gdresource',
tscn = 'gdresource',
+ tres = 'gdresource',
gd = 'gdscript',
+ gdshader = 'gdshader',
+ shader = 'gdshader',
ged = 'gedcom',
gmi = 'gemtext',
gemini = 'gemtext',
@@ -444,6 +448,8 @@ local extension = {
gsp = 'gsp',
gjs = 'javascript.glimmer',
gts = 'typescript.glimmer',
+ gyp = 'gyp',
+ gypi = 'gyp',
hack = 'hack',
hackpartial = 'hack',
haml = 'haml',
@@ -469,6 +475,8 @@ local extension = {
hex = 'hex',
['h32'] = 'hex',
hjson = 'hjson',
+ m3u = 'hlsplaylist',
+ m3u8 = 'hlsplaylist',
hog = 'hog',
hws = 'hollywood',
hoon = 'hoon',
@@ -538,6 +546,7 @@ local extension = {
mjs = 'javascript',
javascript = 'javascript',
js = 'javascript',
+ jsm = 'javascript',
cjs = 'javascript',
jsx = 'javascriptreact',
clp = 'jess',
@@ -554,6 +563,8 @@ local extension = {
['json-patch'] = 'json',
json5 = 'json5',
jsonc = 'jsonc',
+ jsonnet = 'jsonnet',
+ libjsonnet = 'jsonnet',
jsp = 'jsp',
jl = 'julia',
kv = 'kivy',
@@ -605,6 +616,7 @@ local extension = {
nse = 'lua',
rockspec = 'lua',
lua = 'lua',
+ lrc = 'lyrics',
m = function(path, bufnr)
return require('vim.filetype.detect').m(bufnr)
end,
@@ -695,6 +707,9 @@ local extension = {
nanorc = 'nanorc',
ncf = 'ncf',
nginx = 'nginx',
+ nim = 'nim',
+ nims = 'nim',
+ nimble = 'nim',
ninja = 'ninja',
nix = 'nix',
nqc = 'nqc',
@@ -741,6 +756,7 @@ local extension = {
php = 'php',
phpt = 'php',
phtml = 'php',
+ theme = 'php',
pike = 'pike',
pmod = 'pike',
rcp = 'pilrc',
@@ -758,6 +774,7 @@ local extension = {
po = 'po',
pot = 'po',
pod = 'pod',
+ filter = 'poefilter',
pk = 'poke',
ps = 'postscr',
epsi = 'postscr',
@@ -900,7 +917,9 @@ local extension = {
sig = function(path, bufnr)
return require('vim.filetype.detect').sig(bufnr)
end,
- sil = 'sil',
+ sil = function(path, bufnr)
+ return require('vim.filetype.detect').sil(bufnr)
+ end,
sim = 'simula',
['s85'] = 'sinda',
sin = 'sinda',
@@ -950,6 +969,9 @@ local extension = {
srec = 'srec',
mot = 'srec',
['s19'] = 'srec',
+ srt = 'srt',
+ ssa = 'ssa',
+ ass = 'ssa',
st = 'st',
imata = 'stata',
['do'] = 'stata',
@@ -1002,6 +1024,8 @@ local extension = {
ts = function(path, bufnr)
return M.getlines(bufnr, 1):find('<%?xml') and 'xml' or 'typescript'
end,
+ mts = 'typescript',
+ cts = 'typescript',
tsx = 'typescriptreact',
uc = 'uc',
uit = 'uil',
@@ -1011,6 +1035,12 @@ local extension = {
dsm = 'vb',
ctl = 'vb',
vbs = 'vb',
+ vdf = 'vdf',
+ vdmpp = 'vdmpp',
+ vpp = 'vdmpp',
+ vdmrt = 'vdmrt',
+ vdmsl = 'vdmsl',
+ vdm = 'vdmsl',
vr = 'vera',
vri = 'vera',
vrh = 'vera',
@@ -1278,7 +1308,6 @@ local filename = {
WORKSPACE = 'bzl',
BUILD = 'bzl',
['cabal.project'] = 'cabalproject',
- [vim.env.HOME .. '/cabal.config'] = 'cabalconfig',
['cabal.config'] = 'cabalconfig',
calendar = 'calendar',
catalog = 'catalog',
@@ -1371,6 +1400,8 @@ local filename = {
['EDIT_DESCRIPTION'] = 'gitcommit',
['.gitconfig'] = 'gitconfig',
['.gitmodules'] = 'gitconfig',
+ ['.gitattributes'] = 'gitattributes',
+ ['.gitignore'] = 'gitignore',
['gitolite.conf'] = 'gitolite',
['git-rebase-todo'] = 'gitrebase',
gkrellmrc = 'gkrellmrc',
@@ -1427,6 +1458,7 @@ local filename = {
['.sawfishrc'] = 'lisp',
['/etc/login.access'] = 'loginaccess',
['/etc/login.defs'] = 'logindefs',
+ ['.luacheckrc'] = 'lua',
['lynx.cfg'] = 'lynx',
['m3overrides'] = 'm3build',
['m3makefile'] = 'm3build',
@@ -1472,6 +1504,8 @@ local filename = {
['/etc/shadow-'] = 'passwd',
['/etc/shadow'] = 'passwd',
['/etc/passwd.edit'] = 'passwd',
+ ['latexmkrc'] = 'perl',
+ ['.latexmkrc'] = 'perl',
['pf.conf'] = 'pf',
['main.cf'] = 'pfmain',
pinerc = 'pine',
@@ -1684,6 +1718,7 @@ local pattern = {
['.*/meta%-.*/conf/.*%.conf'] = 'bitbake',
['bzr_log%..*'] = 'bzr',
['.*enlightenment/.*%.cfg'] = 'c',
+ ['${HOME}/cabal%.config'] = 'cabalconfig',
['cabal%.project%..*'] = starsetf('cabalproject'),
['.*/%.calendar/.*'] = starsetf('calendar'),
['.*/share/calendar/.*/calendar%..*'] = starsetf('calendar'),
@@ -1702,7 +1737,7 @@ local pattern = {
{ priority = -1 },
},
['[cC]hange[lL]og.*'] = starsetf(function(path, bufnr)
- require('vim.filetype.detect').changelog(bufnr)
+ return require('vim.filetype.detect').changelog(bufnr)
end),
['.*%.%.ch'] = 'chill',
['.*%.cmake%.in'] = 'cmake',
@@ -1808,26 +1843,21 @@ local pattern = {
['.*/%.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,
+ ['${XDG_CONFIG_HOME}/git/config'] = 'gitconfig',
+ ['.*%.git/info/attributes'] = 'gitattributes',
+ ['.*/etc/gitattributes'] = 'gitattributes',
+ ['.*/%.config/git/attributes'] = 'gitattributes',
+ ['${XDG_CONFIG_HOME}/git/attributes'] = 'gitattributes',
+ ['.*%.git/info/exclude'] = 'gitignore',
+ ['.*/%.config/git/ignore'] = 'gitignore',
+ ['${XDG_CONFIG_HOME}/git/ignore'] = 'gitignore',
['%.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,
+ ['${GNUPGHOME}/options'] = 'gpg',
+ ['${GNUPGHOME}/gpg%.conf'] = 'gpg',
['.*/etc/group'] = 'group',
['.*/etc/gshadow'] = 'group',
['.*/etc/group%.edit'] = 'group',
@@ -1841,7 +1871,7 @@ local pattern = {
['.*/etc/grub%.conf'] = 'grub',
-- gtkrc* and .gtkrc*
['%.?gtkrc.*'] = starsetf('gtkrc'),
- [vim.env.VIMRUNTIME .. '/doc/.*%.txt'] = 'help',
+ ['${VIMRUNTIME}/doc/.*%.txt'] = 'help',
['hg%-editor%-.*%.txt'] = 'hgcommit',
['.*/etc/host%.conf'] = 'hostconf',
['.*/etc/hosts%.deny'] = 'hostsaccess',
@@ -2245,6 +2275,9 @@ end
--- Filename patterns can specify an optional priority to resolve cases when a
--- file path matches multiple patterns. Higher priorities are matched first.
--- When omitted, the priority defaults to 0.
+--- A pattern can contain environment variables of the form "${SOME_VAR}" that will
+--- be automatically expanded. If the environment variable is not set, the pattern
+--- won't be matched.
---
--- See $VIMRUNTIME/lua/vim/filetype.lua for more examples.
---
@@ -2273,6 +2306,8 @@ end
--- ['.*/etc/foo/.*'] = 'fooscript',
--- -- Using an optional priority
--- ['.*/etc/foo/.*%.conf'] = { 'dosini', { priority = 10 } },
+--- -- A pattern containing an environment variable
+--- ['${XDG_CONFIG_HOME}/foo/git'] = 'git',
--- ['README.(%a+)$'] = function(path, bufnr, ext)
--- if ext == 'md' then
--- return 'markdown'
@@ -2346,8 +2381,28 @@ local function dispatch(ft, path, bufnr, ...)
end
end
+-- Lookup table/cache for patterns that contain an environment variable pattern, e.g. ${SOME_VAR}.
+local expand_env_lookup = {}
+
---@private
local function match_pattern(name, path, tail, pat)
+ if expand_env_lookup[pat] == nil then
+ expand_env_lookup[pat] = pat:find('%${') ~= nil
+ end
+ if expand_env_lookup[pat] then
+ local return_early
+ pat = pat:gsub('%${(%S-)}', function(env)
+ -- If an environment variable is present in the pattern but not set, there is no match
+ if not vim.env[env] then
+ return_early = true
+ return nil
+ end
+ return vim.env[env]
+ end)
+ if return_early then
+ return false
+ end
+ end
-- If the pattern contains a / match against the full path, otherwise just the tail
local fullpat = '^' .. pat .. '$'
local matches
diff --git a/runtime/lua/vim/filetype/detect.lua b/runtime/lua/vim/filetype/detect.lua
index 2be9dcff88..7fc7f1b7ca 100644
--- a/runtime/lua/vim/filetype/detect.lua
+++ b/runtime/lua/vim/filetype/detect.lua
@@ -1194,6 +1194,19 @@ function M.shell(path, contents, name)
return name
end
+-- Swift Intermediate Language or SILE
+function M.sil(bufnr)
+ for _, line in ipairs(getlines(bufnr, 1, 100)) do
+ if line:find('^%s*[\\%%]') then
+ return 'sile'
+ elseif line:find('^%s*%S') then
+ return 'sil'
+ end
+ end
+ -- No clue, default to "sil"
+ return 'sil'
+end
+
-- SMIL or SNMP MIB file
function M.smi(bufnr)
local line = getlines(bufnr, 1)
diff --git a/runtime/lua/vim/fs.lua b/runtime/lua/vim/fs.lua
index ce845eda15..7bd635d8b6 100644
--- a/runtime/lua/vim/fs.lua
+++ b/runtime/lua/vim/fs.lua
@@ -76,8 +76,11 @@ end
--- 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 names (string|table|fun(name: string): boolean) Names of the files
+--- and directories to find.
+--- Must be base names, paths and globs are not supported.
+--- If a function it is called per file and dir within the
+--- traversed directories to test if they match.
---@param opts (table) Optional keyword arguments:
--- - path (string): Path to begin searching from. If
--- omitted, the current working directory is used.
@@ -98,7 +101,7 @@ end
function M.find(names, opts)
opts = opts or {}
vim.validate({
- names = { names, { 's', 't' } },
+ names = { names, { 's', 't', 'f' } },
path = { opts.path, 's', true },
upward = { opts.upward, 'b', true },
stop = { opts.stop, 's', true },
@@ -123,18 +126,31 @@ function M.find(names, opts)
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
+ local test
+
+ if type(names) == 'function' then
+ test = function(p)
+ local t = {}
+ for name, type in M.dir(p) do
+ if names(name) and (not opts.type or opts.type == type) then
+ table.insert(t, p .. '/' .. name)
+ end
end
+ return t
end
+ else
+ test = function(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
+ return t
+ end
end
for _, match in ipairs(test(path)) do
@@ -162,17 +178,25 @@ function M.find(names, opts)
break
end
- for other, type in M.dir(dir) do
+ 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 type(names) == 'function' then
+ if names(other) and (not opts.type or opts.type == type_) then
if add(f) then
return matches
end
end
+ else
+ 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
end
- if type == 'directory' then
+ if type_ == 'directory' then
dirs[#dirs + 1] = f
end
end
diff --git a/runtime/lua/vim/highlight.lua b/runtime/lua/vim/highlight.lua
index ddd504a0e0..0fde515bd9 100644
--- a/runtime/lua/vim/highlight.lua
+++ b/runtime/lua/vim/highlight.lua
@@ -41,7 +41,7 @@ end
---@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)
+-- - regtype type of range (see |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)
diff --git a/runtime/lua/vim/inspect.lua b/runtime/lua/vim/inspect.lua
index 0a53fb203b..c232f69590 100644
--- a/runtime/lua/vim/inspect.lua
+++ b/runtime/lua/vim/inspect.lua
@@ -89,8 +89,38 @@ local function escape(str)
)
end
+-- List of lua keywords
+local luaKeywords = {
+ ['and'] = true,
+ ['break'] = true,
+ ['do'] = true,
+ ['else'] = true,
+ ['elseif'] = true,
+ ['end'] = true,
+ ['false'] = true,
+ ['for'] = true,
+ ['function'] = true,
+ ['goto'] = true,
+ ['if'] = true,
+ ['in'] = true,
+ ['local'] = true,
+ ['nil'] = true,
+ ['not'] = true,
+ ['or'] = true,
+ ['repeat'] = true,
+ ['return'] = true,
+ ['then'] = true,
+ ['true'] = true,
+ ['until'] = true,
+ ['while'] = true,
+}
+
local function isIdentifier(str)
- return type(str) == 'string' and not not str:match('^[_%a][_%a%d]*$')
+ return type(str) == 'string'
+ -- identifier must start with a letter and underscore, and be followed by letters, numbers, and underscores
+ and not not str:match('^[_%a][_%a%d]*$')
+ -- lua keywords are not valid identifiers
+ and not luaKeywords[str]
end
local flr = math.floor
diff --git a/runtime/lua/vim/keymap.lua b/runtime/lua/vim/keymap.lua
index 219de16b5c..af41794c53 100644
--- a/runtime/lua/vim/keymap.lua
+++ b/runtime/lua/vim/keymap.lua
@@ -36,17 +36,17 @@ local keymap = {}
---@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.
--
----@param opts table A table of |:map-arguments|.
---- + Accepts options accepted by the {opts} parameter in |nvim_set_keymap()|,
---- with the following notable differences:
---- - replace_keycodes: Defaults to `true` if "expr" is `true`.
---- - noremap: Always overridden with the inverse of "remap" (see below).
---- + In addition to those options, the table accepts the following keys:
---- - buffer: (number or boolean) Add a mapping to the given buffer.
---- When `0` or `true`, use the current buffer.
---- - remap: (boolean) Make the mapping recursive.
---- This is the inverse of the "noremap" option from |nvim_set_keymap()|.
---- Defaults to `false`.
+---@param opts table|nil A table of |:map-arguments|.
+--- + Accepts options accepted by the {opts} parameter in |nvim_set_keymap()|,
+--- with the following notable differences:
+--- - replace_keycodes: Defaults to `true` if "expr" is `true`.
+--- - noremap: Always overridden with the inverse of "remap" (see below).
+--- + In addition to those options, the table accepts the following keys:
+--- - buffer: (number or boolean) Add a mapping to the given buffer.
+--- When `0` or `true`, use the current buffer.
+--- - remap: (boolean) Make the mapping recursive.
+--- This is the inverse of the "noremap" option from |nvim_set_keymap()|.
+--- Defaults to `false`.
---@see |nvim_set_keymap()|
function keymap.set(mode, lhs, rhs, opts)
vim.validate({
@@ -57,7 +57,6 @@ function keymap.set(mode, lhs, rhs, opts)
})
opts = vim.deepcopy(opts) or {}
- local is_rhs_luaref = type(rhs) == 'function'
mode = type(mode) == 'string' and { mode } or mode
if opts.expr and opts.replace_keycodes ~= false then
@@ -73,7 +72,7 @@ function keymap.set(mode, lhs, rhs, opts)
opts.remap = nil
end
- if is_rhs_luaref then
+ if type(rhs) == 'function' then
opts.callback = rhs
rhs = ''
end
@@ -99,9 +98,9 @@ end
---
--- vim.keymap.del({'n', 'i', 'v'}, '<leader>w', { buffer = 5 })
--- </pre>
----@param opts table A table of optional arguments:
---- - buffer: (number or boolean) Remove a mapping from the given buffer.
---- When "true" or 0, use the current buffer.
+---@param opts table|nil A table of optional arguments:
+--- - buffer: (number or boolean) Remove a mapping from the given buffer.
+--- When "true" or 0, use the current buffer.
---@see |vim.keymap.set()|
---
function keymap.del(modes, lhs, opts)
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua
index fd64c1a495..199964e24e 100644
--- a/runtime/lua/vim/lsp.lua
+++ b/runtime/lua/vim/lsp.lua
@@ -5,7 +5,6 @@ local protocol = require('vim.lsp.protocol')
local util = require('vim.lsp.util')
local sync = require('vim.lsp.sync')
-local vim = vim
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,
@@ -289,7 +288,12 @@ local function validate_client_config(config)
'flags.debounce_text_changes must be a number with the debounce time in milliseconds'
)
- local cmd, cmd_args = lsp._cmd_parts(config.cmd)
+ local cmd, cmd_args
+ if type(config.cmd) == 'function' then
+ cmd = config.cmd
+ else
+ cmd, cmd_args = lsp._cmd_parts(config.cmd)
+ end
local offset_encoding = valid_encodings.UTF16
if config.offset_encoding then
offset_encoding = validate_encoding(config.offset_encoding)
@@ -818,7 +822,7 @@ end
--- })
--- </pre>
---
---- See |lsp.start_client| for all available options. The most important are:
+--- See |vim.lsp.start_client()| for all available options. The most important are:
---
--- `name` is an arbitrary name for the LSP client. It should be unique per
--- language server.
@@ -829,7 +833,7 @@ end
---
--- `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
+--- 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.
@@ -844,26 +848,35 @@ end
---
---
--- To ensure a language server is only started for languages it can handle,
---- make sure to call |vim.lsp.start| within a |FileType| autocmd.
+--- 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 config table Same configuration as documented in |vim.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
+--- - bufnr (number)
+--- Buffer handle to attach to if starting or re-using a
+--- client (0 for current).
+---@return number|nil 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()
+ config.name = config.name
+ if not config.name and type(config.cmd) == 'table' then
+ config.name = config.cmd[1] and vim.fs.basename(config.cmd[1]) or nil
+ end
+ local bufnr = opts.bufnr
+ if bufnr == nil or bufnr == 0 then
+ bufnr = api.nvim_get_current_buf()
+ end
for _, clients in ipairs({ uninitialized_clients, lsp.get_active_clients() }) do
for _, client in pairs(clients) do
if reuse_client(client, config) then
@@ -893,8 +906,13 @@ end
--- The following parameters describe fields in the {config} table.
---
---
----@param cmd: (required, string or list treated like |jobstart()|) Base command
---- that initiates the LSP client.
+---@param cmd: (table|string|fun(dispatchers: table):table) command string or
+--- list treated like |jobstart()|. The command must launch the language server
+--- process. `cmd` can also be a function that creates an RPC client.
+--- The function receives a dispatchers table and must return a table with the
+--- functions `request`, `notify`, `is_closing` and `terminate`
+--- See |vim.lsp.rpc.request()| and |vim.lsp.rpc.notify()|
+--- For TCP there is a built-in rpc client factory: |vim.lsp.rpc.connect()|
---
---@param cmd_cwd: (string, default=|getcwd()|) Directory to launch
--- the `cmd` process. Not related to `root_dir`.
@@ -950,7 +968,7 @@ end
---@param on_error Callback with parameters (code, ...), invoked
--- when the client operation throws an error. `code` is a number describing
--- the error. Other arguments may be passed depending on the error kind. See
---- |vim.lsp.rpc.client_errors| for possible errors.
+--- `vim.lsp.rpc.client_errors` for possible errors.
--- Use `vim.lsp.rpc.client_errors[code]` to get human-friendly name.
---
---@param before_init Callback with parameters (initialize_params, config)
@@ -986,8 +1004,8 @@ end
--- notifications to the server by the given number in milliseconds. No debounce
--- occurs if nil
--- - exit_timeout (number|boolean, default false): Milliseconds to wait for server to
---- exit cleanly after sending the 'shutdown' request before sending kill -15.
---- If set to false, nvim exits immediately after sending the 'shutdown' request to the server.
+--- exit cleanly after sending the "shutdown" request before sending kill -15.
+--- If set to false, nvim exits immediately after sending the "shutdown" request to the server.
---
---@param root_dir string Directory where the LSP
--- server will base its workspaceFolders, rootUri, and rootPath
@@ -1065,7 +1083,7 @@ function lsp.start_client(config)
---
---@param code (number) Error code
---@param err (...) Other arguments may be passed depending on the error kind
- ---@see |vim.lsp.rpc.client_errors| for possible errors. Use
+ ---@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()
@@ -1134,41 +1152,47 @@ function lsp.start_client(config)
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)
+ client_ids[client_id] = nil
+ if vim.tbl_isempty(client_ids) then
+ unset_defaults(bufnr)
+ end
end)
end
end
- local client = active_clients[client_id] and active_clients[client_id]
- or uninitialized_clients[client_id]
- active_clients[client_id] = nil
- uninitialized_clients[client_id] = nil
- -- Client can be absent if executable starts, but initialize fails
- -- init/attach won't have happened
- if client then
- changetracking.reset(client)
- 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)
- vim.schedule(function()
+ -- Schedule the deletion of the client object so that it exists in the execution of LspDetach
+ -- autocommands
+ vim.schedule(function()
+ local client = active_clients[client_id] and active_clients[client_id]
+ or uninitialized_clients[client_id]
+ active_clients[client_id] = nil
+ uninitialized_clients[client_id] = nil
+
+ -- Client can be absent if executable starts, but initialize fails
+ -- init/attach won't have happened
+ if client then
+ changetracking.reset(client)
+ 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)
vim.notify(msg, vim.log.levels.WARN)
- end)
- end
+ end
+ end)
end
-- Start the RPC client.
- local rpc = lsp_rpc.start(cmd, cmd_args, dispatch, {
- cwd = config.cmd_cwd,
- env = config.cmd_env,
- detached = config.detached,
- })
+ local rpc
+ if type(cmd) == 'function' then
+ rpc = cmd(dispatch)
+ else
+ rpc = lsp_rpc.start(cmd, cmd_args, dispatch, {
+ cwd = config.cmd_cwd,
+ env = config.cmd_env,
+ detached = config.detached,
+ })
+ end
-- Return nil if client fails to start
if not rpc then
@@ -1464,14 +1488,13 @@ function lsp.start_client(config)
--- you request to stop a client which has previously been requested to
--- shutdown, it will automatically escalate and force shutdown.
---
- ---@param force (bool, optional)
+ ---@param force boolean|nil
function client.stop(force)
- local handle = rpc.handle
- if handle:is_closing() then
+ if rpc.is_closing() then
return
end
if force or not client.initialized or graceful_shutdown_failed then
- handle:kill(15)
+ rpc.terminate()
return
end
-- Sending a signal after a process has exited is acceptable.
@@ -1480,7 +1503,7 @@ function lsp.start_client(config)
rpc.notify('exit')
else
-- If there was an error in the shutdown request, then term to be safe.
- handle:kill(15)
+ rpc.terminate()
graceful_shutdown_failed = true
end
end)
@@ -1492,7 +1515,7 @@ function lsp.start_client(config)
---@returns (bool) true if client is stopped or in the process of being
---stopped; false otherwise
function client.is_stopped()
- return rpc.handle:is_closing()
+ return rpc.is_closing()
end
---@private
@@ -1627,6 +1650,7 @@ function lsp.buf_attach_client(bufnr, client_id)
if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then
client.notify('textDocument/didClose', params)
end
+ client.attached_buffers[bufnr] = nil
end)
util.buf_versions[bufnr] = nil
all_buffer_active_clients[bufnr] = nil
diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua
index 6a070928d9..c593e72d62 100644
--- a/runtime/lua/vim/lsp/buf.lua
+++ b/runtime/lua/vim/lsp/buf.lua
@@ -1,4 +1,3 @@
-local vim = vim
local api = vim.api
local validate = vim.validate
local util = require('vim.lsp.util')
@@ -111,7 +110,7 @@ end
--- about the context in which a completion was triggered (how it was triggered,
--- and by which trigger character, if applicable)
---
----@see |vim.lsp.protocol.constants.CompletionTriggerKind|
+---@see vim.lsp.protocol.constants.CompletionTriggerKind
function M.completion(context)
local params = util.make_position_params()
params.context = context
@@ -119,35 +118,30 @@ function M.completion(context)
end
---@private
---- If there is more than one client that supports the given method,
---- asks the user to select one.
---
----@returns The client that the user selected or nil
-local function select_client(method, on_choice)
- validate({
- on_choice = { on_choice, 'function', false },
- })
- local clients = vim.tbl_values(vim.lsp.buf_get_clients())
- clients = vim.tbl_filter(function(client)
- return client.supports_method(method)
- end, clients)
- -- better UX when choices are always in the same order (between restarts)
- table.sort(clients, function(a, b)
- return a.name < b.name
- end)
-
- if #clients > 1 then
- vim.ui.select(clients, {
- prompt = 'Select a language server:',
- format_item = function(client)
- return client.name
- end,
- }, on_choice)
- elseif #clients < 1 then
- on_choice(nil)
- else
- on_choice(clients[1])
- end
+---@return table {start={row, col}, end={row, col}} using (1, 0) indexing
+local function range_from_selection()
+ -- TODO: Use `vim.region()` instead https://github.com/neovim/neovim/pull/13896
+
+ -- [bufnum, lnum, col, off]; both row and column 1-indexed
+ local start = vim.fn.getpos('v')
+ local end_ = vim.fn.getpos('.')
+ local start_row = start[2]
+ local start_col = start[3]
+ local end_row = end_[2]
+ local end_col = end_[3]
+
+ -- A user can start visual selection at the end and move backwards
+ -- Normalize the range to start < end
+ if start_row == end_row and end_col < start_col then
+ end_col, start_col = start_col, end_col
+ elseif end_row < start_row then
+ start_row, end_row = end_row, start_row
+ start_col, end_col = end_col, start_col
+ end
+ return {
+ ['start'] = { start_row, start_col - 1 },
+ ['end'] = { end_row, end_col - 1 },
+ }
end
--- Formats a buffer using the attached (and optionally filtered) language
@@ -184,7 +178,12 @@ end
--- 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.
-
+---
+--- - range (table|nil) Range to format.
+--- Table must contain `start` and `end` keys with {row, col} tuples using
+--- (1,0) indexing.
+--- Defaults to current selection in visual mode
+--- Defaults to `nil` in other modes, formatting the full buffer
function M.format(options)
options = options or {}
local bufnr = options.bufnr or api.nvim_get_current_buf()
@@ -206,16 +205,32 @@ function M.format(options)
vim.notify('[LSP] Format request failed, no matching language servers.')
end
+ local mode = api.nvim_get_mode().mode
+ local range = options.range
+ if not range and mode == 'v' or mode == 'V' then
+ range = range_from_selection()
+ end
+
+ ---@private
+ local function set_range(client, params)
+ if range then
+ local range_params =
+ util.make_given_range_params(range.start, range['end'], bufnr, client.offset_encoding)
+ params.range = range_params.range
+ end
+ return params
+ end
+
+ local method = range and 'textDocument/rangeFormatting' or 'textDocument/formatting'
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']
+ local params = set_range(client, util.make_formatting_params(options.formatting_options))
+ client.request(method, params, function(...)
+ local handler = client.handlers[method] or vim.lsp.handlers[method]
handler(...)
do_format(next(clients, idx))
end, bufnr)
@@ -224,8 +239,8 @@ function M.format(options)
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)
+ local params = set_range(client, util.make_formatting_params(options.formatting_options))
+ local result, err = client.request_sync(method, params, timeout_ms, bufnr)
if result and result.result then
util.apply_text_edits(result.result, bufnr, client.offset_encoding)
elseif err then
@@ -235,138 +250,6 @@ function M.format(options)
end
end
---- Formats the current buffer.
----
----@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 = api.nvim_get_current_buf()
- select_client('textDocument/formatting', function(client)
- if client == nil then
- return
- end
-
- return client.request('textDocument/formatting', params, nil, bufnr)
- end)
-end
-
---- Performs |vim.lsp.buf.formatting()| synchronously.
----
---- Useful for running on save, to make sure buffer is formatted prior to being
---- saved. {timeout_ms} is passed on to |vim.lsp.buf_request_sync()|. Example:
----
---- <pre>
---- autocmd BufWritePre <buffer> lua vim.lsp.buf.formatting_sync()
---- </pre>
----
----@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 = api.nvim_get_current_buf()
- select_client('textDocument/formatting', function(client)
- if client == nil then
- return
- end
-
- local result, err = client.request_sync('textDocument/formatting', params, timeout_ms, bufnr)
- if result and result.result then
- util.apply_text_edits(result.result, bufnr, client.offset_encoding)
- elseif err then
- vim.notify('vim.lsp.buf.formatting_sync: ' .. err, vim.log.levels.WARN)
- end
- end)
-end
-
---- Formats the current buffer by sequentially requesting formatting from attached clients.
----
---- Useful when multiple clients with formatting capability are attached.
----
---- Since it's synchronous, can be used for running on save, to make sure buffer is formatted
---- prior to being saved. {timeout_ms} is passed on to the |vim.lsp.client| `request_sync` method.
---- Example:
---- <pre>
---- vim.api.nvim_command[[autocmd BufWritePre <buffer> lua vim.lsp.buf.formatting_seq_sync()]]
---- </pre>
----
----@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)
- 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
- -- if the client exists, move to the end of the list
- for i, client in pairs(clients) do
- if client.name == client_name then
- table.insert(clients, table.remove(clients, i))
- break
- end
- end
- end
-
- -- loop through the clients and make synchronous formatting requests
- for _, client in pairs(clients) do
- 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,
- api.nvim_get_current_buf()
- )
- if result and result.result then
- util.apply_text_edits(result.result, bufnr, client.offset_encoding)
- elseif err then
- vim.notify(
- string.format('vim.lsp.buf.formatting_seq_sync: (%s) %s', client.name, err),
- vim.log.levels.WARN
- )
- end
- end
- end
-end
-
---- Formats a given range.
----
----@param options Table with valid `FormattingOptions` entries.
----@param start_pos ({number, number}, optional) mark-indexed position.
----Defaults to the start of the last visual selection.
----@param end_pos ({number, number}, optional) mark-indexed position.
----Defaults to the end of the last visual selection.
-function M.range_formatting(options, start_pos, end_pos)
- local params = util.make_given_range_params(start_pos, end_pos)
- params.options = util.make_formatting_params(options).options
- select_client('textDocument/rangeFormatting', function(client)
- if client == nil then
- return
- end
-
- return client.request('textDocument/rangeFormatting', params)
- end)
-end
-
--- Renames all references to the symbol under the cursor.
---
---@param new_name string|nil If not provided, the user will be prompted for a new
@@ -565,14 +448,14 @@ end
--- Lists all the call sites of the symbol under the cursor in the
--- |quickfix| window. If the symbol can resolve to multiple
---- items, the user can pick one in the |inputlist|.
+--- items, the user can pick one in the |inputlist()|.
function M.incoming_calls()
call_hierarchy('callHierarchy/incomingCalls')
end
--- Lists all the items that are called by the symbol under the
--- cursor in the |quickfix| window. If the symbol can resolve to
---- multiple items, the user can pick one in the |inputlist|.
+--- multiple items, the user can pick one in the |inputlist()|.
function M.outgoing_calls()
call_hierarchy('callHierarchy/outgoingCalls')
end
@@ -681,9 +564,9 @@ end
---
--- Note: Usage of |vim.lsp.buf.document_highlight()| requires the following highlight groups
--- to be defined or you won't be able to see the actual highlights.
---- |LspReferenceText|
---- |LspReferenceRead|
---- |LspReferenceWrite|
+--- |hl-LspReferenceText|
+--- |hl-LspReferenceRead|
+--- |hl-LspReferenceWrite|
function M.document_highlight()
local params = util.make_position_params()
request('textDocument/documentHighlight', params)
@@ -885,23 +768,8 @@ function M.code_action(options)
local end_ = assert(options.range['end'], 'range must have a `end` property')
params = util.make_given_range_params(start, end_)
elseif mode == 'v' or mode == 'V' then
- -- [bufnum, lnum, col, off]; both row and column 1-indexed
- local start = vim.fn.getpos('v')
- local end_ = vim.fn.getpos('.')
- local start_row = start[2]
- local start_col = start[3]
- local end_row = end_[2]
- local end_col = end_[3]
-
- -- A user can start visual selection at the end and move backwards
- -- Normalize the range to start < end
- if start_row == end_row and end_col < start_col then
- end_col, start_col = start_col, end_col
- elseif end_row < start_row then
- start_row, end_row = end_row, start_row
- start_col, end_col = end_col, start_col
- end
- params = util.make_given_range_params({ start_row, start_col - 1 }, { end_row, end_col - 1 })
+ local range = range_from_selection()
+ params = util.make_given_range_params(range.start, range['end'])
else
params = util.make_range_params()
end
@@ -909,34 +777,6 @@ function M.code_action(options)
code_action_request(params, options)
end
---- Performs |vim.lsp.buf.code_action()| for a given range.
----
----
----@param context table|nil `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`.
----@param start_pos ({number, number}, optional) mark-indexed position.
----Defaults to the start of the last visual selection.
----@param end_pos ({number, number}, optional) mark-indexed position.
----Defaults to the end of the last visual selection.
-function M.range_code_action(context, start_pos, end_pos)
- vim.deprecate('vim.lsp.buf.range_code_action', 'vim.lsp.buf.code_action', '0.9.0')
- validate({ context = { context, 't', true } })
- context = context or {}
- if not context.diagnostics then
- 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
- code_action_request(params)
-end
-
--- Executes an LSP server command.
---
---@param command_params table A valid `ExecuteCommandParams` object
diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua
index 624436bc9b..93fd621161 100644
--- a/runtime/lua/vim/lsp/handlers.lua
+++ b/runtime/lua/vim/lsp/handlers.lua
@@ -1,7 +1,6 @@
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
local M = {}
@@ -389,7 +388,7 @@ M['textDocument/implementation'] = location_handler
---@param config table Configuration table.
--- - border: (default=nil)
--- - Add borders to the floating window
---- - See |vim.api.nvim_open_win()|
+--- - See |nvim_open_win()|
function M.signature_help(_, result, ctx, config)
config = config or {}
config.focus_id = ctx.method
@@ -512,6 +511,52 @@ M['window/showMessage'] = function(_, result, ctx, _)
return result
end
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_showDocument
+M['window/showDocument'] = function(_, result, ctx, _)
+ local uri = result.uri
+
+ if result.external then
+ -- TODO(lvimuser): ask the user for confirmation
+ local cmd
+ if vim.fn.has('win32') == 1 then
+ cmd = { 'cmd.exe', '/c', 'start', '""', vim.fn.shellescape(uri) }
+ elseif vim.fn.has('macunix') == 1 then
+ cmd = { 'open', vim.fn.shellescape(uri) }
+ else
+ cmd = { 'xdg-open', vim.fn.shellescape(uri) }
+ end
+
+ local ret = vim.fn.system(cmd)
+ if vim.v.shellerror ~= 0 then
+ return {
+ success = false,
+ error = {
+ code = protocol.ErrorCodes.UnknownErrorCode,
+ message = ret,
+ },
+ }
+ end
+
+ return { success = true }
+ end
+
+ 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)
+ if not client then
+ err_message({ 'LSP[', client_name, '] client has shut down after sending ', ctx.method })
+ return vim.NIL
+ end
+
+ local location = {
+ uri = uri,
+ range = result.selection,
+ }
+
+ local success = util.show_document(location, client.offset_encoding, true, result.takeFocus)
+ return { success = success or false }
+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)
diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua
index 27da60b4ae..4034753322 100644
--- a/runtime/lua/vim/lsp/protocol.lua
+++ b/runtime/lua/vim/lsp/protocol.lua
@@ -778,7 +778,7 @@ function protocol.make_client_capabilities()
},
},
showDocument = {
- support = false,
+ support = true,
},
},
}
diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua
index 0926912066..ff62623544 100644
--- a/runtime/lua/vim/lsp/rpc.lua
+++ b/runtime/lua/vim/lsp/rpc.lua
@@ -1,4 +1,3 @@
-local vim = vim
local uv = vim.loop
local log = require('vim.lsp.log')
local protocol = require('vim.lsp.protocol')
@@ -241,8 +240,401 @@ function default_dispatchers.on_error(code, err)
local _ = log.error() and log.error('client_error:', client_errors[code], err)
end
+---@private
+local function create_read_loop(handle_body, on_no_chunk, on_error)
+ local parse_chunk = coroutine.wrap(request_parser_loop)
+ parse_chunk()
+ return function(err, chunk)
+ if err then
+ on_error(err)
+ return
+ end
+
+ if not chunk then
+ if on_no_chunk then
+ on_no_chunk()
+ end
+ return
+ end
+
+ while true do
+ local headers, body = parse_chunk(chunk)
+ if headers then
+ handle_body(body)
+ chunk = ''
+ else
+ break
+ end
+ end
+ end
+end
+
+---@class RpcClient
+---@field message_index number
+---@field message_callbacks table
+---@field notify_reply_callbacks table
+---@field transport table
+---@field dispatchers table
+
+---@class RpcClient
+local Client = {}
+
+---@private
+function Client:encode_and_send(payload)
+ local _ = log.debug() and log.debug('rpc.send', payload)
+ if self.transport.is_closing() then
+ return false
+ end
+ local encoded = vim.json.encode(payload)
+ self.transport.write(format_message_with_content_length(encoded))
+ return true
+end
+
+---@private
+--- Sends a notification to the LSP server.
+---@param method (string) The invoked LSP method
+---@param params (table|nil): Parameters for the invoked LSP method
+---@returns (bool) `true` if notification could be sent, `false` if not
+function Client:notify(method, params)
+ return self:encode_and_send({
+ jsonrpc = '2.0',
+ method = method,
+ params = params,
+ })
+end
+
+---@private
+--- sends an error object to the remote LSP process.
+function Client:send_response(request_id, err, result)
+ return self:encode_and_send({
+ id = request_id,
+ jsonrpc = '2.0',
+ error = err,
+ result = result,
+ })
+end
+
+---@private
+--- Sends a request to the LSP server and runs {callback} upon response.
+---
+---@param method (string) The invoked LSP method
+---@param params (table|nil) Parameters for the invoked LSP method
+---@param callback (function) Callback to invoke
+---@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
+function Client:request(method, params, callback, notify_reply_callback)
+ validate({
+ callback = { callback, 'f' },
+ notify_reply_callback = { notify_reply_callback, 'f', true },
+ })
+ self.message_index = self.message_index + 1
+ local message_id = self.message_index
+ local result = self:encode_and_send({
+ id = message_id,
+ jsonrpc = '2.0',
+ method = method,
+ params = params,
+ })
+ local message_callbacks = self.message_callbacks
+ local notify_reply_callbacks = self.notify_reply_callbacks
+ if result then
+ if message_callbacks then
+ message_callbacks[message_id] = schedule_wrap(callback)
+ else
+ return false
+ end
+ if notify_reply_callback and notify_reply_callbacks then
+ notify_reply_callbacks[message_id] = schedule_wrap(notify_reply_callback)
+ end
+ return result, message_id
+ else
+ return false
+ end
+end
+
+---@private
+function Client:on_error(errkind, ...)
+ assert(client_errors[errkind])
+ -- TODO what to do if this fails?
+ pcall(self.dispatchers.on_error, errkind, ...)
+end
+
+---@private
+function Client:pcall_handler(errkind, status, head, ...)
+ if not status then
+ self:on_error(errkind, head, ...)
+ return status, head
+ end
+ return status, head, ...
+end
+
+---@private
+function Client:try_call(errkind, fn, ...)
+ return self:pcall_handler(errkind, pcall(fn, ...))
+end
+
+-- TODO periodically check message_callbacks for old requests past a certain
+-- time and log them. This would require storing the timestamp. I could call
+-- them with an error then, perhaps.
+
+---@private
+function Client:handle_body(body)
+ local ok, decoded = pcall(vim.json.decode, body, { luanil = { object = true } })
+ if not ok then
+ self:on_error(client_errors.INVALID_SERVER_JSON, decoded)
+ return
+ end
+ local _ = log.debug() and log.debug('rpc.receive', decoded)
+
+ if type(decoded.method) == 'string' and decoded.id then
+ local err
+ -- Schedule here so that the users functions don't trigger an error and
+ -- we can still use the result.
+ schedule(function()
+ local status, result
+ status, result, err = self:try_call(
+ client_errors.SERVER_REQUEST_HANDLER_ERROR,
+ self.dispatchers.server_request,
+ decoded.method,
+ decoded.params
+ )
+ local _ = log.debug()
+ and log.debug(
+ 'server_request: callback result',
+ { status = status, result = result, err = err }
+ )
+ if status then
+ if result == nil and err == nil then
+ error(
+ string.format(
+ 'method %q: either a result or an error must be sent to the server in response',
+ decoded.method
+ )
+ )
+ end
+ if err then
+ assert(
+ type(err) == 'table',
+ 'err must be a table. Use rpc_response_error to help format errors.'
+ )
+ local code_name = assert(
+ protocol.ErrorCodes[err.code],
+ 'Errors must use protocol.ErrorCodes. Use rpc_response_error to help format errors.'
+ )
+ err.message = err.message or code_name
+ end
+ else
+ -- On an exception, result will contain the error message.
+ err = rpc_response_error(protocol.ErrorCodes.InternalError, result)
+ result = nil
+ end
+ self:send_response(decoded.id, err, result)
+ end)
+ -- 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 = assert(tonumber(decoded.id), 'response id must be a number')
+
+ -- Notify the user that a response was received for the request
+ local notify_reply_callbacks = self.notify_reply_callbacks
+ 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' },
+ })
+ notify_reply_callback(result_id)
+ notify_reply_callbacks[result_id] = nil
+ end
+
+ local message_callbacks = self.message_callbacks
+
+ -- Do not surface RequestCancelled to users, it is RPC-internal.
+ 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)
+ mute_error = true
+ end
+
+ if mute_error then
+ -- Clear any callback since this is cancelled now.
+ -- This is safe to do assuming that these conditions hold:
+ -- - The server will not send a result callback after this cancellation.
+ -- - If the server sent this cancellation ACK after sending the result, the user of this RPC
+ -- client will ignore the result themselves.
+ if result_id and message_callbacks then
+ message_callbacks[result_id] = nil
+ end
+ return
+ end
+ end
+
+ local callback = message_callbacks and message_callbacks[result_id]
+ if callback then
+ message_callbacks[result_id] = nil
+ validate({
+ callback = { callback, 'f' },
+ })
+ if decoded.error then
+ decoded.error = setmetatable(decoded.error, {
+ __tostring = format_rpc_error,
+ })
+ end
+ self:try_call(
+ client_errors.SERVER_RESULT_CALLBACK_ERROR,
+ callback,
+ decoded.error,
+ decoded.result
+ )
+ else
+ self:on_error(client_errors.NO_RESULT_CALLBACK_FOUND, decoded)
+ local _ = log.error() and log.error('No callback found for server response id ' .. result_id)
+ end
+ elseif type(decoded.method) == 'string' then
+ -- Notification
+ self:try_call(
+ client_errors.NOTIFICATION_HANDLER_ERROR,
+ self.dispatchers.notification,
+ decoded.method,
+ decoded.params
+ )
+ else
+ -- Invalid server message
+ self:on_error(client_errors.INVALID_SERVER_MESSAGE, decoded)
+ end
+end
+
+---@private
+---@return RpcClient
+local function new_client(dispatchers, transport)
+ local state = {
+ message_index = 0,
+ message_callbacks = {},
+ notify_reply_callbacks = {},
+ transport = transport,
+ dispatchers = dispatchers,
+ }
+ return setmetatable(state, { __index = Client })
+end
+
+---@private
+---@param client RpcClient
+local function public_client(client)
+ local result = {}
+
+ ---@private
+ function result.is_closing()
+ return client.transport.is_closing()
+ end
+
+ ---@private
+ function result.terminate()
+ client.transport.terminate()
+ end
+
+ --- Sends a request to the LSP server and runs {callback} upon response.
+ ---
+ ---@param method (string) The invoked LSP method
+ ---@param params (table|nil) Parameters for the invoked LSP method
+ ---@param callback (function) Callback to invoke
+ ---@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
+ function result.request(method, params, callback, notify_reply_callback)
+ return client:request(method, params, callback, notify_reply_callback)
+ end
+
+ --- Sends a notification to the LSP server.
+ ---@param method (string) The invoked LSP method
+ ---@param params (table|nil): Parameters for the invoked LSP method
+ ---@returns (bool) `true` if notification could be sent, `false` if not
+ function result.notify(method, params)
+ return client:notify(method, params)
+ end
+
+ return result
+end
+
+---@private
+local function merge_dispatchers(dispatchers)
+ if dispatchers then
+ local user_dispatchers = dispatchers
+ dispatchers = {}
+ for dispatch_name, default_dispatch in pairs(default_dispatchers) do
+ 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))
+ 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.
+ then
+ user_dispatcher = schedule_wrap(user_dispatcher)
+ end
+ dispatchers[dispatch_name] = user_dispatcher
+ else
+ dispatchers[dispatch_name] = default_dispatch
+ end
+ end
+ else
+ dispatchers = default_dispatchers
+ end
+ return dispatchers
+end
+
+--- Create a LSP RPC client factory that connects via TCP to the given host
+--- and port
+---
+---@param host string
+---@param port number
+---@return function
+local function connect(host, port)
+ return function(dispatchers)
+ dispatchers = merge_dispatchers(dispatchers)
+ local tcp = uv.new_tcp()
+ local closing = false
+ local transport = {
+ write = function(msg)
+ tcp:write(msg)
+ end,
+ is_closing = function()
+ return closing
+ end,
+ terminate = function()
+ if not closing then
+ closing = true
+ tcp:shutdown()
+ tcp:close()
+ dispatchers.on_exit(0, 0)
+ end
+ end,
+ }
+ local client = new_client(dispatchers, transport)
+ tcp:connect(host, port, function(err)
+ if err then
+ vim.schedule(function()
+ vim.notify(
+ string.format('Could not connect to %s:%s, reason: %s', host, port, vim.inspect(err)),
+ vim.log.levels.WARN
+ )
+ end)
+ return
+ end
+ local handle_body = function(body)
+ client:handle_body(body)
+ end
+ tcp:read_start(create_read_loop(handle_body, transport.terminate, function(read_err)
+ client:on_error(client_errors.READ_ERROR, read_err)
+ end))
+ end)
+
+ return public_client(client)
+ end
+end
+
--- Starts an LSP server process and create an LSP RPC client object to
---- interact with it. Communication with the server is currently limited to stdio.
+--- interact with it. Communication with the spawned process happens via stdio. For
+--- communication via TCP, spawn a process manually and use |vim.lsp.rpc.connect()|
---
---@param cmd (string) Command to start the LSP server.
---@param cmd_args (table) List of additional string arguments to pass to {cmd}.
@@ -261,11 +653,8 @@ end
---@returns Methods:
--- - `notify()` |vim.lsp.rpc.notify()|
--- - `request()` |vim.lsp.rpc.request()|
----
----@returns Members:
---- - {pid} (number) The LSP server's PID.
---- - {handle} A handle for low-level interaction with the LSP server process
---- |vim.loop|.
+--- - `is_closing()` returns a boolean indicating if the RPC is closing.
+--- - `terminate()` terminates the RPC client.
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 })
@@ -278,161 +667,64 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
if extra_spawn_params and extra_spawn_params.cwd then
assert(is_dir(extra_spawn_params.cwd), 'cwd must be a directory')
end
- if dispatchers then
- local user_dispatchers = dispatchers
- dispatchers = {}
- for dispatch_name, default_dispatch in pairs(default_dispatchers) do
- 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))
- 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.
- then
- user_dispatcher = schedule_wrap(user_dispatcher)
- end
- dispatchers[dispatch_name] = user_dispatcher
- else
- dispatchers[dispatch_name] = default_dispatch
- end
- end
- else
- dispatchers = default_dispatchers
- end
+ dispatchers = merge_dispatchers(dispatchers)
local stdin = uv.new_pipe(false)
local stdout = uv.new_pipe(false)
local stderr = uv.new_pipe(false)
-
- local message_index = 0
- local message_callbacks = {}
- local notify_reply_callbacks = {}
-
local handle, pid
- do
- ---@private
- --- Callback for |vim.loop.spawn()| Closes all streams and runs the `on_exit` dispatcher.
- ---@param code (number) Exit code
- ---@param signal (number) Signal that was used to terminate (if any)
- local function onexit(code, signal)
- stdin:close()
- stdout:close()
- stderr:close()
- handle:close()
- -- Make sure that message_callbacks/notify_reply_callbacks can be gc'd.
- message_callbacks = nil
- notify_reply_callbacks = nil
- dispatchers.on_exit(code, signal)
- end
- local spawn_params = {
- 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
- stdin:close()
- stdout:close()
- stderr:close()
- 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)
+
+ local client = new_client(dispatchers, {
+ write = function(msg)
+ stdin:write(msg)
+ end,
+ is_closing = function()
+ return handle == nil or handle:is_closing()
+ end,
+ terminate = function()
+ if handle then
+ handle:kill(15)
end
- vim.notify(msg, vim.log.levels.WARN)
- return
- end
- end
+ end,
+ })
---@private
- --- Encodes {payload} into a JSON-RPC message and sends it to the remote
- --- process.
- ---
- ---@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 encoded = vim.json.encode(payload)
- stdin:write(format_message_with_content_length(encoded))
- return true
- end
-
- -- FIXME: DOC: Should be placed on the RPC client object returned by
- -- `start()`
- --
- --- Sends a notification to the LSP server.
- ---@param method (string) The invoked LSP method
- ---@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,
- })
+ --- Callback for |vim.loop.spawn()| Closes all streams and runs the `on_exit` dispatcher.
+ ---@param code (number) Exit code
+ ---@param signal (number) Signal that was used to terminate (if any)
+ local function onexit(code, signal)
+ stdin:close()
+ stdout:close()
+ stderr:close()
+ handle:close()
+ dispatchers.on_exit(code, signal)
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,
- })
+ local spawn_params = {
+ 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
-
- -- FIXME: DOC: Should be placed on the RPC client object returned by
- -- `start()`
- --
- --- Sends a request to the LSP server and runs {callback} upon response.
- ---
- ---@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|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 },
- })
- 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,
- })
- if result then
- if message_callbacks then
- message_callbacks[message_id] = schedule_wrap(callback)
- else
- return false
- end
- if notify_reply_callback and notify_reply_callbacks then
- notify_reply_callbacks[message_id] = schedule_wrap(notify_reply_callback)
- end
- return result, message_id
+ handle, pid = uv.spawn(cmd, spawn_params, onexit)
+ if handle == nil then
+ stdin:close()
+ stdout:close()
+ stderr:close()
+ 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
- return false
+ msg = msg .. string.format(' with error message: %s', pid)
end
+ vim.notify(msg, vim.log.levels.WARN)
+ return
end
stderr:read_start(function(_, chunk)
@@ -441,195 +733,22 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
end
end)
- ---@private
- local function on_error(errkind, ...)
- assert(client_errors[errkind])
- -- TODO what to do if this fails?
- pcall(dispatchers.on_error, errkind, ...)
- end
- ---@private
- local function pcall_handler(errkind, status, head, ...)
- if not status then
- on_error(errkind, head, ...)
- return status, head
- end
- return status, head, ...
+ local handle_body = function(body)
+ client:handle_body(body)
end
- ---@private
- local function try_call(errkind, fn, ...)
- return pcall_handler(errkind, pcall(fn, ...))
- end
-
- -- TODO periodically check message_callbacks for old requests past a certain
- -- time and log them. This would require storing the timestamp. I could call
- -- them with an error then, perhaps.
+ stdout:read_start(create_read_loop(handle_body, nil, function(err)
+ client:on_error(client_errors.READ_ERROR, err)
+ end))
- ---@private
- local function handle_body(body)
- local ok, decoded = pcall(vim.json.decode, body, { luanil = { object = true } })
- if not ok then
- on_error(client_errors.INVALID_SERVER_JSON, decoded)
- return
- end
- local _ = log.debug() and log.debug('rpc.receive', decoded)
-
- if type(decoded.method) == 'string' and decoded.id then
- local err
- -- Schedule here so that the users functions don't trigger an error and
- -- we can still use the result.
- schedule(function()
- local status, result
- status, result, err = 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
- )
- )
- end
- if err then
- assert(
- type(err) == 'table',
- 'err must be a table. Use rpc_response_error to help format errors.'
- )
- local code_name = assert(
- protocol.ErrorCodes[err.code],
- 'Errors must use protocol.ErrorCodes. Use rpc_response_error to help format errors.'
- )
- err.message = err.message or code_name
- end
- else
- -- On an exception, result will contain the error message.
- err = rpc_response_error(protocol.ErrorCodes.InternalError, result)
- result = nil
- end
- send_response(decoded.id, err, result)
- end)
- -- 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 = assert(tonumber(decoded.id), 'response id must be a number')
-
- -- 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' },
- })
- notify_reply_callback(result_id)
- notify_reply_callbacks[result_id] = nil
- end
-
- -- Do not surface RequestCancelled to users, it is RPC-internal.
- 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)
- mute_error = true
- end
-
- if mute_error then
- -- Clear any callback since this is cancelled now.
- -- This is safe to do assuming that these conditions hold:
- -- - The server will not send a result callback after this cancellation.
- -- - If the server sent this cancellation ACK after sending the result, the user of this RPC
- -- client will ignore the result themselves.
- if result_id and message_callbacks then
- message_callbacks[result_id] = nil
- end
- return
- end
- end
-
- local callback = message_callbacks and message_callbacks[result_id]
- if callback then
- message_callbacks[result_id] = nil
- validate({
- callback = { callback, 'f' },
- })
- if decoded.error then
- decoded.error = setmetatable(decoded.error, {
- __tostring = format_rpc_error,
- })
- end
- 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)
- end
- elseif type(decoded.method) == 'string' then
- -- Notification
- 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)
- end
- end
-
- local request_parser = coroutine.wrap(request_parser_loop)
- request_parser()
- stdout:read_start(function(err, chunk)
- if err then
- -- TODO better handling. Can these be intermittent errors?
- on_error(client_errors.READ_ERROR, err)
- return
- end
- -- This should signal that we are done reading from the client.
- if not chunk then
- return
- end
- -- Flush anything in the parser by looping until we don't get a result
- -- anymore.
- while true do
- local headers, body = request_parser(chunk)
- -- If we successfully parsed, then handle the response.
- if headers then
- handle_body(body)
- -- Set chunk to empty so that we can call request_parser to get
- -- anything existing in the parser to flush.
- chunk = ''
- else
- break
- end
- end
- end)
-
- return {
- pid = pid,
- handle = handle,
- request = request,
- notify = notify,
- }
+ return public_client(client)
end
return {
start = start,
+ connect = connect,
rpc_response_error = rpc_response_error,
format_rpc_error = format_rpc_error,
client_errors = client_errors,
+ create_read_loop = create_read_loop,
}
-- vim:sw=2 ts=2 et
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index 283099bbcf..b0f9c1660e 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -1,6 +1,5 @@
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
@@ -110,6 +109,15 @@ local function split_lines(value)
return split(value, '\n', true)
end
+---@private
+local function create_window_without_focus()
+ local prev = vim.api.nvim_get_current_win()
+ vim.cmd.new()
+ local new = vim.api.nvim_get_current_win()
+ vim.api.nvim_set_current_win(prev)
+ return new
+end
+
--- Convert byte index to `encoding` index.
--- Convenience wrapper around vim.str_utfindex
---@param line string line to be indexed
@@ -459,35 +467,52 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding)
text = split(text_edit.newText, '\n', true),
}
- -- Some LSP servers may return +1 range of the buffer content but nvim_buf_set_text can't accept it so we should fix it here.
local max = 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 the whole edit is after the lines in the buffer we can simply add the new text to the end
+ -- of the buffer.
+ if max <= e.start_row then
+ api.nvim_buf_set_lines(bufnr, max, max, false, e.text)
+ else
+ local last_line_len = #(get_line(bufnr, math.min(e.end_row, max - 1)) or '')
+ -- 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.
if max <= e.end_row then
e.end_row = max - 1
- e.end_col = len
+ e.end_col = last_line_len
+ has_eol_text_edit = true
+ else
+ -- If the replacement is over the end of a line (i.e. e.end_col is out of bounds and the
+ -- replacement text ends with a newline We can likely assume that the replacement is assumed
+ -- to be meant to replace the newline with another newline and we need to make sure this
+ -- doesn't add an extra empty line. E.g. when the last line to be replaced contains a '\r'
+ -- in the file some servers (clangd on windows) will include that character in the line
+ -- while nvim_buf_set_text doesn't count it as part of the line.
+ if
+ e.end_col > last_line_len
+ and #text_edit.newText > 0
+ and string.sub(text_edit.newText, -1) == '\n'
+ then
+ table.remove(e.text, #e.text)
+ end
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)
- is_cursor_fixed = true
- elseif e.end_row == cursor.row and e.end_col <= cursor.col then
- cursor.row = cursor.row + (#e.text - row_count)
- cursor.col = #e.text[#e.text] + (cursor.col - e.end_col)
- if #e.text == 1 then
- cursor.col = cursor.col + e.start_col
+ -- Make sure we don't go out of bounds for e.end_col
+ e.end_col = math.min(last_line_len, e.end_col)
+
+ 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)
+ is_cursor_fixed = true
+ elseif e.end_row == cursor.row and e.end_col <= cursor.col then
+ cursor.row = cursor.row + (#e.text - row_count)
+ cursor.col = #e.text[#e.text] + (cursor.col - e.end_col)
+ if #e.text == 1 then
+ cursor.col = cursor.col + e.start_col
+ end
+ is_cursor_fixed = true
end
- is_cursor_fixed = true
end
end
@@ -755,8 +780,11 @@ local function create_file(change)
-- from spec: Overwrite wins over `ignoreIfExists`
local fname = vim.uri_to_fname(change.uri)
if not opts.ignoreIfExists or opts.overwrite then
+ vim.fn.mkdir(vim.fs.dirname(fname), 'p')
local file = io.open(fname, 'w')
- file:close()
+ if file then
+ file:close()
+ end
end
vim.fn.bufadd(fname)
end
@@ -887,8 +915,8 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers
return
end
--The active signature. If omitted or the value lies outside the range of
- --`signatures` the value defaults to zero or is ignored if `signatures.length
- --=== 0`. Whenever possible implementors should make an active decision about
+ --`signatures` the value defaults to zero or is ignored if `signatures.length == 0`.
+ --Whenever possible implementors should make an active decision about
--the active signature and shouldn't rely on a default value.
local contents = {}
local active_hl
@@ -1036,50 +1064,80 @@ function M.make_floating_popup_options(width, height, opts)
}
end
---- Jumps to a location.
+--- Shows document and optionally jumps to the location.
---
---@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, offset_encoding, reuse_win)
+---@param offset_encoding "utf-8" | "utf-16" | "utf-32"
+---@param opts table options
+--- - reuse_win (boolean) Jump to existing window if buffer is already open.
+--- - focus (boolean) Whether to focus/jump to location if possible. Defaults to true.
+---@return boolean `true` if succeeded
+function M.show_document(location, offset_encoding, opts)
-- location may be Location or LocationLink
local uri = location.uri or location.targetUri
if uri == nil then
- return
+ return false
end
if offset_encoding == nil then
- vim.notify_once(
- 'jump_to_location must be called with valid offset encoding',
- vim.log.levels.WARN
- )
+ vim.notify_once('show_document 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'")
- -- 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')
+ opts = opts or {}
+ local focus = vim.F.if_nil(opts.focus, true)
+ if focus then
+ -- Save position in jumplist
+ vim.cmd("normal! m'")
- --- Jump to new location (adjusting for UTF-16 encoding of characters)
- local win = reuse_win and bufwinid(bufnr)
- if win then
+ -- 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')
+ end
+
+ local win = opts.reuse_win and bufwinid(bufnr)
+ or focus and api.nvim_get_current_win()
+ or create_window_without_focus()
+
+ api.nvim_buf_set_option(bufnr, 'buflisted', true)
+ api.nvim_win_set_buf(win, bufnr)
+ if focus then
api.nvim_set_current_win(win)
- else
- api.nvim_buf_set_option(bufnr, 'buflisted', true)
- api.nvim_set_current_buf(bufnr)
end
+
+ -- location may be Location or LocationLink
local range = location.range or location.targetSelectionRange
- local row = range.start.line
- 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')
+ if range then
+ --- Jump to new location (adjusting for encoding of characters)
+ local row = range.start.line
+ local col = get_line_byte_from_position(bufnr, range.start, offset_encoding)
+ api.nvim_win_set_cursor(win, { row + 1, col })
+ api.nvim_win_call(win, function()
+ -- Open folds under the cursor
+ vim.cmd('normal! zv')
+ end)
+ end
+
return true
end
+--- Jumps to a location.
+---
+---@param location table (`Location`|`LocationLink`)
+---@param offset_encoding "utf-8" | "utf-16" | "utf-32"
+---@param reuse_win boolean Jump to existing window if buffer is already open.
+---@return boolean `true` if the jump succeeded
+function M.jump_to_location(location, offset_encoding, reuse_win)
+ if offset_encoding == nil then
+ vim.notify_once(
+ 'jump_to_location must be called with valid offset encoding',
+ vim.log.levels.WARN
+ )
+ end
+
+ return M.show_document(location, offset_encoding, { reuse_win = reuse_win, focus = true })
+end
+
--- Previews a location in a floating window
---
--- behavior depends on type of location:
@@ -1484,7 +1542,7 @@ end
---
---@param contents table of lines to show in window
---@param syntax string of syntax to set for opened buffer
----@param opts table with optional fields (additional keys are passed on to |vim.api.nvim_open_win()|)
+---@param opts table with optional fields (additional keys are passed on to |nvim_open_win()|)
--- - height: (number) height of floating window
--- - width: (number) width of floating window
--- - wrap: (boolean, default true) wrap long lines
@@ -1799,7 +1857,7 @@ end
--- CAUTION: Modifies the input in-place!
---
---@param lines (table) list of lines
----@returns (string) filetype or 'markdown' if it was unchanged.
+---@returns (string) filetype or "markdown" if it was unchanged.
function M.try_trim_markdown_code_blocks(lines)
local language_id = lines[1]:match('^```(.*)')
if language_id then
@@ -1972,7 +2030,7 @@ function M.make_workspace_params(added, removed)
end
--- Returns indentation size.
---
----@see |shiftwidth|
+---@see 'shiftwidth'
---@param bufnr (number|nil): Buffer handle, defaults to current
---@returns (number) indentation size
function M.get_effective_tabstop(bufnr)
diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua
index e1b4ed4ea9..f03d608e56 100644
--- a/runtime/lua/vim/shared.lua
+++ b/runtime/lua/vim/shared.lua
@@ -6,7 +6,7 @@
-- 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 {}
+vim = vim or {}
--- Returns a deep copy of the given object. Non-table objects are copied as
--- in a typical Lua assignment, whereas table objects are copied recursively.
@@ -14,8 +14,9 @@ local vim = vim or {}
--- same functions as those in the input table. Userdata and threads are not
--- copied and will throw an error.
---
----@param orig table Table to copy
----@return table Table of copied keys and (nested) values.
+---@generic T: table
+---@param orig T Table to copy
+---@return T Table of copied keys and (nested) values.
function vim.deepcopy(orig) end -- luacheck: no unused
vim.deepcopy = (function()
local function _id(v)
@@ -62,7 +63,7 @@ end)()
---@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
+---@return fun():string (function) Iterator over the split components
function vim.gsplit(s, sep, plain)
vim.validate({ s = { s, 's' }, sep = { sep, 's' }, plain = { plain, 'b', true } })
@@ -99,21 +100,23 @@ end
---
--- Examples:
--- <pre>
---- split(":aa::b:", ":") --> {'','aa','','b',''}
---- split("axaby", "ab?") --> {'','x','y'}
---- split("x*yz*o", "*", {plain=true}) --> {'x','yz','o'}
---- split("|x|y|z|", "|", {trimempty=true}) --> {'x', 'y', 'z'}
+--- split(":aa::b:", ":") => {'','aa','','b',''}
+--- split("axaby", "ab?") => {'','x','y'}
+--- split("x*yz*o", "*", {plain=true}) => {'x','yz','o'}
+--- split("|x|y|z|", "|", {trimempty=true}) => {'x', 'y', 'z'}
--- </pre>
---
---@see |vim.gsplit()|
---
+---@alias split_kwargs {plain: boolean, trimempty: boolean} | boolean | nil
+---
---@param s string String to split
---@param sep string Separator or pattern
----@param kwargs table Keyword arguments:
+---@param kwargs? {plain: boolean, trimempty: boolean} (table|nil) Keyword arguments:
--- - plain: (boolean) If `true` use `sep` literally (passed to string.find)
--- - trimempty: (boolean) If `true` remove empty items from the front
--- and back of the list
----@return table List of split components
+---@return string[] List of split components
function vim.split(s, sep, kwargs)
local plain
local trimempty = false
@@ -156,8 +159,9 @@ end
---
---@see From https://github.com/premake/premake-core/blob/master/src/base/table.lua
---
----@param t table Table
----@return table List of keys
+---@param t table<T, any> (table) Table
+---@generic T: table
+---@return T[] (list) List of keys
function vim.tbl_keys(t)
assert(type(t) == 'table', string.format('Expected table, got %s', type(t)))
@@ -171,8 +175,9 @@ 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 Table
----@return table List of values
+---@generic T
+---@param t table<any, T> (table) Table
+---@return T[] (list) List of values
function vim.tbl_values(t)
assert(type(t) == 'table', string.format('Expected table, got %s', type(t)))
@@ -185,8 +190,9 @@ end
--- Apply a function to all values of a table.
---
----@param func function|table Function or callable table
----@param t table Table
+---@generic T
+---@param func fun(value: T): any (function) Function
+---@param t table<any, T> (table) Table
---@return table Table of transformed values
function vim.tbl_map(func, t)
vim.validate({ func = { func, 'c' }, t = { t, 't' } })
@@ -200,9 +206,10 @@ end
--- Filter a table using a predicate function
---
----@param func function|table Function or callable table
----@param t table Table
----@return table Table of filtered values
+---@generic T
+---@param func fun(value: T): boolean (function) Function
+---@param t table<any, T> (table) Table
+---@return T[] (table) Table of filtered values
function vim.tbl_filter(func, t)
vim.validate({ func = { func, 'c' }, t = { t, 't' } })
@@ -302,14 +309,16 @@ end
--- Merges recursively two or more map-like tables.
---
----@see |tbl_extend()|
+---@see |vim.tbl_extend()|
---
----@param behavior string Decides what to do if a key is found in more than one map:
+---@generic T1: table
+---@generic T2: table
+---@param behavior "error"|"keep"|"force" (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 ... table Two or more map-like tables
----@return table Merged table
+---@param ... T2 Two or more map-like tables
+---@return T1|T2 (table) Merged table
function vim.tbl_deep_extend(behavior, ...)
return tbl_extend(behavior, true, ...)
end
@@ -405,11 +414,12 @@ end
---
---@see |vim.tbl_extend()|
---
----@param dst table List which will be modified and appended to
+---@generic T: table
+---@param dst T List which will be modified and appended to
---@param src table List from which values will be inserted
----@param start number Start index on src. Defaults to 1
----@param finish number Final index on src. Defaults to `#src`
----@return table dst
+---@param start? number Start index on src. Defaults to 1
+---@param finish? number Final index on src. Defaults to `#src`
+---@return T dst
function vim.list_extend(dst, src, start, finish)
vim.validate({
dst = { dst, 't' },
@@ -504,10 +514,11 @@ end
--- Creates a copy of a table containing only elements from start to end (inclusive)
---
----@param list table Table
+---@generic T
+---@param list T[] (list) 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)
+---@return T[] (list) 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
@@ -715,5 +726,30 @@ function vim.is_callable(f)
return type(m.__call) == 'function'
end
+--- Creates a table whose members are automatically created when accessed, if they don't already
+--- exist.
+---
+--- They mimic defaultdict in python.
+---
+--- If {create} is `nil`, this will create a defaulttable whose constructor function is
+--- this function, effectively allowing to create nested tables on the fly:
+---
+--- <pre>
+--- local a = vim.defaulttable()
+--- a.b.c = 1
+--- </pre>
+---
+---@param create function|nil The function called to create a missing value.
+---@return table Empty table with metamethod
+function vim.defaulttable(create)
+ create = create or vim.defaulttable
+ return setmetatable({}, {
+ __index = function(tbl, key)
+ rawset(tbl, key, create())
+ return rawget(tbl, key)
+ end,
+ })
+end
+
return vim
-- vim:sw=2 ts=2 et
diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua
index 70f2c425ed..6cd00516bf 100644
--- a/runtime/lua/vim/treesitter.lua
+++ b/runtime/lua/vim/treesitter.lua
@@ -3,10 +3,7 @@ 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 parsers = setmetatable({}, { __mode = 'v' })
local M = vim.tbl_extend('error', query, language)
@@ -28,13 +25,15 @@ setmetatable(M, {
end,
})
---- Creates a new parser.
+--- Creates a new parser
---
---- It is not recommended to use this, use vim.treesitter.get_parser() instead.
+--- It is not recommended to use this; use |get_parser()| instead.
---
----@param bufnr The buffer the parser will be tied to
----@param lang The language of the parser
----@param opts Options to pass to the created language tree
+---@param bufnr string Buffer the parser will be tied to (0 for current buffer)
+---@param lang string Language of the parser
+---@param opts (table|nil) Options to pass to the created language tree
+---
+---@return LanguageTree |LanguageTree| object to use for parsing
function M._create_parser(bufnr, lang, opts)
language.require_language(lang)
if bufnr == 0 then
@@ -74,16 +73,15 @@ function M._create_parser(bufnr, lang, opts)
return self
end
---- Gets the parser for this bufnr / ft combination.
+--- Returns the parser for a specific buffer and filetype and attaches it to the buffer
---
---- If needed this will create the parser.
---- Unconditionally attach the provided callback
+--- If needed, this will create the parser.
---
----@param bufnr The buffer the parser should be tied to
----@param lang The filetype of this parser
----@param opts Options object to pass to the created language tree
+---@param bufnr (number|nil) Buffer the parser should be tied to (default: current buffer)
+---@param lang (string|nil) Filetype of this parser (default: buffer filetype)
+---@param opts (table|nil) Options to pass to the created language tree
---
----@returns The parser
+---@return LanguageTree |LanguageTree| object to use for parsing
function M.get_parser(bufnr, lang, opts)
opts = opts or {}
@@ -103,11 +101,13 @@ function M.get_parser(bufnr, lang, opts)
return parsers[bufnr]
end
---- Gets a string parser
+--- Returns a string parser
+---
+---@param str string Text to parse
+---@param lang string Language of this string
+---@param opts (table|nil) Options to pass to the created language tree
---
----@param str The string to parse
----@param lang The language of this string
----@param opts Options to pass to the created language tree
+---@return LanguageTree |LanguageTree| object to use for parsing
function M.get_string_parser(str, lang, opts)
vim.validate({
str = { str, 'string' },
@@ -118,4 +118,233 @@ function M.get_string_parser(str, lang, opts)
return LanguageTree.new(str, lang, opts)
end
+--- Determines whether a node is the ancestor of another
+---
+---@param dest userdata Possible ancestor |tsnode|
+---@param source userdata Possible descendant |tsnode|
+---
+---@return boolean True if {dest} is an ancestor of {source}
+function M.is_ancestor(dest, source)
+ if not (dest and source) then
+ return false
+ end
+
+ local current = source
+ while current ~= nil do
+ if current == dest then
+ return true
+ end
+
+ current = current:parent()
+ end
+
+ return false
+end
+
+--- Returns the node's range or an unpacked range table
+---
+---@param node_or_range (userdata|table) |tsnode| or table of positions
+---
+---@return table `{ start_row, start_col, end_row, end_col }`
+function M.get_node_range(node_or_range)
+ if type(node_or_range) == 'table' then
+ return unpack(node_or_range)
+ else
+ return node_or_range:range()
+ end
+end
+
+--- Determines whether (line, col) position is in node range
+---
+---@param node userdata |tsnode| defining the range
+---@param line number Line (0-based)
+---@param col number Column (0-based)
+---
+---@return boolean True if the position is in node range
+function M.is_in_node_range(node, line, col)
+ local start_line, start_col, end_line, end_col = M.get_node_range(node)
+ if line >= start_line and line <= end_line then
+ if line == start_line and line == end_line then
+ return col >= start_col and col < end_col
+ elseif line == start_line then
+ return col >= start_col
+ elseif line == end_line then
+ return col < end_col
+ else
+ return true
+ end
+ else
+ return false
+ end
+end
+
+--- Determines if a node contains a range
+---
+---@param node userdata |tsnode|
+---@param range table
+---
+---@return boolean True if the {node} contains the {range}
+function M.node_contains(node, range)
+ local start_row, start_col, end_row, end_col = node: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])
+
+ return start_fits and end_fits
+end
+
+--- Returns a list of highlight captures at the given position
+---
+--- Each capture is represented by a table containing the capture name as a string as
+--- well as a table of metadata (`priority`, `conceal`, ...; empty if none are defined).
+---
+---@param bufnr number Buffer number (0 for current buffer)
+---@param row number Position row
+---@param col number Position column
+---
+---@return table[] List of captures `{ capture = "capture name", metadata = { ... } }`
+function M.get_captures_at_pos(bufnr, row, col)
+ if bufnr == 0 then
+ bufnr = a.nvim_get_current_buf()
+ end
+ local buf_highlighter = M.highlighter.active[bufnr]
+
+ if not buf_highlighter then
+ return {}
+ end
+
+ local matches = {}
+
+ buf_highlighter.tree:for_each_tree(function(tstree, tree)
+ if not tstree then
+ return
+ end
+
+ local root = tstree:root()
+ local root_start_row, _, root_end_row, _ = root:range()
+
+ -- Only worry about trees within the line range
+ if root_start_row > row or root_end_row < row then
+ return
+ end
+
+ local q = buf_highlighter:get_query(tree:lang())
+
+ -- Some injected languages may not have highlight queries.
+ if not q:query() then
+ return
+ end
+
+ local iter = q:query():iter_captures(root, buf_highlighter.bufnr, row, row + 1)
+
+ for capture, node, metadata in iter do
+ if M.is_in_node_range(node, row, col) then
+ local c = q._query.captures[capture] -- name of the capture in the query
+ if c ~= nil then
+ table.insert(matches, { capture = c, metadata = metadata })
+ end
+ end
+ end
+ end, true)
+ return matches
+end
+
+--- Returns a list of highlight capture names under the cursor
+---
+---@param winnr (number|nil) Window handle or 0 for current window (default)
+---
+---@return string[] List of capture names
+function M.get_captures_at_cursor(winnr)
+ winnr = winnr or 0
+ local bufnr = a.nvim_win_get_buf(winnr)
+ local cursor = a.nvim_win_get_cursor(winnr)
+
+ local data = M.get_captures_at_pos(bufnr, cursor[1] - 1, cursor[2])
+
+ local captures = {}
+
+ for _, capture in ipairs(data) do
+ table.insert(captures, capture.capture)
+ end
+
+ return captures
+end
+
+--- Returns the smallest named node at the given position
+---
+---@param bufnr number Buffer number (0 for current buffer)
+---@param row number Position row
+---@param col number Position column
+---@param opts table Optional keyword arguments:
+--- - ignore_injections boolean Ignore injected languages (default true)
+---
+---@return userdata |tsnode| under the cursor
+function M.get_node_at_pos(bufnr, row, col, opts)
+ if bufnr == 0 then
+ bufnr = a.nvim_get_current_buf()
+ end
+ local ts_range = { row, col, row, col }
+
+ local root_lang_tree = M.get_parser(bufnr)
+ if not root_lang_tree then
+ return
+ end
+
+ return root_lang_tree:named_node_for_range(ts_range, opts)
+end
+
+--- Returns the smallest named node under the cursor
+---
+---@param winnr (number|nil) Window handle or 0 for current window (default)
+---
+---@return string Name of node under the cursor
+function M.get_node_at_cursor(winnr)
+ winnr = winnr or 0
+ local bufnr = a.nvim_win_get_buf(winnr)
+ local cursor = a.nvim_win_get_cursor(winnr)
+
+ return M.get_node_at_pos(bufnr, cursor[1] - 1, cursor[2], { ignore_injections = false }):type()
+end
+
+--- Starts treesitter highlighting for a buffer
+---
+--- Can be used in an ftplugin or FileType autocommand.
+---
+--- Note: By default, disables regex syntax highlighting, which may be required for some plugins.
+--- In this case, add ``vim.bo.syntax = 'on'`` after the call to `start`.
+---
+--- Example:
+--- <pre>
+--- vim.api.nvim_create_autocmd( 'FileType', { pattern = 'tex',
+--- callback = function(args)
+--- vim.treesitter.start(args.buf, 'latex')
+--- vim.bo[args.buf].syntax = 'on' -- only if additional legacy syntax is needed
+--- end
+--- })
+--- </pre>
+---
+---@param bufnr (number|nil) Buffer to be highlighted (default: current buffer)
+---@param lang (string|nil) Language of the parser (default: buffer filetype)
+function M.start(bufnr, lang)
+ bufnr = bufnr or a.nvim_get_current_buf()
+
+ local parser = M.get_parser(bufnr, lang)
+
+ M.highlighter.new(parser)
+
+ vim.b[bufnr].ts_highlight = true
+end
+
+--- Stops treesitter highlighting for a buffer
+---
+---@param bufnr (number|nil) Buffer to stop highlighting (default: current buffer)
+function M.stop(bufnr)
+ bufnr = bufnr or a.nvim_get_current_buf()
+
+ if M.highlighter.active[bufnr] then
+ M.highlighter.active[bufnr]:destroy()
+ end
+
+ vim.bo[bufnr].syntax = 'on'
+end
+
return M
diff --git a/runtime/lua/vim/treesitter/health.lua b/runtime/lua/vim/treesitter/health.lua
index 3bd59ca282..4995c80a02 100644
--- a/runtime/lua/vim/treesitter/health.lua
+++ b/runtime/lua/vim/treesitter/health.lua
@@ -3,7 +3,7 @@ local ts = vim.treesitter
--- Lists the parsers currently installed
---
----@return A list of parsers
+---@return string[] list of parser files
function M.list_parsers()
return vim.api.nvim_get_runtime_file('parser/*', true)
end
diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua
index e27a5fa9c3..83a26aff13 100644
--- a/runtime/lua/vim/treesitter/highlighter.lua
+++ b/runtime/lua/vim/treesitter/highlighter.lua
@@ -2,6 +2,7 @@ local a = vim.api
local query = require('vim.treesitter.query')
-- support reload for quick experimentation
+---@class TSHighlighter
local TSHighlighter = rawget(vim.treesitter, 'TSHighlighter') or {}
TSHighlighter.__index = TSHighlighter
@@ -12,105 +13,18 @@ TSHighlighterQuery.__index = TSHighlighterQuery
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
- a.nvim_set_hl(0, from, { link = to, default = true })
- end
-
- return from
-end
-
--- 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)
- return firstc ~= string.lower(firstc)
-end
-
---@private
function TSHighlighterQuery.new(lang, query_string)
local self = setmetatable({}, { __index = TSHighlighterQuery })
self.hl_cache = setmetatable({}, {
__index = function(table, capture)
- local hl, is_vim_highlight = self:_get_hl_from_capture(capture)
- if not is_vim_highlight then
- hl = _link_default_highlight_once(lang .. hl, hl)
+ local name = self._query.captures[capture]
+ local id = 0
+ if not vim.startswith(name, '_') then
+ id = a.nvim_get_hl_id_by_name('@' .. name .. '.' .. lang)
end
- local id = a.nvim_get_hl_id_by_name(hl)
-
rawset(table, capture, id)
return id
end,
@@ -130,25 +44,12 @@ function TSHighlighterQuery:query()
return self._query
end
----@private
---- Get the hl from capture.
---- Returns a tuple { highlight_name: string, is_builtin: bool }
-function TSHighlighterQuery:_get_hl_from_capture(capture)
- local name = self._query.captures[capture]
-
- if is_highlight_name(name) then
- -- From "Normal.left" only keep "Normal"
- return vim.split(name, '.', true)[1], true
- else
- return TSHighlighter.hl_map[name] or 0, false
- end
-end
-
--- Creates a new highlighter using @param tree
---
----@param tree The language tree to use for highlighting
----@param opts Table used to configure the highlighter
---- - queries: Table to overwrite queries used by the highlighter
+---@param tree LanguageTree |LanguageTree| parser object to use for highlighting
+---@param opts (table|nil) Configuration of the highlighter:
+--- - queries table overwrite queries used by the highlighter
+---@return TSHighlighter Created highlighter object
function TSHighlighter.new(tree, opts)
local self = setmetatable({}, TSHighlighter)
@@ -187,7 +88,7 @@ function TSHighlighter.new(tree, opts)
end
end
- a.nvim_buf_set_option(self.bufnr, 'syntax', '')
+ vim.bo[self.bufnr].syntax = ''
TSHighlighter.active[self.bufnr] = self
@@ -196,9 +97,13 @@ 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.cmd.runtime({ 'syntax/synload.vim', bang = true })
end
+ a.nvim_buf_call(self.bufnr, function()
+ vim.opt_local.spelloptions:append('noplainbuffer')
+ end)
+
self.tree:parse()
return self
@@ -246,8 +151,10 @@ function TSHighlighter:on_changedtree(changes)
end
--- Gets the query used for @param lang
----
----@param lang A language used by the highlighter.
+--
+---@private
+---@param lang string Language used by the highlighter.
+---@return Query
function TSHighlighter:get_query(lang)
if not self._queries[lang] then
self._queries[lang] = TSHighlighterQuery.new(lang)
@@ -257,7 +164,7 @@ function TSHighlighter:get_query(lang)
end
---@private
-local function on_line_impl(self, buf, line)
+local function on_line_impl(self, buf, line, spell)
self.tree:for_each_tree(function(tstree, tree)
if not tstree then
return
@@ -294,7 +201,9 @@ local function on_line_impl(self, buf, line)
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
+ local is_spell = highlighter_query:query().captures[capture] == 'spell'
+
+ if hl and end_row >= line and (not spell or is_spell) then
a.nvim_buf_set_extmark(buf, ns, start_row, start_col, {
end_line = end_row,
end_col = end_col,
@@ -302,6 +211,7 @@ local function on_line_impl(self, buf, line)
ephemeral = true,
priority = tonumber(metadata.priority) or 100, -- Low but leaves room below
conceal = metadata.conceal,
+ spell = is_spell,
})
end
if start_row > line then
@@ -318,7 +228,21 @@ function TSHighlighter._on_line(_, _win, buf, line, _)
return
end
- on_line_impl(self, buf, line)
+ on_line_impl(self, buf, line, false)
+end
+
+---@private
+function TSHighlighter._on_spell_nav(_, _, buf, srow, _, erow, _)
+ local self = TSHighlighter.active[buf]
+ if not self then
+ return
+ end
+
+ self:reset_highlight_state()
+
+ for row = srow, erow do
+ on_line_impl(self, buf, row, true)
+ end
end
---@private
@@ -345,6 +269,7 @@ a.nvim_set_decoration_provider(ns, {
on_buf = TSHighlighter._on_buf,
on_win = TSHighlighter._on_win,
on_line = TSHighlighter._on_line,
+ _on_spell_nav = TSHighlighter._on_spell_nav,
})
return TSHighlighter
diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua
index dfb6f5be84..c92d63b8c4 100644
--- a/runtime/lua/vim/treesitter/language.lua
+++ b/runtime/lua/vim/treesitter/language.lua
@@ -2,14 +2,16 @@ local a = vim.api
local M = {}
---- Asserts that the provided language is installed, and optionally provide a path for the parser
+--- Asserts that a parser for the language {lang} is installed.
---
---- Parsers are searched in the `parser` runtime directory.
+--- Parsers are searched in the `parser` runtime directory, or the provided {path}
---
----@param lang The language the parser should parse
----@param path Optional path the parser is located at
----@param silent Don't throw an error if language not found
-function M.require_language(lang, path, silent)
+---@param lang string Language the parser should parse
+---@param path (string|nil) Optional path the parser is located at
+---@param silent (boolean|nil) Don't throw an error if language not found
+---@param symbol_name (string|nil) Internal symbol name for the language to load
+---@return boolean If the specified language is installed
+function M.require_language(lang, path, silent, symbol_name)
if vim._ts_has_language(lang) then
return true
end
@@ -21,7 +23,6 @@ function M.require_language(lang, path, silent)
return false
end
- -- TODO(bfredl): help tag?
error("no parser for '" .. lang .. "' language, see :help treesitter-parsers")
end
path = paths[1]
@@ -29,10 +30,10 @@ function M.require_language(lang, path, silent)
if silent then
return pcall(function()
- vim._ts_add_language(path, lang)
+ vim._ts_add_language(path, lang, symbol_name)
end)
else
- vim._ts_add_language(path, lang)
+ vim._ts_add_language(path, lang, symbol_name)
end
return true
@@ -42,7 +43,8 @@ end
---
--- Inspecting provides some useful information on the language like node names, ...
---
----@param lang The language.
+---@param lang string Language
+---@return table
function M.inspect_language(lang)
M.require_language(lang)
return vim._ts_inspect_language(lang)
diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua
index 4d3b0631a2..e9d70c4204 100644
--- a/runtime/lua/vim/treesitter/languagetree.lua
+++ b/runtime/lua/vim/treesitter/languagetree.lua
@@ -2,19 +2,35 @@ local a = vim.api
local query = require('vim.treesitter.query')
local language = require('vim.treesitter.language')
+---@class LanguageTree
+---@field _callbacks function[] Callback handlers
+---@field _children LanguageTree[] Injected languages
+---@field _injection_query table Queries defining injected languages
+---@field _opts table Options
+---@field _parser userdata Parser for language
+---@field _regions table List of regions this tree should manage and parse
+---@field _lang string Language name
+---@field _regions table
+---@field _source (number|string) Buffer or string to parse
+---@field _trees userdata[] Reference to parsed |tstree| (one for each language)
+---@field _valid boolean If the parsed tree is valid
+
local LanguageTree = {}
LanguageTree.__index = LanguageTree
---- Represents a single treesitter parser for a language.
---- The language can contain child languages with in its range,
---- hence the tree.
+--- A |LanguageTree| holds the treesitter parser for a given language {lang} used
+--- to parse a buffer. As the buffer may contain injected languages, the LanguageTree
+--- needs to store parsers for these child languages as well (which in turn may contain
+--- child languages themselves, hence the name).
---
----@param source Can be a bufnr or a string of text to parse
----@param lang The language this tree represents
----@param opts Options table
----@param opts.injections A table of language to injection query strings.
---- This is useful for overriding the built-in runtime file
---- searching for the injection language query per language.
+---@param source (number|string) Buffer or a string of text to parse
+---@param lang string Root language this tree represents
+---@param opts (table|nil) Optional keyword arguments:
+--- - injections table Mapping language to injection query strings.
+--- This is useful for overriding the built-in
+--- runtime file searching for the injection language
+--- query per language.
+---@return LanguageTree |LanguageTree| parser object
function LanguageTree.new(source, lang, opts)
language.require_language(lang)
opts = opts or {}
@@ -94,6 +110,9 @@ end
--- for the language this tree represents.
--- This will run the injection query for this language to
--- determine if any child languages should be created.
+---
+---@return userdata[] Table of parsed |tstree|
+---@return table Change list
function LanguageTree:parse()
if self._valid then
return self._trees
@@ -167,10 +186,10 @@ function LanguageTree:parse()
return self._trees, changes
end
---- Invokes the callback for each LanguageTree and it's children recursively
+--- Invokes the callback for each |LanguageTree| and its children recursively
---
----@param fn The function to invoke. This is invoked with arguments (tree: LanguageTree, lang: string)
----@param include_self Whether to include the invoking tree in the results.
+---@param fn function(tree: LanguageTree, lang: string)
+---@param include_self boolean Whether to include the invoking tree in the results
function LanguageTree:for_each_child(fn, include_self)
if include_self then
fn(self, self._lang)
@@ -181,12 +200,11 @@ function LanguageTree:for_each_child(fn, include_self)
end
end
---- Invokes the callback for each treesitter trees recursively.
+--- Invokes the callback for each |LanguageTree| recursively.
---
---- Note, this includes the invoking language tree's trees as well.
+--- Note: This includes the invoking tree's child trees as well.
---
----@param fn The callback to invoke. The callback is invoked with arguments
---- (tree: TSTree, languageTree: LanguageTree)
+---@param fn function(tree: TSTree, languageTree: LanguageTree)
function LanguageTree:for_each_tree(fn)
for _, tree in ipairs(self._trees) do
fn(tree, self)
@@ -197,11 +215,13 @@ function LanguageTree:for_each_tree(fn)
end
end
---- Adds a child language to this tree.
+--- Adds a child language to this |LanguageTree|.
---
--- If the language already exists as a child, it will first be removed.
---
----@param lang The language to add.
+---@private
+---@param lang string Language to add.
+---@return LanguageTree Injected |LanguageTree|
function LanguageTree:add_child(lang)
if self._children[lang] then
self:remove_child(lang)
@@ -215,9 +235,10 @@ function LanguageTree:add_child(lang)
return self._children[lang]
end
---- Removes a child language from this tree.
+--- Removes a child language from this |LanguageTree|.
---
----@param lang The language to remove.
+---@private
+---@param lang string Language to remove.
function LanguageTree:remove_child(lang)
local child = self._children[lang]
@@ -229,12 +250,11 @@ function LanguageTree:remove_child(lang)
end
end
---- Destroys this language tree and all its children.
+--- Destroys this |LanguageTree| and all its children.
---
--- Any cleanup logic should be performed here.
---
---- Note:
---- This DOES NOT remove this tree from a parent. Instead,
+--- 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
@@ -243,23 +263,24 @@ function LanguageTree:destroy()
end
end
---- Sets the included regions that should be parsed by this parser.
+--- Sets the included regions that should be parsed by this |LanguageTree|.
--- A region is a set of nodes and/or ranges that will be parsed in the same context.
---
---- For example, `{ { node1 }, { node2} }` is two separate regions.
---- This will be parsed by the parser in two different contexts... thus resulting
+--- For example, `{ { node1 }, { node2} }` contains two separate regions.
+--- They will be parsed by the parser in two different contexts, thus resulting
--- in two separate trees.
---
---- `{ { node1, node2 } }` is a single region consisting of two nodes.
---- This will be parsed by the parser in a single context... thus resulting
+--- On the other hand, `{ { node1, node2 } }` is a single region consisting of
+--- two nodes. This will be parsed by the parser in a single context, thus resulting
--- in a single tree.
---
--- This allows for embedded languages to be parsed together across different
--- nodes, which is useful for templating languages like ERB and EJS.
---
---- Note, this call invalidates the tree and requires it to be parsed again.
+--- Note: This call invalidates the tree and requires it to be parsed again.
---
----@param regions (table) list of regions this tree should manage and parse.
+---@private
+---@param regions table List of regions this tree should manage and parse.
function LanguageTree:set_included_regions(regions)
-- Transform the tables from 4 element long to 6 element long (with byte offset)
for _, region in ipairs(regions) do
@@ -288,7 +309,7 @@ function LanguageTree:set_included_regions(regions)
-- Trees are no longer valid now that we have changed regions.
-- TODO(vigoux,steelsojka): Look into doing this smarter so we can use some of the
-- old trees for incremental parsing. Currently, this only
- -- effects injected languages.
+ -- affects injected languages.
self._trees = {}
self:invalidate()
end
@@ -299,7 +320,7 @@ function LanguageTree:included_regions()
end
---@private
-local function get_node_range(node, id, metadata)
+local function get_range_from_metadata(node, id, metadata)
if metadata[id] and metadata[id].range then
return metadata[id].range
end
@@ -362,7 +383,7 @@ function LanguageTree:_get_injections()
elseif name == 'combined' then
combined = true
elseif name == 'content' and #ranges == 0 then
- table.insert(ranges, get_node_range(node, id, metadata))
+ table.insert(ranges, get_range_from_metadata(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
@@ -371,7 +392,7 @@ function LanguageTree:_get_injections()
end
if #ranges == 0 then
- table.insert(ranges, get_node_range(node, id, metadata))
+ table.insert(ranges, get_range_from_metadata(node, id, metadata))
end
end
end
@@ -493,8 +514,8 @@ function LanguageTree:_on_detach(...)
self:_do_callback('detach', ...)
end
---- Registers callbacks for the parser.
----@param cbs table An |nvim_buf_attach()|-like table argument with the following keys :
+--- Registers callbacks for the |LanguageTree|.
+---@param cbs table An |nvim_buf_attach()|-like table argument with the following handlers:
--- - `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
@@ -536,9 +557,10 @@ local function tree_contains(tree, range)
return start_fits and end_fits
end
---- Determines whether {range} is contained in this language tree
+--- Determines whether {range} is contained in the |LanguageTree|.
---
----@param range A range, that is a `{ start_line, start_col, end_line, end_col }` table.
+---@param range table `{ start_line, start_col, end_line, end_col }`
+---@return boolean
function LanguageTree:contains(range)
for _, tree in pairs(self._trees) do
if tree_contains(tree, range) then
@@ -549,9 +571,50 @@ function LanguageTree:contains(range)
return false
end
---- Gets the appropriate language that contains {range}
+--- Gets the tree that contains {range}.
+---
+---@param range table `{ start_line, start_col, end_line, end_col }`
+---@param opts table|nil Optional keyword arguments:
+--- - ignore_injections boolean Ignore injected languages (default true)
+---@return userdata|nil Contained |tstree|
+function LanguageTree:tree_for_range(range, opts)
+ opts = opts or {}
+ local ignore = vim.F.if_nil(opts.ignore_injections, true)
+
+ if not ignore then
+ for _, child in pairs(self._children) do
+ for _, tree in pairs(child:trees()) do
+ if tree_contains(tree, range) then
+ return tree
+ end
+ end
+ end
+ end
+
+ for _, tree in pairs(self._trees) do
+ if tree_contains(tree, range) then
+ return tree
+ end
+ end
+
+ return nil
+end
+
+--- Gets the smallest named node that contains {range}.
+---
+---@param range table `{ start_line, start_col, end_line, end_col }`
+---@param opts table|nil Optional keyword arguments:
+--- - ignore_injections boolean Ignore injected languages (default true)
+---@return userdata|nil Found |tsnode|
+function LanguageTree:named_node_for_range(range, opts)
+ local tree = self:tree_for_range(range, opts)
+ return tree:root():named_descendant_for_range(unpack(range))
+end
+
+--- Gets the appropriate language that contains {range}.
---
----@param range A text range, see |LanguageTree:contains|
+---@param range table `{ start_line, start_col, end_line, end_col }`
+---@return LanguageTree Managing {range}
function LanguageTree:language_for_range(range)
for _, child in pairs(self._children) do
if child:contains(range) then
diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua
index 103e85abfd..7ca7384a88 100644
--- a/runtime/lua/vim/treesitter/query.lua
+++ b/runtime/lua/vim/treesitter/query.lua
@@ -3,6 +3,11 @@ local language = require('vim.treesitter.language')
-- query: pattern matching on trees
-- predicate matching is implemented in lua
+--
+---@class Query
+---@field captures string[] List of captures used in query
+---@field info table Contains used queries, predicates, directives
+---@field query userdata Parsed query
local Query = {}
Query.__index = Query
@@ -34,11 +39,24 @@ local function safe_read(filename, read_quantifier)
return content
end
+---@private
+--- Adds {ilang} to {base_langs}, only if {ilang} is different than {lang}
+---
+---@return boolean true If lang == ilang
+local function add_included_lang(base_langs, lang, ilang)
+ if lang == ilang then
+ return true
+ end
+ table.insert(base_langs, ilang)
+ return false
+end
+
--- Gets the list of files used to make up a query
---
----@param lang The language
----@param query_name The name of the query to load
----@param is_included Internal parameter, most of the time left as `nil`
+---@param lang string Language to get query for
+---@param query_name string Name of the query to load (e.g., "highlights")
+---@param is_included (boolean|nil) Internal parameter, most of the time left as `nil`
+---@return string[] query_files List of files to load for given query and language
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))
@@ -47,6 +65,9 @@ function M.get_query_files(lang, query_name, is_included)
return {}
end
+ local base_query = nil
+ local extensions = {}
+
local base_langs = {}
-- Now get the base languages by looking at the first line of every file
@@ -55,27 +76,53 @@ function M.get_query_files(lang, query_name, is_included)
--
-- {language} ::= {lang} | ({lang})
local MODELINE_FORMAT = '^;+%s*inherits%s*:?%s*([a-z_,()]+)%s*$'
+ local EXTENDS_FORMAT = '^;+%s*extends%s*$'
- for _, file in ipairs(lang_files) do
- local modeline = safe_read(file, '*l')
+ for _, filename in ipairs(lang_files) do
+ local file, err = io.open(filename, 'r')
+ if not file then
+ error(err)
+ end
- if modeline then
- local langlist = modeline:match(MODELINE_FORMAT)
+ local extension = false
+ for modeline in
+ function()
+ return file:read('*l')
+ end
+ do
+ if not vim.startswith(modeline, ';') then
+ break
+ end
+
+ local langlist = modeline:match(MODELINE_FORMAT)
if langlist then
for _, incllang in ipairs(vim.split(langlist, ',', true)) do
local is_optional = incllang:match('%(.*%)')
if is_optional then
if not is_included then
- table.insert(base_langs, incllang:sub(2, #incllang - 1))
+ if add_included_lang(base_langs, lang, incllang:sub(2, #incllang - 1)) then
+ extension = true
+ end
end
else
- table.insert(base_langs, incllang)
+ if add_included_lang(base_langs, lang, incllang) then
+ extension = true
+ end
end
end
+ elseif modeline:match(EXTENDS_FORMAT) then
+ extension = true
end
end
+
+ if extension then
+ table.insert(extensions, filename)
+ elseif base_query == nil then
+ base_query = filename
+ end
+ io.close(file)
end
local query_files = {}
@@ -83,7 +130,8 @@ function M.get_query_files(lang, query_name, is_included)
local base_files = M.get_query_files(base_lang, query_name, true)
vim.list_extend(query_files, base_files)
end
- vim.list_extend(query_files, lang_files)
+ vim.list_extend(query_files, { base_query })
+ vim.list_extend(query_files, extensions)
return query_files
end
@@ -109,24 +157,24 @@ local explicit_queries = setmetatable({}, {
end,
})
---- Sets the runtime query {query_name} for {lang}
+--- Sets the runtime query named {query_name} for {lang}
---
--- This allows users to override any runtime files and/or configuration
--- set by plugins.
---
----@param lang string: The language to use for the query
----@param query_name string: The name of the query (i.e. "highlights")
----@param text string: The query text (unparsed).
+---@param lang string Language to use for the query
+---@param query_name string Name of the query (e.g., "highlights")
+---@param text string Query text (unparsed).
function M.set_query(lang, query_name, text)
explicit_queries[lang][query_name] = M.parse_query(lang, text)
end
--- Returns the runtime query {query_name} for {lang}.
---
----@param lang The language to use for the query
----@param query_name The name of the query (i.e. "highlights")
+---@param lang string Language to use for the query
+---@param query_name string Name of the query (e.g. "highlights")
---
----@return The corresponding query, parsed.
+---@return Query Parsed query
function M.get_query(lang, query_name)
if explicit_queries[lang][query_name] then
return explicit_queries[lang][query_name]
@@ -140,12 +188,9 @@ 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,
-})
+local query_cache = vim.defaulttable(function()
+ return setmetatable({}, { __mode = 'v' })
+end)
--- Parse {query} as a string. (If the query is in a file, the caller
--- should read the contents into a string before calling).
@@ -160,10 +205,10 @@ local query_cache = setmetatable({}, {
--- -` info.captures` also points to `captures`.
--- - `info.patterns` contains information about predicates.
---
----@param lang string The language
----@param query string A string containing the query (s-expr syntax)
+---@param lang string Language to use for the query
+---@param query string Query in s-expr syntax
---
----@returns The query
+---@return Query Parsed query
function M.parse_query(lang, query)
language.require_language(lang)
local cached = query_cache[lang][query]
@@ -181,9 +226,15 @@ end
--- Gets the text corresponding to a given node
---
----@param node the node
----@param source The buffer or string from which the node is extracted
-function M.get_node_text(node, source)
+---@param node userdata |tsnode|
+---@param source (number|string) Buffer or string from which the {node} is extracted
+---@param opts (table|nil) Optional parameters.
+--- - concat: (boolean) Concatenate result in a string (default true)
+---@return (string[]|string)
+function M.get_node_text(node, source, opts)
+ opts = opts or {}
+ local concat = vim.F.if_nil(opts.concat, true)
+
local start_row, start_col, start_byte = node:start()
local end_row, end_col, end_byte = node:end_()
@@ -210,7 +261,7 @@ function M.get_node_text(node, source)
end
end
- return table.concat(lines, '\n')
+ return concat and table.concat(lines, '\n') or lines
elseif type(source) == 'string' then
return source:sub(start_byte + 1, end_byte)
end
@@ -367,9 +418,8 @@ local directive_handlers = {
--- Adds a new predicate to be used in queries
---
----@param name the name of the predicate, without leading #
----@param handler the handler function to be used
---- signature will be (match, pattern, bufnr, predicate)
+---@param name string Name of the predicate, without leading #
+---@param handler function(match:string, pattern:string, bufnr:number, predicate:function)
function M.add_predicate(name, handler, force)
if predicate_handlers[name] and not force then
error(string.format('Overriding %s', name))
@@ -385,9 +435,8 @@ end
--- 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, metadata)
+---@param name string Name of the directive, without leading #
+---@param handler function(match:string, pattern:string, bufnr:number, predicate:function, metadata:table)
function M.add_directive(name, handler, force)
if directive_handlers[name] and not force then
error(string.format('Overriding %s', name))
@@ -397,12 +446,13 @@ function M.add_directive(name, handler, force)
end
--- Lists the currently available directives to use in queries.
----@return The list of supported directives.
+---@return string[] List of supported directives.
function M.list_directives()
return vim.tbl_keys(directive_handlers)
end
----@return The list of supported predicates.
+--- Lists the currently available predicates to use in queries.
+---@return string[] List of supported predicates.
function M.list_predicates()
return vim.tbl_keys(predicate_handlers)
end
@@ -489,17 +539,16 @@ end
--- Iterate over all captures from all matches inside {node}
---
---- {source} is needed if the query contains predicates, then the caller
+--- {source} is needed if the query contains predicates; then the caller
--- must ensure to use a freshly parsed tree consistent with the current
--- text of the buffer (if relevant). {start_row} and {end_row} can be used to limit
--- matches inside a row range (this is typically used with root node
---- as the node, i e to get syntax highlight matches in the current
---- viewport). When omitted the start and end row values are used from the given node.
+--- as the {node}, i.e., to get syntax highlight matches in the current
+--- viewport). When omitted, the {start} and {end} row values are used from the given node.
---
---- The iterator returns three values, a numeric id identifying the capture,
+--- The iterator returns three values: a numeric id identifying the capture,
--- the captured node, and metadata from any directives processing the match.
--- The following example shows how to get captures by name:
----
--- <pre>
--- for id, node, metadata in query:iter_captures(tree:root(), bufnr, first, last) do
--- local name = query.captures[id] -- name of the capture in the query
@@ -510,13 +559,14 @@ end
--- end
--- </pre>
---
----@param node The node under which the search will occur
----@param source The source buffer or string to extract text from
----@param start The starting line of the search
----@param stop The stopping line of the search (end-exclusive)
+---@param node userdata |tsnode| under which the search will occur
+---@param source (number|string) Source buffer or string to extract text from
+---@param start number Starting line for the search
+---@param stop number Stopping line for the search (end-exclusive)
---
----@returns The matching capture id
----@returns The captured node
+---@return number capture Matching capture id
+---@return table capture_node Capture for {node}
+---@return table metadata for the {capture}
function Query:iter_captures(node, source, start, stop)
if type(source) == 'number' and source == 0 then
source = vim.api.nvim_get_current_buf()
@@ -546,14 +596,13 @@ end
--- Iterates the matches of self on a given range.
---
---- Iterate over all matches within a node. The arguments are the same as
---- for |query:iter_captures()| but the iterated values are different:
+--- Iterate over all matches within a {node}. The arguments are the same as
+--- for |Query:iter_captures()| but the iterated values are different:
--- an (1-based) index of the pattern in the query, a table mapping
--- capture indices to nodes, and metadata from any directives processing the match.
---- If the query has more than one pattern the capture table might be sparse,
+--- If the query has more than one pattern, the capture table might be sparse
--- and e.g. `pairs()` method should be used over `ipairs`.
---- Here an example iterating over all captures in every match:
----
+--- Here is an example iterating over all captures in every match:
--- <pre>
--- for pattern, match, metadata in cquery:iter_matches(tree:root(), bufnr, first, last) do
--- for id, node in pairs(match) do
@@ -567,13 +616,14 @@ end
--- end
--- </pre>
---
----@param node The node under which the search will occur
----@param source The source buffer or string to search
----@param start The starting line of the search
----@param stop The stopping line of the search (end-exclusive)
+---@param node userdata |tsnode| under which the search will occur
+---@param source (number|string) Source buffer or string to search
+---@param start number Starting line for the search
+---@param stop number Stopping line for the search (end-exclusive)
---
----@returns The matching pattern id
----@returns The matching match
+---@return number pattern id
+---@return table match
+---@return table metadata
function Query:iter_matches(node, source, start, stop)
if type(source) == 'number' and source == 0 then
source = vim.api.nvim_get_current_buf()