aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua/vim')
-rw-r--r--runtime/lua/vim/F.lua7
-rw-r--r--runtime/lua/vim/_meta.lua631
-rw-r--r--runtime/lua/vim/lsp.lua52
-rw-r--r--runtime/lua/vim/lsp/buf.lua136
-rw-r--r--runtime/lua/vim/lsp/codelens.lua231
-rw-r--r--runtime/lua/vim/lsp/diagnostic.lua87
-rw-r--r--runtime/lua/vim/lsp/handlers.lua44
-rw-r--r--runtime/lua/vim/lsp/log.lua2
-rw-r--r--runtime/lua/vim/lsp/rpc.lua30
-rw-r--r--runtime/lua/vim/lsp/util.lua550
-rw-r--r--runtime/lua/vim/treesitter.lua38
-rw-r--r--runtime/lua/vim/treesitter/health.lua4
-rw-r--r--runtime/lua/vim/treesitter/highlighter.lua22
-rw-r--r--runtime/lua/vim/treesitter/language.lua20
-rw-r--r--runtime/lua/vim/treesitter/languagetree.lua160
-rw-r--r--runtime/lua/vim/treesitter/query.lua181
16 files changed, 1807 insertions, 388 deletions
diff --git a/runtime/lua/vim/F.lua b/runtime/lua/vim/F.lua
index 5887e978b9..7925ff6e44 100644
--- a/runtime/lua/vim/F.lua
+++ b/runtime/lua/vim/F.lua
@@ -20,5 +20,12 @@ function F.npcall(fn, ...)
return F.ok_or_nil(pcall(fn, ...))
end
+--- Wrap a function to return nil if it fails, otherwise the value
+function F.nil_wrap(fn)
+ return function(...)
+ return F.npcall(fn, ...)
+ end
+end
+
return F
diff --git a/runtime/lua/vim/_meta.lua b/runtime/lua/vim/_meta.lua
new file mode 100644
index 0000000000..02d1154df4
--- /dev/null
+++ b/runtime/lua/vim/_meta.lua
@@ -0,0 +1,631 @@
+-- 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 = {}
+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 is_global_option = function(info) return info.scope == "global" end
+local is_buffer_option = function(info) return info.scope == "buf" end
+local is_window_option = function(info) return info.scope == "win" end
+
+local get_scoped_options = function(scope)
+ local result = {}
+ for name, option_info in pairs(options_info) do
+ if option_info.scope == scope then
+ result[name] = true
+ end
+ end
+
+ return result
+end
+
+local buf_options = get_scoped_options("buf")
+local glb_options = get_scoped_options("global")
+local win_options = get_scoped_options("win")
+
+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 mt = {}
+ function mt:__newindex(k, v)
+ if not validator(k) then
+ return
+ 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
+
+ return get(k)
+ 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_buf_get_option(bufnr or 0, k)
+ end
+
+ local function set(k, v)
+ return a.nvim_buf_set_option(bufnr or 0, k, v)
+ end
+
+ return make_meta_accessor(get, set, nil, function(k)
+ if type(k) == 'string' then
+ 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
+ end
+
+ return true
+ end)
+ end
+
+ vim.bo = new_buf_opt_accessor(nil)
+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_win_get_option(winnr or 0, k)
+ end
+
+ local function set(k, v)
+ return a.nvim_win_set_option(winnr or 0, k, v)
+ end
+
+ return make_meta_accessor(get, set, nil, function(k)
+ if type(k) == 'string' then
+ 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
+
+--[[
+Local window setter
+
+buffer options: does not get copied when split
+ nvim_set_option(buf_opt, value) -> sets the default for NEW buffers
+ this sets the hidden global default for buffer options
+
+ nvim_buf_set_option(...) -> sets the local value for the buffer
+
+ set opt=value, does BOTH global default AND buffer local value
+ setlocal opt=value, does ONLY buffer local value
+
+window options: gets copied
+ does not need to call nvim_set_option because nobody knows what the heck this does⸮
+ We call it anyway for more readable code.
+
+
+ Command global value local value
+ :set option=value set set
+ :setlocal option=value - set
+:setglobal option=value set -
+--]]
+local function set_scoped_option(k, v, set_type)
+ local info = options_info[k]
+
+ -- Don't let people do setlocal with global options.
+ -- That is a feature that doesn't make sense.
+ if set_type == SET_TYPES.LOCAL and is_global_option(info) then
+ error(string.format("Unable to setlocal option: '%s', which is a global option.", k))
+ end
+
+ -- Only `setlocal` skips setting the default/global value
+ -- This will more-or-less noop for window options, but that's OK
+ if set_type ~= SET_TYPES.LOCAL then
+ a.nvim_set_option(k, v)
+ end
+
+ if is_window_option(info) then
+ if set_type ~= SET_TYPES.GLOBAL then
+ a.nvim_win_set_option(0, k, v)
+ end
+ elseif is_buffer_option(info) then
+ if set_type == SET_TYPES.LOCAL
+ or (set_type == SET_TYPES.SET and not info.global_local) then
+ a.nvim_buf_set_option(0, k, v)
+ end
+ end
+end
+
+--[[
+Local window getter
+
+ Command global value local value
+ :set option? - display
+ :setlocal option? - display
+:setglobal option? display -
+--]]
+local function get_scoped_option(k, set_type)
+ local info = assert(options_info[k], "Must be a valid option: " .. tostring(k))
+
+ if set_type == SET_TYPES.GLOBAL or is_global_option(info) then
+ return a.nvim_get_option(k)
+ end
+
+ if is_buffer_option(info) then
+ local was_set, value = pcall(a.nvim_buf_get_option, 0, k)
+ if was_set then return value end
+
+ if info.global_local then
+ return a.nvim_get_option(k)
+ end
+
+ error("buf_get: This should not be able to happen, given my understanding of options // " .. k)
+ end
+
+ if is_window_option(info) then
+ if vim.api.nvim_get_option_info(k).was_set then
+ local was_set, value = pcall(a.nvim_win_get_option, 0, k)
+ if was_set then return value end
+ end
+
+ return a.nvim_get_option(k)
+ end
+
+ error("This fallback case should not be possible. " .. k)
+end
+
+-- vim global option
+-- this ONLY sets the global option. like `setglobal`
+vim.go = make_meta_accessor(a.nvim_get_option, a.nvim_set_option)
+
+-- vim `set` style options.
+-- it has no additional metamethod magic.
+vim.o = make_meta_accessor(
+ function(k) return get_scoped_option(k, SET_TYPES.SET) end,
+ function(k, v) return set_scoped_option(k, v, SET_TYPES.SET) end
+)
+
+---@brief [[
+--- vim.opt, vim.opt_local and vim.opt_global implementation
+---
+--- To be used as helpers for working with options within neovim.
+--- For information on how to use, see :help vim.opt
+---
+---@brief ]]
+
+--- Preserves the order and does not mutate the original list
+local remove_duplicate_values = function(t)
+ local result, seen = {}, {}
+ if type(t) == "function" then error(debug.traceback("asdf")) end
+ for _, v in ipairs(t) do
+ if not seen[v] then
+ table.insert(result, v)
+ end
+
+ seen[v] = true
+ end
+
+ 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 OptionType
+--- 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
+
+
+--- 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(_, value)
+ if type(value) == "string" then return value end
+ local result = ''
+ for k in pairs(value) do
+ result = result .. k
+ end
+
+ return result
+ end,
+
+ [OptionTypes.ARRAY] = function(_, value)
+ if type(value) == "string" then return value end
+ return table.concat(remove_duplicate_values(value), ",")
+ end,
+
+ [OptionTypes.MAP] = function(_, value)
+ if type(value) == "string" then return value end
+ if type(value) == "function" then error(debug.traceback("asdf")) end
+
+ local result = {}
+ for opt_key, opt_value in pairs(value) do
+ table.insert(result, string.format("%s:%s", opt_key, opt_value))
+ end
+
+ table.sort(result)
+ return table.concat(result, ",")
+ end,
+ }
+
+ return function(name, info, value)
+ return to_vim_value[get_option_type(name, info)](info, value)
+ 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 conver to lua values.
+ -- Each function takes (info, vim_value) -> lua_value
+ local to_lua_value = {
+ [OptionTypes.BOOLEAN] = function(_, value) return value end,
+ [OptionTypes.NUMBER] = function(_, value) return value end,
+ [OptionTypes.STRING] = function(_, value) return value end,
+
+ [OptionTypes.ARRAY] = function(_, value)
+ if type(value) == "table" then
+ value = remove_duplicate_values(value)
+ return value
+ end
+
+ return vim.split(value, ",")
+ end,
+
+ [OptionTypes.SET] = function(info, value)
+ if type(value) == "table" then return value end
+
+ assert(info.flaglist, "That is the only one I know how to handle")
+
+ local result = {}
+ for i = 1, #value do
+ result[value:sub(i, i)] = true
+ end
+
+ return result
+ end,
+
+ [OptionTypes.MAP] = function(info, raw_value)
+ if type(raw_value) == "table" then return raw_value end
+
+ assert(info.commalist, "Only commas are supported currently")
+
+ local result = {}
+
+ 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)
+ value = vim.trim(value)
+
+ result[key] = value
+ end
+
+ return result
+ end,
+ }
+
+ return function(name, info, option_value)
+ return to_lua_value[get_option_type(name, info)](info, option_value)
+ end
+end)()
+
+--- 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
+
+--- Handles the '^' operator
+local prepend_value = (function()
+ local methods = {
+ [OptionTypes.NUMBER] = function()
+ error("The '^' operator is not currently supported for")
+ end,
+
+ [OptionTypes.STRING] = function(left, right)
+ return right .. left
+ end,
+
+ [OptionTypes.ARRAY] = function(left, right)
+ for i = #right, 1, -1 do
+ table.insert(left, 1, right[i])
+ end
+
+ return left
+ end,
+
+ [OptionTypes.MAP] = function(left, right)
+ return vim.tbl_extend("force", left, right)
+ end,
+
+ [OptionTypes.SET] = function(left, right)
+ return vim.tbl_extend("force", left, right)
+ end,
+ }
+
+ return function(name, info, current, new)
+ return value_mutator(
+ name, info, convert_value_to_lua(name, info, current), convert_value_to_lua(name, info, new), methods
+ )
+ end
+end)()
+
+--- Handles the '+' operator
+local add_value = (function()
+ local methods = {
+ [OptionTypes.NUMBER] = function(left, right)
+ return left + right
+ end,
+
+ [OptionTypes.STRING] = function(left, right)
+ return left .. right
+ end,
+
+ [OptionTypes.ARRAY] = function(left, right)
+ for _, v in ipairs(right) do
+ table.insert(left, v)
+ end
+
+ return left
+ end,
+
+ [OptionTypes.MAP] = function(left, right)
+ return vim.tbl_extend("force", left, right)
+ end,
+
+ [OptionTypes.SET] = function(left, right)
+ return vim.tbl_extend("force", left, right)
+ end,
+ }
+
+ return function(name, info, current, new)
+ return value_mutator(
+ name, info, convert_value_to_lua(name, info, current), convert_value_to_lua(name, info, new), methods
+ )
+ end
+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
+ end
+
+ 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
+
+ return left
+ 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
+ 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 function(name, info, current, new)
+ return value_mutator(name, info, convert_value_to_lua(name, info, current), new, methods)
+ end
+end)()
+
+local create_option_metatable = function(set_type)
+ local set_mt, option_mt
+
+ local make_option = function(name, value)
+ local info = assert(options_info[name], "Not a valid option name: " .. name)
+
+ if type(value) == "table" and getmetatable(value) == option_mt then
+ assert(name == value._name, "must be the same value, otherwise that's weird.")
+
+ value = value._value
+ end
+
+ return setmetatable({
+ _name = name,
+ _value = value,
+ _info = info,
+ }, option_mt)
+ end
+
+ -- TODO(tjdevries): consider supporting `nil` for set to remove the local option.
+ -- vim.cmd [[set option<]]
+
+ option_mt = {
+ -- To set a value, instead use:
+ -- opt[my_option] = value
+ _set = function(self)
+ local value = convert_value_to_vim(self._name, self._info, self._value)
+ set_scoped_option(self._name, value, set_type)
+
+ return self
+ end,
+
+ get = function(self)
+ return convert_value_to_lua(self._name, self._info, self._value)
+ end,
+
+ append = function(self, right)
+ return self:__add(right):_set()
+ end,
+
+ __add = function(self, right)
+ return make_option(self._name, add_value(self._name, self._info, self._value, right))
+ end,
+
+ prepend = function(self, right)
+ return self:__pow(right):_set()
+ end,
+
+ __pow = function(self, right)
+ return make_option(self._name, prepend_value(self._name, self._info, self._value, right))
+ end,
+
+ remove = function(self, right)
+ return self:__sub(right):_set()
+ end,
+
+ __sub = function(self, right)
+ return make_option(self._name, remove_value(self._name, self._info, self._value, right))
+ end
+ }
+ option_mt.__index = option_mt
+
+ set_mt = {
+ __index = function(_, k)
+ return make_option(k, get_scoped_option(k, set_type))
+ end,
+
+ __newindex = function(_, k, v)
+ local opt = make_option(k, v)
+ opt:_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))
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua
index 26700288af..75faf9bcc7 100644
--- a/runtime/lua/vim/lsp.lua
+++ b/runtime/lua/vim/lsp.lua
@@ -20,6 +20,7 @@ local lsp = {
buf = require'vim.lsp.buf';
diagnostic = require'vim.lsp.diagnostic';
+ codelens = require'vim.lsp.codelens';
util = util;
-- Allow raw RPC access.
@@ -483,6 +484,13 @@ end
--- result. You can use this with `client.cancel_request(request_id)`
--- to cancel the request.
---
+--- - request_sync(method, params, timeout_ms, bufnr)
+--- Sends a request to the server and synchronously waits for the response.
+--- This is a wrapper around {client.request}
+--- Returns: { err=err, result=result }, a dictionary, where `err` and `result` come from
+--- the |lsp-handler|. On timeout, cancel or error, returns `(nil, err)` where `err` is a
+--- string describing the failure reason. If the request was unsuccessful returns `nil`.
+---
--- - notify(method, params)
--- Sends a notification to an LSP server.
--- Returns: a boolean to indicate if the notification was successful. If
@@ -891,6 +899,42 @@ function lsp.start_client(config)
end
--@private
+ --- Sends a request to the server and synchronously waits for the response.
+ ---
+ --- This is a wrapper around {client.request}
+ ---
+ --@param method (string) LSP method name.
+ --@param params (table) LSP request params.
+ --@param timeout_ms (number, optional, default=1000) Maximum time in
+ ---milliseconds to wait for a result.
+ --@param bufnr (number) Buffer handle (0 for current).
+ --@returns { err=err, result=result }, a dictionary, where `err` and `result` come from the |lsp-handler|.
+ ---On timeout, cancel or error, returns `(nil, err)` where `err` is a
+ ---string describing the failure reason. If the request was unsuccessful
+ ---returns `nil`.
+ --@see |vim.lsp.buf_request_sync()|
+ function client.request_sync(method, params, timeout_ms, bufnr)
+ local request_result = nil
+ local function _sync_handler(err, _, result)
+ request_result = { err = err, result = result }
+ end
+
+ local success, request_id = client.request(method, params, _sync_handler,
+ bufnr)
+ if not success then return nil end
+
+ local wait_result, reason = vim.wait(timeout_ms or 1000, function()
+ return request_result ~= nil
+ end, 10)
+
+ if not wait_result then
+ client.cancel_request(request_id)
+ return nil, wait_result_reason[reason]
+ end
+ return request_result
+ end
+
+ --@private
--- Sends a notification to an LSP server.
---
--@param method (string) LSP method name.
@@ -1289,12 +1333,12 @@ end
---
--- Calls |vim.lsp.buf_request_all()| but blocks Nvim while awaiting the result.
--- Parameters are the same as |vim.lsp.buf_request()| but the return result is
---- different. Wait maximum of {timeout_ms} (default 100) ms.
+--- different. Wait maximum of {timeout_ms} (default 1000) ms.
---
--@param bufnr (number) Buffer handle, or 0 for current.
--@param method (string) LSP method name
--@param params (optional, table) Parameters to send to the server
---@param timeout_ms (optional, number, default=100) Maximum time in
+--@param timeout_ms (optional, number, default=1000) Maximum time in
--- milliseconds to wait for a result.
---
--@returns Map of client_id:request_result. On timeout, cancel or error,
@@ -1307,7 +1351,7 @@ function lsp.buf_request_sync(bufnr, method, params, timeout_ms)
request_results = it
end)
- local wait_result, reason = vim.wait(timeout_ms or 100, function()
+ local wait_result, reason = vim.wait(timeout_ms or 1000, function()
return request_results ~= nil
end, 10)
@@ -1378,7 +1422,7 @@ function lsp.omnifunc(findstart, base)
local items = {}
lsp.buf_request(bufnr, 'textDocument/completion', params, function(err, _, result)
- if err or not result then return end
+ if err or not result or vim.fn.mode() ~= "i" then return end
local matches = util.text_document_completion_list_to_complete_items(result, prefix)
-- TODO(ashkan): is this the best way to do this?
vim.list_extend(items, matches)
diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua
index 31116985e2..c63b947c99 100644
--- a/runtime/lua/vim/lsp/buf.lua
+++ b/runtime/lua/vim/lsp/buf.lua
@@ -111,6 +111,39 @@ function M.completion(context)
return request('textDocument/completion', params)
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)
+ 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
+ local choices = {}
+ for k,v in ipairs(clients) do
+ table.insert(choices, string.format("%d %s", k, v.name))
+ end
+ local user_choice = vim.fn.confirm(
+ "Select a language server:",
+ table.concat(choices, "\n"),
+ 0,
+ "Question"
+ )
+ if user_choice == 0 then return nil end
+ return clients[user_choice]
+ elseif #clients < 1 then
+ return nil
+ else
+ return clients[1]
+ end
+end
+
--- Formats the current buffer.
---
--@param options (optional, table) Can be used to specify FormattingOptions.
@@ -119,8 +152,11 @@ end
--
--@see https://microsoft.github.io/language-server-protocol/specification#textDocument_formatting
function M.formatting(options)
+ local client = select_client("textDocument/formatting")
+ if client == nil then return end
+
local params = util.make_formatting_params(options)
- return request('textDocument/formatting', params)
+ return client.request("textDocument/formatting", params)
end
--- Performs |vim.lsp.buf.formatting()| synchronously.
@@ -134,14 +170,62 @@ end
---
--@param options Table with valid `FormattingOptions` entries
--@param timeout_ms (number) Request timeout
+--@see |vim.lsp.buf.formatting_seq_sync|
function M.formatting_sync(options, timeout_ms)
+ local client = select_client("textDocument/formatting")
+ if client == nil then return end
+
local params = util.make_formatting_params(options)
- local result = vim.lsp.buf_request_sync(0, "textDocument/formatting", params, timeout_ms)
- if not result or vim.tbl_isempty(result) then return end
- local _, formatting_result = next(result)
- result = formatting_result.result
- if not result then return end
- vim.lsp.util.apply_text_edits(result)
+ local result, err = client.request_sync("textDocument/formatting", params, timeout_ms)
+ if result and result.result then
+ util.apply_text_edits(result.result)
+ elseif err then
+ vim.notify("vim.lsp.buf.formatting_sync: " .. err, vim.log.levels.WARN)
+ end
+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 (optional, table) `FormattingOptions` entries
+--@param timeout_ms (optional, number) Request timeout
+--@param order (optional, table) List of client names. Formatting is requested from clients
+---in the following order: first all clients that are not in the `order` list, then
+---the remaining clients in the order as they occur in the `order` list.
+function M.formatting_seq_sync(options, timeout_ms, order)
+ local clients = vim.tbl_values(vim.lsp.buf_get_clients());
+
+ -- sort the clients according to `order`
+ for _, client_name in ipairs(order or {}) do
+ -- if the client exists, move to the end of the list
+ for i, client in ipairs(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 ipairs(clients) do
+ if client.resolved_capabilities.document_formatting then
+ local params = util.make_formatting_params(options)
+ local result, err = client.request_sync("textDocument/formatting", params, timeout_ms)
+ if result and result.result then
+ util.apply_text_edits(result.result)
+ 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.
@@ -152,15 +236,12 @@ end
--@param end_pos ({number, number}, optional) mark-indexed position.
---Defaults to the end of the last visual selection.
function M.range_formatting(options, start_pos, end_pos)
- validate { options = {options, 't', true} }
- local sts = vim.bo.softtabstop;
- options = vim.tbl_extend('keep', options or {}, {
- tabSize = (sts > 0 and sts) or (sts < 0 and vim.bo.shiftwidth) or vim.bo.tabstop;
- insertSpaces = vim.bo.expandtab;
- })
+ local client = select_client("textDocument/rangeFormatting")
+ if client == nil then return end
+
local params = util.make_given_range_params(start_pos, end_pos)
- params.options = options
- return request('textDocument/rangeFormatting', params)
+ params.options = util.make_formatting_params(options).options
+ return client.request("textDocument/rangeFormatting", params)
end
--- Renames all references to the symbol under the cursor.
@@ -216,26 +297,31 @@ local function pick_call_hierarchy_item(call_hierarchy_items)
return choice
end
+--@private
+local function call_hierarchy(method)
+ local params = util.make_position_params()
+ request('textDocument/prepareCallHierarchy', params, function(err, _, result)
+ if err then
+ vim.notify(err.message, vim.log.levels.WARN)
+ return
+ end
+ local call_hierarchy_item = pick_call_hierarchy_item(result)
+ vim.lsp.buf_request(0, method, { item = call_hierarchy_item })
+ end)
+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|.
function M.incoming_calls()
- local params = util.make_position_params()
- request('textDocument/prepareCallHierarchy', params, function(_, _, result)
- local call_hierarchy_item = pick_call_hierarchy_item(result)
- vim.lsp.buf_request(0, 'callHierarchy/incomingCalls', { item = call_hierarchy_item })
- end)
+ 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|.
function M.outgoing_calls()
- local params = util.make_position_params()
- request('textDocument/prepareCallHierarchy', params, function(_, _, result)
- local call_hierarchy_item = pick_call_hierarchy_item(result)
- vim.lsp.buf_request(0, 'callHierarchy/outgoingCalls', { item = call_hierarchy_item })
- end)
+ call_hierarchy('callHierarchy/outgoingCalls')
end
--- List workspace folders.
diff --git a/runtime/lua/vim/lsp/codelens.lua b/runtime/lua/vim/lsp/codelens.lua
new file mode 100644
index 0000000000..fbd37e3830
--- /dev/null
+++ b/runtime/lua/vim/lsp/codelens.lua
@@ -0,0 +1,231 @@
+local util = require('vim.lsp.util')
+local api = vim.api
+local M = {}
+
+--- bufnr → true|nil
+--- to throttle refreshes to at most one at a time
+local active_refreshes = {}
+
+--- bufnr -> client_id -> lenses
+local lens_cache_by_buf = setmetatable({}, {
+ __index = function(t, b)
+ local key = b > 0 and b or api.nvim_get_current_buf()
+ return rawget(t, key)
+ end
+})
+
+local namespaces = setmetatable({}, {
+ __index = function(t, key)
+ local value = api.nvim_create_namespace('vim_lsp_codelens:' .. key)
+ rawset(t, key, value)
+ return value
+ end;
+})
+
+--@private
+M.__namespaces = namespaces
+
+
+--@private
+local function execute_lens(lens, bufnr, client_id)
+ local line = lens.range.start.line
+ api.nvim_buf_clear_namespace(bufnr, namespaces[client_id], line, line + 1)
+
+ -- Need to use the client that returned the lens → must not use buf_request
+ local client = vim.lsp.get_client_by_id(client_id)
+ assert(client, 'Client is required to execute lens, client_id=' .. client_id)
+ client.request('workspace/executeCommand', lens.command, function(...)
+ local result = vim.lsp.handlers['workspace/executeCommand'](...)
+ M.refresh()
+ return result
+ end, bufnr)
+end
+
+
+--- Return all lenses for the given buffer
+---
+---@return table (`CodeLens[]`)
+function M.get(bufnr)
+ local lenses_by_client = lens_cache_by_buf[bufnr]
+ if not lenses_by_client then return {} end
+ local lenses = {}
+ for _, client_lenses in pairs(lenses_by_client) do
+ vim.list_extend(lenses, client_lenses)
+ end
+ return lenses
+end
+
+
+--- Run the code lens in the current line
+---
+function M.run()
+ local line = api.nvim_win_get_cursor(0)[1]
+ local bufnr = api.nvim_get_current_buf()
+ local options = {}
+ local lenses_by_client = lens_cache_by_buf[bufnr] or {}
+ for client, lenses in pairs(lenses_by_client) do
+ for _, lens in pairs(lenses) do
+ if lens.range.start.line == (line - 1) then
+ table.insert(options, {client=client, lens=lens})
+ end
+ end
+ end
+ if #options == 0 then
+ vim.notify('No executable codelens found at current line')
+ elseif #options == 1 then
+ local option = options[1]
+ execute_lens(option.lens, bufnr, option.client)
+ else
+ local options_strings = {"Code lenses:"}
+ for i, option in ipairs(options) do
+ table.insert(options_strings, string.format('%d. %s', i, option.lens.command.title))
+ end
+ local choice = vim.fn.inputlist(options_strings)
+ if choice < 1 or choice > #options then
+ return
+ end
+ local option = options[choice]
+ execute_lens(option.lens, bufnr, option.client)
+ end
+end
+
+
+--- Display the lenses using virtual text
+---
+---@param lenses table of lenses to display (`CodeLens[] | null`)
+---@param bufnr number
+---@param client_id number
+function M.display(lenses, bufnr, client_id)
+ if not lenses or not next(lenses) then
+ return
+ end
+ local lenses_by_lnum = {}
+ for _, lens in pairs(lenses) do
+ local line_lenses = lenses_by_lnum[lens.range.start.line]
+ if not line_lenses then
+ line_lenses = {}
+ lenses_by_lnum[lens.range.start.line] = line_lenses
+ end
+ table.insert(line_lenses, lens)
+ end
+ local ns = namespaces[client_id]
+ local num_lines = api.nvim_buf_line_count(bufnr)
+ for i = 0, num_lines do
+ local line_lenses = lenses_by_lnum[i]
+ api.nvim_buf_clear_namespace(bufnr, ns, i, i + 1)
+ local chunks = {}
+ for _, lens in pairs(line_lenses or {}) do
+ local text = lens.command and lens.command.title or 'Unresolved lens ...'
+ table.insert(chunks, {text, 'LspCodeLens' })
+ end
+ if #chunks > 0 then
+ api.nvim_buf_set_virtual_text(bufnr, ns, i, chunks, {})
+ end
+ end
+end
+
+
+--- Store lenses for a specific buffer and client
+---
+---@param lenses table of lenses to store (`CodeLens[] | null`)
+---@param bufnr number
+---@param client_id number
+function M.save(lenses, bufnr, client_id)
+ local lenses_by_client = lens_cache_by_buf[bufnr]
+ if not lenses_by_client then
+ lenses_by_client = {}
+ lens_cache_by_buf[bufnr] = lenses_by_client
+ local ns = namespaces[client_id]
+ api.nvim_buf_attach(bufnr, false, {
+ on_detach = function(b) lens_cache_by_buf[b] = nil end,
+ on_lines = function(_, b, _, first_lnum, last_lnum)
+ api.nvim_buf_clear_namespace(b, ns, first_lnum, last_lnum)
+ end
+ })
+ end
+ lenses_by_client[client_id] = lenses
+end
+
+
+--@private
+local function resolve_lenses(lenses, bufnr, client_id, callback)
+ lenses = lenses or {}
+ local num_lens = vim.tbl_count(lenses)
+ if num_lens == 0 then
+ callback()
+ return
+ end
+
+ --@private
+ local function countdown()
+ num_lens = num_lens - 1
+ if num_lens == 0 then
+ callback()
+ end
+ end
+ local ns = namespaces[client_id]
+ local client = vim.lsp.get_client_by_id(client_id)
+ for _, lens in pairs(lenses or {}) do
+ if lens.command then
+ countdown()
+ else
+ client.request('codeLens/resolve', lens, function(_, _, result)
+ if result and result.command then
+ lens.command = result.command
+ -- Eager display to have some sort of incremental feedback
+ -- Once all lenses got resolved there will be a full redraw for all lenses
+ -- So that multiple lens per line are properly displayed
+ api.nvim_buf_set_virtual_text(
+ bufnr,
+ ns,
+ lens.range.start.line,
+ {{ lens.command.title, 'LspCodeLens' },},
+ {}
+ )
+ end
+ countdown()
+ end, bufnr)
+ end
+ end
+end
+
+
+--- |lsp-handler| for the method `textDocument/codeLens`
+---
+function M.on_codelens(err, _, result, client_id, bufnr)
+ assert(not err, vim.inspect(err))
+
+ M.save(result, bufnr, client_id)
+
+ -- Eager display for any resolved (and unresolved) lenses and refresh them
+ -- once resolved.
+ M.display(result, bufnr, client_id)
+ resolve_lenses(result, bufnr, client_id, function()
+ M.display(result, bufnr, client_id)
+ active_refreshes[bufnr] = nil
+ end)
+end
+
+
+--- Refresh the codelens for the current buffer
+---
+--- It is recommended to trigger this using an autocmd or via keymap.
+---
+--- <pre>
+--- autocmd BufEnter,CursorHold,InsertLeave <buffer> lua vim.lsp.codelens.refresh()
+--- </pre>
+---
+function M.refresh()
+ local params = {
+ textDocument = util.make_text_document_params()
+ }
+ local bufnr = api.nvim_get_current_buf()
+ if active_refreshes[bufnr] then
+ return
+ end
+ active_refreshes[bufnr] = true
+ vim.lsp.buf_request(0, 'textDocument/codeLens', params)
+end
+
+
+return M
diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua
index 6f2f846a3b..d4580885db 100644
--- a/runtime/lua/vim/lsp/diagnostic.lua
+++ b/runtime/lua/vim/lsp/diagnostic.lua
@@ -271,8 +271,12 @@ local function set_diagnostic_cache(diagnostics, bufnr, client_id)
end
-- Account for servers that place diagnostics on terminating newline
if buf_line_count > 0 then
- local start = diagnostic.range.start
- start.line = math.min(start.line, buf_line_count - 1)
+ diagnostic.range.start.line = math.max(math.min(
+ diagnostic.range.start.line, buf_line_count - 1
+ ), 0)
+ diagnostic.range["end"].line = math.max(math.min(
+ diagnostic.range["end"].line, buf_line_count - 1
+ ), 0)
end
end
@@ -317,9 +321,9 @@ function M.save(diagnostics, bufnr, client_id)
-- Clean up our data when the buffer unloads.
api.nvim_buf_attach(bufnr, false, {
- on_detach = function(b)
+ on_detach = function(_, b)
clear_diagnostic_cache(b, client_id)
- _diagnostic_cleanup[bufnr][client_id] = nil
+ _diagnostic_cleanup[b][client_id] = nil
end
})
end
@@ -330,15 +334,19 @@ end
-- Diagnostic Retrieval {{{
---- Get all diagnostics for all clients
+--- Get all diagnostics for clients
---
----@return {bufnr: Diagnostic[]}
-function M.get_all()
+---@param client_id number Restrict included diagnostics to the client
+--- If nil, diagnostics of all clients are included.
+---@return table with diagnostics grouped by bufnr (bufnr: Diagnostic[])
+function M.get_all(client_id)
local diagnostics_by_bufnr = {}
for bufnr, buf_diagnostics in pairs(diagnostic_cache) do
diagnostics_by_bufnr[bufnr] = {}
- for _, client_diagnostics in pairs(buf_diagnostics) do
- vim.list_extend(diagnostics_by_bufnr[bufnr], client_diagnostics)
+ for cid, client_diagnostics in pairs(buf_diagnostics) do
+ if client_id == nil or cid == client_id then
+ vim.list_extend(diagnostics_by_bufnr[bufnr], client_diagnostics)
+ end
end
end
return diagnostics_by_bufnr
@@ -1151,6 +1159,7 @@ function M.show_line_diagnostics(opts, bufnr, line_nr, client_id)
end
end
+ opts.focus_id = "line_diagnostics"
local popup_bufnr, winnr = util.open_floating_preview(lines, 'plaintext', opts)
for i, hi in ipairs(highlights) do
local prefixlen, hiname = unpack(hi)
@@ -1161,13 +1170,6 @@ function M.show_line_diagnostics(opts, bufnr, line_nr, client_id)
return popup_bufnr, winnr
end
-local loclist_type_map = {
- [DiagnosticSeverity.Error] = 'E',
- [DiagnosticSeverity.Warning] = 'W',
- [DiagnosticSeverity.Information] = 'I',
- [DiagnosticSeverity.Hint] = 'I',
-}
-
--- Clear diagnotics and diagnostic cache
---
@@ -1196,44 +1198,29 @@ end
--- - Exclusive severity to consider. Overrides {severity_limit}
--- - {severity_limit}: (DiagnosticSeverity)
--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
+--- - {workspace}: (boolean, default false)
+--- - Set the list with workspace diagnostics
function M.set_loclist(opts)
opts = opts or {}
-
local open_loclist = if_nil(opts.open_loclist, true)
-
- local bufnr = vim.api.nvim_get_current_buf()
- local buffer_diags = M.get(bufnr, opts.client_id)
-
- if opts.severity then
- buffer_diags = filter_to_severity_limit(opts.severity, buffer_diags)
- elseif opts.severity_limit then
- buffer_diags = filter_by_severity_limit(opts.severity_limit, buffer_diags)
- end
-
- local items = {}
- local insert_diag = function(diag)
- local pos = diag.range.start
- local row = pos.line
- local col = util.character_offset(bufnr, row, pos.character)
-
- local line = (api.nvim_buf_get_lines(bufnr, row, row + 1, false) or {""})[1]
-
- table.insert(items, {
- bufnr = bufnr,
- lnum = row + 1,
- col = col + 1,
- text = line .. " | " .. diag.message,
- type = loclist_type_map[diag.severity or DiagnosticSeverity.Error] or 'E',
- })
- end
-
- for _, diag in ipairs(buffer_diags) do
- insert_diag(diag)
+ local current_bufnr = api.nvim_get_current_buf()
+ local diags = opts.workspace and M.get_all(opts.client_id) or {
+ [current_bufnr] = M.get(current_bufnr, opts.client_id)
+ }
+ local predicate = function(d)
+ local severity = to_severity(opts.severity)
+ if severity then
+ return d.severity == severity
+ end
+ severity = to_severity(opts.severity_limit)
+ if severity then
+ return d.severity == severity
+ end
+ return true
end
-
- table.sort(items, function(a, b) return a.lnum < b.lnum end)
-
- util.set_loclist(items)
+ local items = util.diagnostics_to_items(diags, predicate)
+ local win_id = vim.api.nvim_get_current_win()
+ util.set_loclist(items, win_id)
if open_loclist then
vim.cmd [[lopen]]
end
diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua
index 525ec4ce5b..5d38150fc0 100644
--- a/runtime/lua/vim/lsp/handlers.lua
+++ b/runtime/lua/vim/lsp/handlers.lua
@@ -187,6 +187,10 @@ M['textDocument/publishDiagnostics'] = function(...)
return require('vim.lsp.diagnostic').on_publish_diagnostics(...)
end
+M['textDocument/codeLens'] = function(...)
+ return require('vim.lsp.codelens').on_codelens(...)
+end
+
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references
M['textDocument/references'] = function(_, _, result)
if not result then return end
@@ -260,24 +264,18 @@ end
--- - See |vim.api.nvim_open_win()|
function M.hover(_, method, result, _, _, config)
config = config or {}
- local bufnr, winnr = util.focusable_float(method, function()
- if not (result and result.contents) then
- -- return { 'No information available' }
- return
- end
- local markdown_lines = util.convert_input_to_markdown_lines(result.contents)
- markdown_lines = util.trim_empty_lines(markdown_lines)
- if vim.tbl_isempty(markdown_lines) then
- -- return { 'No information available' }
- return
- end
- local bufnr, winnr = util.fancy_floating_markdown(markdown_lines, {
- border = config.border
- })
- util.close_preview_autocmd({"CursorMoved", "BufHidden", "InsertCharPre"}, winnr)
- return bufnr, winnr
- end)
- return bufnr, winnr
+ config.focus_id = method
+ if not (result and result.contents) then
+ -- return { 'No information available' }
+ return
+ end
+ local markdown_lines = util.convert_input_to_markdown_lines(result.contents)
+ markdown_lines = util.trim_empty_lines(markdown_lines)
+ if vim.tbl_isempty(markdown_lines) then
+ -- return { 'No information available' }
+ return
+ end
+ return util.open_floating_preview(markdown_lines, "markdown", config)
end
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover
@@ -335,23 +333,21 @@ M['textDocument/implementation'] = location_handler
--- - See |vim.api.nvim_open_win()|
function M.signature_help(_, method, result, _, bufnr, config)
config = config or {}
+ config.focus_id = method
-- When use `autocmd CompleteDone <silent><buffer> lua vim.lsp.buf.signature_help()` to call signatureHelp handler
-- If the completion item doesn't have signatures It will make noise. Change to use `print` that can use `<silent>` to ignore
if not (result and result.signatures and result.signatures[1]) then
print('No signature help available')
return
end
- local lines = util.convert_signature_help_to_markdown_lines(result)
+ local ft = api.nvim_buf_get_option(bufnr, 'filetype')
+ local lines = util.convert_signature_help_to_markdown_lines(result, ft)
lines = util.trim_empty_lines(lines)
if vim.tbl_isempty(lines) then
print('No signature help available')
return
end
- local syntax = api.nvim_buf_get_option(bufnr, 'syntax')
- local p_bufnr, _ = util.focusable_preview(method, function()
- return lines, util.try_trim_markdown_code_blocks(lines), config
- end)
- api.nvim_buf_set_option(p_bufnr, 'syntax', syntax)
+ return util.open_floating_preview(lines, "markdown", config)
end
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp
diff --git a/runtime/lua/vim/lsp/log.lua b/runtime/lua/vim/lsp/log.lua
index 331e980e67..471a311c16 100644
--- a/runtime/lua/vim/lsp/log.lua
+++ b/runtime/lua/vim/lsp/log.lua
@@ -10,7 +10,7 @@ local log = {}
-- Can be used to lookup the number from the name or the name from the number.
-- Levels by name: 'trace', 'debug', 'info', 'warn', 'error'
-- Level numbers begin with 'trace' at 0
-log.levels = vim.log.levels
+log.levels = vim.deepcopy(vim.log.levels)
-- Default log level is warn.
local current_log_level = log.levels.WARN
diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua
index 1aa8326514..98835d6708 100644
--- a/runtime/lua/vim/lsp/rpc.lua
+++ b/runtime/lua/vim/lsp/rpc.lua
@@ -518,32 +518,38 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
send_response(decoded.id, err, result)
end)
-- This works because we are expecting vim.NIL here
- elseif decoded.id and (decoded.result or decoded.error) then
+ elseif decoded.id and (decoded.result ~= vim.NIL or decoded.error ~= vim.NIL) then
-- Server Result
decoded.error = convert_NIL(decoded.error)
decoded.result = convert_NIL(decoded.result)
+ -- We sent a number, so we expect a number.
+ local result_id = tonumber(decoded.id)
+
-- Do not surface RequestCancelled or ContentModified 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
elseif decoded.error.code == protocol.ErrorCodes.ContentModified then
local _ = log.debug() and log.debug("Received content modified ack", decoded)
+ mute_error = true
end
- local result_id = tonumber(decoded.id)
- -- 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 then
- message_callbacks[result_id] = nil
+
+ 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 then
+ message_callbacks[result_id] = nil
+ end
+ return
end
- return
end
- -- We sent a number, so we expect a number.
- local result_id = tonumber(decoded.id)
local callback = message_callbacks[result_id]
if callback then
message_callbacks[result_id] = nil
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index 92ec447b55..326005dac4 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -4,6 +4,7 @@ local validate = vim.validate
local api = vim.api
local list_extend = vim.list_extend
local highlight = require 'vim.highlight'
+local uv = vim.loop
local npcall = vim.F.npcall
local split = vim.split
@@ -29,6 +30,16 @@ local default_border = {
{" ", "NormalFloat"},
}
+
+local DiagnosticSeverity = protocol.DiagnosticSeverity
+local loclist_type_map = {
+ [DiagnosticSeverity.Error] = 'E',
+ [DiagnosticSeverity.Warning] = 'W',
+ [DiagnosticSeverity.Information] = 'I',
+ [DiagnosticSeverity.Hint] = 'I',
+}
+
+
--@private
-- Check the border given by opts or the default border for the additional
-- size it adds to a float.
@@ -39,14 +50,37 @@ local function get_border_size(opts)
local width = 0
if type(border) == 'string' then
- -- 'single', 'double', etc.
- height = 2
- width = 2
+ local border_size = {none = {0, 0}, single = {2, 2}, double = {2, 2}, rounded = {2, 2}, solid = {2, 2}, shadow = {1, 1}}
+ if border_size[border] == nil then
+ error("floating preview border is not correct. Please refer to the docs |vim.api.nvim_open_win()|"
+ .. vim.inspect(border))
+ end
+ height, width = unpack(border_size[border])
else
- height = height + vim.fn.strdisplaywidth(border[2][1]) -- top
- height = height + vim.fn.strdisplaywidth(border[6][1]) -- bottom
- width = width + vim.fn.strdisplaywidth(border[4][1]) -- right
- width = width + vim.fn.strdisplaywidth(border[8][1]) -- left
+ local function border_width(id)
+ if type(border[id]) == "table" then
+ -- border specified as a table of <character, highlight group>
+ return vim.fn.strdisplaywidth(border[id][1])
+ elseif type(border[id]) == "string" then
+ -- border specified as a list of border characters
+ return vim.fn.strdisplaywidth(border[id])
+ end
+ error("floating preview border is not correct. Please refer to the docs |vim.api.nvim_open_win()|" .. vim.inspect(border))
+ end
+ local function border_height(id)
+ if type(border[id]) == "table" then
+ -- border specified as a table of <character, highlight group>
+ return #border[id][1] > 0 and 1 or 0
+ elseif type(border[id]) == "string" then
+ -- border specified as a list of border characters
+ return #border[id] > 0 and 1 or 0
+ end
+ error("floating preview border is not correct. Please refer to the docs |vim.api.nvim_open_win()|" .. vim.inspect(border))
+ end
+ height = height + border_height(2) -- top
+ height = height + border_height(6) -- bottom
+ width = width + border_width(4) -- right
+ width = width + border_width(8) -- left
end
return { height = height, width = width }
@@ -73,7 +107,7 @@ function M.set_lines(lines, A, B, new_lines)
-- way the LSP describes the range including the last newline is by
-- specifying a line number after what we would call the last line.
local i_n = math.min(B[1] + 1, #lines)
- if not (i_0 >= 1 and i_0 <= #lines and i_n >= 1 and i_n <= #lines) then
+ if not (i_0 >= 1 and i_0 <= #lines + 1 and i_n >= 1 and i_n <= #lines) then
error("Invalid range: "..vim.inspect{A = A; B = B; #lines, new_lines})
end
local prefix = ""
@@ -566,13 +600,15 @@ end
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
local function get_completion_word(item)
if item.textEdit ~= nil and item.textEdit.newText ~= nil and item.textEdit.newText ~= "" then
- if protocol.InsertTextFormat[item.insertTextFormat] == "PlainText" then
+ local insert_text_format = protocol.InsertTextFormat[item.insertTextFormat]
+ if insert_text_format == "PlainText" or insert_text_format == nil then
return item.textEdit.newText
else
return M.parse_snippet(item.textEdit.newText)
end
elseif item.insertText ~= nil and item.insertText ~= "" then
- if protocol.InsertTextFormat[item.insertTextFormat] == "PlainText" then
+ local insert_text_format = protocol.InsertTextFormat[item.insertTextFormat]
+ if insert_text_format == "PlainText" or insert_text_format == nil then
return item.insertText
else
return M.parse_snippet(item.insertText)
@@ -770,14 +806,20 @@ function M.convert_input_to_markdown_lines(input, contents)
assert(type(input) == 'table', "Expected a table for Hover.contents")
-- MarkupContent
if input.kind then
- -- The kind can be either plaintext or markdown. However, either way we
- -- will just be rendering markdown, so we handle them both the same way.
- -- TODO these can have escaped/sanitized html codes in markdown. We
- -- should make sure we handle this correctly.
+ -- The kind can be either plaintext or markdown.
+ -- If it's plaintext, then wrap it in a <text></text> block
-- Some servers send input.value as empty, so let's ignore this :(
+ input.value = input.value or ''
+
+ if input.kind == "plaintext" then
+ -- wrap this in a <text></text> block so that stylize_markdown
+ -- can properly process it as plaintext
+ input.value = string.format("<text>\n%s\n</text>", input.value or "")
+ end
+
-- assert(type(input.value) == 'string')
- list_extend(contents, split_lines(input.value or ''))
+ list_extend(contents, split_lines(input.value))
-- MarkupString variation 2
elseif input.language then
-- Some servers send input.value as empty, so let's ignore this :(
@@ -802,9 +844,10 @@ end
--- Converts `textDocument/SignatureHelp` response to markdown lines.
---
--@param signature_help Response of `textDocument/SignatureHelp`
+--@param ft optional filetype that will be use as the `lang` for the label markdown code block
--@returns list of lines of converted markdown.
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp
-function M.convert_signature_help_to_markdown_lines(signature_help)
+function M.convert_signature_help_to_markdown_lines(signature_help, ft)
if not signature_help.signatures then
return
end
@@ -822,7 +865,12 @@ function M.convert_signature_help_to_markdown_lines(signature_help)
if not signature then
return
end
- vim.list_extend(contents, vim.split(signature.label, '\n', true))
+ local label = signature.label
+ if ft then
+ -- wrap inside a code block so fancy_markdown can render it properly
+ label = ("```%s\n%s\n```"):format(ft, label)
+ end
+ vim.list_extend(contents, vim.split(label, '\n', true))
if signature.documentation then
M.convert_input_to_markdown_lines(signature.documentation, contents)
end
@@ -906,6 +954,7 @@ function M.make_floating_popup_options(width, height, opts)
anchor = anchor,
col = col + (opts.offset_x or 0),
height = height,
+ focusable = opts.focusable,
relative = 'cursor',
row = row + (opts.offset_y or 0),
style = 'minimal',
@@ -914,23 +963,6 @@ function M.make_floating_popup_options(width, height, opts)
}
end
-local function _should_add_to_tagstack(new_item)
- local stack = vim.fn.gettagstack()
-
- -- Check if we're at the bottom of the tagstack.
- if stack.curidx <= 1 then return true end
-
- local top_item = stack.items[stack.curidx-1]
-
- -- Check if the item at the top of the tagstack is exactly the
- -- same as the one we want to push.
- if top_item.tagname ~= new_item.tagname then return true end
- for i, v in ipairs(top_item.from) do
- if v ~= new_item.from[i] then return true end
- end
- return false
-end
-
--- Jumps to a location.
---
--@param location (`Location`|`LocationLink`)
@@ -939,33 +971,22 @@ function M.jump_to_location(location)
-- location may be Location or LocationLink
local uri = location.uri or location.targetUri
if uri == nil then return end
+ local bufnr = vim.uri_to_bufnr(uri)
+ -- Save position in jumplist
+ vim.cmd "normal! m'"
- local from_bufnr = vim.fn.bufnr('%')
- local from = {from_bufnr, vim.fn.line('.'), vim.fn.col('.'), 0}
- local item = {tagname=vim.fn.expand('<cword>'), from=from}
+ -- 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')
--- Jump to new location (adjusting for UTF-16 encoding of characters)
- local bufnr = vim.uri_to_bufnr(uri)
api.nvim_set_current_buf(bufnr)
api.nvim_buf_set_option(0, 'buflisted', true)
local range = location.range or location.targetSelectionRange
local row = range.start.line
local col = get_line_byte_from_position(0, range.start)
- -- This prevents the tagstack to be filled with items that provide
- -- no motion when CTRL-T is pressed because they're both the source
- -- and the destination.
- local motionless =
- bufnr == from_bufnr and
- row+1 == from[2] and col+1 == from[3]
- if not motionless and _should_add_to_tagstack(item) then
- local winid = vim.fn.win_getid()
- local items = {item}
- vim.fn.settagstack(winid, {items=items}, 't')
- end
-
- -- Jump to new location
api.nvim_win_set_cursor(0, {row + 1, col})
-
return true
end
@@ -977,7 +998,7 @@ end
---
--@param location a single `Location` or `LocationLink`
--@returns (bufnr,winnr) buffer and window number of floating window or nil
-function M.preview_location(location)
+function M.preview_location(location, opts)
-- location may be LocationLink or Location (more useful for the former)
local uri = location.targetUri or location.uri
if uri == nil then return end
@@ -988,7 +1009,15 @@ function M.preview_location(location)
local range = location.targetRange or location.range
local contents = api.nvim_buf_get_lines(bufnr, range.start.line, range["end"].line+1, false)
local syntax = api.nvim_buf_get_option(bufnr, 'syntax')
- return M.open_floating_preview(contents, syntax)
+ if syntax == "" then
+ -- When no syntax is set, we use filetype as fallback. This might not result
+ -- in a valid syntax definition. See also ft detection in fancy_floating_win.
+ -- An empty syntax is more common now with TreeSitter, since TS disables syntax.
+ syntax = api.nvim_buf_get_option(bufnr, 'filetype')
+ end
+ opts = opts or {}
+ opts.focus_id = "location"
+ return M.open_floating_preview(contents, syntax, opts)
end
--@private
@@ -1010,7 +1039,9 @@ end
---buffer id, the newly created window will be the new focus associated with
---the current buffer via the tag `unique_name`.
--@returns (pbufnr, pwinnr) if `fn()` has created a new window; nil otherwise
+---@deprecated please use open_floating_preview directly
function M.focusable_float(unique_name, fn)
+ vim.notify("focusable_float is deprecated. Please use open_floating_preview and pass focus_id = [unique_name] instead", vim.log.levels.WARN)
-- Go back to previous window if we are in a focusable one
if npcall(api.nvim_win_get_var, 0, unique_name) then
return api.nvim_command("wincmd p")
@@ -1039,10 +1070,10 @@ end
--@param fn (function) The return values of this function will be passed
---directly to |vim.lsp.util.open_floating_preview()|, in the case that a new
---floating window should be created
+---@deprecated please use open_floating_preview directly
function M.focusable_preview(unique_name, fn)
- return M.focusable_float(unique_name, function()
- return M.open_floating_preview(fn())
- end)
+ vim.notify("focusable_preview is deprecated. Please use open_floating_preview and pass focus_id = [unique_name] instead", vim.log.levels.WARN)
+ return M.open_floating_preview(fn(), {focus_id = unique_name})
end
--- Trims empty lines from input and pad top and bottom with empty lines
@@ -1074,13 +1105,20 @@ end
--- TODO: refactor to separate stripping/converting and make use of open_floating_preview
---
+--- @deprecated please use open_floating_preview directly
+function M.fancy_floating_markdown(contents, opts)
+ vim.notify("fancy_floating_markdown is deprecated. Please use open_floating_preview and pass focus_id = [unique_name] instead", vim.log.levels.WARN)
+ return M.open_floating_preview(contents, "markdown", opts)
+end
+
--- Converts markdown into syntax highlighted regions by stripping the code
--- blocks and converting them into highlighted code.
--- This will by default insert a blank line separator after those code block
--- regions to improve readability.
---- The result is shown in a floating preview.
+---
+--- This method configures the given buffer and returns the lines to set.
+---
+--- If you want to open a popup with fancy markdown, use `open_floating_preview` instead
---
---@param contents table of lines to show in window
---@param opts dictionary with optional fields
@@ -1095,29 +1133,57 @@ end
--- - pad_bottom number of lines to pad contents at bottom
--- - separator insert separator after code block
---@returns width,height size of float
-function M.fancy_floating_markdown(contents, opts)
+function M.stylize_markdown(bufnr, contents, opts)
validate {
contents = { contents, 't' };
opts = { opts, 't', true };
}
opts = opts or {}
+ -- table of fence types to {ft, begin, end}
+ -- when ft is nil, we get the ft from the regex match
+ local matchers = {
+ block = {nil, "```+([a-zA-Z0-9_]*)", "```+"},
+ pre = {"", "<pre>", "</pre>"},
+ code = {"", "<code>", "</code>"},
+ text = {"plaintex", "<text>", "</text>"},
+ }
+
+ local match_begin = function(line)
+ for type, pattern in pairs(matchers) do
+ local ret = line:match(string.format("^%%s*%s%%s*$", pattern[2]))
+ if ret then
+ return {
+ type = type,
+ ft = pattern[1] or ret
+ }
+ end
+ end
+ end
+
+ local match_end = function(line, match)
+ local pattern = matchers[match.type]
+ return line:match(string.format("^%%s*%s%%s*$", pattern[3]))
+ end
+
+ -- Clean up
+ contents = M._trim(contents, opts)
+
local stripped = {}
local highlights = {}
+ -- keep track of lnums that contain markdown
+ local markdown_lines = {}
do
local i = 1
while i <= #contents do
local line = contents[i]
- -- TODO(ashkan): use a more strict regex for filetype?
- local ft = line:match("^```([a-zA-Z0-9_]*)$")
- -- local ft = line:match("^```(.*)$")
- -- TODO(ashkan): validate the filetype here.
- if ft then
+ local match = match_begin(line)
+ if match then
local start = #stripped
i = i + 1
while i <= #contents do
line = contents[i]
- if line == "```" then
+ if match_end(line, match) then
i = i + 1
break
end
@@ -1125,76 +1191,96 @@ function M.fancy_floating_markdown(contents, opts)
i = i + 1
end
table.insert(highlights, {
- ft = ft;
+ ft = match.ft;
start = start + 1;
finish = #stripped + 1 - 1;
})
else
table.insert(stripped, line)
+ markdown_lines[#stripped] = true
i = i + 1
end
end
end
- -- Clean up
- stripped = M._trim(stripped, opts)
-- Compute size of float needed to show (wrapped) lines
opts.wrap_at = opts.wrap_at or (vim.wo["wrap"] and api.nvim_win_get_width(0))
local width, height = M._make_floating_popup_size(stripped, opts)
+ local sep_line = string.rep("─", math.min(width, opts.wrap_at or width))
+
+ for l in pairs(markdown_lines) do
+ if stripped[l]:match("^---+$") then
+ stripped[l] = sep_line
+ end
+ end
+
-- Insert blank line separator after code block
local insert_separator = opts.separator
if insert_separator == nil then insert_separator = true end
if insert_separator then
- for i, h in ipairs(highlights) do
- h.start = h.start + i - 1
- h.finish = h.finish + i - 1
+ local offset = 0
+ for _, h in ipairs(highlights) do
+ h.start = h.start + offset
+ h.finish = h.finish + offset
+ -- check if a seperator already exists and use that one instead of creating a new one
if h.finish + 1 <= #stripped then
- table.insert(stripped, h.finish + 1, string.rep("─", math.min(width, opts.wrap_at or width)))
- height = height + 1
+ if stripped[h.finish + 1] ~= sep_line then
+ table.insert(stripped, h.finish + 1, sep_line)
+ offset = offset + 1
+ height = height + 1
+ end
end
end
end
- -- Make the floating window.
- local bufnr = api.nvim_create_buf(false, true)
- local winnr = api.nvim_open_win(bufnr, false, M.make_floating_popup_options(width, height, opts))
- vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, stripped)
- api.nvim_buf_set_option(bufnr, 'modifiable', false)
- -- Switch to the floating window to apply the syntax highlighting.
- -- This is because the syntax command doesn't accept a target.
- local cwin = vim.api.nvim_get_current_win()
- vim.api.nvim_set_current_win(winnr)
- api.nvim_win_set_option(winnr, 'conceallevel', 2)
- api.nvim_win_set_option(winnr, 'concealcursor', 'n')
+ vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, stripped)
- vim.cmd("ownsyntax lsp_markdown")
local idx = 1
--@private
+ -- keep track of syntaxes we already inlcuded.
+ -- no need to include the same syntax more than once
+ local langs = {}
local function apply_syntax_to_region(ft, start, finish)
- if ft == '' then return end
+ if ft == "" then
+ vim.cmd(string.format("syntax region markdownCode start=+\\%%%dl+ end=+\\%%%dl+ keepend extend", start, finish + 1))
+ return
+ end
local name = ft..idx
idx = idx + 1
local lang = "@"..ft:upper()
- -- TODO(ashkan): better validation before this.
- if not pcall(vim.cmd, string.format("syntax include %s syntax/%s.vim", lang, ft)) then
- return
+ if not langs[lang] then
+ -- HACK: reset current_syntax, since some syntax files like markdown won't load if it is already set
+ pcall(vim.api.nvim_buf_del_var, bufnr, "current_syntax")
+ -- TODO(ashkan): better validation before this.
+ if not pcall(vim.cmd, string.format("syntax include %s syntax/%s.vim", lang, ft)) then
+ return
+ end
+ langs[lang] = true
end
- vim.cmd(string.format("syntax region %s start=+\\%%%dl+ end=+\\%%%dl+ contains=%s", name, start, finish + 1, lang))
- end
- -- Previous highlight region.
- -- TODO(ashkan): this wasn't working for some reason, but I would like to
- -- make sure that regions between code blocks are definitely markdown.
- -- local ph = {start = 0; finish = 1;}
- for _, h in ipairs(highlights) do
- -- apply_syntax_to_region('markdown', ph.finish, h.start)
- apply_syntax_to_region(h.ft, h.start, h.finish)
- -- ph = h
+ vim.cmd(string.format("syntax region %s start=+\\%%%dl+ end=+\\%%%dl+ contains=%s keepend", name, start, finish + 1, lang))
end
- vim.api.nvim_set_current_win(cwin)
- return bufnr, winnr
+ -- needs to run in the buffer for the regions to work
+ api.nvim_buf_call(bufnr, function()
+ -- we need to apply lsp_markdown regions speperately, since otherwise
+ -- markdown regions can "bleed" through the other syntax regions
+ -- and mess up the formatting
+ local last = 1
+ for _, h in ipairs(highlights) do
+ if last < h.start then
+ apply_syntax_to_region("lsp_markdown", last, h.start - 1)
+ end
+ apply_syntax_to_region(h.ft, h.start, h.finish)
+ last = h.finish + 1
+ end
+ if last < #stripped then
+ apply_syntax_to_region("lsp_markdown", last, #stripped)
+ end
+ end)
+
+ return stripped
end
--- Creates autocommands to close a preview window when events happen.
@@ -1203,7 +1289,9 @@ end
--@param winnr (number) window id of preview window
--@see |autocmd-events|
function M.close_preview_autocmd(events, winnr)
- api.nvim_command("autocmd "..table.concat(events, ',').." <buffer> ++once lua pcall(vim.api.nvim_win_close, "..winnr..", true)")
+ if #events > 0 then
+ api.nvim_command("autocmd "..table.concat(events, ',').." <buffer> ++once lua pcall(vim.api.nvim_win_close, "..winnr..", true)")
+ end
end
--@internal
@@ -1286,15 +1374,19 @@ end
--@param contents table of lines to show in window
--@param syntax string of syntax to set for opened buffer
--@param opts dictionary with optional fields
--- - height of floating window
--- - width of floating window
--- - wrap_at character to wrap at for computing height
--- - max_width maximal width of floating window
--- - max_height maximal height of floating window
--- - pad_left number of columns to pad contents at left
--- - pad_right number of columns to pad contents at right
--- - pad_top number of lines to pad contents at top
--- - pad_bottom number of lines to pad contents at bottom
+--- - height of floating window
+--- - width of floating window
+--- - wrap boolean enable wrapping of long lines (defaults to true)
+--- - wrap_at character to wrap at for computing height when wrap is enabled
+--- - max_width maximal width of floating window
+--- - max_height maximal height of floating window
+--- - pad_left number of columns to pad contents at left
+--- - pad_right number of columns to pad contents at right
+--- - pad_top number of lines to pad contents at top
+--- - pad_bottom number of lines to pad contents at bottom
+--- - focus_id if a popup with this id is opened, then focus it
+--- - close_events list of events that closes the floating window
+--- - focusable (boolean, default true): Make float focusable
--@returns bufnr,winnr buffer and window number of the newly created floating
---preview window
function M.open_floating_preview(contents, syntax, opts)
@@ -1304,27 +1396,85 @@ function M.open_floating_preview(contents, syntax, opts)
opts = { opts, 't', true };
}
opts = opts or {}
+ opts.wrap = opts.wrap ~= false -- wrapping by default
+ opts.stylize_markdown = opts.stylize_markdown ~= false
+ opts.close_events = opts.close_events or {"CursorMoved", "CursorMovedI", "BufHidden", "InsertCharPre"}
+
+ local bufnr = api.nvim_get_current_buf()
+
+ -- check if this popup is focusable and we need to focus
+ if opts.focus_id and opts.focusable ~= false then
+ -- Go back to previous window if we are in a focusable one
+ local current_winnr = api.nvim_get_current_win()
+ if npcall(api.nvim_win_get_var, current_winnr, opts.focus_id) then
+ api.nvim_command("wincmd p")
+ return bufnr, current_winnr
+ end
+ do
+ local win = find_window_by_var(opts.focus_id, bufnr)
+ if win and api.nvim_win_is_valid(win) and vim.fn.pumvisible() == 0 then
+ -- focus and return the existing buf, win
+ api.nvim_set_current_win(win)
+ api.nvim_command("stopinsert")
+ return api.nvim_win_get_buf(win), win
+ end
+ end
+ end
+
+ -- check if another floating preview already exists for this buffer
+ -- and close it if needed
+ local existing_float = npcall(api.nvim_buf_get_var, bufnr, "lsp_floating_preview")
+ if existing_float and api.nvim_win_is_valid(existing_float) then
+ api.nvim_win_close(existing_float, true)
+ end
+
+ local floating_bufnr = api.nvim_create_buf(false, true)
+ local do_stylize = syntax == "markdown" and opts.stylize_markdown
+
-- Clean up input: trim empty lines from the end, pad
contents = M._trim(contents, opts)
+ if do_stylize then
+ -- applies the syntax and sets the lines to the buffer
+ contents = M.stylize_markdown(floating_bufnr, contents, opts)
+ else
+ if syntax then
+ api.nvim_buf_set_option(floating_bufnr, 'syntax', syntax)
+ end
+ api.nvim_buf_set_lines(floating_bufnr, 0, -1, true, contents)
+ end
+
-- Compute size of float needed to show (wrapped) lines
- opts.wrap_at = opts.wrap_at or (vim.wo["wrap"] and api.nvim_win_get_width(0))
+ if opts.wrap then
+ opts.wrap_at = opts.wrap_at or api.nvim_win_get_width(0)
+ else
+ opts.wrap_at = nil
+ end
local width, height = M._make_floating_popup_size(contents, opts)
- local floating_bufnr = api.nvim_create_buf(false, true)
- if syntax then
- api.nvim_buf_set_option(floating_bufnr, 'syntax', syntax)
- end
local float_option = M.make_floating_popup_options(width, height, opts)
local floating_winnr = api.nvim_open_win(floating_bufnr, false, float_option)
- if syntax == 'markdown' then
+ if do_stylize then
api.nvim_win_set_option(floating_winnr, 'conceallevel', 2)
+ api.nvim_win_set_option(floating_winnr, 'concealcursor', 'n')
end
- api.nvim_buf_set_lines(floating_bufnr, 0, -1, true, contents)
+ -- disable folding
+ api.nvim_win_set_option(floating_winnr, 'foldenable', false)
+ -- soft wrapping
+ api.nvim_win_set_option(floating_winnr, 'wrap', opts.wrap)
+
api.nvim_buf_set_option(floating_bufnr, 'modifiable', false)
api.nvim_buf_set_option(floating_bufnr, 'bufhidden', 'wipe')
- M.close_preview_autocmd({"CursorMoved", "CursorMovedI", "BufHidden", "BufLeave"}, floating_winnr)
+ api.nvim_buf_set_keymap(floating_bufnr, "n", "q", "<cmd>bdelete<cr>", {silent = true, noremap = true})
+ M.close_preview_autocmd(opts.close_events, floating_winnr)
+
+ -- save focus_id
+ if opts.focus_id then
+ api.nvim_win_set_var(floating_winnr, opts.focus_id, bufnr)
+ end
+ api.nvim_buf_set_var(bufnr, "lsp_floating_preview", floating_winnr)
+
return floating_bufnr, floating_winnr
end
@@ -1363,6 +1513,88 @@ local position_sort = sort_by_key(function(v)
return {v.start.line, v.start.character}
end)
+-- Gets the zero-indexed line from the given uri.
+-- For non-file uris, we load the buffer and get the line.
+-- If a loaded buffer exists, then that is used.
+-- Otherwise we get the line using libuv which is a lot faster than loading the buffer.
+--@param uri string uri of the resource to get the line from
+--@param row number zero-indexed line number
+--@return string the line at row in filename
+function M.get_line(uri, row)
+ return M.get_lines(uri, { row })[row]
+end
+
+-- Gets the zero-indexed lines from the given uri.
+-- For non-file uris, we load the buffer and get the lines.
+-- If a loaded buffer exists, then that is used.
+-- Otherwise we get the lines using libuv which is a lot faster than loading the buffer.
+--@param uri string uri of the resource to get the lines from
+--@param rows number[] zero-indexed line numbers
+--@return table<number string> a table mapping rows to lines
+function M.get_lines(uri, rows)
+ rows = type(rows) == "table" and rows or { rows }
+
+ local function buf_lines(bufnr)
+ local lines = {}
+ for _, row in pairs(rows) do
+ lines[row] = (vim.api.nvim_buf_get_lines(bufnr, row, row + 1, false) or { "" })[1]
+ end
+ return lines
+ end
+
+ -- load the buffer if this is not a file uri
+ -- Custom language server protocol extensions can result in servers sending URIs with custom schemes. Plugins are able to load these via `BufReadCmd` autocmds.
+ if uri:sub(1, 4) ~= "file" then
+ local bufnr = vim.uri_to_bufnr(uri)
+ vim.fn.bufload(bufnr)
+ return buf_lines(bufnr)
+ end
+
+ local filename = vim.uri_to_fname(uri)
+
+ -- use loaded buffers if available
+ if vim.fn.bufloaded(filename) == 1 then
+ local bufnr = vim.fn.bufnr(filename, false)
+ return buf_lines(bufnr)
+ end
+
+ -- get the data from the file
+ local fd = uv.fs_open(filename, "r", 438)
+ if not fd then return "" end
+ local stat = uv.fs_fstat(fd)
+ local data = uv.fs_read(fd, stat.size, 0)
+ uv.fs_close(fd)
+
+ local lines = {} -- rows we need to retrieve
+ local need = 0 -- keep track of how many unique rows we need
+ for _, row in pairs(rows) do
+ if not lines[row] then
+ need = need + 1
+ end
+ lines[row] = true
+ end
+
+ local found = 0
+ local lnum = 0
+
+ for line in string.gmatch(data, "([^\n]*)\n?") do
+ if lines[lnum] == true then
+ lines[lnum] = line
+ found = found + 1
+ if found == need then break end
+ end
+ lnum = lnum + 1
+ end
+
+ -- change any lines we didn't find to the empty string
+ for i, line in pairs(lines) do
+ if line == true then
+ lines[i] = ""
+ end
+ end
+ return lines
+end
+
--- Returns the items with the byte position calculated correctly and in sorted
--- order, for display in quickfix and location lists.
---
@@ -1391,14 +1623,24 @@ function M.locations_to_items(locations)
for _, uri in ipairs(keys) do
local rows = grouped[uri]
table.sort(rows, position_sort)
- local bufnr = vim.uri_to_bufnr(uri)
- vim.fn.bufload(bufnr)
local filename = vim.uri_to_fname(uri)
+
+ -- list of row numbers
+ local uri_rows = {}
for _, temp in ipairs(rows) do
local pos = temp.start
local row = pos.line
- local line = (api.nvim_buf_get_lines(bufnr, row, row + 1, false) or {""})[1]
- local col = M.character_offset(bufnr, row, pos.character)
+ table.insert(uri_rows, row)
+ end
+
+ -- get all the lines for this uri
+ local lines = M.get_lines(uri, uri_rows)
+
+ for _, temp in ipairs(rows) do
+ local pos = temp.start
+ local row = pos.line
+ local line = lines[row] or ""
+ local col = pos.character
table.insert(items, {
filename = filename,
lnum = row + 1,
@@ -1410,12 +1652,13 @@ function M.locations_to_items(locations)
return items
end
---- Fills current window's location list with given list of items.
+--- Fills target window's location list with given list of items.
--- Can be obtained with e.g. |vim.lsp.util.locations_to_items()|.
+--- Defaults to current window.
---
--@param items (table) list of items
-function M.set_loclist(items)
- vim.fn.setloclist(0, {}, ' ', {
+function M.set_loclist(items, win_id)
+ vim.fn.setloclist(win_id or 0, {}, ' ', {
title = 'Language Server';
items = items;
})
@@ -1456,13 +1699,13 @@ function M.symbols_to_items(symbols, bufnr)
kind = kind,
text = '['..kind..'] '..symbol.name,
})
- elseif symbol.range then -- DocumentSymbole type
+ elseif symbol.selectionRange then -- DocumentSymbole type
local kind = M._get_symbol_kind_name(symbol.kind)
table.insert(_items, {
-- bufnr = _bufnr,
filename = vim.api.nvim_buf_get_name(_bufnr),
- lnum = symbol.range.start.line + 1,
- col = symbol.range.start.character + 1,
+ lnum = symbol.selectionRange.start.line + 1,
+ col = symbol.selectionRange.start.character + 1,
kind = kind,
text = '['..kind..'] '..symbol.name
})
@@ -1592,6 +1835,12 @@ function M.make_given_range_params(start_pos, end_pos)
if B[2] > 0 then
B = {B[1], M.character_offset(0, B[1], B[2])}
end
+ -- we need to offset the end character position otherwise we loose the last
+ -- character of the selection, as LSP end position is exclusive
+ -- see https://microsoft.github.io/language-server-protocol/specification#range
+ if vim.o.selection ~= 'exclusive' then
+ B[2] = B[2] + 1
+ end
return {
textDocument = M.make_text_document_params(),
range = {
@@ -1650,8 +1899,9 @@ end
--@param row 0-indexed line
--@param col 0-indexed byte offset in line
--@returns (number, number) UTF-32 and UTF-16 index of the character in line {row} column {col} in buffer {buf}
-function M.character_offset(buf, row, col)
- local line = api.nvim_buf_get_lines(buf, row, row+1, true)[1]
+function M.character_offset(bufnr, row, col)
+ local uri = vim.uri_from_bufnr(bufnr)
+ local line = M.get_line(uri, row)
-- If the col is past the EOL, use the line length.
if col > #line then
return str_utfindex(line)
@@ -1674,6 +1924,40 @@ function M.lookup_section(settings, section)
return settings
end
+
+--- Convert diagnostics grouped by bufnr to a list of items for use in the
+--- quickfix or location list.
+---
+--@param diagnostics_by_bufnr table bufnr -> Diagnostic[]
+--@param predicate an optional function to filter the diagnostics.
+-- If present, only diagnostic items matching will be included.
+--@return table (A list of items)
+function M.diagnostics_to_items(diagnostics_by_bufnr, predicate)
+ local items = {}
+ for bufnr, diagnostics in pairs(diagnostics_by_bufnr or {}) do
+ for _, d in pairs(diagnostics) do
+ if not predicate or predicate(d) then
+ table.insert(items, {
+ bufnr = bufnr,
+ lnum = d.range.start.line + 1,
+ col = d.range.start.character + 1,
+ text = d.message,
+ type = loclist_type_map[d.severity or DiagnosticSeverity.Error] or 'E'
+ })
+ end
+ end
+ end
+ table.sort(items, function(a, b)
+ if a.bufnr == b.bufnr then
+ return a.lnum < b.lnum
+ else
+ return a.bufnr < b.bufnr
+ end
+ end)
+ return items
+end
+
+
M._get_line_byte_from_position = get_line_byte_from_position
M._warn_once = warn_once
diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua
index f223c7b8c8..de997b2d86 100644
--- a/runtime/lua/vim/treesitter.lua
+++ b/runtime/lua/vim/treesitter.lua
@@ -25,12 +25,12 @@ setmetatable(M, {
})
--- Creates a new parser.
---
--- It is not recommended to use this, use vim.treesitter.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 language tree
+---
+--- It is not recommended to use this, use vim.treesitter.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
function M._create_parser(bufnr, lang, opts)
language.require_language(lang)
if bufnr == 0 then
@@ -41,10 +41,12 @@ function M._create_parser(bufnr, lang, opts)
local self = LanguageTree.new(bufnr, lang, opts)
+ ---@private
local function bytes_cb(_, ...)
self:_on_bytes(...)
end
+ ---@private
local function detach_cb(_, ...)
if parsers[bufnr] == self then
parsers[bufnr] = nil
@@ -52,6 +54,7 @@ function M._create_parser(bufnr, lang, opts)
self:_on_detach(...)
end
+ ---@private
local function reload_cb(_, ...)
self:_on_reload(...)
end
@@ -64,15 +67,15 @@ function M._create_parser(bufnr, lang, opts)
end
--- Gets the parser for this bufnr / ft combination.
---
--- If needed this will create the parser.
--- Unconditionnally attach the provided callback
---
--- @param bufnr The buffer the parser should be tied to
--- @param ft The filetype of this parser
--- @param opts Options object to pass to the parser
---
--- @returns The parser
+---
+--- If needed this will create the parser.
+--- Unconditionnally attach the provided callback
+---
+--- @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
+---
+--- @returns The parser
function M.get_parser(bufnr, lang, opts)
opts = opts or {}
@@ -92,6 +95,11 @@ function M.get_parser(bufnr, lang, opts)
return parsers[bufnr]
end
+--- Gets a string parser
+---
+--- @param str The string to parse
+--- @param lang The language of this string
+--- @param opts Options to pass to the created language tree
function M.get_string_parser(str, lang, opts)
vim.validate {
str = { str, 'string' },
diff --git a/runtime/lua/vim/treesitter/health.lua b/runtime/lua/vim/treesitter/health.lua
index dd0b11a6c7..e031ba1bd6 100644
--- a/runtime/lua/vim/treesitter/health.lua
+++ b/runtime/lua/vim/treesitter/health.lua
@@ -1,10 +1,14 @@
local M = {}
local ts = vim.treesitter
+--- Lists the parsers currently installed
+---
+---@return A list of parsers
function M.list_parsers()
return vim.api.nvim_get_runtime_file('parser/*', true)
end
+--- Performs a healthcheck for treesitter integration
function M.check_health()
local report_info = vim.fn['health#report_info']
local report_ok = vim.fn['health#report_ok']
diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua
index fe7e1052c9..84b6a5f135 100644
--- a/runtime/lua/vim/treesitter/highlighter.lua
+++ b/runtime/lua/vim/treesitter/highlighter.lua
@@ -70,11 +70,13 @@ TSHighlighter.hl_map = {
["include"] = "Include",
}
+---@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 })
@@ -99,10 +101,12 @@ function TSHighlighterQuery.new(lang, query_string)
return self
end
+---@private
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)
@@ -116,6 +120,11 @@ function TSHighlighterQuery:_get_hl_from_capture(capture)
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
function TSHighlighter.new(tree, opts)
local self = setmetatable({}, TSHighlighter)
@@ -165,12 +174,14 @@ function TSHighlighter.new(tree, opts)
return self
end
+--- Removes all internal references to the highlighter
function TSHighlighter:destroy()
if TSHighlighter.active[self.bufnr] then
TSHighlighter.active[self.bufnr] = nil
end
end
+---@private
function TSHighlighter:get_highlight_state(tstree)
if not self._highlight_states[tstree] then
self._highlight_states[tstree] = {
@@ -182,24 +193,31 @@ function TSHighlighter:get_highlight_state(tstree)
return self._highlight_states[tstree]
end
+---@private
function TSHighlighter:reset_highlight_state()
self._highlight_states = {}
end
+---@private
function TSHighlighter:on_bytes(_, _, start_row, _, _, _, _, _, new_end)
a.nvim__buf_redraw_range(self.bufnr, start_row, start_row + new_end + 1)
end
+---@private
function TSHighlighter:on_detach()
self:destroy()
end
+---@private
function TSHighlighter:on_changedtree(changes)
for _, ch in ipairs(changes or {}) do
a.nvim__buf_redraw_range(self.bufnr, ch[1], ch[3]+1)
end
end
+--- Gets the query used for @param lang
+---
+--- @param lang A language used by the highlighter.
function TSHighlighter:get_query(lang)
if not self._queries[lang] then
self._queries[lang] = TSHighlighterQuery.new(lang)
@@ -208,6 +226,7 @@ function TSHighlighter:get_query(lang)
return self._queries[lang]
end
+---@private
local function on_line_impl(self, buf, line)
self.tree:for_each_tree(function(tstree, tree)
if not tstree then return end
@@ -251,6 +270,7 @@ local function on_line_impl(self, buf, line)
end, true)
end
+---@private
function TSHighlighter._on_line(_, _win, buf, line, _)
local self = TSHighlighter.active[buf]
if not self then return end
@@ -258,6 +278,7 @@ function TSHighlighter._on_line(_, _win, buf, line, _)
on_line_impl(self, buf, line)
end
+---@private
function TSHighlighter._on_buf(_, buf)
local self = TSHighlighter.active[buf]
if self then
@@ -265,6 +286,7 @@ function TSHighlighter._on_buf(_, buf)
end
end
+---@private
function TSHighlighter._on_win(_, _win, buf, _topline)
local self = TSHighlighter.active[buf]
if not self then
diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua
index eed28e0e41..6dc37c7848 100644
--- a/runtime/lua/vim/treesitter/language.lua
+++ b/runtime/lua/vim/treesitter/language.lua
@@ -3,12 +3,12 @@ local a = vim.api
local M = {}
--- Asserts that the provided language is installed, and optionally provide a path for the parser
---
--- Parsers are searched in the `parser` runtime directory.
---
--- @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
+---
+--- Parsers are searched in the `parser` runtime directory.
+---
+--- @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)
if vim._ts_has_language(lang) then
return true
@@ -37,10 +37,10 @@ function M.require_language(lang, path, silent)
end
--- Inspects the provided language.
---
--- Inspecting provides some useful informations on the language like node names, ...
---
--- @param lang The language.
+---
+--- Inspecting provides some useful informations on the language like node names, ...
+---
+--- @param lang The language.
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 2f5aeb0710..899d90e464 100644
--- a/runtime/lua/vim/treesitter/languagetree.lua
+++ b/runtime/lua/vim/treesitter/languagetree.lua
@@ -5,16 +5,16 @@ local language = require'vim.treesitter.language'
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.
---
--- @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.
+--- Represents a single treesitter parser for a language.
+--- The language can contain child languages with in its range,
+--- hence the tree.
+---
+--- @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.
function LanguageTree.new(source, lang, opts)
language.require_language(lang)
opts = opts or {}
@@ -50,7 +50,7 @@ function LanguageTree.new(source, lang, opts)
return self
end
--- Invalidates this parser and all its children
+--- Invalidates this parser and all its children
function LanguageTree:invalidate(reload)
self._valid = false
@@ -64,38 +64,38 @@ function LanguageTree:invalidate(reload)
end
end
--- Returns all trees this language tree contains.
--- Does not include child languages.
+--- Returns all trees this language tree contains.
+--- Does not include child languages.
function LanguageTree:trees()
return self._trees
end
--- Gets the language of this tree layer.
+--- Gets the language of this tree node.
function LanguageTree:lang()
return self._lang
end
--- Determines whether this tree is valid.
--- If the tree is invalid, `parse()` must be called
--- to get the an updated tree.
+--- Determines whether this tree is valid.
+--- If the tree is invalid, `parse()` must be called
+--- to get the an updated tree.
function LanguageTree:is_valid()
return self._valid
end
--- Returns a map of language to child tree.
+--- Returns a map of language to child tree.
function LanguageTree:children()
return self._children
end
--- Returns the source content of the language tree (bufnr or string).
+--- Returns the source content of the language tree (bufnr or string).
function LanguageTree:source()
return self._source
end
--- Parses all defined regions using a treesitter parser
--- for the language this tree represents.
--- This will run the injection query for this language to
--- determine if any child languages should be created.
+--- Parses all defined regions using a treesitter parser
+--- for the language this tree represents.
+--- This will run the injection query for this language to
+--- determine if any child languages should be created.
function LanguageTree:parse()
if self._valid then
return self._trees
@@ -169,9 +169,10 @@ function LanguageTree:parse()
return self._trees, changes
end
--- Invokes the callback for each LanguageTree and it's 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.
+--- Invokes the callback for each LanguageTree and it's 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.
function LanguageTree:for_each_child(fn, include_self)
if include_self then
fn(self, self._lang)
@@ -182,10 +183,12 @@ function LanguageTree:for_each_child(fn, include_self)
end
end
--- Invokes the callback for each treesitter trees recursively.
--- Note, this includes the invoking language tree's trees as well.
--- @param fn The callback to invoke. The callback is invoked with arguments
--- (tree: TSTree, languageTree: LanguageTree)
+--- Invokes the callback for each treesitter trees recursively.
+---
+--- Note, this includes the invoking language tree's trees as well.
+---
+--- @param fn The callback to invoke. The callback is invoked with arguments
+--- (tree: TSTree, languageTree: LanguageTree)
function LanguageTree:for_each_tree(fn)
for _, tree in ipairs(self._trees) do
fn(tree, self)
@@ -196,9 +199,11 @@ function LanguageTree:for_each_tree(fn)
end
end
--- Adds a child language to this tree.
--- If the language already exists as a child, it will first be removed.
--- @param lang The language to add.
+--- Adds a child language to this tree.
+---
+--- If the language already exists as a child, it will first be removed.
+---
+--- @param lang The language to add.
function LanguageTree:add_child(lang)
if self._children[lang] then
self:remove_child(lang)
@@ -212,8 +217,9 @@ function LanguageTree:add_child(lang)
return self._children[lang]
end
--- Removes a child language from this tree.
--- @param lang The language to remove.
+--- Removes a child language from this tree.
+---
+--- @param lang The language to remove.
function LanguageTree:remove_child(lang)
local child = self._children[lang]
@@ -225,10 +231,11 @@ function LanguageTree:remove_child(lang)
end
end
--- Destroys this language tree and all its children.
--- Any cleanup logic should be performed here.
--- Note, this DOES NOT remove this tree from a parent.
--- `remove_child` must be called on the parent to remove it.
+--- Destroys this language tree and all its children.
+---
+--- Any cleanup logic should be performed here.
+--- Note, this DOES NOT remove this tree from a parent.
+--- `remove_child` must be called on the parent to remove it.
function LanguageTree:destroy()
-- Cleanup here
for _, child in ipairs(self._children) do
@@ -236,23 +243,23 @@ function LanguageTree:destroy()
end
end
--- Sets the included regions that should be parsed by this parser.
--- 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
--- 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
--- 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.
---
--- @param regions A list of regions this tree should manage and parse.
+--- Sets the included regions that should be parsed by this parser.
+--- 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
+--- 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
+--- 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.
+---
+--- @param regions A list of regions this tree should manage and parse.
function LanguageTree:set_included_regions(regions)
-- TODO(vigoux): I don't think string parsers are useful for now
if type(self._source) == "number" then
@@ -281,16 +288,18 @@ function LanguageTree:set_included_regions(regions)
self:invalidate()
end
--- Gets the set of included regions
+--- Gets the set of included regions
function LanguageTree:included_regions()
return self._regions
end
--- Gets language injection points by language.
--- This is where most of the injection processing occurs.
--- TODO: Allow for an offset predicate to tailor the injection range
--- instead of using the entire nodes range.
--- @private
+--- Gets language injection points by language.
+---
+--- This is where most of the injection processing occurs.
+---
+--- TODO: Allow for an offset predicate to tailor the injection range
+--- instead of using the entire nodes range.
+--- @private
function LanguageTree:_get_injections()
if not self._injection_query then return {} end
@@ -395,12 +404,14 @@ function LanguageTree:_get_injections()
return result
end
+---@private
function LanguageTree:_do_callback(cb_name, ...)
for _, cb in ipairs(self._callbacks[cb_name]) do
cb(...)
end
end
+---@private
function LanguageTree:_on_bytes(bufnr, changed_tick,
start_row, start_col, start_byte,
old_row, old_col, old_byte,
@@ -425,24 +436,26 @@ function LanguageTree:_on_bytes(bufnr, changed_tick,
new_row, new_col, new_byte)
end
+---@private
function LanguageTree:_on_reload()
self:invalidate(true)
end
+---@private
function LanguageTree:_on_detach(...)
self:invalidate(true)
self:_do_callback('detach', ...)
end
--- Registers callbacks for the parser
--- @param cbs An `nvim_buf_attach`-like table argument with the following keys :
--- `on_bytes` : see `nvim_buf_attach`, but this will be called _after_ the parsers callback.
--- `on_changedtree` : a callback that will be called every time the tree has syntactical changes.
--- it will only be passed one argument, that is a table of the ranges (as node ranges) that
--- changed.
--- `on_child_added` : emitted when a child is added to the tree.
--- `on_child_removed` : emitted when a child is removed from the tree.
+--- @param cbs An `nvim_buf_attach`-like table argument with the following keys :
+--- `on_bytes` : see `nvim_buf_attach`, but this will be called _after_ the parsers callback.
+--- `on_changedtree` : a callback that will be called every time the tree has syntactical changes.
+--- it will only be passed one argument, that is a table of the ranges (as node ranges) that
+--- changed.
+--- `on_child_added` : emitted when a child is added to the tree.
+--- `on_child_removed` : emitted when a child is removed from the tree.
function LanguageTree:register_cbs(cbs)
if not cbs then return end
@@ -467,6 +480,7 @@ function LanguageTree:register_cbs(cbs)
end
end
+---@private
local function tree_contains(tree, range)
local start_row, start_col, end_row, end_col = tree:root():range()
local start_fits = start_row < range[1] or (start_row == range[1] and start_col <= range[2])
@@ -479,6 +493,11 @@ local function tree_contains(tree, range)
return false
end
+--- Determines wether @param range is contained in this language tree
+---
+--- This goes down the tree to recursively check childs.
+---
+--- @param range A range, that is a `{ start_line, start_col, end_line, end_col }` table.
function LanguageTree:contains(range)
for _, tree in pairs(self._trees) do
if tree_contains(tree, range) then
@@ -489,6 +508,9 @@ function LanguageTree:contains(range)
return false
end
+--- Gets the appropriate language that contains @param range
+---
+--- @param range A text range, see |LanguageTree:contains|
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 ed5146be44..b81eb18945 100644
--- a/runtime/lua/vim/treesitter/query.lua
+++ b/runtime/lua/vim/treesitter/query.lua
@@ -8,6 +8,7 @@ Query.__index = Query
local M = {}
+---@private
local function dedupe_files(files)
local result = {}
local seen = {}
@@ -22,6 +23,7 @@ local function dedupe_files(files)
return result
end
+---@private
local function safe_read(filename, read_quantifier)
local file, err = io.open(filename, 'r')
if not file then
@@ -32,6 +34,11 @@ local function safe_read(filename, read_quantifier)
return content
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`
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))
@@ -79,6 +86,7 @@ function M.get_query_files(lang, query_name, is_included)
return query_files
end
+---@private
local function read_query_files(filenames)
local contents = {}
@@ -103,19 +111,20 @@ local explicit_queries = setmetatable({}, {
---
--- 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: 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).
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")
---
--- @return The corresponding query, parsed.
+---
+--- @param lang The language to use for the query
+--- @param query_name The name of the query (i.e. "highlights")
+---
+--- @return The corresponding query, parsed.
function M.get_query(lang, query_name)
if explicit_queries[lang][query_name] then
return explicit_queries[lang][query_name]
@@ -129,12 +138,23 @@ function M.get_query(lang, query_name)
end
end
---- Parses a query.
---
--- @param language The language
--- @param query A string containing the query (s-expr syntax)
---
--- @returns The query
+--- Parse {query} as a string. (If the query is in a file, the caller
+--- should read the contents into a string before calling).
+---
+--- Returns a `Query` (see |lua-treesitter-query|) object which can be used to
+--- search nodes in the syntax tree for the patterns defined in {query}
+--- using `iter_*` methods below.
+---
+--- Exposes `info` and `captures` with additional information about the {query}.
+--- - `captures` contains the list of unique capture names defined in
+--- {query}.
+--- -` info.captures` also points to `captures`.
+--- - `info.patterns` contains information about predicates.
+---
+--- @param lang The language
+--- @param query A string containing the query (s-expr syntax)
+---
+--- @returns The query
function M.parse_query(lang, query)
language.require_language(lang)
local self = setmetatable({}, Query)
@@ -147,8 +167,9 @@ end
-- TODO(vigoux): support multiline nodes too
--- Gets the text corresponding to a given node
--- @param node the node
--- @param bufnr the buffer from which the node is extracted.
+---
+--- @param node the node
+--- @param bsource The buffer or string from which the node is extracted
function M.get_node_text(node, source)
local start_row, start_col, start_byte = node:start()
local end_row, end_col, end_byte = node:end_()
@@ -200,6 +221,7 @@ local predicate_handlers = {
["match?"] = (function()
local magic_prefixes = {['\\v']=true, ['\\m']=true, ['\\M']=true, ['\\V']=true}
+ ---@private
local function check_magic(str)
if string.len(str) < 2 or magic_prefixes[string.sub(str,1,2)] then
return str
@@ -209,7 +231,7 @@ local predicate_handlers = {
local compiled_vim_regexes = setmetatable({}, {
__index = function(t, pattern)
- local res = vim.regex(check_magic(vim.fn.escape(pattern, '\\')))
+ local res = vim.regex(check_magic(pattern))
rawset(t, pattern, res)
return res
end
@@ -238,7 +260,25 @@ local predicate_handlers = {
end
return false
- end
+ end,
+
+ ["any-of?"] = function(match, _, source, predicate)
+ local node = match[predicate[2]]
+ local node_text = M.get_node_text(node, source)
+
+ -- Since 'predicate' will not be used by callers of this function, use it
+ -- to store a string set built from the list of words to check against.
+ local string_set = predicate["string_set"]
+ if not string_set then
+ string_set = {}
+ for i=3,#predicate do
+ string_set[predicate[i]] = true
+ end
+ predicate["string_set"] = string_set
+ end
+
+ return string_set[node_text]
+ end,
}
-- As we provide lua-match? also expose vim-match?
@@ -253,7 +293,11 @@ local directive_handlers = {
["set!"] = function(_, _, _, pred, metadata)
if #pred == 4 then
-- (#set! @capture "key" "value")
- metadata[pred[2]][pred[3]] = pred[4]
+ local capture = pred[2]
+ if not metadata[capture] then
+ metadata[capture] = {}
+ end
+ metadata[capture][pred[3]] = pred[4]
else
-- (#set! "key" "value")
metadata[pred[2]] = pred[3]
@@ -282,10 +326,10 @@ 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 the name of the predicate, without leading #
+--- @param handler the handler function to be used
+--- signature will be (match, pattern, bufnr, predicate)
function M.add_predicate(name, handler, force)
if predicate_handlers[name] and not force then
error(string.format("Overriding %s", name))
@@ -295,10 +339,10 @@ function M.add_predicate(name, handler, force)
end
--- Adds a new directive to be used in queries
---
--- @param name the name of the directive, without leading #
--- @param handler the handler function to be used
--- signature will be (match, pattern, bufnr, predicate)
+---
+--- @param name the name of the directive, without leading #
+--- @param handler the handler function to be used
+--- signature will be (match, pattern, bufnr, predicate)
function M.add_directive(name, handler, force)
if directive_handlers[name] and not force then
error(string.format("Overriding %s", name))
@@ -312,14 +356,17 @@ function M.list_predicates()
return vim.tbl_keys(predicate_handlers)
end
+---@private
local function xor(x, y)
return (x or y) and not (x and y)
end
+---@private
local function is_directive(name)
return string.sub(name, -1) == "!"
end
+---@private
function Query:match_preds(match, pattern, source)
local preds = self.info.patterns[pattern]
@@ -358,7 +405,7 @@ function Query:match_preds(match, pattern, source)
return true
end
---- Applies directives against a match and pattern.
+---@private
function Query:apply_directives(match, pattern, source, metadata)
local preds = self.info.patterns[pattern]
@@ -380,6 +427,7 @@ end
--- Returns the start and stop value if set else the node's range.
-- When the node's range is used, the stop is incremented by 1
-- to make the search inclusive.
+---@private
local function value_or_node_range(start, stop, node)
if start == nil and stop == nil then
local node_start, _, node_stop, _ = node:range()
@@ -389,15 +437,36 @@ local function value_or_node_range(start, stop, node)
return start, stop
end
---- Iterates of the captures of self on a given range.
---
--- @param node The node under which the search will occur
--- @param buffer The source buffer to search
--- @param start The starting line of the search
--- @param stop The stopping line of the search (end-exclusive)
---
--- @returns The matching capture id
--- @returns The captured node
+--- Iterate over all captures from all matches inside {node}
+---
+--- {source} is needed if the query contains predicates, then the caller
+--- must ensure to use a freshly parsed tree consistent with the current
+--- text of the buffer (if relevent). {start_row} and {end_row} can be used to limit
+--- 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.
+---
+--- 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
+--- -- typically useful info about the node:
+--- local type = node:type() -- type of the captured node
+--- local row1, col1, row2, col2 = node:range() -- range of the capture
+--- ... use the info here ...
+--- end
+--- </pre>
+---
+--- @param node The node under which the search will occur
+--- @param source The source buffer or string to exctract text from
+--- @param start The starting line of the search
+--- @param stop The stopping line of the search (end-exclusive)
+---
+--- @returns The matching capture id
+--- @returns The captured node
function Query:iter_captures(node, source, start, stop)
if type(source) == "number" and source == 0 then
source = vim.api.nvim_get_current_buf()
@@ -406,6 +475,7 @@ function Query:iter_captures(node, source, start, stop)
start, stop = value_or_node_range(start, stop, node)
local raw_iter = node:_rawquery(self.query, true, start, stop)
+ ---@private
local function iter()
local capture, captured_node, match = raw_iter()
local metadata = {}
@@ -425,14 +495,35 @@ function Query:iter_captures(node, source, start, stop)
end
--- Iterates the matches of self on a given range.
---
--- @param node The node under which the search will occur
--- @param buffer The source buffer to search
--- @param start The starting line of the search
--- @param stop The stopping line of the search (end-exclusive)
---
--- @returns The matching pattern id
--- @returns The matching match
+---
+--- 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,
+--- and e.g. `pairs()` method should be used over `ipairs`.
+--- Here 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
+--- local name = query.captures[id]
+--- -- `node` was captured by the `name` capture in the match
+---
+--- local node_data = metadata[id] -- Node level metadata
+---
+--- ... use the info here ...
+--- 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)
+---
+--- @returns The matching pattern id
+--- @returns The matching match
function Query:iter_matches(node, source, start, stop)
if type(source) == "number" and source == 0 then
source = vim.api.nvim_get_current_buf()