aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/lsp
diff options
context:
space:
mode:
authorJosh Rahm <rahm@google.com>2022-07-18 19:37:18 +0000
committerJosh Rahm <rahm@google.com>2022-07-18 19:37:18 +0000
commit308e1940dcd64aa6c344c403d4f9e0dda58d9c5c (patch)
tree35fe43e01755e0f312650667004487a44d6b7941 /runtime/lua/vim/lsp
parent96a00c7c588b2f38a2424aeeb4ea3581d370bf2d (diff)
parente8c94697bcbe23a5c7b07c292b90a6b70aadfa87 (diff)
downloadrneovim-308e1940dcd64aa6c344c403d4f9e0dda58d9c5c.tar.gz
rneovim-308e1940dcd64aa6c344c403d4f9e0dda58d9c5c.tar.bz2
rneovim-308e1940dcd64aa6c344c403d4f9e0dda58d9c5c.zip
Merge remote-tracking branch 'upstream/master' into rahm
Diffstat (limited to 'runtime/lua/vim/lsp')
-rw-r--r--runtime/lua/vim/lsp/_snippet.lua247
-rw-r--r--runtime/lua/vim/lsp/buf.lua545
-rw-r--r--runtime/lua/vim/lsp/codelens.lua66
-rw-r--r--runtime/lua/vim/lsp/diagnostic.lua503
-rw-r--r--runtime/lua/vim/lsp/handlers.lua263
-rw-r--r--runtime/lua/vim/lsp/health.lua17
-rw-r--r--runtime/lua/vim/lsp/log.lua118
-rw-r--r--runtime/lua/vim/lsp/protocol.lua584
-rw-r--r--runtime/lua/vim/lsp/rpc.lua272
-rw-r--r--runtime/lua/vim/lsp/sync.lua107
-rw-r--r--runtime/lua/vim/lsp/tagfunc.lua7
-rw-r--r--runtime/lua/vim/lsp/util.lua929
12 files changed, 1990 insertions, 1668 deletions
diff --git a/runtime/lua/vim/lsp/_snippet.lua b/runtime/lua/vim/lsp/_snippet.lua
index 0140b0aee3..3488639fb4 100644
--- a/runtime/lua/vim/lsp/_snippet.lua
+++ b/runtime/lua/vim/lsp/_snippet.lua
@@ -41,7 +41,7 @@ P.take_until = function(targets, specials)
parsed = true,
value = {
raw = table.concat(raw, ''),
- esc = table.concat(esc, '')
+ esc = table.concat(esc, ''),
},
pos = new_pos,
}
@@ -156,10 +156,10 @@ P.seq = function(...)
return function(input, pos)
local values = {}
local new_pos = pos
- for _, parser in ipairs(parsers) do
+ for i, parser in ipairs(parsers) do
local result = parser(input, new_pos)
if result.parsed then
- table.insert(values, result.value)
+ values[i] = result.value
new_pos = result.pos
else
return P.unmatch(pos)
@@ -248,49 +248,122 @@ S.format = P.any(
capture_index = values[3],
}, Node)
end),
- P.map(P.seq(S.dollar, S.open, S.int, S.colon, S.slash, P.any(
- P.token('upcase'),
- P.token('downcase'),
- P.token('capitalize'),
- P.token('camelcase'),
- P.token('pascalcase')
- ), S.close), function(values)
- return setmetatable({
- type = Node.Type.FORMAT,
- capture_index = values[3],
- modifier = values[6],
- }, Node)
- end),
- P.map(P.seq(S.dollar, S.open, S.int, S.colon, P.any(
- P.seq(S.question, P.take_until({ ':' }, { '\\' }), S.colon, P.take_until({ '}' }, { '\\' })),
- P.seq(S.plus, P.take_until({ '}' }, { '\\' })),
- P.seq(S.minus, P.take_until({ '}' }, { '\\' }))
- ), S.close), function(values)
+ P.map(
+ P.seq(
+ S.dollar,
+ S.open,
+ S.int,
+ S.colon,
+ S.slash,
+ P.any(
+ P.token('upcase'),
+ P.token('downcase'),
+ P.token('capitalize'),
+ P.token('camelcase'),
+ P.token('pascalcase')
+ ),
+ S.close
+ ),
+ function(values)
+ return setmetatable({
+ type = Node.Type.FORMAT,
+ capture_index = values[3],
+ modifier = values[6],
+ }, Node)
+ end
+ ),
+ P.map(
+ P.seq(
+ S.dollar,
+ S.open,
+ S.int,
+ S.colon,
+ P.seq(
+ S.question,
+ P.opt(P.take_until({ ':' }, { '\\' })),
+ S.colon,
+ P.opt(P.take_until({ '}' }, { '\\' }))
+ ),
+ S.close
+ ),
+ function(values)
+ return setmetatable({
+ type = Node.Type.FORMAT,
+ capture_index = values[3],
+ if_text = values[5][2] and values[5][2].esc or '',
+ else_text = values[5][4] and values[5][4].esc or '',
+ }, Node)
+ end
+ ),
+ P.map(
+ P.seq(
+ S.dollar,
+ S.open,
+ S.int,
+ S.colon,
+ P.seq(S.plus, P.opt(P.take_until({ '}' }, { '\\' }))),
+ S.close
+ ),
+ function(values)
+ return setmetatable({
+ type = Node.Type.FORMAT,
+ capture_index = values[3],
+ if_text = values[5][2] and values[5][2].esc or '',
+ else_text = '',
+ }, Node)
+ end
+ ),
+ P.map(
+ P.seq(
+ S.dollar,
+ S.open,
+ S.int,
+ S.colon,
+ S.minus,
+ P.opt(P.take_until({ '}' }, { '\\' })),
+ S.close
+ ),
+ function(values)
+ return setmetatable({
+ type = Node.Type.FORMAT,
+ capture_index = values[3],
+ if_text = '',
+ else_text = values[6] and values[6].esc or '',
+ }, Node)
+ end
+ ),
+ P.map(
+ P.seq(S.dollar, S.open, S.int, S.colon, P.opt(P.take_until({ '}' }, { '\\' })), S.close),
+ function(values)
+ return setmetatable({
+ type = Node.Type.FORMAT,
+ capture_index = values[3],
+ if_text = '',
+ else_text = values[5] and values[5].esc or '',
+ }, Node)
+ end
+ )
+)
+
+S.transform = P.map(
+ P.seq(
+ S.slash,
+ P.take_until({ '/' }, { '\\' }),
+ S.slash,
+ P.many(P.any(S.format, S.text({ '$', '/' }, { '\\' }))),
+ S.slash,
+ P.opt(P.pattern('[ig]+'))
+ ),
+ function(values)
return setmetatable({
- type = Node.Type.FORMAT,
- capture_index = values[3],
- if_text = values[5][2].esc,
- else_text = (values[5][4] or {}).esc,
+ type = Node.Type.TRANSFORM,
+ pattern = values[2].raw,
+ format = values[4],
+ option = values[6],
}, Node)
- end)
+ end
)
-S.transform = P.map(P.seq(
- S.slash,
- P.take_until({ '/' }, { '\\' }),
- S.slash,
- P.many(P.any(S.format, S.text({ '$', '/' }, { '\\' }))),
- S.slash,
- P.opt(P.pattern('[ig]+'))
-), function(values)
- return setmetatable({
- type = Node.Type.TRANSFORM,
- pattern = values[2].raw,
- format = values[4],
- option = values[6],
- }, Node)
-end)
-
S.tabstop = P.any(
P.map(P.seq(S.dollar, S.int), function(values)
return setmetatable({
@@ -314,34 +387,52 @@ S.tabstop = P.any(
)
S.placeholder = P.any(
- P.map(P.seq(S.dollar, S.open, S.int, S.colon, P.many(P.any(S.toplevel, S.text({ '$', '}' }, { '\\' }))), S.close), function(values)
- return setmetatable({
- type = Node.Type.PLACEHOLDER,
- tabstop = values[3],
- children = values[5],
- }, Node)
- end)
+ P.map(
+ P.seq(
+ S.dollar,
+ S.open,
+ S.int,
+ S.colon,
+ P.opt(P.many(P.any(S.toplevel, S.text({ '$', '}' }, { '\\' })))),
+ S.close
+ ),
+ function(values)
+ return setmetatable({
+ type = Node.Type.PLACEHOLDER,
+ tabstop = values[3],
+ -- insert empty text if opt did not match.
+ children = values[5] or {
+ setmetatable({
+ type = Node.Type.TEXT,
+ raw = '',
+ esc = '',
+ }, Node),
+ },
+ }, Node)
+ end
+ )
)
-S.choice = P.map(P.seq(
- S.dollar,
- S.open,
- S.int,
- S.pipe,
- P.many(
- P.map(P.seq(S.text({ ',', '|' }), P.opt(S.comma)), function(values)
+S.choice = P.map(
+ P.seq(
+ S.dollar,
+ S.open,
+ S.int,
+ S.pipe,
+ P.many(P.map(P.seq(S.text({ ',', '|' }), P.opt(S.comma)), function(values)
return values[1].esc
- end)
+ end)),
+ S.pipe,
+ S.close
),
- S.pipe,
- S.close
-), function(values)
- return setmetatable({
- type = Node.Type.CHOICE,
- tabstop = values[3],
- items = values[5],
- }, Node)
-end)
+ function(values)
+ return setmetatable({
+ type = Node.Type.CHOICE,
+ tabstop = values[3],
+ items = values[5],
+ }, Node)
+ end
+)
S.variable = P.any(
P.map(P.seq(S.dollar, S.var), function(values)
@@ -363,13 +454,23 @@ S.variable = P.any(
transform = values[4],
}, Node)
end),
- P.map(P.seq(S.dollar, S.open, S.var, S.colon, P.many(P.any(S.toplevel, S.text({ '$', '}' }, { '\\' }))), S.close), function(values)
- return setmetatable({
- type = Node.Type.VARIABLE,
- name = values[3],
- children = values[5],
- }, Node)
- end)
+ P.map(
+ P.seq(
+ S.dollar,
+ S.open,
+ S.var,
+ S.colon,
+ P.many(P.any(S.toplevel, S.text({ '$', '}' }, { '\\' }))),
+ S.close
+ ),
+ function(values)
+ return setmetatable({
+ type = Node.Type.VARIABLE,
+ name = values[3],
+ children = values[5],
+ }, Node)
+ end
+ )
)
S.snippet = P.map(P.many(P.any(S.toplevel, S.text({ '$' }, { '}', '\\' }))), function(values)
diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua
index c1d777ae6c..50a51e897c 100644
--- a/runtime/lua/vim/lsp/buf.lua
+++ b/runtime/lua/vim/lsp/buf.lua
@@ -1,30 +1,12 @@
local vim = vim
+local api = vim.api
local validate = vim.validate
-local vfn = vim.fn
-local util = require 'vim.lsp.util'
+local util = require('vim.lsp.util')
+local npcall = vim.F.npcall
local M = {}
---@private
---- Returns nil if {status} is false or nil, otherwise returns the rest of the
---- arguments.
-local function ok_or_nil(status, ...)
- if not status then return end
- return ...
-end
-
----@private
---- Swallows errors.
----
----@param fn Function to run
----@param ... Function arguments
----@returns Result of `fn(...)` if there are no errors, otherwise nil.
---- Returns nil if errors occur during {fn}, otherwise returns
-local function npcall(fn, ...)
- return ok_or_nil(pcall(fn, ...))
-end
-
----@private
--- Sends an async request to all active clients attached to the current
--- buffer.
---
@@ -39,10 +21,10 @@ end
---
---@see |vim.lsp.buf_request()|
local function request(method, params, handler)
- validate {
- method = {method, 's'};
- handler = {handler, 'f', true};
- }
+ validate({
+ method = { method, 's' },
+ handler = { handler, 'f', true },
+ })
return vim.lsp.buf_request(0, method, params, handler)
end
@@ -51,7 +33,7 @@ end
---
---@returns `true` if server responds.
function M.server_ready()
- return not not vim.lsp.buf_notify(0, "window/progress", {})
+ return not not vim.lsp.buf_notify(0, 'window/progress', {})
end
--- Displays hover information about the symbol under the cursor in a floating
@@ -61,26 +43,45 @@ function M.hover()
request('textDocument/hover', params)
end
+---@private
+local function request_with_options(name, params, options)
+ local req_handler
+ if options then
+ req_handler = function(err, result, ctx, config)
+ local client = vim.lsp.get_client_by_id(ctx.client_id)
+ local handler = client.handlers[name] or vim.lsp.handlers[name]
+ handler(err, result, ctx, vim.tbl_extend('force', config or {}, options))
+ end
+ end
+ request(name, params, req_handler)
+end
+
--- Jumps to the declaration of the symbol under the cursor.
---@note Many servers do not implement this method. Generally, see |vim.lsp.buf.definition()| instead.
---
-function M.declaration()
+---@param options table|nil additional options
+--- - reuse_win: (boolean) Jump to existing window if buffer is already open.
+function M.declaration(options)
local params = util.make_position_params()
- request('textDocument/declaration', params)
+ request_with_options('textDocument/declaration', params, options)
end
--- Jumps to the definition of the symbol under the cursor.
---
-function M.definition()
+---@param options table|nil additional options
+--- - reuse_win: (boolean) Jump to existing window if buffer is already open.
+function M.definition(options)
local params = util.make_position_params()
- request('textDocument/definition', params)
+ request_with_options('textDocument/definition', params, options)
end
--- Jumps to the definition of the type of the symbol under the cursor.
---
-function M.type_definition()
+---@param options table|nil additional options
+--- - reuse_win: (boolean) Jump to existing window if buffer is already open.
+function M.type_definition(options)
local params = util.make_position_params()
- request('textDocument/typeDefinition', params)
+ request_with_options('textDocument/typeDefinition', params, options)
end
--- Lists all the implementations for the symbol under the cursor in the
@@ -117,9 +118,9 @@ end
--
---@returns The client that the user selected or nil
local function select_client(method, on_choice)
- validate {
+ validate({
on_choice = { on_choice, 'function', false },
- }
+ })
local clients = vim.tbl_values(vim.lsp.buf_get_clients())
clients = vim.tbl_filter(function(client)
return client.supports_method(method)
@@ -143,16 +144,105 @@ local function select_client(method, on_choice)
end
end
+--- Formats a buffer using the attached (and optionally filtered) language
+--- server clients.
+---
+--- @param options table|nil Optional table which holds the following optional fields:
+--- - formatting_options (table|nil):
+--- Can be used to specify FormattingOptions. Some unspecified options will be
+--- automatically derived from the current Neovim options.
+--- @see https://microsoft.github.io/language-server-protocol/specification#textDocument_formatting
+--- - timeout_ms (integer|nil, default 1000):
+--- Time in milliseconds to block for formatting requests. No effect if async=true
+--- - bufnr (number|nil):
+--- Restrict formatting to the clients attached to the given buffer, defaults to the current
+--- buffer (0).
+---
+--- - filter (function|nil):
+--- Predicate used to filter clients. Receives a client as argument and must return a
+--- boolean. Clients matching the predicate are included. Example:
+---
+--- <pre>
+--- -- Never request typescript-language-server for formatting
+--- vim.lsp.buf.format {
+--- filter = function(client) return client.name ~= "tsserver" end
+--- }
+--- </pre>
+---
+--- - async boolean|nil
+--- If true the method won't block. Defaults to false.
+--- Editing the buffer while formatting asynchronous can lead to unexpected
+--- changes.
+---
+--- - id (number|nil):
+--- Restrict formatting to the client with ID (client.id) matching this field.
+--- - name (string|nil):
+--- Restrict formatting to the client with name (client.name) matching this field.
+
+function M.format(options)
+ options = options or {}
+ local bufnr = options.bufnr or api.nvim_get_current_buf()
+ local clients = vim.lsp.get_active_clients({
+ id = options.id,
+ bufnr = bufnr,
+ name = options.name,
+ })
+
+ if options.filter then
+ clients = vim.tbl_filter(options.filter, clients)
+ end
+
+ clients = vim.tbl_filter(function(client)
+ return client.supports_method('textDocument/formatting')
+ end, clients)
+
+ if #clients == 0 then
+ vim.notify('[LSP] Format request failed, no matching language servers.')
+ end
+
+ if options.async then
+ local do_format
+ do_format = function(idx, client)
+ if not client then
+ return
+ end
+ local params = util.make_formatting_params(options.formatting_options)
+ client.request('textDocument/formatting', params, function(...)
+ local handler = client.handlers['textDocument/formatting']
+ or vim.lsp.handlers['textDocument/formatting']
+ handler(...)
+ do_format(next(clients, idx))
+ end, bufnr)
+ end
+ do_format(next(clients))
+ else
+ local timeout_ms = options.timeout_ms or 1000
+ for _, client in pairs(clients) do
+ local params = util.make_formatting_params(options.formatting_options)
+ local result, err = client.request_sync('textDocument/formatting', params, timeout_ms, bufnr)
+ if result and result.result then
+ util.apply_text_edits(result.result, bufnr, client.offset_encoding)
+ elseif err then
+ vim.notify(string.format('[LSP][%s] %s', client.name, err), vim.log.levels.WARN)
+ end
+ end
+ end
+end
+
--- Formats the current buffer.
---
----@param options (optional, table) Can be used to specify FormattingOptions.
+---@param options (table|nil) Can be used to specify FormattingOptions.
--- Some unspecified options will be automatically derived from the current
--- Neovim options.
--
---@see https://microsoft.github.io/language-server-protocol/specification#textDocument_formatting
function M.formatting(options)
+ vim.notify_once(
+ 'vim.lsp.buf.formatting is deprecated. Use vim.lsp.buf.format { async = true } instead',
+ vim.log.levels.WARN
+ )
local params = util.make_formatting_params(options)
- local bufnr = vim.api.nvim_get_current_buf()
+ local bufnr = api.nvim_get_current_buf()
select_client('textDocument/formatting', function(client)
if client == nil then
return
@@ -171,12 +261,16 @@ end
--- autocmd BufWritePre <buffer> lua vim.lsp.buf.formatting_sync()
--- </pre>
---
----@param options Table with valid `FormattingOptions` entries
+---@param options table|nil with valid `FormattingOptions` entries
---@param timeout_ms (number) Request timeout
---@see |vim.lsp.buf.formatting_seq_sync|
function M.formatting_sync(options, timeout_ms)
+ vim.notify_once(
+ 'vim.lsp.buf.formatting_sync is deprecated. Use vim.lsp.buf.format instead',
+ vim.log.levels.WARN
+ )
local params = util.make_formatting_params(options)
- local bufnr = vim.api.nvim_get_current_buf()
+ local bufnr = api.nvim_get_current_buf()
select_client('textDocument/formatting', function(client)
if client == nil then
return
@@ -184,7 +278,7 @@ function M.formatting_sync(options, timeout_ms)
local result, err = client.request_sync('textDocument/formatting', params, timeout_ms, bufnr)
if result and result.result then
- util.apply_text_edits(result.result, bufnr)
+ util.apply_text_edits(result.result, bufnr, client.offset_encoding)
elseif err then
vim.notify('vim.lsp.buf.formatting_sync: ' .. err, vim.log.levels.WARN)
end
@@ -202,14 +296,18 @@ end
--- vim.api.nvim_command[[autocmd BufWritePre <buffer> lua vim.lsp.buf.formatting_seq_sync()]]
--- </pre>
---
----@param options (optional, table) `FormattingOptions` entries
----@param timeout_ms (optional, number) Request timeout
----@param order (optional, table) List of client names. Formatting is requested from clients
+---@param options (table|nil) `FormattingOptions` entries
+---@param timeout_ms (number|nil) Request timeout
+---@param order (table|nil) List of client names. Formatting is requested from clients
---in the following order: first all clients that are not in the `order` list, then
---the remaining clients in the order as they occur in the `order` list.
function M.formatting_seq_sync(options, timeout_ms, order)
- local clients = vim.tbl_values(vim.lsp.buf_get_clients());
- local bufnr = vim.api.nvim_get_current_buf()
+ vim.notify_once(
+ 'vim.lsp.buf.formatting_seq_sync is deprecated. Use vim.lsp.buf.format instead',
+ vim.log.levels.WARN
+ )
+ local clients = vim.tbl_values(vim.lsp.buf_get_clients())
+ local bufnr = api.nvim_get_current_buf()
-- sort the clients according to `order`
for _, client_name in pairs(order or {}) do
@@ -224,13 +322,21 @@ function M.formatting_seq_sync(options, timeout_ms, order)
-- loop through the clients and make synchronous formatting requests
for _, client in pairs(clients) do
- if client.resolved_capabilities.document_formatting then
+ if vim.tbl_get(client.server_capabilities, 'documentFormattingProvider') then
local params = util.make_formatting_params(options)
- local result, err = client.request_sync("textDocument/formatting", params, timeout_ms, vim.api.nvim_get_current_buf())
+ local result, err = client.request_sync(
+ 'textDocument/formatting',
+ params,
+ timeout_ms,
+ api.nvim_get_current_buf()
+ )
if result and result.result then
- util.apply_text_edits(result.result, bufnr)
+ util.apply_text_edits(result.result, bufnr, client.offset_encoding)
elseif err then
- vim.notify(string.format("vim.lsp.buf.formatting_seq_sync: (%s) %s", client.name, err), vim.log.levels.WARN)
+ vim.notify(
+ string.format('vim.lsp.buf.formatting_seq_sync: (%s) %s', client.name, err),
+ vim.log.levels.WARN
+ )
end
end
end
@@ -257,50 +363,133 @@ end
--- Renames all references to the symbol under the cursor.
---
----@param new_name (string) If not provided, the user will be prompted for a new
----name using |vim.ui.input()|.
-function M.rename(new_name)
- local opts = {
- prompt = "New Name: "
- }
+---@param new_name string|nil If not provided, the user will be prompted for a new
+--- name using |vim.ui.input()|.
+---@param options table|nil additional options
+--- - filter (function|nil):
+--- Predicate used to filter clients. Receives a client as argument and
+--- must return a boolean. Clients matching the predicate are included.
+--- - name (string|nil):
+--- Restrict clients used for rename to ones where client.name matches
+--- this field.
+function M.rename(new_name, options)
+ options = options or {}
+ local bufnr = options.bufnr or api.nvim_get_current_buf()
+ local clients = vim.lsp.get_active_clients({
+ bufnr = bufnr,
+ name = options.name,
+ })
+ if options.filter then
+ clients = vim.tbl_filter(options.filter, clients)
+ end
- ---@private
- local function on_confirm(input)
- if not (input and #input > 0) then return end
- local params = util.make_position_params()
- params.newName = input
- request('textDocument/rename', params)
+ -- Clients must at least support rename, prepareRename is optional
+ clients = vim.tbl_filter(function(client)
+ return client.supports_method('textDocument/rename')
+ end, clients)
+
+ if #clients == 0 then
+ vim.notify('[LSP] Rename, no matching language servers with rename capability.')
end
+ local win = api.nvim_get_current_win()
+
+ -- Compute early to account for cursor movements after going async
+ local cword = vim.fn.expand('<cword>')
+
---@private
- local function prepare_rename(err, result)
- if err == nil and result == nil then
- vim.notify('nothing to rename', vim.log.levels.INFO)
+ local function get_text_at_range(range, offset_encoding)
+ return api.nvim_buf_get_text(
+ bufnr,
+ range.start.line,
+ util._get_line_byte_from_position(bufnr, range.start, offset_encoding),
+ range['end'].line,
+ util._get_line_byte_from_position(bufnr, range['end'], offset_encoding),
+ {}
+ )[1]
+ end
+
+ local try_use_client
+ try_use_client = function(idx, client)
+ if not client then
return
end
- if result and result.placeholder then
- opts.default = result.placeholder
- if not new_name then npcall(vim.ui.input, opts, on_confirm) end
- elseif result and result.start and result['end'] and
- result.start.line == result['end'].line then
- local line = vfn.getline(result.start.line+1)
- local start_char = result.start.character+1
- local end_char = result['end'].character
- opts.default = string.sub(line, start_char, end_char)
- if not new_name then npcall(vim.ui.input, opts, on_confirm) end
+
+ ---@private
+ local function rename(name)
+ local params = util.make_position_params(win, client.offset_encoding)
+ params.newName = name
+ local handler = client.handlers['textDocument/rename']
+ or vim.lsp.handlers['textDocument/rename']
+ client.request('textDocument/rename', params, function(...)
+ handler(...)
+ try_use_client(next(clients, idx))
+ end, bufnr)
+ end
+
+ if client.supports_method('textDocument/prepareRename') then
+ local params = util.make_position_params(win, client.offset_encoding)
+ client.request('textDocument/prepareRename', params, function(err, result)
+ if err or result == nil then
+ if next(clients, idx) then
+ try_use_client(next(clients, idx))
+ else
+ local msg = err and ('Error on prepareRename: ' .. (err.message or ''))
+ or 'Nothing to rename'
+ vim.notify(msg, vim.log.levels.INFO)
+ end
+ return
+ end
+
+ if new_name then
+ rename(new_name)
+ return
+ end
+
+ local prompt_opts = {
+ prompt = 'New Name: ',
+ }
+ -- result: Range | { range: Range, placeholder: string }
+ if result.placeholder then
+ prompt_opts.default = result.placeholder
+ elseif result.start then
+ prompt_opts.default = get_text_at_range(result, client.offset_encoding)
+ elseif result.range then
+ prompt_opts.default = get_text_at_range(result.range, client.offset_encoding)
+ else
+ prompt_opts.default = cword
+ end
+ vim.ui.input(prompt_opts, function(input)
+ if not input or #input == 0 then
+ return
+ end
+ rename(input)
+ end)
+ end, bufnr)
else
- -- fallback to guessing symbol using <cword>
- --
- -- this can happen if the language server does not support prepareRename,
- -- returns an unexpected response, or requests for "default behavior"
- --
- -- see https://microsoft.github.io/language-server-protocol/specification#textDocument_prepareRename
- opts.default = vfn.expand('<cword>')
- if not new_name then npcall(vim.ui.input, opts, on_confirm) end
+ assert(
+ client.supports_method('textDocument/rename'),
+ 'Client must support textDocument/rename'
+ )
+ if new_name then
+ rename(new_name)
+ return
+ end
+
+ local prompt_opts = {
+ prompt = 'New Name: ',
+ default = cword,
+ }
+ vim.ui.input(prompt_opts, function(input)
+ if not input or #input == 0 then
+ return
+ end
+ rename(input)
+ end)
end
- if new_name then on_confirm(new_name) end
end
- request('textDocument/prepareRename', util.make_position_params(), prepare_rename)
+
+ try_use_client(next(clients))
end
--- Lists all the references to the symbol under the cursor in the quickfix window.
@@ -308,10 +497,10 @@ end
---@param context (table) Context for the request
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references
function M.references(context)
- validate { context = { context, 't', true } }
+ validate({ context = { context, 't', true } })
local params = util.make_position_params()
params.context = context or {
- includeDeclaration = true;
+ includeDeclaration = true,
}
request('textDocument/references', params)
end
@@ -325,14 +514,16 @@ end
---@private
local function pick_call_hierarchy_item(call_hierarchy_items)
- if not call_hierarchy_items then return end
+ if not call_hierarchy_items then
+ return
+ end
if #call_hierarchy_items == 1 then
return call_hierarchy_items[1]
end
local items = {}
for i, item in pairs(call_hierarchy_items) do
local entry = item.detail or item.name
- table.insert(items, string.format("%d. %s", i, entry))
+ table.insert(items, string.format('%d. %s', i, entry))
end
local choice = vim.fn.inputlist(items)
if choice < 1 or choice > #items then
@@ -354,8 +545,8 @@ local function call_hierarchy(method)
if client then
client.request(method, { item = call_hierarchy_item }, nil, ctx.bufnr)
else
- vim.notify(string.format(
- 'Client with id=%d disappeared during call hierarchy request', ctx.client_id),
+ vim.notify(
+ string.format('Client with id=%d disappeared during call hierarchy request', ctx.client_id),
vim.log.levels.WARN
)
end
@@ -391,20 +582,26 @@ end
--- Add the folder at path to the workspace folders. If {path} is
--- not provided, the user will be prompted for a path using |input()|.
function M.add_workspace_folder(workspace_folder)
- workspace_folder = workspace_folder or npcall(vfn.input, "Workspace Folder: ", vfn.expand('%:p:h'), 'dir')
- vim.api.nvim_command("redraw")
- if not (workspace_folder and #workspace_folder > 0) then return end
+ workspace_folder = workspace_folder
+ or npcall(vim.fn.input, 'Workspace Folder: ', vim.fn.expand('%:p:h'), 'dir')
+ api.nvim_command('redraw')
+ if not (workspace_folder and #workspace_folder > 0) then
+ return
+ end
if vim.fn.isdirectory(workspace_folder) == 0 then
- print(workspace_folder, " is not a valid directory")
+ print(workspace_folder, ' is not a valid directory')
return
end
- local params = util.make_workspace_params({{uri = vim.uri_from_fname(workspace_folder); name = workspace_folder}}, {{}})
+ local params = util.make_workspace_params(
+ { { uri = vim.uri_from_fname(workspace_folder), name = workspace_folder } },
+ { {} }
+ )
for _, client in pairs(vim.lsp.buf_get_clients()) do
local found = false
for _, folder in pairs(client.workspace_folders or {}) do
if folder.name == workspace_folder then
found = true
- print(workspace_folder, "is already part of this workspace")
+ print(workspace_folder, 'is already part of this workspace')
break
end
end
@@ -422,10 +619,16 @@ end
--- {path} is not provided, the user will be prompted for
--- a path using |input()|.
function M.remove_workspace_folder(workspace_folder)
- workspace_folder = workspace_folder or npcall(vfn.input, "Workspace Folder: ", vfn.expand('%:p:h'))
- vim.api.nvim_command("redraw")
- if not (workspace_folder and #workspace_folder > 0) then return end
- local params = util.make_workspace_params({{}}, {{uri = vim.uri_from_fname(workspace_folder); name = workspace_folder}})
+ workspace_folder = workspace_folder
+ or npcall(vim.fn.input, 'Workspace Folder: ', vim.fn.expand('%:p:h'))
+ api.nvim_command('redraw')
+ if not (workspace_folder and #workspace_folder > 0) then
+ return
+ end
+ local params = util.make_workspace_params(
+ { {} },
+ { { uri = vim.uri_from_fname(workspace_folder), name = workspace_folder } }
+ )
for _, client in pairs(vim.lsp.buf_get_clients()) do
for idx, folder in pairs(client.workspace_folders) do
if folder.name == workspace_folder then
@@ -435,7 +638,7 @@ function M.remove_workspace_folder(workspace_folder)
end
end
end
- print(workspace_folder, "is not currently part of the workspace")
+ print(workspace_folder, 'is not currently part of the workspace')
end
--- Lists all symbols in the current workspace in the quickfix window.
@@ -446,8 +649,11 @@ end
---
---@param query (string, optional)
function M.workspace_symbol(query)
- query = query or npcall(vfn.input, "Query: ")
- local params = {query = query}
+ query = query or npcall(vim.fn.input, 'Query: ')
+ if query == nil then
+ return
+ end
+ local params = { query = query }
request('workspace/symbol', params)
end
@@ -477,7 +683,6 @@ function M.clear_references()
util.buf_clear_references()
end
-
---@private
--
--- This is not public because the main extension point is
@@ -488,11 +693,42 @@ end
--- from multiple clients to have 1 single UI prompt for the user, yet we still
--- need to be able to link a `CodeAction|Command` to the right client for
--- `codeAction/resolve`
-local function on_code_action_results(results, ctx)
+local function on_code_action_results(results, ctx, options)
local action_tuples = {}
+
+ ---@private
+ local function action_filter(a)
+ -- filter by specified action kind
+ if options and options.context and options.context.only then
+ if not a.kind then
+ return false
+ end
+ local found = false
+ for _, o in ipairs(options.context.only) do
+ -- action kinds are hierarchical with . as a separator: when requesting only
+ -- 'quickfix' this filter allows both 'quickfix' and 'quickfix.foo', for example
+ if a.kind:find('^' .. o .. '$') or a.kind:find('^' .. o .. '%.') then
+ found = true
+ break
+ end
+ end
+ if not found then
+ return false
+ end
+ end
+ -- filter by user function
+ if options and options.filter and not options.filter(a) then
+ return false
+ end
+ -- no filter removed this action
+ return true
+ end
+
for client_id, result in pairs(results) do
for _, action in pairs(result.result or {}) do
- table.insert(action_tuples, { client_id, action })
+ if action_filter(action) then
+ table.insert(action_tuples, { client_id, action })
+ end
end
end
if #action_tuples == 0 then
@@ -503,7 +739,7 @@ local function on_code_action_results(results, ctx)
---@private
local function apply_action(action, client)
if action.edit then
- util.apply_workspace_edit(action.edit)
+ util.apply_workspace_edit(action.edit, client.offset_encoding)
end
if action.command then
local command = type(action.command) == 'table' and action.command or action
@@ -513,7 +749,14 @@ local function on_code_action_results(results, ctx)
enriched_ctx.client_id = client.id
fn(command, enriched_ctx)
else
- M.execute_command(command)
+ -- Not using command directly to exclude extra properties,
+ -- see https://github.com/python-lsp/python-lsp-server/issues/146
+ local params = {
+ command = command.command,
+ arguments = command.arguments,
+ workDoneToken = command.workDoneToken,
+ }
+ client.request('workspace/executeCommand', params, nil, ctx.bufnr)
end
end
end
@@ -537,11 +780,11 @@ local function on_code_action_results(results, ctx)
--
local client = vim.lsp.get_client_by_id(action_tuple[1])
local action = action_tuple[2]
- if not action.edit
- and client
- and type(client.resolved_capabilities.code_action) == 'table'
- and client.resolved_capabilities.code_action.resolveProvider then
-
+ if
+ not action.edit
+ and client
+ and vim.tbl_get(client.server_capabilities, 'codeActionProvider', 'resolveProvider')
+ then
client.request('codeAction/resolve', action, function(err, resolved_action)
if err then
vim.notify(err.code .. ': ' .. err.message, vim.log.levels.ERROR)
@@ -554,6 +797,13 @@ local function on_code_action_results(results, ctx)
end
end
+ -- If options.apply is given, and there are just one remaining code action,
+ -- apply it directly without querying the user.
+ if options and options.apply and #action_tuples == 1 then
+ on_user_choice(action_tuples[1])
+ return
+ end
+
vim.ui.select(action_tuples, {
prompt = 'Code actions:',
kind = 'codeaction',
@@ -564,39 +814,53 @@ local function on_code_action_results(results, ctx)
}, on_user_choice)
end
-
--- Requests code actions from all clients and calls the handler exactly once
--- with all aggregated results
---@private
-local function code_action_request(params)
- local bufnr = vim.api.nvim_get_current_buf()
+local function code_action_request(params, options)
+ local bufnr = api.nvim_get_current_buf()
local method = 'textDocument/codeAction'
vim.lsp.buf_request_all(bufnr, method, params, function(results)
- on_code_action_results(results, { bufnr = bufnr, method = method, params = params })
+ local ctx = { bufnr = bufnr, method = method, params = params }
+ on_code_action_results(results, ctx, options)
end)
end
--- Selects a code action available at the current
--- cursor position.
---
----@param context table|nil `CodeActionContext` of the LSP specification:
---- - diagnostics: (table|nil)
---- LSP `Diagnostic[]`. Inferred from the current
---- position if not provided.
---- - only: (string|nil)
---- LSP `CodeActionKind` used to filter the code actions.
---- Most language servers support values like `refactor`
---- or `quickfix`.
+---@param options table|nil Optional table which holds the following optional fields:
+--- - context (table|nil):
+--- Corresponds to `CodeActionContext` of the LSP specification:
+--- - diagnostics (table|nil):
+--- LSP `Diagnostic[]`. Inferred from the current
+--- position if not provided.
+--- - only (table|nil):
+--- List of LSP `CodeActionKind`s used to filter the code actions.
+--- Most language servers support values like `refactor`
+--- or `quickfix`.
+--- - filter (function|nil):
+--- Predicate function taking an `CodeAction` and returning a boolean.
+--- - apply (boolean|nil):
+--- When set to `true`, and there is just one remaining action
+--- (after filtering), the action is applied without user query.
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction
-function M.code_action(context)
- validate { context = { context, 't', true } }
- context = context or {}
+function M.code_action(options)
+ validate({ options = { options, 't', true } })
+ options = options or {}
+ -- Detect old API call code_action(context) which should now be
+ -- code_action({ context = context} )
+ if options.diagnostics or options.only then
+ options = { options = options }
+ end
+ local context = options.context or {}
if not context.diagnostics then
- context.diagnostics = vim.lsp.diagnostic.get_line_diagnostics()
+ local bufnr = api.nvim_get_current_buf()
+ context.diagnostics = vim.lsp.diagnostic.get_line_diagnostics(bufnr)
end
local params = util.make_range_params()
params.context = context
- code_action_request(params)
+ code_action_request(params, options)
end
--- Performs |vim.lsp.buf.code_action()| for a given range.
@@ -606,8 +870,8 @@ end
--- - diagnostics: (table|nil)
--- LSP `Diagnostic[]`. Inferred from the current
--- position if not provided.
---- - only: (string|nil)
---- LSP `CodeActionKind` used to filter the code actions.
+--- - only: (table|nil)
+--- List of LSP `CodeActionKind`s used to filter the code actions.
--- Most language servers support values like `refactor`
--- or `quickfix`.
---@param start_pos ({number, number}, optional) mark-indexed position.
@@ -615,10 +879,11 @@ end
---@param end_pos ({number, number}, optional) mark-indexed position.
---Defaults to the end of the last visual selection.
function M.range_code_action(context, start_pos, end_pos)
- validate { context = { context, 't', true } }
+ validate({ context = { context, 't', true } })
context = context or {}
if not context.diagnostics then
- context.diagnostics = vim.lsp.diagnostic.get_line_diagnostics()
+ local bufnr = api.nvim_get_current_buf()
+ context.diagnostics = vim.lsp.diagnostic.get_line_diagnostics(bufnr)
end
local params = util.make_given_range_params(start_pos, end_pos)
params.context = context
@@ -630,16 +895,16 @@ end
---@param command_params table A valid `ExecuteCommandParams` object
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand
function M.execute_command(command_params)
- validate {
+ validate({
command = { command_params.command, 's' },
- arguments = { command_params.arguments, 't', true }
- }
+ arguments = { command_params.arguments, 't', true },
+ })
command_params = {
- command=command_params.command,
- arguments=command_params.arguments,
- workDoneToken=command_params.workDoneToken,
+ command = command_params.command,
+ arguments = command_params.arguments,
+ workDoneToken = command_params.workDoneToken,
}
- request('workspace/executeCommand', command_params )
+ request('workspace/executeCommand', command_params)
end
return M
diff --git a/runtime/lua/vim/lsp/codelens.lua b/runtime/lua/vim/lsp/codelens.lua
index 9eb64c9a2e..4fa02c8db2 100644
--- a/runtime/lua/vim/lsp/codelens.lua
+++ b/runtime/lua/vim/lsp/codelens.lua
@@ -1,4 +1,5 @@
local util = require('vim.lsp.util')
+local log = require('vim.lsp.log')
local api = vim.api
local M = {}
@@ -11,7 +12,7 @@ local lens_cache_by_buf = setmetatable({}, {
__index = function(t, b)
local key = b > 0 and b or api.nvim_get_current_buf()
return rawget(t, key)
- end
+ end,
})
local namespaces = setmetatable({}, {
@@ -19,13 +20,12 @@ local namespaces = setmetatable({}, {
local value = api.nvim_create_namespace('vim_lsp_codelens:' .. key)
rawset(t, key, value)
return value
- end;
+ end,
})
---@private
M.__namespaces = namespaces
-
---@private
local function execute_lens(lens, bufnr, client_id)
local line = lens.range.start.line
@@ -43,10 +43,14 @@ local function execute_lens(lens, bufnr, client_id)
local command_provider = client.server_capabilities.executeCommandProvider
local commands = type(command_provider) == 'table' and command_provider.commands or {}
if not vim.tbl_contains(commands, command.command) then
- vim.notify(string.format(
- "Language server does not support command `%s`. This command may require a client extension.", command.command),
- vim.log.levels.WARN)
- return
+ vim.notify(
+ string.format(
+ 'Language server does not support command `%s`. This command may require a client extension.',
+ command.command
+ ),
+ vim.log.levels.WARN
+ )
+ return
end
client.request('workspace/executeCommand', command, function(...)
local result = vim.lsp.handlers['workspace/executeCommand'](...)
@@ -55,14 +59,15 @@ local function execute_lens(lens, bufnr, client_id)
end, bufnr)
end
-
--- Return all lenses for the given buffer
---
---@param bufnr number Buffer number. 0 can be used for the current buffer.
---@return table (`CodeLens[]`)
function M.get(bufnr)
local lenses_by_client = lens_cache_by_buf[bufnr or 0]
- if not lenses_by_client then return {} end
+ if not lenses_by_client then
+ return {}
+ end
local lenses = {}
for _, client_lenses in pairs(lenses_by_client) do
vim.list_extend(lenses, client_lenses)
@@ -70,7 +75,6 @@ function M.get(bufnr)
return lenses
end
-
--- Run the code lens in the current line
---
function M.run()
@@ -81,7 +85,7 @@ function M.run()
for client, lenses in pairs(lenses_by_client) do
for _, lens in pairs(lenses) do
if lens.range.start.line == (line - 1) then
- table.insert(options, {client=client, lens=lens})
+ table.insert(options, { client = client, lens = lens })
end
end
end
@@ -104,7 +108,6 @@ function M.run()
end
end
-
--- Display the lenses using virtual text
---
---@param lenses table of lenses to display (`CodeLens[] | null`)
@@ -130,21 +133,25 @@ function M.display(lenses, bufnr, client_id)
api.nvim_buf_clear_namespace(bufnr, ns, i, i + 1)
local chunks = {}
local num_line_lenses = #line_lenses
+ table.sort(line_lenses, function(a, b)
+ return a.range.start.character < b.range.start.character
+ end)
for j, lens in ipairs(line_lenses) do
local text = lens.command and lens.command.title or 'Unresolved lens ...'
- table.insert(chunks, {text, 'LspCodeLens' })
+ table.insert(chunks, { text, 'LspCodeLens' })
if j < num_line_lenses then
- table.insert(chunks, {' | ', 'LspCodeLensSeparator' })
+ table.insert(chunks, { ' | ', 'LspCodeLensSeparator' })
end
end
if #chunks > 0 then
- api.nvim_buf_set_extmark(bufnr, ns, i, 0, { virt_text = chunks,
- hl_mode="combine" })
+ api.nvim_buf_set_extmark(bufnr, ns, i, 0, {
+ virt_text = chunks,
+ hl_mode = 'combine',
+ })
end
end
end
-
--- Store lenses for a specific buffer and client
---
---@param lenses table of lenses to store (`CodeLens[] | null`)
@@ -157,16 +164,17 @@ function M.save(lenses, bufnr, client_id)
lens_cache_by_buf[bufnr] = lenses_by_client
local ns = namespaces[client_id]
api.nvim_buf_attach(bufnr, false, {
- on_detach = function(b) lens_cache_by_buf[b] = nil end,
+ on_detach = function(b)
+ lens_cache_by_buf[b] = nil
+ end,
on_lines = function(_, b, _, first_lnum, last_lnum)
api.nvim_buf_clear_namespace(b, ns, first_lnum, last_lnum)
- end
+ end,
})
end
lenses_by_client[client_id] = lenses
end
-
---@private
local function resolve_lenses(lenses, bufnr, client_id, callback)
lenses = lenses or {}
@@ -200,8 +208,7 @@ local function resolve_lenses(lenses, bufnr, client_id, callback)
ns,
lens.range.start.line,
0,
- { virt_text = {{ lens.command.title, 'LspCodeLens' }},
- hl_mode="combine" }
+ { virt_text = { { lens.command.title, 'LspCodeLens' } }, hl_mode = 'combine' }
)
end
countdown()
@@ -210,11 +217,14 @@ local function resolve_lenses(lenses, bufnr, client_id, callback)
end
end
-
--- |lsp-handler| for the method `textDocument/codeLens`
---
function M.on_codelens(err, result, ctx, _)
- assert(not err, vim.inspect(err))
+ if err then
+ active_refreshes[ctx.bufnr] = nil
+ local _ = log.error() and log.error('codelens', err)
+ return
+ end
M.save(result, ctx.bufnr, ctx.client_id)
@@ -222,12 +232,11 @@ function M.on_codelens(err, result, ctx, _)
-- once resolved.
M.display(result, ctx.bufnr, ctx.client_id)
resolve_lenses(result, ctx.bufnr, ctx.client_id, function()
- M.display(result, ctx.bufnr, ctx.client_id)
active_refreshes[ctx.bufnr] = nil
+ M.display(result, ctx.bufnr, ctx.client_id)
end)
end
-
--- Refresh the codelens for the current buffer
---
--- It is recommended to trigger this using an autocmd or via keymap.
@@ -238,15 +247,14 @@ end
---
function M.refresh()
local params = {
- textDocument = util.make_text_document_params()
+ textDocument = util.make_text_document_params(),
}
local bufnr = api.nvim_get_current_buf()
if active_refreshes[bufnr] then
return
end
active_refreshes[bufnr] = true
- vim.lsp.buf_request(0, 'textDocument/codeLens', params)
+ vim.lsp.buf_request(0, 'textDocument/codeLens', params, M.on_codelens)
end
-
return M
diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua
index f38b469f3c..1f9d084e2b 100644
--- a/runtime/lua/vim/lsp/diagnostic.lua
+++ b/runtime/lua/vim/lsp/diagnostic.lua
@@ -22,17 +22,6 @@ local function get_client_id(client_id)
end
---@private
-local function get_bufnr(bufnr)
- if not bufnr then
- return vim.api.nvim_get_current_buf()
- elseif bufnr == 0 then
- return vim.api.nvim_get_current_buf()
- end
-
- return bufnr
-end
-
----@private
local function severity_lsp_to_vim(severity)
if type(severity) == 'string' then
severity = vim.lsp.protocol.DiagnosticSeverity[severity]
@@ -50,12 +39,12 @@ end
---@private
local function line_byte_from_position(lines, lnum, col, offset_encoding)
- if not lines or offset_encoding == "utf-8" then
+ if not lines or offset_encoding == 'utf-8' then
return col
end
local line = lines[lnum + 1]
- local ok, result = pcall(vim.str_byteindex, line, col, offset_encoding == "utf-16")
+ local ok, result = pcall(vim.str_byteindex, line, col, offset_encoding == 'utf-16')
if ok then
return result
end
@@ -75,7 +64,7 @@ local function get_buf_lines(bufnr)
return
end
- local content = f:read("*a")
+ local content = f:read('*a')
if not content then
-- Some LSP servers report diagnostics at a directory level, in which case
-- io.read() returns nil
@@ -83,7 +72,7 @@ local function get_buf_lines(bufnr)
return
end
- local lines = vim.split(content, "\n")
+ local lines = vim.split(content, '\n')
f:close()
return lines
end
@@ -92,10 +81,10 @@ end
local function diagnostic_lsp_to_vim(diagnostics, bufnr, client_id)
local buf_lines = get_buf_lines(bufnr)
local client = vim.lsp.get_client_by_id(client_id)
- local offset_encoding = client and client.offset_encoding or "utf-16"
+ local offset_encoding = client and client.offset_encoding or 'utf-16'
return vim.tbl_map(function(diagnostic)
local start = diagnostic.range.start
- local _end = diagnostic.range["end"]
+ local _end = diagnostic.range['end']
return {
lnum = start.line,
col = line_byte_from_position(buf_lines, start.line, start.character, offset_encoding),
@@ -104,8 +93,10 @@ local function diagnostic_lsp_to_vim(diagnostics, bufnr, client_id)
severity = severity_lsp_to_vim(diagnostic.severity),
message = diagnostic.message,
source = diagnostic.source,
+ code = diagnostic.code,
user_data = {
lsp = {
+ -- usage of user_data.lsp.code is deprecated in favor of the top-level code field
code = diagnostic.code,
codeDescription = diagnostic.codeDescription,
tags = diagnostic.tags,
@@ -120,13 +111,14 @@ end
---@private
local function diagnostic_vim_to_lsp(diagnostics)
return vim.tbl_map(function(diagnostic)
- return vim.tbl_extend("error", {
+ return vim.tbl_extend('keep', {
+ -- "keep" the below fields over any duplicate fields in diagnostic.user_data.lsp
range = {
start = {
line = diagnostic.lnum,
character = diagnostic.col,
},
- ["end"] = {
+ ['end'] = {
line = diagnostic.end_lnum,
character = diagnostic.end_col,
},
@@ -134,6 +126,7 @@ local function diagnostic_vim_to_lsp(diagnostics)
severity = severity_vim_to_lsp(diagnostic.severity),
message = diagnostic.message,
source = diagnostic.source,
+ code = diagnostic.code,
}, diagnostic.user_data and (diagnostic.user_data.lsp or {}) or {})
end, diagnostics)
end
@@ -144,10 +137,10 @@ local _client_namespaces = {}
---
---@param client_id number The id of the LSP client
function M.get_namespace(client_id)
- vim.validate { client_id = { client_id, 'n' } }
+ vim.validate({ client_id = { client_id, 'n' } })
if not _client_namespaces[client_id] then
local client = vim.lsp.get_client_by_id(client_id)
- local name = string.format("vim.lsp.%s.%d", client and client.name or "unknown", client_id)
+ local name = string.format('vim.lsp.%s.%d', client and client.name or 'unknown', client_id)
_client_namespaces[client_id] = vim.api.nvim_create_namespace(name)
end
return _client_namespaces[client_id]
@@ -168,8 +161,8 @@ end
--- },
--- -- Use a function to dynamically turn signs off
--- -- and on, using buffer local variables
---- signs = function(bufnr, client_id)
---- return vim.bo[bufnr].show_signs == false
+--- signs = function(namespace, bufnr)
+--- return vim.b[bufnr].show_signs == true
--- end,
--- -- Disable a feature
--- update_in_insert = false,
@@ -181,7 +174,12 @@ end
function M.on_publish_diagnostics(_, result, ctx, config)
local client_id = ctx.client_id
local uri = result.uri
- local bufnr = vim.uri_to_bufnr(uri)
+ local fname = vim.uri_to_fname(uri)
+ local diagnostics = result.diagnostics
+ if #diagnostics == 0 and vim.fn.bufexists(fname) == 0 then
+ return
+ end
+ local bufnr = vim.fn.bufadd(fname)
if not bufnr then
return
@@ -189,13 +187,12 @@ function M.on_publish_diagnostics(_, result, ctx, config)
client_id = get_client_id(client_id)
local namespace = M.get_namespace(client_id)
- local diagnostics = result.diagnostics
if config then
for _, opt in pairs(config) do
if type(opt) == 'table' then
if not opt.severity and opt.severity_limit then
- opt.severity = {min=severity_lsp_to_vim(opt.severity_limit)}
+ opt.severity = { min = severity_lsp_to_vim(opt.severity_limit) }
end
end
end
@@ -230,72 +227,6 @@ function M.reset(client_id, buffer_client_map)
end)
end
--- Deprecated Functions {{{
-
-
---- Save diagnostics to the current buffer.
----
----@deprecated Prefer |vim.diagnostic.set()|
----
---- Handles saving diagnostics from multiple clients in the same buffer.
----@param diagnostics Diagnostic[]
----@param bufnr number
----@param client_id number
----@private
-function M.save(diagnostics, bufnr, client_id)
- vim.notify_once('vim.lsp.diagnostic.save is deprecated. See :h deprecated', vim.log.levels.WARN)
- local namespace = M.get_namespace(client_id)
- vim.diagnostic.set(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id))
-end
--- }}}
-
---- Get all diagnostics for clients
----
----@deprecated Prefer |vim.diagnostic.get()|
----
----@param client_id number Restrict included diagnostics to the client
---- If nil, diagnostics of all clients are included.
----@return table with diagnostics grouped by bufnr (bufnr: Diagnostic[])
-function M.get_all(client_id)
- vim.notify_once('vim.lsp.diagnostic.get_all is deprecated. See :h deprecated', vim.log.levels.WARN)
- local result = {}
- local namespace
- if client_id then
- namespace = M.get_namespace(client_id)
- end
- for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
- local diagnostics = diagnostic_vim_to_lsp(vim.diagnostic.get(bufnr, {namespace = namespace}))
- result[bufnr] = diagnostics
- end
- return result
-end
-
---- Return associated diagnostics for bufnr
----
----@deprecated Prefer |vim.diagnostic.get()|
----
----@param bufnr number
----@param client_id number|nil If nil, then return all of the diagnostics.
---- Else, return just the diagnostics associated with the client_id.
----@param predicate function|nil Optional function for filtering diagnostics
-function M.get(bufnr, client_id, predicate)
- vim.notify_once('vim.lsp.diagnostic.get is deprecated. See :h deprecated', vim.log.levels.WARN)
- predicate = predicate or function() return true end
- if client_id == nil then
- local all_diagnostics = {}
- vim.lsp.for_each_buffer_client(bufnr, function(_, iter_client_id, _)
- local iter_diagnostics = vim.tbl_filter(predicate, M.get(bufnr, iter_client_id))
- for _, diagnostic in ipairs(iter_diagnostics) do
- table.insert(all_diagnostics, diagnostic)
- end
- end)
- return all_diagnostics
- end
-
- local namespace = M.get_namespace(client_id)
- return diagnostic_vim_to_lsp(vim.tbl_filter(predicate, vim.diagnostic.get(bufnr, {namespace=namespace})))
-end
-
--- Get the diagnostics by line
---
--- Marked private as this is used internally by the LSP subsystem, but
@@ -317,7 +248,7 @@ function M.get_line_diagnostics(bufnr, line_nr, opts, client_id)
if opts.severity then
opts.severity = severity_lsp_to_vim(opts.severity)
elseif opts.severity_limit then
- opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
+ opts.severity = { min = severity_lsp_to_vim(opts.severity_limit) }
end
if client_id then
@@ -333,390 +264,4 @@ function M.get_line_diagnostics(bufnr, line_nr, opts, client_id)
return diagnostic_vim_to_lsp(vim.diagnostic.get(bufnr, opts))
end
---- Get the counts for a particular severity
----
----@deprecated Prefer |vim.diagnostic.get_count()|
----
----@param bufnr number The buffer number
----@param severity DiagnosticSeverity
----@param client_id number the client id
-function M.get_count(bufnr, severity, client_id)
- vim.notify_once('vim.lsp.diagnostic.get_count is deprecated. See :h deprecated', vim.log.levels.WARN)
- severity = severity_lsp_to_vim(severity)
- local opts = { severity = severity }
- if client_id ~= nil then
- opts.namespace = M.get_namespace(client_id)
- end
-
- return #vim.diagnostic.get(bufnr, opts)
-end
-
---- Get the previous diagnostic closest to the cursor_position
----
----@deprecated Prefer |vim.diagnostic.get_prev()|
----
----@param opts table See |vim.lsp.diagnostic.goto_next()|
----@return table Previous diagnostic
-function M.get_prev(opts)
- vim.notify_once('vim.lsp.diagnostic.get_prev is deprecated. See :h deprecated', vim.log.levels.WARN)
- if opts then
- if opts.severity then
- opts.severity = severity_lsp_to_vim(opts.severity)
- elseif opts.severity_limit then
- opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
- end
- end
- return diagnostic_vim_to_lsp({vim.diagnostic.get_prev(opts)})[1]
-end
-
---- Return the pos, {row, col}, for the prev diagnostic in the current buffer.
----
----@deprecated Prefer |vim.diagnostic.get_prev_pos()|
----
----@param opts table See |vim.lsp.diagnostic.goto_next()|
----@return table Previous diagnostic position
-function M.get_prev_pos(opts)
- vim.notify_once('vim.lsp.diagnostic.get_prev_pos is deprecated. See :h deprecated', vim.log.levels.WARN)
- if opts then
- if opts.severity then
- opts.severity = severity_lsp_to_vim(opts.severity)
- elseif opts.severity_limit then
- opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
- end
- end
- return vim.diagnostic.get_prev_pos(opts)
-end
-
---- Move to the previous diagnostic
----
----@deprecated Prefer |vim.diagnostic.goto_prev()|
----
----@param opts table See |vim.lsp.diagnostic.goto_next()|
-function M.goto_prev(opts)
- vim.notify_once('vim.lsp.diagnostic.goto_prev is deprecated. See :h deprecated', vim.log.levels.WARN)
- if opts then
- if opts.severity then
- opts.severity = severity_lsp_to_vim(opts.severity)
- elseif opts.severity_limit then
- opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
- end
- end
- return vim.diagnostic.goto_prev(opts)
-end
-
---- Get the next diagnostic closest to the cursor_position
----
----@deprecated Prefer |vim.diagnostic.get_next()|
----
----@param opts table See |vim.lsp.diagnostic.goto_next()|
----@return table Next diagnostic
-function M.get_next(opts)
- vim.notify_once('vim.lsp.diagnostic.get_next is deprecated. See :h deprecated', vim.log.levels.WARN)
- if opts then
- if opts.severity then
- opts.severity = severity_lsp_to_vim(opts.severity)
- elseif opts.severity_limit then
- opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
- end
- end
- return diagnostic_vim_to_lsp({vim.diagnostic.get_next(opts)})[1]
-end
-
---- Return the pos, {row, col}, for the next diagnostic in the current buffer.
----
----@deprecated Prefer |vim.diagnostic.get_next_pos()|
----
----@param opts table See |vim.lsp.diagnostic.goto_next()|
----@return table Next diagnostic position
-function M.get_next_pos(opts)
- vim.notify_once('vim.lsp.diagnostic.get_next_pos is deprecated. See :h deprecated', vim.log.levels.WARN)
- if opts then
- if opts.severity then
- opts.severity = severity_lsp_to_vim(opts.severity)
- elseif opts.severity_limit then
- opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
- end
- end
- return vim.diagnostic.get_next_pos(opts)
-end
-
---- Move to the next diagnostic
----
----@deprecated Prefer |vim.diagnostic.goto_next()|
-function M.goto_next(opts)
- vim.notify_once('vim.lsp.diagnostic.goto_next is deprecated. See :h deprecated', vim.log.levels.WARN)
- if opts then
- if opts.severity then
- opts.severity = severity_lsp_to_vim(opts.severity)
- elseif opts.severity_limit then
- opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
- end
- end
- return vim.diagnostic.goto_next(opts)
-end
-
---- Set signs for given diagnostics
----
----@deprecated Prefer |vim.diagnostic._set_signs()|
----
----@param diagnostics Diagnostic[]
----@param bufnr number The buffer number
----@param client_id number the client id
----@param sign_ns number|nil
----@param opts table Configuration for signs. Keys:
---- - priority: Set the priority of the signs.
---- - severity_limit (DiagnosticSeverity):
---- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
-function M.set_signs(diagnostics, bufnr, client_id, _, opts)
- vim.notify_once('vim.lsp.diagnostic.set_signs is deprecated. See :h deprecated', vim.log.levels.WARN)
- local namespace = M.get_namespace(client_id)
- if opts and not opts.severity and opts.severity_limit then
- opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
- end
-
- vim.diagnostic._set_signs(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id), opts)
-end
-
---- Set underline for given diagnostics
----
----@deprecated Prefer |vim.diagnostic._set_underline()|
----
----@param diagnostics Diagnostic[]
----@param bufnr number: The buffer number
----@param client_id number: The client id
----@param diagnostic_ns number|nil: The namespace
----@param opts table: Configuration table:
---- - severity_limit (DiagnosticSeverity):
---- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
-function M.set_underline(diagnostics, bufnr, client_id, _, opts)
- vim.notify_once('vim.lsp.diagnostic.set_underline is deprecated. See :h deprecated', vim.log.levels.WARN)
- local namespace = M.get_namespace(client_id)
- if opts and not opts.severity and opts.severity_limit then
- opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
- end
- return vim.diagnostic._set_underline(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id), opts)
-end
-
---- Set virtual text given diagnostics
----
----@deprecated Prefer |vim.diagnostic._set_virtual_text()|
----
----@param diagnostics Diagnostic[]
----@param bufnr number
----@param client_id number
----@param diagnostic_ns number
----@param opts table Options on how to display virtual text. Keys:
---- - prefix (string): Prefix to display before virtual text on line
---- - spacing (number): Number of spaces to insert before virtual text
---- - severity_limit (DiagnosticSeverity):
---- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
-function M.set_virtual_text(diagnostics, bufnr, client_id, _, opts)
- vim.notify_once('vim.lsp.diagnostic.set_virtual_text is deprecated. See :h deprecated', vim.log.levels.WARN)
- local namespace = M.get_namespace(client_id)
- if opts and not opts.severity and opts.severity_limit then
- opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
- end
- return vim.diagnostic._set_virtual_text(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id), opts)
-end
-
---- Default function to get text chunks to display using |nvim_buf_set_extmark()|.
----
----@deprecated Prefer |vim.diagnostic.get_virt_text_chunks()|
----
----@param bufnr number The buffer to display the virtual text in
----@param line number The line number to display the virtual text on
----@param line_diags Diagnostic[] The diagnostics associated with the line
----@param opts table See {opts} from |vim.lsp.diagnostic.set_virtual_text()|
----@return an array of [text, hl_group] arrays. This can be passed directly to
---- the {virt_text} option of |nvim_buf_set_extmark()|.
-function M.get_virtual_text_chunks_for_line(bufnr, _, line_diags, opts)
- vim.notify_once('vim.lsp.diagnostic.get_virtual_text_chunks_for_line is deprecated. See :h deprecated', vim.log.levels.WARN)
- return vim.diagnostic._get_virt_text_chunks(diagnostic_lsp_to_vim(line_diags, bufnr), opts)
-end
-
---- Open a floating window with the diagnostics from {position}
----
----@deprecated Prefer |vim.diagnostic.show_position_diagnostics()|
----
----@param opts table|nil Configuration keys
---- - severity: (DiagnosticSeverity, default nil)
---- - Only return diagnostics with this severity. Overrides severity_limit
---- - severity_limit: (DiagnosticSeverity, default nil)
---- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
---- - all opts for |show_diagnostics()| can be used here
----@param buf_nr number|nil The buffer number
----@param position table|nil The (0,0)-indexed position
----@return table {popup_bufnr, win_id}
-function M.show_position_diagnostics(opts, buf_nr, position)
- vim.notify_once('vim.lsp.diagnostic.show_position_diagnostics is deprecated. See :h deprecated', vim.log.levels.WARN)
- opts = opts or {}
- opts.scope = "cursor"
- opts.pos = position
- if opts.severity then
- opts.severity = severity_lsp_to_vim(opts.severity)
- elseif opts.severity_limit then
- opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
- end
- return vim.diagnostic.open_float(buf_nr, opts)
-end
-
---- Open a floating window with the diagnostics from {line_nr}
----
----@deprecated Prefer |vim.diagnostic.open_float()|
----
----@param opts table Configuration table
---- - all opts for |vim.lsp.diagnostic.get_line_diagnostics()| and
---- |show_diagnostics()| can be used here
----@param buf_nr number|nil The buffer number
----@param line_nr number|nil The line number
----@param client_id number|nil the client id
----@return table {popup_bufnr, win_id}
-function M.show_line_diagnostics(opts, buf_nr, line_nr, client_id)
- vim.notify_once('vim.lsp.diagnostic.show_line_diagnostics is deprecated. See :h deprecated', vim.log.levels.WARN)
- opts = opts or {}
- opts.scope = "line"
- opts.pos = line_nr
- if client_id then
- opts.namespace = M.get_namespace(client_id)
- end
- return vim.diagnostic.open_float(buf_nr, opts)
-end
-
---- Redraw diagnostics for the given buffer and client
----
----@deprecated Prefer |vim.diagnostic.show()|
----
---- This calls the "textDocument/publishDiagnostics" handler manually using
---- the cached diagnostics already received from the server. This can be useful
---- for redrawing diagnostics after making changes in diagnostics
---- configuration. |lsp-handler-configuration|
----
----@param bufnr (optional, number): Buffer handle, defaults to current
----@param client_id (optional, number): Redraw diagnostics for the given
---- client. The default is to redraw diagnostics for all attached
---- clients.
-function M.redraw(bufnr, client_id)
- vim.notify_once('vim.lsp.diagnostic.redraw is deprecated. See :h deprecated', vim.log.levels.WARN)
- bufnr = get_bufnr(bufnr)
- if not client_id then
- return vim.lsp.for_each_buffer_client(bufnr, function(client)
- M.redraw(bufnr, client.id)
- end)
- end
-
- local namespace = M.get_namespace(client_id)
- return vim.diagnostic.show(namespace, bufnr)
-end
-
---- Sets the quickfix list
----
----@deprecated Prefer |vim.diagnostic.setqflist()|
----
----@param opts table|nil Configuration table. Keys:
---- - {open}: (boolean, default true)
---- - Open quickfix list after set
---- - {client_id}: (number)
---- - If nil, will consider all clients attached to buffer.
---- - {severity}: (DiagnosticSeverity)
---- - Exclusive severity to consider. Overrides {severity_limit}
---- - {severity_limit}: (DiagnosticSeverity)
---- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
---- - {workspace}: (boolean, default true)
---- - Set the list with workspace diagnostics
-function M.set_qflist(opts)
- vim.notify_once('vim.lsp.diagnostic.set_qflist is deprecated. See :h deprecated', vim.log.levels.WARN)
- opts = opts or {}
- if opts.severity then
- opts.severity = severity_lsp_to_vim(opts.severity)
- elseif opts.severity_limit then
- opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
- end
- if opts.client_id then
- opts.client_id = nil
- opts.namespace = M.get_namespace(opts.client_id)
- end
- local workspace = vim.F.if_nil(opts.workspace, true)
- opts.bufnr = not workspace and 0
- return vim.diagnostic.setqflist(opts)
-end
-
---- Sets the location list
----
----@deprecated Prefer |vim.diagnostic.setloclist()|
----
----@param opts table|nil Configuration table. Keys:
---- - {open}: (boolean, default true)
---- - Open loclist after set
---- - {client_id}: (number)
---- - If nil, will consider all clients attached to buffer.
---- - {severity}: (DiagnosticSeverity)
---- - Exclusive severity to consider. Overrides {severity_limit}
---- - {severity_limit}: (DiagnosticSeverity)
---- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
---- - {workspace}: (boolean, default false)
---- - Set the list with workspace diagnostics
-function M.set_loclist(opts)
- vim.notify_once('vim.lsp.diagnostic.set_loclist is deprecated. See :h deprecated', vim.log.levels.WARN)
- opts = opts or {}
- if opts.severity then
- opts.severity = severity_lsp_to_vim(opts.severity)
- elseif opts.severity_limit then
- opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
- end
- if opts.client_id then
- opts.client_id = nil
- opts.namespace = M.get_namespace(opts.client_id)
- end
- local workspace = vim.F.if_nil(opts.workspace, false)
- opts.bufnr = not workspace and 0
- return vim.diagnostic.setloclist(opts)
-end
-
---- Disable diagnostics for the given buffer and client
----
----@deprecated Prefer |vim.diagnostic.disable()|
----
----@param bufnr (optional, number): Buffer handle, defaults to current
----@param client_id (optional, number): Disable diagnostics for the given
---- client. The default is to disable diagnostics for all attached
---- clients.
--- Note that when diagnostics are disabled for a buffer, the server will still
--- send diagnostic information and the client will still process it. The
--- diagnostics are simply not displayed to the user.
-function M.disable(bufnr, client_id)
- vim.notify_once('vim.lsp.diagnostic.disable is deprecated. See :h deprecated', vim.log.levels.WARN)
- if not client_id then
- return vim.lsp.for_each_buffer_client(bufnr, function(client)
- M.disable(bufnr, client.id)
- end)
- end
-
- bufnr = get_bufnr(bufnr)
- local namespace = M.get_namespace(client_id)
- return vim.diagnostic.disable(bufnr, namespace)
-end
-
---- Enable diagnostics for the given buffer and client
----
----@deprecated Prefer |vim.diagnostic.enable()|
----
----@param bufnr (optional, number): Buffer handle, defaults to current
----@param client_id (optional, number): Enable diagnostics for the given
---- client. The default is to enable diagnostics for all attached
---- clients.
-function M.enable(bufnr, client_id)
- vim.notify_once('vim.lsp.diagnostic.enable is deprecated. See :h deprecated', vim.log.levels.WARN)
- if not client_id then
- return vim.lsp.for_each_buffer_client(bufnr, function(client)
- M.enable(bufnr, client.id)
- end)
- end
-
- bufnr = get_bufnr(bufnr)
- local namespace = M.get_namespace(client_id)
- return vim.diagnostic.enable(bufnr, namespace)
-end
-
--- }}}
-
return M
diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua
index a48302cc4b..3b869d8f5c 100644
--- a/runtime/lua/vim/lsp/handlers.lua
+++ b/runtime/lua/vim/lsp/handlers.lua
@@ -1,6 +1,6 @@
-local log = require 'vim.lsp.log'
-local protocol = require 'vim.lsp.protocol'
-local util = require 'vim.lsp.util'
+local log = require('vim.lsp.log')
+local protocol = require('vim.lsp.protocol')
+local util = require('vim.lsp.util')
local vim = vim
local api = vim.api
@@ -12,8 +12,8 @@ local M = {}
--- Writes to error buffer.
---@param ... (table of strings) Will be concatenated before being written
local function err_message(...)
- vim.notify(table.concat(vim.tbl_flatten{...}), vim.log.levels.ERROR)
- api.nvim_command("redraw")
+ vim.notify(table.concat(vim.tbl_flatten({ ... })), vim.log.levels.ERROR)
+ api.nvim_command('redraw')
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand
@@ -25,51 +25,56 @@ end
local function progress_handler(_, result, ctx, _)
local client_id = ctx.client_id
local client = vim.lsp.get_client_by_id(client_id)
- local client_name = client and client.name or string.format("id=%d", client_id)
+ local client_name = client and client.name or string.format('id=%d', client_id)
if not client then
- err_message("LSP[", client_name, "] client has shut down after sending the message")
+ err_message('LSP[', client_name, '] client has shut down during progress update')
return vim.NIL
end
- local val = result.value -- unspecified yet
- local token = result.token -- string or number
-
+ local val = result.value -- unspecified yet
+ local token = result.token -- string or number
+ if type(val) ~= 'table' then
+ val = { content = val }
+ end
if val.kind then
if val.kind == 'begin' then
client.messages.progress[token] = {
title = val.title,
+ cancellable = val.cancellable,
message = val.message,
percentage = val.percentage,
}
elseif val.kind == 'report' then
- client.messages.progress[token].message = val.message;
- client.messages.progress[token].percentage = val.percentage;
+ client.messages.progress[token].cancellable = val.cancellable
+ client.messages.progress[token].message = val.message
+ client.messages.progress[token].percentage = val.percentage
elseif val.kind == 'end' then
if client.messages.progress[token] == nil then
- err_message("LSP[", client_name, "] received `end` message with no corresponding `begin`")
+ err_message('LSP[', client_name, '] received `end` message with no corresponding `begin`')
else
client.messages.progress[token].message = val.message
client.messages.progress[token].done = true
end
end
else
- table.insert(client.messages, {content = val, show_once = true, shown = 0})
+ client.messages.progress[token] = val
+ client.messages.progress[token].done = true
end
- vim.api.nvim_command("doautocmd <nomodeline> User LspProgressUpdate")
+ api.nvim_exec_autocmds('User', { pattern = 'LspProgressUpdate', modeline = false })
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#progress
M['$/progress'] = progress_handler
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_workDoneProgress_create
-M['window/workDoneProgress/create'] = function(_, result, ctx)
+M['window/workDoneProgress/create'] = function(_, result, ctx)
local client_id = ctx.client_id
local client = vim.lsp.get_client_by_id(client_id)
- local token = result.token -- string or number
- local client_name = client and client.name or string.format("id=%d", client_id)
+ local token = result.token -- string or number
+ local client_name = client and client.name or string.format('id=%d', client_id)
if not client then
- err_message("LSP[", client_name, "] client has shut down after sending the message")
+ err_message('LSP[', client_name, '] client has shut down while creating progress report')
return vim.NIL
end
client.messages.progress[token] = {}
@@ -78,20 +83,19 @@ end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_showMessageRequest
M['window/showMessageRequest'] = function(_, result)
-
local actions = result.actions
print(result.message)
- local option_strings = {result.message, "\nRequest Actions:"}
+ local option_strings = { result.message, '\nRequest Actions:' }
for i, action in ipairs(actions) do
local title = action.title:gsub('\r\n', '\\r\\n')
title = title:gsub('\n', '\\n')
- table.insert(option_strings, string.format("%d. %s", i, title))
+ table.insert(option_strings, string.format('%d. %s', i, title))
end
-- window/showMessageRequest can return either MessageActionItem[] or null.
local choice = vim.fn.inputlist(option_strings)
if choice < 1 or choice > #actions then
- return vim.NIL
+ return vim.NIL
else
return actions[choice]
end
@@ -100,27 +104,32 @@ end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#client_registerCapability
M['client/registerCapability'] = function(_, _, ctx)
local client_id = ctx.client_id
- local warning_tpl = "The language server %s triggers a registerCapability "..
- "handler despite dynamicRegistration set to false. "..
- "Report upstream, this warning is harmless"
+ local warning_tpl = 'The language server %s triggers a registerCapability '
+ .. 'handler despite dynamicRegistration set to false. '
+ .. 'Report upstream, this warning is harmless'
local client = vim.lsp.get_client_by_id(client_id)
- local client_name = client and client.name or string.format("id=%d", client_id)
+ local client_name = client and client.name or string.format('id=%d', client_id)
local warning = string.format(warning_tpl, client_name)
log.warn(warning)
return vim.NIL
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit
-M['workspace/applyEdit'] = function(_, workspace_edit)
- if not workspace_edit then return end
+M['workspace/applyEdit'] = function(_, workspace_edit, ctx)
+ if not workspace_edit then
+ return
+ end
-- TODO(ashkan) Do something more with label?
+ local client_id = ctx.client_id
+ local client = vim.lsp.get_client_by_id(client_id)
if workspace_edit.label then
- print("Workspace edit", workspace_edit.label)
+ print('Workspace edit', workspace_edit.label)
end
- local status, result = pcall(util.apply_workspace_edit, workspace_edit.edit)
+ local status, result =
+ pcall(util.apply_workspace_edit, workspace_edit.edit, client.offset_encoding)
return {
- applied = status;
- failureReason = result;
+ applied = status,
+ failureReason = result,
}
end
@@ -129,7 +138,11 @@ M['workspace/configuration'] = function(_, result, ctx)
local client_id = ctx.client_id
local client = vim.lsp.get_client_by_id(client_id)
if not client then
- err_message("LSP[id=", client_id, "] client has shut down after sending the message")
+ err_message(
+ 'LSP[',
+ client_id,
+ '] client has shut down after sending a workspace/configuration request'
+ )
return
end
if not result.items then
@@ -139,7 +152,7 @@ M['workspace/configuration'] = function(_, result, ctx)
local response = {}
for _, item in ipairs(result.items) do
if item.section then
- local value = util.lookup_section(client.config.settings, item.section) or vim.NIL
+ local value = util.lookup_section(client.config.settings, item.section)
-- For empty sections with no explicit '' key, return settings as is
if value == vim.NIL and item.section == '' then
value = client.config.settings or vim.NIL
@@ -150,6 +163,17 @@ M['workspace/configuration'] = function(_, result, ctx)
return response
end
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_workspaceFolders
+M['workspace/workspaceFolders'] = function(_, _, ctx)
+ local client_id = ctx.client_id
+ local client = vim.lsp.get_client_by_id(client_id)
+ if not client then
+ err_message('LSP[id=', client_id, '] client has shut down after sending the message')
+ return
+ end
+ return client.workspace_folders or vim.NIL
+end
+
M['textDocument/publishDiagnostics'] = function(...)
return require('vim.lsp.diagnostic').on_publish_diagnostics(...)
end
@@ -158,7 +182,30 @@ M['textDocument/codeLens'] = function(...)
return require('vim.lsp.codelens').on_codelens(...)
end
-
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references
+M['textDocument/references'] = function(_, result, ctx, config)
+ if not result or vim.tbl_isempty(result) then
+ vim.notify('No references found')
+ else
+ local client = vim.lsp.get_client_by_id(ctx.client_id)
+ config = config or {}
+ if config.loclist then
+ vim.fn.setloclist(0, {}, ' ', {
+ title = 'References',
+ items = util.locations_to_items(result, client.offset_encoding),
+ context = ctx,
+ })
+ api.nvim_command('lopen')
+ else
+ vim.fn.setqflist({}, ' ', {
+ title = 'References',
+ items = util.locations_to_items(result, client.offset_encoding),
+ context = ctx,
+ })
+ api.nvim_command('botright copen')
+ end
+ end
+end
---@private
--- Return a function that converts LSP responses to list items and opens the list
@@ -169,69 +216,88 @@ end
--- loclist: (boolean) use the location list (default is to use the quickfix list)
---
---@param map_result function `((resp, bufnr) -> list)` to convert the response
----@param entity name of the resource used in a `not found` error message
-local function response_to_list(map_result, entity)
- return function(_,result, ctx, config)
+---@param entity string name of the resource used in a `not found` error message
+---@param title_fn function Function to call to generate list title
+local function response_to_list(map_result, entity, title_fn)
+ return function(_, result, ctx, config)
if not result or vim.tbl_isempty(result) then
vim.notify('No ' .. entity .. ' found')
else
config = config or {}
if config.loclist then
vim.fn.setloclist(0, {}, ' ', {
- title = 'Language Server';
- items = map_result(result, ctx.bufnr);
+ title = title_fn(ctx),
+ items = map_result(result, ctx.bufnr),
+ context = ctx,
})
- api.nvim_command("lopen")
+ api.nvim_command('lopen')
else
vim.fn.setqflist({}, ' ', {
- title = 'Language Server';
- items = map_result(result, ctx.bufnr);
+ title = title_fn(ctx),
+ items = map_result(result, ctx.bufnr),
+ context = ctx,
})
- api.nvim_command("botright copen")
+ api.nvim_command('botright copen')
end
end
end
end
-
---see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references
-M['textDocument/references'] = response_to_list(util.locations_to_items, 'references')
-
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol
-M['textDocument/documentSymbol'] = response_to_list(util.symbols_to_items, 'document symbols')
+M['textDocument/documentSymbol'] = response_to_list(
+ util.symbols_to_items,
+ 'document symbols',
+ function(ctx)
+ local fname = vim.fn.fnamemodify(vim.uri_to_fname(ctx.params.textDocument.uri), ':.')
+ return string.format('Symbols in %s', fname)
+ end
+)
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_symbol
-M['workspace/symbol'] = response_to_list(util.symbols_to_items, 'symbols')
+M['workspace/symbol'] = response_to_list(util.symbols_to_items, 'symbols', function(ctx)
+ return string.format("Symbols matching '%s'", ctx.params.query)
+end)
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rename
-M['textDocument/rename'] = function(_, result, _)
- if not result then return end
- util.apply_workspace_edit(result)
+M['textDocument/rename'] = function(_, result, ctx, _)
+ if not result then
+ return
+ end
+ local client = vim.lsp.get_client_by_id(ctx.client_id)
+ util.apply_workspace_edit(result, client.offset_encoding)
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rangeFormatting
M['textDocument/rangeFormatting'] = function(_, result, ctx, _)
- if not result then return end
- util.apply_text_edits(result, ctx.bufnr)
+ if not result then
+ return
+ end
+ local client = vim.lsp.get_client_by_id(ctx.client_id)
+ util.apply_text_edits(result, ctx.bufnr, client.offset_encoding)
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting
M['textDocument/formatting'] = function(_, result, ctx, _)
- if not result then return end
- util.apply_text_edits(result, ctx.bufnr)
+ if not result then
+ return
+ end
+ local client = vim.lsp.get_client_by_id(ctx.client_id)
+ util.apply_text_edits(result, ctx.bufnr, client.offset_encoding)
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
M['textDocument/completion'] = function(_, result, _, _)
- if vim.tbl_isempty(result or {}) then return end
+ if vim.tbl_isempty(result or {}) then
+ return
+ end
local row, col = unpack(api.nvim_win_get_cursor(0))
- local line = assert(api.nvim_buf_get_lines(0, row-1, row, false)[1])
- local line_to_cursor = line:sub(col+1)
+ local line = assert(api.nvim_buf_get_lines(0, row - 1, row, false)[1])
+ local line_to_cursor = line:sub(col + 1)
local textMatch = vim.fn.match(line_to_cursor, '\\k*$')
- local prefix = line_to_cursor:sub(textMatch+1)
+ local prefix = line_to_cursor:sub(textMatch + 1)
local matches = util.text_document_completion_list_to_complete_items(result, prefix)
- vim.fn.complete(textMatch+1, matches)
+ vim.fn.complete(textMatch + 1, matches)
end
--- |lsp-handler| for the method "textDocument/hover"
@@ -251,16 +317,16 @@ function M.hover(_, result, ctx, config)
config = config or {}
config.focus_id = ctx.method
if not (result and result.contents) then
- -- return { 'No information available' }
+ vim.notify('No information available')
return
end
local markdown_lines = util.convert_input_to_markdown_lines(result.contents)
markdown_lines = util.trim_empty_lines(markdown_lines)
if vim.tbl_isempty(markdown_lines) then
- -- return { 'No information available' }
+ vim.notify('No information available')
return
end
- return util.open_floating_preview(markdown_lines, "markdown", config)
+ return util.open_floating_preview(markdown_lines, 'markdown', config)
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover
@@ -272,24 +338,30 @@ M['textDocument/hover'] = M.hover
---@param result (table) result of LSP method; a location or a list of locations.
---@param ctx (table) table containing the context of the request, including the method
---(`textDocument/definition` can return `Location` or `Location[]`
-local function location_handler(_, result, ctx, _)
+local function location_handler(_, result, ctx, config)
if result == nil or vim.tbl_isempty(result) then
local _ = log.info() and log.info(ctx.method, 'No location found')
return nil
end
+ local client = vim.lsp.get_client_by_id(ctx.client_id)
+
+ config = config or {}
-- textDocument/definition can return Location or Location[]
-- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition
if vim.tbl_islist(result) then
- util.jump_to_location(result[1])
+ util.jump_to_location(result[1], client.offset_encoding, config.reuse_win)
if #result > 1 then
- vim.fn.setqflist({}, ' ', {title = 'LSP locations', items = util.locations_to_items(result)})
- api.nvim_command("copen")
+ vim.fn.setqflist({}, ' ', {
+ title = 'LSP locations',
+ items = util.locations_to_items(result, client.offset_encoding),
+ })
+ api.nvim_command('botright copen')
end
else
- util.jump_to_location(result)
+ util.jump_to_location(result, client.offset_encoding, config.reuse_win)
end
end
@@ -328,7 +400,8 @@ function M.signature_help(_, result, ctx, config)
return
end
local client = vim.lsp.get_client_by_id(ctx.client_id)
- local triggers = client.resolved_capabilities.signature_help_trigger_characters
+ local triggers =
+ vim.tbl_get(client.server_capabilities, 'signatureHelpProvider', 'triggerCharacters')
local ft = api.nvim_buf_get_option(ctx.bufnr, 'filetype')
local lines, hl = util.convert_signature_help_to_markdown_lines(result, ft, triggers)
lines = util.trim_empty_lines(lines)
@@ -338,9 +411,9 @@ function M.signature_help(_, result, ctx, config)
end
return
end
- local fbuf, fwin = util.open_floating_preview(lines, "markdown", config)
+ local fbuf, fwin = util.open_floating_preview(lines, 'markdown', config)
if hl then
- api.nvim_buf_add_highlight(fbuf, -1, "LspSignatureActiveParameter", 0, unpack(hl))
+ api.nvim_buf_add_highlight(fbuf, -1, 'LspSignatureActiveParameter', 0, unpack(hl))
end
return fbuf, fwin
end
@@ -350,10 +423,14 @@ M['textDocument/signatureHelp'] = M.signature_help
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentHighlight
M['textDocument/documentHighlight'] = function(_, result, ctx, _)
- if not result then return end
+ if not result then
+ return
+ end
local client_id = ctx.client_id
local client = vim.lsp.get_client_by_id(client_id)
- if not client then return end
+ if not client then
+ return
+ end
util.buf_highlight_references(ctx.bufnr, result, client.offset_encoding)
end
@@ -366,7 +443,9 @@ end
---@returns `CallHierarchyOutgoingCall[]` if {direction} is `"to"`,
local make_call_hierarchy_handler = function(direction)
return function(_, result)
- if not result then return end
+ if not result then
+ return
+ end
local items = {}
for _, call_hierarchy_call in pairs(result) do
local call_hierarchy_item = call_hierarchy_call[direction]
@@ -379,8 +458,8 @@ local make_call_hierarchy_handler = function(direction)
})
end
end
- vim.fn.setqflist({}, ' ', {title = 'LSP call hierarchy', items = items})
- api.nvim_command("copen")
+ vim.fn.setqflist({}, ' ', { title = 'LSP call hierarchy', items = items })
+ api.nvim_command('botright copen')
end
end
@@ -396,15 +475,15 @@ M['window/logMessage'] = function(_, result, ctx, _)
local message = result.message
local client_id = ctx.client_id
local client = vim.lsp.get_client_by_id(client_id)
- local client_name = client and client.name or string.format("id=%d", client_id)
+ local client_name = client and client.name or string.format('id=%d', client_id)
if not client then
- err_message("LSP[", client_name, "] client has shut down after sending the message")
+ err_message('LSP[', client_name, '] client has shut down after sending ', message)
end
if message_type == protocol.MessageType.Error then
log.error(message)
elseif message_type == protocol.MessageType.Warning then
log.warn(message)
- elseif message_type == protocol.MessageType.Info or message_type == protocol.MessageType.Log then
+ elseif message_type == protocol.MessageType.Info or message_type == protocol.MessageType.Log then
log.info(message)
else
log.debug(message)
@@ -418,15 +497,15 @@ M['window/showMessage'] = function(_, result, ctx, _)
local message = result.message
local client_id = ctx.client_id
local client = vim.lsp.get_client_by_id(client_id)
- local client_name = client and client.name or string.format("id=%d", client_id)
+ local client_name = client and client.name or string.format('id=%d', client_id)
if not client then
- err_message("LSP[", client_name, "] client has shut down after sending the message")
+ err_message('LSP[', client_name, '] client has shut down after sending ', message)
end
if message_type == protocol.MessageType.Error then
- err_message("LSP[", client_name, "] ", message)
+ err_message('LSP[', client_name, '] ', message)
else
local message_type_name = protocol.MessageType[message_type]
- api.nvim_out_write(string.format("LSP[%s][%s] %s\n", client_name, message_type_name, message))
+ api.nvim_out_write(string.format('LSP[%s][%s] %s\n', client_name, message_type_name, message))
end
return result
end
@@ -434,9 +513,13 @@ end
-- Add boilerplate error validation and logging for all of these.
for k, fn in pairs(M) do
M[k] = function(err, result, ctx, config)
- local _ = log.trace() and log.trace('default_handler', ctx.method, {
- err = err, result = result, ctx=vim.inspect(ctx), config = config
- })
+ local _ = log.trace()
+ and log.trace('default_handler', ctx.method, {
+ err = err,
+ result = result,
+ ctx = vim.inspect(ctx),
+ config = config,
+ })
if err then
-- LSP spec:
@@ -448,7 +531,7 @@ for k, fn in pairs(M) do
-- Per LSP, don't show ContentModified error to the user.
if err.code ~= protocol.ErrorCodes.ContentModified then
local client = vim.lsp.get_client_by_id(ctx.client_id)
- local client_name = client and client.name or string.format("client_id=%d", ctx.client_id)
+ local client_name = client and client.name or string.format('client_id=%d', ctx.client_id)
err_message(client_name .. ': ' .. tostring(err.code) .. ': ' .. err.message)
end
diff --git a/runtime/lua/vim/lsp/health.lua b/runtime/lua/vim/lsp/health.lua
index ed3eea59df..ba730e3d6d 100644
--- a/runtime/lua/vim/lsp/health.lua
+++ b/runtime/lua/vim/lsp/health.lua
@@ -8,20 +8,25 @@ function M.check()
local log = require('vim.lsp.log')
local current_log_level = log.get_level()
local log_level_string = log.levels[current_log_level]
- report_info(string.format("LSP log level : %s", log_level_string))
+ report_info(string.format('LSP log level : %s', log_level_string))
if current_log_level < log.levels.WARN then
- report_warn(string.format("Log level %s will cause degraded performance and high disk usage", log_level_string))
+ report_warn(
+ string.format(
+ 'Log level %s will cause degraded performance and high disk usage',
+ log_level_string
+ )
+ )
end
local log_path = vim.lsp.get_log_path()
- report_info(string.format("Log path: %s", log_path))
+ report_info(string.format('Log path: %s', log_path))
- local log_size = vim.loop.fs_stat(log_path).size
+ local log_file = vim.loop.fs_stat(log_path)
+ local log_size = log_file and log_file.size or 0
local report_fn = (log_size / 1000000 > 100 and report_warn or report_info)
- report_fn(string.format("Log size: %d KB", log_size / 1000 ))
+ report_fn(string.format('Log size: %d KB', log_size / 1000))
end
return M
-
diff --git a/runtime/lua/vim/lsp/log.lua b/runtime/lua/vim/lsp/log.lua
index e0b5653587..6c6ba0f206 100644
--- a/runtime/lua/vim/lsp/log.lua
+++ b/runtime/lua/vim/lsp/log.lua
@@ -8,22 +8,29 @@ local log = {}
-- Log level dictionary with reverse lookup as well.
--
-- Can be used to lookup the number from the name or the name from the number.
--- Levels by name: "TRACE", "DEBUG", "INFO", "WARN", "ERROR"
+-- Levels by name: "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "OFF"
-- Level numbers begin with "TRACE" at 0
log.levels = vim.deepcopy(vim.log.levels)
-- Default log level is warn.
local current_log_level = log.levels.WARN
-local log_date_format = "%F %H:%M:%S"
-local format_func = function(arg) return vim.inspect(arg, {newline=''}) end
+local log_date_format = '%F %H:%M:%S'
+local format_func = function(arg)
+ return vim.inspect(arg, { newline = '' })
+end
do
- local path_sep = vim.loop.os_uname().version:match("Windows") and "\\" or "/"
+ local path_sep = vim.loop.os_uname().version:match('Windows') and '\\' or '/'
---@private
local function path_join(...)
- return table.concat(vim.tbl_flatten{...}, path_sep)
+ return table.concat(vim.tbl_flatten({ ... }), path_sep)
end
- local logfilename = path_join(vim.fn.stdpath('cache'), 'lsp.log')
+ local logfilename = path_join(vim.fn.stdpath('log'), 'lsp.log')
+
+ -- TODO: Ideally the directory should be created in open_logfile(), right
+ -- before opening the log file, but open_logfile() can be called from libuv
+ -- callbacks, where using fn.mkdir() is not allowed.
+ vim.fn.mkdir(vim.fn.stdpath('log'), 'p')
--- Returns the log filename.
---@returns (string) log filename
@@ -31,21 +38,40 @@ do
return logfilename
end
- vim.fn.mkdir(vim.fn.stdpath('cache'), "p")
- local logfile = assert(io.open(logfilename, "a+"))
-
- local log_info = vim.loop.fs_stat(logfilename)
- if log_info and log_info.size > 1e9 then
- local warn_msg = string.format(
- "LSP client log is large (%d MB): %s",
- log_info.size / (1000 * 1000),
- logfilename
- )
- vim.notify(warn_msg)
+ local logfile, openerr
+ ---@private
+ --- Opens log file. Returns true if file is open, false on error
+ local function open_logfile()
+ -- Try to open file only once
+ if logfile then
+ return true
+ end
+ if openerr then
+ return false
+ end
+
+ logfile, openerr = io.open(logfilename, 'a+')
+ if not logfile then
+ local err_msg = string.format('Failed to open LSP client log file: %s', openerr)
+ vim.notify(err_msg, vim.log.levels.ERROR)
+ return false
+ end
+
+ local log_info = vim.loop.fs_stat(logfilename)
+ if log_info and log_info.size > 1e9 then
+ local warn_msg = string.format(
+ 'LSP client log is large (%d MB): %s',
+ log_info.size / (1000 * 1000),
+ logfilename
+ )
+ vim.notify(warn_msg)
+ end
+
+ -- Start message for logging
+ logfile:write(string.format('[START][%s] LSP logging initiated\n', os.date(log_date_format)))
+ return true
end
- -- Start message for logging
- logfile:write(string.format("[START][%s] LSP logging initiated\n", os.date(log_date_format)))
for level, levelnr in pairs(log.levels) do
-- Also export the log level on the root object.
log[level] = levelnr
@@ -63,23 +89,38 @@ do
-- ```
--
-- This way you can avoid string allocations if the log level isn't high enough.
- log[level:lower()] = function(...)
- local argc = select("#", ...)
- if levelnr < current_log_level then return false end
- if argc == 0 then return true end
- local info = debug.getinfo(2, "Sl")
- local header = string.format("[%s][%s] ...%s:%s", level, os.date(log_date_format), string.sub(info.short_src, #info.short_src - 15), info.currentline)
- local parts = { header }
- for i = 1, argc do
- local arg = select(i, ...)
- if arg == nil then
- table.insert(parts, "nil")
- else
- table.insert(parts, format_func(arg))
+ if level ~= 'OFF' then
+ log[level:lower()] = function(...)
+ local argc = select('#', ...)
+ if levelnr < current_log_level then
+ return false
+ end
+ if argc == 0 then
+ return true
+ end
+ if not open_logfile() then
+ return false
+ end
+ local info = debug.getinfo(2, 'Sl')
+ local header = string.format(
+ '[%s][%s] ...%s:%s',
+ level,
+ os.date(log_date_format),
+ string.sub(info.short_src, #info.short_src - 15),
+ info.currentline
+ )
+ local parts = { header }
+ for i = 1, argc do
+ local arg = select(i, ...)
+ if arg == nil then
+ table.insert(parts, 'nil')
+ else
+ table.insert(parts, format_func(arg))
+ end
end
+ logfile:write(table.concat(parts, '\t'), '\n')
+ logfile:flush()
end
- logfile:write(table.concat(parts, '\t'), "\n")
- logfile:flush()
end
end
end
@@ -92,10 +133,11 @@ vim.tbl_add_reverse_lookup(log.levels)
---@param level (string or number) One of `vim.lsp.log.levels`
function log.set_level(level)
if type(level) == 'string' then
- current_log_level = assert(log.levels[level:upper()], string.format("Invalid log level: %q", level))
+ current_log_level =
+ assert(log.levels[level:upper()], string.format('Invalid log level: %q', level))
else
- assert(type(level) == 'number', "level must be a number or string")
- assert(log.levels[level], string.format("Invalid log level: %d", level))
+ assert(type(level) == 'number', 'level must be a number or string')
+ assert(log.levels[level], string.format('Invalid log level: %d', level))
current_log_level = level
end
end
@@ -109,7 +151,7 @@ end
--- Sets formatting function used to format logs
---@param handle function function to apply to logging arguments, pass vim.inspect for multi-line formatting
function log.set_format_func(handle)
- assert(handle == vim.inspect or type(handle) == 'function', "handle must be a function")
+ assert(handle == vim.inspect or type(handle) == 'function', 'handle must be a function')
format_func = handle
end
diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua
index 86c9e2fd58..6ecb9959d5 100644
--- a/runtime/lua/vim/lsp/protocol.lua
+++ b/runtime/lua/vim/lsp/protocol.lua
@@ -1,7 +1,5 @@
-- Protocol for the Microsoft Language Server Protocol (mslsp)
-local if_nil = vim.F.if_nil
-
local protocol = {}
--[=[
@@ -25,150 +23,150 @@ end
local constants = {
DiagnosticSeverity = {
-- Reports an error.
- Error = 1;
+ Error = 1,
-- Reports a warning.
- Warning = 2;
+ Warning = 2,
-- Reports an information.
- Information = 3;
+ Information = 3,
-- Reports a hint.
- Hint = 4;
- };
+ Hint = 4,
+ },
DiagnosticTag = {
-- Unused or unnecessary code
- Unnecessary = 1;
+ Unnecessary = 1,
-- Deprecated or obsolete code
- Deprecated = 2;
- };
+ Deprecated = 2,
+ },
MessageType = {
-- An error message.
- Error = 1;
+ Error = 1,
-- A warning message.
- Warning = 2;
+ Warning = 2,
-- An information message.
- Info = 3;
+ Info = 3,
-- A log message.
- Log = 4;
- };
+ Log = 4,
+ },
-- The file event type.
FileChangeType = {
-- The file got created.
- Created = 1;
+ Created = 1,
-- The file got changed.
- Changed = 2;
+ Changed = 2,
-- The file got deleted.
- Deleted = 3;
- };
+ Deleted = 3,
+ },
-- The kind of a completion entry.
CompletionItemKind = {
- Text = 1;
- Method = 2;
- Function = 3;
- Constructor = 4;
- Field = 5;
- Variable = 6;
- Class = 7;
- Interface = 8;
- Module = 9;
- Property = 10;
- Unit = 11;
- Value = 12;
- Enum = 13;
- Keyword = 14;
- Snippet = 15;
- Color = 16;
- File = 17;
- Reference = 18;
- Folder = 19;
- EnumMember = 20;
- Constant = 21;
- Struct = 22;
- Event = 23;
- Operator = 24;
- TypeParameter = 25;
- };
+ Text = 1,
+ Method = 2,
+ Function = 3,
+ Constructor = 4,
+ Field = 5,
+ Variable = 6,
+ Class = 7,
+ Interface = 8,
+ Module = 9,
+ Property = 10,
+ Unit = 11,
+ Value = 12,
+ Enum = 13,
+ Keyword = 14,
+ Snippet = 15,
+ Color = 16,
+ File = 17,
+ Reference = 18,
+ Folder = 19,
+ EnumMember = 20,
+ Constant = 21,
+ Struct = 22,
+ Event = 23,
+ Operator = 24,
+ TypeParameter = 25,
+ },
-- How a completion was triggered
CompletionTriggerKind = {
-- Completion was triggered by typing an identifier (24x7 code
-- complete), manual invocation (e.g Ctrl+Space) or via API.
- Invoked = 1;
+ Invoked = 1,
-- Completion was triggered by a trigger character specified by
-- the `triggerCharacters` properties of the `CompletionRegistrationOptions`.
- TriggerCharacter = 2;
+ TriggerCharacter = 2,
-- Completion was re-triggered as the current completion list is incomplete.
- TriggerForIncompleteCompletions = 3;
- };
+ TriggerForIncompleteCompletions = 3,
+ },
-- A document highlight kind.
DocumentHighlightKind = {
-- A textual occurrence.
- Text = 1;
+ Text = 1,
-- Read-access of a symbol, like reading a variable.
- Read = 2;
+ Read = 2,
-- Write-access of a symbol, like writing to a variable.
- Write = 3;
- };
+ Write = 3,
+ },
-- A symbol kind.
SymbolKind = {
- File = 1;
- Module = 2;
- Namespace = 3;
- Package = 4;
- Class = 5;
- Method = 6;
- Property = 7;
- Field = 8;
- Constructor = 9;
- Enum = 10;
- Interface = 11;
- Function = 12;
- Variable = 13;
- Constant = 14;
- String = 15;
- Number = 16;
- Boolean = 17;
- Array = 18;
- Object = 19;
- Key = 20;
- Null = 21;
- EnumMember = 22;
- Struct = 23;
- Event = 24;
- Operator = 25;
- TypeParameter = 26;
- };
+ File = 1,
+ Module = 2,
+ Namespace = 3,
+ Package = 4,
+ Class = 5,
+ Method = 6,
+ Property = 7,
+ Field = 8,
+ Constructor = 9,
+ Enum = 10,
+ Interface = 11,
+ Function = 12,
+ Variable = 13,
+ Constant = 14,
+ String = 15,
+ Number = 16,
+ Boolean = 17,
+ Array = 18,
+ Object = 19,
+ Key = 20,
+ Null = 21,
+ EnumMember = 22,
+ Struct = 23,
+ Event = 24,
+ Operator = 25,
+ TypeParameter = 26,
+ },
-- Represents reasons why a text document is saved.
TextDocumentSaveReason = {
-- Manually triggered, e.g. by the user pressing save, by starting debugging,
-- or by an API call.
- Manual = 1;
+ Manual = 1,
-- Automatic after a delay.
- AfterDelay = 2;
+ AfterDelay = 2,
-- When the editor lost focus.
- FocusOut = 3;
- };
+ FocusOut = 3,
+ },
ErrorCodes = {
-- Defined by JSON RPC
- ParseError = -32700;
- InvalidRequest = -32600;
- MethodNotFound = -32601;
- InvalidParams = -32602;
- InternalError = -32603;
- serverErrorStart = -32099;
- serverErrorEnd = -32000;
- ServerNotInitialized = -32002;
- UnknownErrorCode = -32001;
+ ParseError = -32700,
+ InvalidRequest = -32600,
+ MethodNotFound = -32601,
+ InvalidParams = -32602,
+ InternalError = -32603,
+ serverErrorStart = -32099,
+ serverErrorEnd = -32000,
+ ServerNotInitialized = -32002,
+ UnknownErrorCode = -32001,
-- Defined by the protocol.
- RequestCancelled = -32800;
- ContentModified = -32801;
- };
+ RequestCancelled = -32800,
+ ContentModified = -32801,
+ },
-- Describes the content type that a client supports in various
-- result literals like `Hover`, `ParameterInfo` or `CompletionItem`.
@@ -177,88 +175,88 @@ local constants = {
-- are reserved for internal usage.
MarkupKind = {
-- Plain text is supported as a content format
- PlainText = 'plaintext';
+ PlainText = 'plaintext',
-- Markdown is supported as a content format
- Markdown = 'markdown';
- };
+ Markdown = 'markdown',
+ },
ResourceOperationKind = {
-- Supports creating new files and folders.
- Create = 'create';
+ Create = 'create',
-- Supports renaming existing files and folders.
- Rename = 'rename';
+ Rename = 'rename',
-- Supports deleting existing files and folders.
- Delete = 'delete';
- };
+ Delete = 'delete',
+ },
FailureHandlingKind = {
-- Applying the workspace change is simply aborted if one of the changes provided
-- fails. All operations executed before the failing operation stay executed.
- Abort = 'abort';
+ Abort = 'abort',
-- All operations are executed transactionally. That means they either all
-- succeed or no changes at all are applied to the workspace.
- Transactional = 'transactional';
+ Transactional = 'transactional',
-- If the workspace edit contains only textual file changes they are executed transactionally.
-- If resource changes (create, rename or delete file) are part of the change the failure
-- handling strategy is abort.
- TextOnlyTransactional = 'textOnlyTransactional';
+ TextOnlyTransactional = 'textOnlyTransactional',
-- The client tries to undo the operations already executed. But there is no
-- guarantee that this succeeds.
- Undo = 'undo';
- };
+ Undo = 'undo',
+ },
-- Known error codes for an `InitializeError`;
InitializeError = {
-- If the protocol version provided by the client can't be handled by the server.
-- @deprecated This initialize error got replaced by client capabilities. There is
-- no version handshake in version 3.0x
- unknownProtocolVersion = 1;
- };
+ unknownProtocolVersion = 1,
+ },
-- Defines how the host (editor) should sync document changes to the language server.
TextDocumentSyncKind = {
-- Documents should not be synced at all.
- None = 0;
+ None = 0,
-- Documents are synced by always sending the full content
-- of the document.
- Full = 1;
+ Full = 1,
-- Documents are synced by sending the full content on open.
-- After that only incremental updates to the document are
-- send.
- Incremental = 2;
- };
+ Incremental = 2,
+ },
WatchKind = {
-- Interested in create events.
- Create = 1;
+ Create = 1,
-- Interested in change events
- Change = 2;
+ Change = 2,
-- Interested in delete events
- Delete = 4;
- };
+ Delete = 4,
+ },
-- Defines whether the insert text in a completion item should be interpreted as
-- plain text or a snippet.
InsertTextFormat = {
-- The primary text to be inserted is treated as a plain string.
- PlainText = 1;
+ PlainText = 1,
-- The primary text to be inserted is treated as a snippet.
--
-- A snippet can define tab stops and placeholders with `$1`, `$2`
-- and `${3:foo};`. `$0` defines the final tab stop, it defaults to
-- the end of the snippet. Placeholders with equal identifiers are linked,
-- that is typing in one will update others too.
- Snippet = 2;
- };
+ Snippet = 2,
+ },
-- A set of predefined code action kinds
CodeActionKind = {
-- Empty kind.
- Empty = '';
+ Empty = '',
-- Base kind for quickfix actions
- QuickFix = 'quickfix';
+ QuickFix = 'quickfix',
-- Base kind for refactoring actions
- Refactor = 'refactor';
+ Refactor = 'refactor',
-- Base kind for refactoring extraction actions
--
-- Example extract actions:
@@ -268,7 +266,7 @@ local constants = {
-- - Extract variable
-- - Extract interface from class
-- - ...
- RefactorExtract = 'refactor.extract';
+ RefactorExtract = 'refactor.extract',
-- Base kind for refactoring inline actions
--
-- Example inline actions:
@@ -277,7 +275,7 @@ local constants = {
-- - Inline variable
-- - Inline constant
-- - ...
- RefactorInline = 'refactor.inline';
+ RefactorInline = 'refactor.inline',
-- Base kind for refactoring rewrite actions
--
-- Example rewrite actions:
@@ -288,14 +286,14 @@ local constants = {
-- - Make method static
-- - Move method to base class
-- - ...
- RefactorRewrite = 'refactor.rewrite';
+ RefactorRewrite = 'refactor.rewrite',
-- Base kind for source actions
--
-- Source code actions apply to the entire file.
- Source = 'source';
+ Source = 'source',
-- Base kind for an organize imports source action
- SourceOrganizeImports = 'source.organizeImports';
- };
+ SourceOrganizeImports = 'source.organizeImports',
+ },
}
for k, v in pairs(constants) do
@@ -622,19 +620,19 @@ function protocol.make_client_capabilities()
return {
textDocument = {
synchronization = {
- dynamicRegistration = false;
+ dynamicRegistration = false,
-- TODO(ashkan) Send textDocument/willSave before saving (BufWritePre)
- willSave = false;
+ willSave = false,
-- TODO(ashkan) Implement textDocument/willSaveWaitUntil
- willSaveWaitUntil = false;
+ willSaveWaitUntil = false,
-- Send textDocument/didSave after saving (BufWritePost)
- didSave = true;
- };
+ didSave = true,
+ },
codeAction = {
- dynamicRegistration = false;
+ dynamicRegistration = false,
codeActionLiteralSupport = {
codeActionKind = {
@@ -642,144 +640,193 @@ function protocol.make_client_capabilities()
local res = vim.tbl_values(protocol.CodeActionKind)
table.sort(res)
return res
- end)();
- };
- };
- dataSupport = true;
+ end)(),
+ },
+ },
+ isPreferredSupport = true,
+ dataSupport = true,
resolveSupport = {
- properties = { 'edit', }
- };
- };
+ properties = { 'edit' },
+ },
+ },
completion = {
- dynamicRegistration = false;
+ dynamicRegistration = false,
completionItem = {
-- Until we can actually expand snippet, move cursor and allow for true snippet experience,
-- this should be disabled out of the box.
-- However, users can turn this back on if they have a snippet plugin.
- snippetSupport = false;
+ snippetSupport = false,
- commitCharactersSupport = false;
- preselectSupport = false;
- deprecatedSupport = false;
- documentationFormat = { protocol.MarkupKind.Markdown; protocol.MarkupKind.PlainText };
- };
+ commitCharactersSupport = false,
+ preselectSupport = false,
+ deprecatedSupport = false,
+ documentationFormat = { protocol.MarkupKind.Markdown, protocol.MarkupKind.PlainText },
+ },
completionItemKind = {
valueSet = (function()
local res = {}
for k in ipairs(protocol.CompletionItemKind) do
- if type(k) == 'number' then table.insert(res, k) end
+ if type(k) == 'number' then
+ table.insert(res, k)
+ end
end
return res
- end)();
- };
+ end)(),
+ },
-- TODO(tjdevries): Implement this
- contextSupport = false;
- };
+ contextSupport = false,
+ },
declaration = {
- linkSupport = true;
- };
+ linkSupport = true,
+ },
definition = {
- linkSupport = true;
- };
+ linkSupport = true,
+ },
implementation = {
- linkSupport = true;
- };
+ linkSupport = true,
+ },
typeDefinition = {
- linkSupport = true;
- };
+ linkSupport = true,
+ },
hover = {
- dynamicRegistration = false;
- contentFormat = { protocol.MarkupKind.Markdown; protocol.MarkupKind.PlainText };
- };
+ dynamicRegistration = false,
+ contentFormat = { protocol.MarkupKind.Markdown, protocol.MarkupKind.PlainText },
+ },
signatureHelp = {
- dynamicRegistration = false;
+ dynamicRegistration = false,
signatureInformation = {
- activeParameterSupport = true;
- documentationFormat = { protocol.MarkupKind.Markdown; protocol.MarkupKind.PlainText };
+ activeParameterSupport = true,
+ documentationFormat = { protocol.MarkupKind.Markdown, protocol.MarkupKind.PlainText },
parameterInformation = {
- labelOffsetSupport = true;
- };
- };
- };
+ labelOffsetSupport = true,
+ },
+ },
+ },
references = {
- dynamicRegistration = false;
- };
+ dynamicRegistration = false,
+ },
documentHighlight = {
- dynamicRegistration = false
- };
+ dynamicRegistration = false,
+ },
documentSymbol = {
- dynamicRegistration = false;
+ dynamicRegistration = false,
symbolKind = {
valueSet = (function()
local res = {}
for k in ipairs(protocol.SymbolKind) do
- if type(k) == 'number' then table.insert(res, k) end
+ if type(k) == 'number' then
+ table.insert(res, k)
+ end
end
return res
- end)();
- };
- hierarchicalDocumentSymbolSupport = true;
- };
+ end)(),
+ },
+ hierarchicalDocumentSymbolSupport = true,
+ },
rename = {
- dynamicRegistration = false;
- prepareSupport = true;
- };
+ dynamicRegistration = false,
+ prepareSupport = true,
+ },
publishDiagnostics = {
- relatedInformation = true;
+ relatedInformation = true,
tagSupport = {
valueSet = (function()
local res = {}
for k in ipairs(protocol.DiagnosticTag) do
- if type(k) == 'number' then table.insert(res, k) end
+ if type(k) == 'number' then
+ table.insert(res, k)
+ end
end
return res
- end)();
- };
- };
- };
+ end)(),
+ },
+ },
+ },
workspace = {
symbol = {
- dynamicRegistration = false;
+ dynamicRegistration = false,
symbolKind = {
valueSet = (function()
local res = {}
for k in ipairs(protocol.SymbolKind) do
- if type(k) == 'number' then table.insert(res, k) end
+ if type(k) == 'number' then
+ table.insert(res, k)
+ end
end
return res
- end)();
- };
- hierarchicalWorkspaceSymbolSupport = true;
- };
- workspaceFolders = true;
- applyEdit = true;
+ end)(),
+ },
+ hierarchicalWorkspaceSymbolSupport = true,
+ },
+ workspaceFolders = true,
+ applyEdit = true,
workspaceEdit = {
- resourceOperations = {'rename', 'create', 'delete',},
- };
- };
+ resourceOperations = { 'rename', 'create', 'delete' },
+ },
+ },
callHierarchy = {
- dynamicRegistration = false;
- };
- experimental = nil;
+ dynamicRegistration = false,
+ },
+ experimental = nil,
window = {
- workDoneProgress = true;
+ workDoneProgress = true,
showMessage = {
messageActionItem = {
- additionalPropertiesSupport = false;
- };
- };
+ additionalPropertiesSupport = false,
+ },
+ },
showDocument = {
- support = false;
- };
- };
+ support = false,
+ },
+ },
}
end
+local if_nil = vim.F.if_nil
--- Creates a normalized object describing LSP server capabilities.
---@param server_capabilities table Table of capabilities supported by the server
---@return table Normalized table of capabilities
function protocol.resolve_capabilities(server_capabilities)
+ local TextDocumentSyncKind = protocol.TextDocumentSyncKind
+ local textDocumentSync = server_capabilities.textDocumentSync
+ if textDocumentSync == nil then
+ -- Defaults if omitted.
+ server_capabilities.textDocumentSync = {
+ openClose = false,
+ change = TextDocumentSyncKind.None,
+ willSave = false,
+ willSaveWaitUntil = false,
+ save = {
+ includeText = false,
+ },
+ }
+ elseif type(textDocumentSync) == 'number' then
+ -- Backwards compatibility
+ if not TextDocumentSyncKind[textDocumentSync] then
+ return nil, 'Invalid server TextDocumentSyncKind for textDocumentSync'
+ end
+ server_capabilities.textDocumentSync = {
+ openClose = true,
+ change = textDocumentSync,
+ willSave = false,
+ willSaveWaitUntil = false,
+ save = {
+ includeText = false,
+ },
+ }
+ elseif type(textDocumentSync) ~= 'table' then
+ return nil, string.format('Invalid type for textDocumentSync: %q', type(textDocumentSync))
+ end
+ return server_capabilities
+end
+
+---@private
+--- Creates a normalized object describing LSP server capabilities.
+-- @deprecated access resolved_capabilities instead
+---@param server_capabilities table Table of capabilities supported by the server
+---@return table Normalized table of capabilities
+function protocol._resolve_capabilities_compat(server_capabilities)
local general_properties = {}
local text_document_sync_properties
do
@@ -788,39 +835,41 @@ function protocol.resolve_capabilities(server_capabilities)
if textDocumentSync == nil then
-- Defaults if omitted.
text_document_sync_properties = {
- text_document_open_close = false;
- text_document_did_change = TextDocumentSyncKind.None;
--- text_document_did_change = false;
- text_document_will_save = false;
- text_document_will_save_wait_until = false;
- text_document_save = false;
- text_document_save_include_text = false;
+ text_document_open_close = false,
+ text_document_did_change = TextDocumentSyncKind.None,
+ -- text_document_did_change = false;
+ text_document_will_save = false,
+ text_document_will_save_wait_until = false,
+ text_document_save = false,
+ text_document_save_include_text = false,
}
elseif type(textDocumentSync) == 'number' then
-- Backwards compatibility
if not TextDocumentSyncKind[textDocumentSync] then
- return nil, "Invalid server TextDocumentSyncKind for textDocumentSync"
+ return nil, 'Invalid server TextDocumentSyncKind for textDocumentSync'
end
text_document_sync_properties = {
- text_document_open_close = true;
- text_document_did_change = textDocumentSync;
- text_document_will_save = false;
- text_document_will_save_wait_until = false;
- text_document_save = true;
- text_document_save_include_text = false;
+ text_document_open_close = true,
+ text_document_did_change = textDocumentSync,
+ text_document_will_save = false,
+ text_document_will_save_wait_until = false,
+ text_document_save = true,
+ text_document_save_include_text = false,
}
elseif type(textDocumentSync) == 'table' then
text_document_sync_properties = {
- text_document_open_close = if_nil(textDocumentSync.openClose, false);
- text_document_did_change = if_nil(textDocumentSync.change, TextDocumentSyncKind.None);
- text_document_will_save = if_nil(textDocumentSync.willSave, false);
- text_document_will_save_wait_until = if_nil(textDocumentSync.willSaveWaitUntil, false);
- text_document_save = if_nil(textDocumentSync.save, false);
- text_document_save_include_text = if_nil(type(textDocumentSync.save) == 'table'
- and textDocumentSync.save.includeText, false);
+ text_document_open_close = if_nil(textDocumentSync.openClose, false),
+ text_document_did_change = if_nil(textDocumentSync.change, TextDocumentSyncKind.None),
+ text_document_will_save = if_nil(textDocumentSync.willSave, false),
+ text_document_will_save_wait_until = if_nil(textDocumentSync.willSaveWaitUntil, false),
+ text_document_save = if_nil(textDocumentSync.save, false),
+ text_document_save_include_text = if_nil(
+ type(textDocumentSync.save) == 'table' and textDocumentSync.save.includeText,
+ false
+ ),
}
else
- return nil, string.format("Invalid type for textDocumentSync: %q", type(textDocumentSync))
+ return nil, string.format('Invalid type for textDocumentSync: %q', type(textDocumentSync))
end
end
general_properties.completion = server_capabilities.completionProvider ~= nil
@@ -831,7 +880,8 @@ function protocol.resolve_capabilities(server_capabilities)
general_properties.document_symbol = server_capabilities.documentSymbolProvider or false
general_properties.workspace_symbol = server_capabilities.workspaceSymbolProvider or false
general_properties.document_formatting = server_capabilities.documentFormattingProvider or false
- general_properties.document_range_formatting = server_capabilities.documentRangeFormattingProvider or false
+ general_properties.document_range_formatting = server_capabilities.documentRangeFormattingProvider
+ or false
general_properties.call_hierarchy = server_capabilities.callHierarchyProvider or false
general_properties.execute_command = server_capabilities.executeCommandProvider ~= nil
@@ -848,18 +898,21 @@ function protocol.resolve_capabilities(server_capabilities)
general_properties.code_lens_resolve = false
elseif type(server_capabilities.codeLensProvider) == 'table' then
general_properties.code_lens = true
- general_properties.code_lens_resolve = server_capabilities.codeLensProvider.resolveProvider or false
+ general_properties.code_lens_resolve = server_capabilities.codeLensProvider.resolveProvider
+ or false
else
- error("The server sent invalid codeLensProvider")
+ error('The server sent invalid codeLensProvider')
end
if server_capabilities.codeActionProvider == nil then
general_properties.code_action = false
- elseif type(server_capabilities.codeActionProvider) == 'boolean'
- or type(server_capabilities.codeActionProvider) == 'table' then
+ elseif
+ type(server_capabilities.codeActionProvider) == 'boolean'
+ or type(server_capabilities.codeActionProvider) == 'table'
+ then
general_properties.code_action = server_capabilities.codeActionProvider
else
- error("The server sent invalid codeActionProvider")
+ error('The server sent invalid codeActionProvider')
end
if server_capabilities.declarationProvider == nil then
@@ -869,7 +922,7 @@ function protocol.resolve_capabilities(server_capabilities)
elseif type(server_capabilities.declarationProvider) == 'table' then
general_properties.declaration = server_capabilities.declarationProvider
else
- error("The server sent invalid declarationProvider")
+ error('The server sent invalid declarationProvider')
end
if server_capabilities.typeDefinitionProvider == nil then
@@ -879,7 +932,7 @@ function protocol.resolve_capabilities(server_capabilities)
elseif type(server_capabilities.typeDefinitionProvider) == 'table' then
general_properties.type_definition = server_capabilities.typeDefinitionProvider
else
- error("The server sent invalid typeDefinitionProvider")
+ error('The server sent invalid typeDefinitionProvider')
end
if server_capabilities.implementationProvider == nil then
@@ -889,7 +942,7 @@ function protocol.resolve_capabilities(server_capabilities)
elseif type(server_capabilities.implementationProvider) == 'table' then
general_properties.implementation = server_capabilities.implementationProvider
else
- error("The server sent invalid implementationProvider")
+ error('The server sent invalid implementationProvider')
end
local workspace = server_capabilities.workspace
@@ -897,45 +950,48 @@ function protocol.resolve_capabilities(server_capabilities)
if workspace == nil or workspace.workspaceFolders == nil then
-- Defaults if omitted.
workspace_properties = {
- workspace_folder_properties = {
- supported = false;
- changeNotifications=false;
- }
+ workspace_folder_properties = {
+ supported = false,
+ changeNotifications = false,
+ },
}
elseif type(workspace.workspaceFolders) == 'table' then
workspace_properties = {
workspace_folder_properties = {
- supported = if_nil(workspace.workspaceFolders.supported, false);
- changeNotifications = if_nil(workspace.workspaceFolders.changeNotifications, false);
-
- }
+ supported = if_nil(workspace.workspaceFolders.supported, false),
+ changeNotifications = if_nil(workspace.workspaceFolders.changeNotifications, false),
+ },
}
else
- error("The server sent invalid workspace")
+ error('The server sent invalid workspace')
end
local signature_help_properties
if server_capabilities.signatureHelpProvider == nil then
signature_help_properties = {
- signature_help = false;
- signature_help_trigger_characters = {};
+ signature_help = false,
+ signature_help_trigger_characters = {},
}
elseif type(server_capabilities.signatureHelpProvider) == 'table' then
signature_help_properties = {
- signature_help = true;
+ signature_help = true,
-- The characters that trigger signature help automatically.
- signature_help_trigger_characters = server_capabilities.signatureHelpProvider.triggerCharacters or {};
+ signature_help_trigger_characters = server_capabilities.signatureHelpProvider.triggerCharacters
+ or {},
}
else
- error("The server sent invalid signatureHelpProvider")
+ error('The server sent invalid signatureHelpProvider')
end
- return vim.tbl_extend("error"
- , text_document_sync_properties
- , signature_help_properties
- , workspace_properties
- , general_properties
- )
+ local capabilities = vim.tbl_extend(
+ 'error',
+ text_document_sync_properties,
+ signature_help_properties,
+ workspace_properties,
+ general_properties
+ )
+
+ return capabilities
end
return protocol
diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua
index 1ecac50df4..913eee19a2 100644
--- a/runtime/lua/vim/lsp/rpc.lua
+++ b/runtime/lua/vim/lsp/rpc.lua
@@ -4,12 +4,14 @@ local log = require('vim.lsp.log')
local protocol = require('vim.lsp.protocol')
local validate, schedule, schedule_wrap = vim.validate, vim.schedule, vim.schedule_wrap
+local is_win = uv.os_uname().version:find('Windows')
+
---@private
--- Checks whether a given path exists and is a directory.
---@param filename (string) path to check
---@returns (bool)
local function is_dir(filename)
- local stat = vim.loop.fs_stat(filename)
+ local stat = uv.fs_stat(filename)
return stat and stat.type == 'directory' or false
end
@@ -32,9 +34,9 @@ local function env_merge(env)
-- Merge.
env = vim.tbl_extend('force', vim.fn.environ(), env)
local final_env = {}
- for k,v in pairs(env) do
+ for k, v in pairs(env) do
assert(type(k) == 'string', 'env must be a dict')
- table.insert(final_env, k..'='..tostring(v))
+ table.insert(final_env, k .. '=' .. tostring(v))
end
return final_env
end
@@ -45,10 +47,12 @@ end
---@param encoded_message (string)
---@returns (table) table containing encoded message and `Content-Length` attribute
local function format_message_with_content_length(encoded_message)
- return table.concat {
- 'Content-Length: '; tostring(#encoded_message); '\r\n\r\n';
- encoded_message;
- }
+ return table.concat({
+ 'Content-Length: ',
+ tostring(#encoded_message),
+ '\r\n\r\n',
+ encoded_message,
+ })
end
---@private
@@ -65,23 +69,25 @@ local function parse_headers(header)
if line == '' then
break
end
- local key, value = line:match("^%s*(%S+)%s*:%s*(.+)%s*$")
+ local key, value = line:match('^%s*(%S+)%s*:%s*(.+)%s*$')
if key then
key = key:lower():gsub('%-', '_')
headers[key] = value
else
- local _ = log.error() and log.error("invalid header line %q", line)
- error(string.format("invalid header line %q", line))
+ local _ = log.error() and log.error('invalid header line %q', line)
+ error(string.format('invalid header line %q', line))
end
end
headers.content_length = tonumber(headers.content_length)
- or error(string.format("Content-Length not found in headers. %q", header))
+ or error(string.format('Content-Length not found in headers. %q', header))
return headers
end
-- This is the start of any possible header patterns. The gsub converts it to a
-- case insensitive pattern.
-local header_start_pattern = ("content"):gsub("%w", function(c) return "["..c..c:upper().."]" end)
+local header_start_pattern = ('content'):gsub('%w', function(c)
+ return '[' .. c .. c:upper() .. ']'
+end)
---@private
--- The actual workhorse.
@@ -100,17 +106,17 @@ local function request_parser_loop()
-- be searching for.
-- TODO(ashkan) I'd like to remove this, but it seems permanent :(
local buffer_start = buffer:find(header_start_pattern)
- local headers = parse_headers(buffer:sub(buffer_start, start-1))
+ local headers = parse_headers(buffer:sub(buffer_start, start - 1))
local content_length = headers.content_length
-- Use table instead of just string to buffer the message. It prevents
-- a ton of strings allocating.
-- ref. http://www.lua.org/pil/11.6.html
- local body_chunks = {buffer:sub(finish+1)}
+ local body_chunks = { buffer:sub(finish + 1) }
local body_length = #body_chunks[1]
-- Keep waiting for data until we have enough.
while body_length < content_length do
local chunk = coroutine.yield()
- or error("Expected more data for the body. The server may have died.") -- TODO hmm.
+ or error('Expected more data for the body. The server may have died.') -- TODO hmm.
table.insert(body_chunks, chunk)
body_length = body_length + #chunk
end
@@ -123,25 +129,30 @@ local function request_parser_loop()
end
local body = table.concat(body_chunks)
-- Yield our data.
- buffer = rest..(coroutine.yield(headers, body)
- or error("Expected more data for the body. The server may have died.")) -- TODO hmm.
+ buffer = rest
+ .. (
+ coroutine.yield(headers, body)
+ or error('Expected more data for the body. The server may have died.')
+ ) -- TODO hmm.
else
-- Get more data since we don't have enough.
- buffer = buffer..(coroutine.yield()
- or error("Expected more data for the header. The server may have died.")) -- TODO hmm.
+ buffer = buffer
+ .. (
+ coroutine.yield() or error('Expected more data for the header. The server may have died.')
+ ) -- TODO hmm.
end
end
end
--- Mapping of error codes used by the client
local client_errors = {
- INVALID_SERVER_MESSAGE = 1;
- INVALID_SERVER_JSON = 2;
- NO_RESULT_CALLBACK_FOUND = 3;
- READ_ERROR = 4;
- NOTIFICATION_HANDLER_ERROR = 5;
- SERVER_REQUEST_HANDLER_ERROR = 6;
- SERVER_RESULT_CALLBACK_ERROR = 7;
+ INVALID_SERVER_MESSAGE = 1,
+ INVALID_SERVER_JSON = 2,
+ NO_RESULT_CALLBACK_FOUND = 3,
+ READ_ERROR = 4,
+ NOTIFICATION_HANDLER_ERROR = 5,
+ SERVER_REQUEST_HANDLER_ERROR = 6,
+ SERVER_RESULT_CALLBACK_ERROR = 7,
}
client_errors = vim.tbl_add_reverse_lookup(client_errors)
@@ -151,26 +162,26 @@ client_errors = vim.tbl_add_reverse_lookup(client_errors)
---@param err (table) The error object
---@returns (string) The formatted error message
local function format_rpc_error(err)
- validate {
- err = { err, 't' };
- }
+ validate({
+ err = { err, 't' },
+ })
-- There is ErrorCodes in the LSP specification,
-- but in ResponseError.code it is not used and the actual type is number.
local code
if protocol.ErrorCodes[err.code] then
- code = string.format("code_name = %s,", protocol.ErrorCodes[err.code])
+ code = string.format('code_name = %s,', protocol.ErrorCodes[err.code])
else
- code = string.format("code_name = unknown, code = %s,", err.code)
+ code = string.format('code_name = unknown, code = %s,', err.code)
end
- local message_parts = {"RPC[Error]", code}
+ local message_parts = { 'RPC[Error]', code }
if err.message then
- table.insert(message_parts, "message =")
- table.insert(message_parts, string.format("%q", err.message))
+ table.insert(message_parts, 'message =')
+ table.insert(message_parts, string.format('%q', err.message))
end
if err.data then
- table.insert(message_parts, "data =")
+ table.insert(message_parts, 'data =')
table.insert(message_parts, vim.inspect(err.data))
end
return table.concat(message_parts, ' ')
@@ -185,11 +196,11 @@ local function rpc_response_error(code, message, data)
-- TODO should this error or just pick a sane error (like InternalError)?
local code_name = assert(protocol.ErrorCodes[code], 'Invalid RPC error code')
return setmetatable({
- code = code;
- message = message or code_name;
- data = data;
+ code = code,
+ message = message or code_name,
+ data = data,
}, {
- __tostring = format_rpc_error;
+ __tostring = format_rpc_error,
})
end
@@ -220,7 +231,7 @@ end
---@param signal (number): Number describing the signal used to terminate (if
---any)
function default_dispatchers.on_exit(code, signal)
- local _ = log.info() and log.info("client_exit", { code = code, signal = signal })
+ local _ = log.info() and log.info('client_exit', { code = code, signal = signal })
end
---@private
--- Default dispatcher for client errors.
@@ -258,15 +269,16 @@ end
--- - {handle} A handle for low-level interaction with the LSP server process
--- |vim.loop|.
local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
- local _ = log.info() and log.info("Starting RPC client", {cmd = cmd, args = cmd_args, extra = extra_spawn_params})
- validate {
- cmd = { cmd, 's' };
- cmd_args = { cmd_args, 't' };
- dispatchers = { dispatchers, 't', true };
- }
+ local _ = log.info()
+ and log.info('Starting RPC client', { cmd = cmd, args = cmd_args, extra = extra_spawn_params })
+ validate({
+ cmd = { cmd, 's' },
+ cmd_args = { cmd_args, 't' },
+ dispatchers = { dispatchers, 't', true },
+ })
if extra_spawn_params and extra_spawn_params.cwd then
- assert(is_dir(extra_spawn_params.cwd), "cwd must be a directory")
+ assert(is_dir(extra_spawn_params.cwd), 'cwd must be a directory')
end
if dispatchers then
local user_dispatchers = dispatchers
@@ -275,11 +287,11 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
local user_dispatcher = user_dispatchers[dispatch_name]
if user_dispatcher then
if type(user_dispatcher) ~= 'function' then
- error(string.format("dispatcher.%s must be a function", dispatch_name))
+ error(string.format('dispatcher.%s must be a function', dispatch_name))
end
-- server_request is wrapped elsewhere.
- if not (dispatch_name == 'server_request'
- or dispatch_name == 'on_exit') -- TODO this blocks the loop exiting for some reason.
+ if
+ not (dispatch_name == 'server_request' or dispatch_name == 'on_exit') -- TODO this blocks the loop exiting for some reason.
then
user_dispatcher = schedule_wrap(user_dispatcher)
end
@@ -317,20 +329,25 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
dispatchers.on_exit(code, signal)
end
local spawn_params = {
- args = cmd_args;
- stdio = {stdin, stdout, stderr};
+ args = cmd_args,
+ stdio = { stdin, stdout, stderr },
+ detached = not is_win,
}
if extra_spawn_params then
spawn_params.cwd = extra_spawn_params.cwd
spawn_params.env = env_merge(extra_spawn_params.env)
+ if extra_spawn_params.detached ~= nil then
+ spawn_params.detached = extra_spawn_params.detached
+ end
end
handle, pid = uv.spawn(cmd, spawn_params, onexit)
if handle == nil then
- local msg = string.format("Spawning language server with cmd: `%s` failed", cmd)
- if string.match(pid, "ENOENT") then
- msg = msg .. ". The language server is either not installed, missing from PATH, or not executable."
+ local msg = string.format('Spawning language server with cmd: `%s` failed', cmd)
+ if string.match(pid, 'ENOENT') then
+ msg = msg
+ .. '. The language server is either not installed, missing from PATH, or not executable.'
else
- msg = msg .. string.format(" with error message: %s", pid)
+ msg = msg .. string.format(' with error message: %s', pid)
end
vim.notify(msg, vim.log.levels.WARN)
return
@@ -344,8 +361,10 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
---@param payload table
---@returns true if the payload could be scheduled, false if the main event-loop is in the process of closing.
local function encode_and_send(payload)
- local _ = log.debug() and log.debug("rpc.send", payload)
- if handle == nil or handle:is_closing() then return false end
+ local _ = log.debug() and log.debug('rpc.send', payload)
+ if handle == nil or handle:is_closing() then
+ return false
+ end
local encoded = vim.json.encode(payload)
stdin:write(format_message_with_content_length(encoded))
return true
@@ -359,22 +378,22 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
---@param params (table): Parameters for the invoked LSP method
---@returns (bool) `true` if notification could be sent, `false` if not
local function notify(method, params)
- return encode_and_send {
- jsonrpc = "2.0";
- method = method;
- params = params;
- }
+ return encode_and_send({
+ jsonrpc = '2.0',
+ method = method,
+ params = params,
+ })
end
---@private
--- sends an error object to the remote LSP process.
local function send_response(request_id, err, result)
- return encode_and_send {
- id = request_id;
- jsonrpc = "2.0";
- error = err;
- result = result;
- }
+ return encode_and_send({
+ id = request_id,
+ jsonrpc = '2.0',
+ error = err,
+ result = result,
+ })
end
-- FIXME: DOC: Should be placed on the RPC client object returned by
@@ -385,21 +404,21 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
---@param method (string) The invoked LSP method
---@param params (table) Parameters for the invoked LSP method
---@param callback (function) Callback to invoke
- ---@param notify_reply_callback (function) Callback to invoke as soon as a request is no longer pending
+ ---@param notify_reply_callback (function|nil) Callback to invoke as soon as a request is no longer pending
---@returns (bool, number) `(true, message_id)` if request could be sent, `false` if not
local function request(method, params, callback, notify_reply_callback)
- validate {
- callback = { callback, 'f' };
- notify_reply_callback = { notify_reply_callback, 'f', true };
- }
+ validate({
+ callback = { callback, 'f' },
+ notify_reply_callback = { notify_reply_callback, 'f', true },
+ })
message_index = message_index + 1
local message_id = message_index
- local result = encode_and_send {
- id = message_id;
- jsonrpc = "2.0";
- method = method;
- params = params;
- }
+ local result = encode_and_send({
+ id = message_id,
+ jsonrpc = '2.0',
+ method = method,
+ params = params,
+ })
if result then
if message_callbacks then
message_callbacks[message_id] = schedule_wrap(callback)
@@ -417,7 +436,7 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
stderr:read_start(function(_err, chunk)
if chunk then
- local _ = log.error() and log.error("rpc", cmd, "stderr", chunk)
+ local _ = log.error() and log.error('rpc', cmd, 'stderr', chunk)
end
end)
@@ -451,7 +470,7 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
on_error(client_errors.INVALID_SERVER_JSON, decoded)
return
end
- local _ = log.debug() and log.debug("rpc.receive", decoded)
+ local _ = log.debug() and log.debug('rpc.receive', decoded)
if type(decoded.method) == 'string' and decoded.id then
local err
@@ -459,17 +478,36 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
-- we can still use the result.
schedule(function()
local status, result
- status, result, err = try_call(client_errors.SERVER_REQUEST_HANDLER_ERROR,
- dispatchers.server_request, decoded.method, decoded.params)
- local _ = log.debug() and log.debug("server_request: callback result", { status = status, result = result, err = err })
+ status, result, err = try_call(
+ client_errors.SERVER_REQUEST_HANDLER_ERROR,
+ dispatchers.server_request,
+ decoded.method,
+ decoded.params
+ )
+ local _ = log.debug()
+ and log.debug(
+ 'server_request: callback result',
+ { status = status, result = result, err = err }
+ )
if status then
if not (result or err) then
-- TODO this can be a problem if `null` is sent for result. needs vim.NIL
- error(string.format("method %q: either a result or an error must be sent to the server in response", decoded.method))
+ error(
+ string.format(
+ 'method %q: either a result or an error must be sent to the server in response',
+ decoded.method
+ )
+ )
end
if err then
- assert(type(err) == 'table', "err must be a table. Use rpc_response_error to help format errors.")
- local code_name = assert(protocol.ErrorCodes[err.code], "Errors must use protocol.ErrorCodes. Use rpc_response_error to help format errors.")
+ assert(
+ type(err) == 'table',
+ 'err must be a table. Use rpc_response_error to help format errors.'
+ )
+ local code_name = assert(
+ protocol.ErrorCodes[err.code],
+ 'Errors must use protocol.ErrorCodes. Use rpc_response_error to help format errors.'
+ )
err.message = err.message or code_name
end
else
@@ -479,18 +517,17 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
end
send_response(decoded.id, err, result)
end)
- -- This works because we are expecting vim.NIL here
+ -- This works because we are expecting vim.NIL here
elseif decoded.id and (decoded.result ~= vim.NIL or decoded.error ~= vim.NIL) then
-
-- We sent a number, so we expect a number.
local result_id = tonumber(decoded.id)
-- Notify the user that a response was received for the request
local notify_reply_callback = notify_reply_callbacks and notify_reply_callbacks[result_id]
if notify_reply_callback then
- validate {
- notify_reply_callback = { notify_reply_callback, 'f' };
- }
+ validate({
+ notify_reply_callback = { notify_reply_callback, 'f' },
+ })
notify_reply_callback(result_id)
notify_reply_callbacks[result_id] = nil
end
@@ -499,7 +536,7 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
if decoded.error then
local mute_error = false
if decoded.error.code == protocol.ErrorCodes.RequestCancelled then
- local _ = log.debug() and log.debug("Received cancellation ack", decoded)
+ local _ = log.debug() and log.debug('Received cancellation ack', decoded)
mute_error = true
end
@@ -519,24 +556,33 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
local callback = message_callbacks and message_callbacks[result_id]
if callback then
message_callbacks[result_id] = nil
- validate {
- callback = { callback, 'f' };
- }
+ validate({
+ callback = { callback, 'f' },
+ })
if decoded.error then
decoded.error = setmetatable(decoded.error, {
- __tostring = format_rpc_error;
+ __tostring = format_rpc_error,
})
end
- try_call(client_errors.SERVER_RESULT_CALLBACK_ERROR,
- callback, decoded.error, decoded.result)
+ try_call(
+ client_errors.SERVER_RESULT_CALLBACK_ERROR,
+ callback,
+ decoded.error,
+ decoded.result
+ )
else
on_error(client_errors.NO_RESULT_CALLBACK_FOUND, decoded)
- local _ = log.error() and log.error("No callback found for server response id "..result_id)
+ local _ = log.error()
+ and log.error('No callback found for server response id ' .. result_id)
end
elseif type(decoded.method) == 'string' then
-- Notification
- try_call(client_errors.NOTIFICATION_HANDLER_ERROR,
- dispatchers.notification, decoded.method, decoded.params)
+ try_call(
+ client_errors.NOTIFICATION_HANDLER_ERROR,
+ dispatchers.notification,
+ decoded.method,
+ decoded.params
+ )
else
-- Invalid server message
on_error(client_errors.INVALID_SERVER_MESSAGE, decoded)
@@ -552,7 +598,9 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
return
end
-- This should signal that we are done reading from the client.
- if not chunk then return end
+ if not chunk then
+ return
+ end
-- Flush anything in the parser by looping until we don't get a result
-- anymore.
while true do
@@ -570,17 +618,17 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
end)
return {
- pid = pid;
- handle = handle;
- request = request;
- notify = notify
+ pid = pid,
+ handle = handle,
+ request = request,
+ notify = notify,
}
end
return {
- start = start;
- rpc_response_error = rpc_response_error;
- format_rpc_error = format_rpc_error;
- client_errors = client_errors;
+ start = start,
+ rpc_response_error = rpc_response_error,
+ format_rpc_error = format_rpc_error,
+ client_errors = client_errors,
}
-- vim:sw=2 ts=2 et
diff --git a/runtime/lua/vim/lsp/sync.lua b/runtime/lua/vim/lsp/sync.lua
index 0f4e5b572b..0d65e86b55 100644
--- a/runtime/lua/vim/lsp/sync.lua
+++ b/runtime/lua/vim/lsp/sync.lua
@@ -79,7 +79,7 @@ local function compute_line_length(line, offset_encoding)
local length
local _
if offset_encoding == 'utf-16' then
- _, length = str_utfindex(line)
+ _, length = str_utfindex(line)
elseif offset_encoding == 'utf-32' then
length, _ = str_utfindex(line)
else
@@ -100,7 +100,7 @@ local function align_end_position(line, byte, offset_encoding)
-- If on the first byte, or an empty string: the trivial case
if byte == 1 or #line == 0 then
char = byte
- -- Called in the case of extending an empty line "" -> "a"
+ -- Called in the case of extending an empty line "" -> "a"
elseif byte == #line + 1 then
char = compute_line_length(line, offset_encoding) + 1
else
@@ -130,14 +130,38 @@ end
---@param new_lastline integer new_lastline from on_lines, adjusted to 1-index
---@param offset_encoding string utf-8|utf-16|utf-32|nil (fallback to utf-8)
---@returns table<int, int> line_idx, byte_idx, and char_idx of first change position
-local function compute_start_range(prev_lines, curr_lines, firstline, lastline, new_lastline, offset_encoding)
+local function compute_start_range(
+ prev_lines,
+ curr_lines,
+ firstline,
+ lastline,
+ new_lastline,
+ offset_encoding
+)
+ local char_idx
+ local byte_idx
-- If firstline == lastline, no existing text is changed. All edit operations
-- occur on a new line pointed to by lastline. This occurs during insertion of
-- new lines(O), the new newline is inserted at the line indicated by
-- new_lastline.
+ if firstline == lastline then
+ local line_idx
+ local line = prev_lines[firstline - 1]
+ if line then
+ line_idx = firstline - 1
+ byte_idx = #line + 1
+ char_idx = compute_line_length(line, offset_encoding) + 1
+ else
+ line_idx = firstline
+ byte_idx = 1
+ char_idx = 1
+ end
+ return { line_idx = line_idx, byte_idx = byte_idx, char_idx = char_idx }
+ end
+
-- If firstline == new_lastline, the first change occurred on a line that was deleted.
-- In this case, the first byte change is also at the first byte of firstline
- if firstline == new_lastline or firstline == lastline then
+ if firstline == new_lastline then
return { line_idx = firstline, byte_idx = 1, char_idx = 1 }
end
@@ -158,14 +182,12 @@ local function compute_start_range(prev_lines, curr_lines, firstline, lastline,
end
-- Convert byte to codepoint if applicable
- local char_idx
- local byte_idx
- if start_byte_idx == 1 or (#prev_line == 0 and start_byte_idx == 1)then
+ if start_byte_idx == 1 or (#prev_line == 0 and start_byte_idx == 1) then
byte_idx = start_byte_idx
char_idx = 1
elseif start_byte_idx == #prev_line + 1 then
byte_idx = start_byte_idx
- char_idx = compute_line_length(prev_line, offset_encoding) + 1
+ char_idx = compute_line_length(prev_line, offset_encoding) + 1
else
byte_idx = start_byte_idx + str_utf_start(prev_line, start_byte_idx)
char_idx = byte_to_utf(prev_line, byte_idx, offset_encoding)
@@ -188,14 +210,30 @@ end
---@param new_lastline integer
---@param offset_encoding string
---@returns (int, int) end_line_idx and end_col_idx of range
-local function compute_end_range(prev_lines, curr_lines, start_range, firstline, lastline, new_lastline, offset_encoding)
+local function compute_end_range(
+ prev_lines,
+ curr_lines,
+ start_range,
+ firstline,
+ lastline,
+ new_lastline,
+ offset_encoding
+)
-- If firstline == new_lastline, the first change occurred on a line that was deleted.
-- In this case, the last_byte...
if firstline == new_lastline then
- return { line_idx = (lastline - new_lastline + firstline), byte_idx = 1, char_idx = 1 }, { line_idx = firstline, byte_idx = 1, char_idx = 1 }
+ return { line_idx = (lastline - new_lastline + firstline), byte_idx = 1, char_idx = 1 }, {
+ line_idx = firstline,
+ byte_idx = 1,
+ char_idx = 1,
+ }
end
if firstline == lastline then
- return { line_idx = firstline, byte_idx = 1, char_idx = 1 }, { line_idx = new_lastline - lastline + firstline, byte_idx = 1, char_idx = 1 }
+ return { line_idx = firstline, byte_idx = 1, char_idx = 1 }, {
+ line_idx = new_lastline - lastline + firstline,
+ byte_idx = 1,
+ char_idx = 1,
+ }
end
-- Compare on last line, at minimum will be the start range
local start_line_idx = start_range.line_idx
@@ -218,14 +256,18 @@ local function compute_end_range(prev_lines, curr_lines, start_range, firstline,
local max_length
if start_line_idx == prev_line_idx then
-- Search until beginning of difference
- max_length = min(prev_line_length - start_range.byte_idx, curr_line_length - start_range.byte_idx) + 1
+ max_length = min(
+ prev_line_length - start_range.byte_idx,
+ curr_line_length - start_range.byte_idx
+ ) + 1
else
max_length = min(prev_line_length, curr_line_length) + 1
end
for idx = 0, max_length do
byte_offset = idx
if
- str_byte(prev_line, prev_line_length - byte_offset) ~= str_byte(curr_line, curr_line_length - byte_offset)
+ str_byte(prev_line, prev_line_length - byte_offset)
+ ~= str_byte(curr_line, curr_line_length - byte_offset)
then
break
end
@@ -239,8 +281,10 @@ local function compute_end_range(prev_lines, curr_lines, start_range, firstline,
if prev_end_byte_idx == 0 then
prev_end_byte_idx = 1
end
- local prev_byte_idx, prev_char_idx = align_end_position(prev_line, prev_end_byte_idx, offset_encoding)
- local prev_end_range = { line_idx = prev_line_idx, byte_idx = prev_byte_idx, char_idx = prev_char_idx }
+ local prev_byte_idx, prev_char_idx =
+ align_end_position(prev_line, prev_end_byte_idx, offset_encoding)
+ local prev_end_range =
+ { line_idx = prev_line_idx, byte_idx = prev_byte_idx, char_idx = prev_char_idx }
local curr_end_range
-- Deletion event, new_range cannot be before start
@@ -252,8 +296,10 @@ local function compute_end_range(prev_lines, curr_lines, start_range, firstline,
if curr_end_byte_idx == 0 then
curr_end_byte_idx = 1
end
- local curr_byte_idx, curr_char_idx = align_end_position(curr_line, curr_end_byte_idx, offset_encoding)
- curr_end_range = { line_idx = curr_line_idx, byte_idx = curr_byte_idx, char_idx = curr_char_idx }
+ local curr_byte_idx, curr_char_idx =
+ align_end_position(curr_line, curr_end_byte_idx, offset_encoding)
+ curr_end_range =
+ { line_idx = curr_line_idx, byte_idx = curr_byte_idx, char_idx = curr_char_idx }
end
return prev_end_range, curr_end_range
@@ -266,14 +312,13 @@ end
---@param end_range table new_end_range returned by last_difference
---@returns string text extracted from defined region
local function extract_text(lines, start_range, end_range, line_ending)
- if not lines[start_range.line_idx] then
- return ""
- end
+ if not lines[start_range.line_idx] then
+ return ''
+ end
-- Trivial case: start and end range are the same line, directly grab changed text
if start_range.line_idx == end_range.line_idx then
-- string.sub is inclusive, end_range is not
return string.sub(lines[start_range.line_idx], start_range.byte_idx, end_range.byte_idx - 1)
-
else
-- Handle deletion case
-- Collect the changed portion of the first changed line
@@ -288,7 +333,7 @@ local function extract_text(lines, start_range, end_range, line_ending)
-- Collect the changed portion of the last changed line.
table.insert(result, string.sub(lines[end_range.line_idx], 1, end_range.byte_idx - 1))
else
- table.insert(result, "")
+ table.insert(result, '')
end
-- Add line ending between all lines
@@ -313,7 +358,10 @@ local function compute_range_length(lines, start_range, end_range, offset_encodi
local start_line = lines[start_range.line_idx]
local range_length
if start_line and #start_line > 0 then
- range_length = compute_line_length(start_line, offset_encoding) - start_range.char_idx + 1 + line_ending_length
+ range_length = compute_line_length(start_line, offset_encoding)
+ - start_range.char_idx
+ + 1
+ + line_ending_length
else
-- Length of newline character
range_length = line_ending_length
@@ -345,7 +393,15 @@ end
---@param new_lastline number line to begin search in new_lines for last difference
---@param offset_encoding string encoding requested by language server
---@returns table TextDocumentContentChangeEvent see https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocumentContentChangeEvent
-function M.compute_diff(prev_lines, curr_lines, firstline, lastline, new_lastline, offset_encoding, line_ending)
+function M.compute_diff(
+ prev_lines,
+ curr_lines,
+ firstline,
+ lastline,
+ new_lastline,
+ offset_encoding,
+ line_ending
+)
-- Find the start of changes between the previous and current buffer. Common between both.
-- Sent to the server as the start of the changed range.
-- Used to grab the changed text from the latest buffer.
@@ -375,7 +431,8 @@ function M.compute_diff(prev_lines, curr_lines, firstline, lastline, new_lastlin
local text = extract_text(curr_lines, start_range, curr_end_range, line_ending)
-- Compute the range of the replaced text. Deprecated but still required for certain language servers
- local range_length = compute_range_length(prev_lines, start_range, prev_end_range, offset_encoding, line_ending)
+ local range_length =
+ compute_range_length(prev_lines, start_range, prev_end_range, offset_encoding, line_ending)
-- convert to 0 based indexing
local result = {
diff --git a/runtime/lua/vim/lsp/tagfunc.lua b/runtime/lua/vim/lsp/tagfunc.lua
index 5c55e8559f..49029f8599 100644
--- a/runtime/lua/vim/lsp/tagfunc.lua
+++ b/runtime/lua/vim/lsp/tagfunc.lua
@@ -1,5 +1,5 @@
local lsp = vim.lsp
-local util = vim.lsp.util
+local util = lsp.util
---@private
local function mk_tag_item(name, range, uri, offset_encoding)
@@ -15,7 +15,7 @@ end
---@private
local function query_definition(pattern)
- local params = lsp.util.make_position_params()
+ local params = util.make_position_params()
local results_by_client, err = lsp.buf_request_sync(0, 'textDocument/definition', params, 1000)
if err then
return {}
@@ -44,7 +44,8 @@ end
---@private
local function query_workspace_symbols(pattern)
- local results_by_client, err = lsp.buf_request_sync(0, 'workspace/symbol', { query = pattern }, 1000)
+ local results_by_client, err =
+ lsp.buf_request_sync(0, 'workspace/symbol', { query = pattern }, 1000)
if err then
return {}
end
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index 89c5ebe8f7..70f5010256 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -1,10 +1,10 @@
-local protocol = require 'vim.lsp.protocol'
-local snippet = require 'vim.lsp._snippet'
+local protocol = require('vim.lsp.protocol')
+local snippet = require('vim.lsp._snippet')
local vim = vim
local validate = vim.validate
local api = vim.api
local list_extend = vim.list_extend
-local highlight = require 'vim.highlight'
+local highlight = require('vim.highlight')
local uv = vim.loop
local npcall = vim.F.npcall
@@ -13,14 +13,14 @@ local split = vim.split
local M = {}
local default_border = {
- {"", "NormalFloat"},
- {"", "NormalFloat"},
- {"", "NormalFloat"},
- {" ", "NormalFloat"},
- {"", "NormalFloat"},
- {"", "NormalFloat"},
- {"", "NormalFloat"},
- {" ", "NormalFloat"},
+ { '', 'NormalFloat' },
+ { '', 'NormalFloat' },
+ { '', 'NormalFloat' },
+ { ' ', 'NormalFloat' },
+ { '', 'NormalFloat' },
+ { '', 'NormalFloat' },
+ { '', 'NormalFloat' },
+ { ' ', 'NormalFloat' },
}
---@private
@@ -35,43 +35,70 @@ local function get_border_size(opts)
local width = 0
if type(border) == 'string' then
- local border_size = {none = {0, 0}, single = {2, 2}, double = {2, 2}, rounded = {2, 2}, solid = {2, 2}, shadow = {1, 1}}
+ local border_size = {
+ none = { 0, 0 },
+ single = { 2, 2 },
+ double = { 2, 2 },
+ rounded = { 2, 2 },
+ solid = { 2, 2 },
+ shadow = { 1, 1 },
+ }
if border_size[border] == nil then
- error(string.format("invalid floating preview border: %s. :help vim.api.nvim_open_win()", vim.inspect(border)))
+ error(
+ string.format(
+ 'invalid floating preview border: %s. :help vim.api.nvim_open_win()',
+ vim.inspect(border)
+ )
+ )
end
height, width = unpack(border_size[border])
else
if 8 % #border ~= 0 then
- error(string.format("invalid floating preview border: %s. :help vim.api.nvim_open_win()", vim.inspect(border)))
+ error(
+ string.format(
+ 'invalid floating preview border: %s. :help vim.api.nvim_open_win()',
+ vim.inspect(border)
+ )
+ )
end
---@private
local function border_width(id)
id = (id - 1) % #border + 1
- if type(border[id]) == "table" then
+ if type(border[id]) == 'table' then
-- border specified as a table of <character, highlight group>
return vim.fn.strdisplaywidth(border[id][1])
- elseif type(border[id]) == "string" then
+ elseif type(border[id]) == 'string' then
-- border specified as a list of border characters
return vim.fn.strdisplaywidth(border[id])
end
- error(string.format("invalid floating preview border: %s. :help vim.api.nvim_open_win()", vim.inspect(border)))
+ error(
+ string.format(
+ 'invalid floating preview border: %s. :help vim.api.nvim_open_win()',
+ vim.inspect(border)
+ )
+ )
end
---@private
local function border_height(id)
id = (id - 1) % #border + 1
- if type(border[id]) == "table" then
+ if type(border[id]) == 'table' then
-- border specified as a table of <character, highlight group>
return #border[id][1] > 0 and 1 or 0
- elseif type(border[id]) == "string" then
+ elseif type(border[id]) == 'string' then
-- border specified as a list of border characters
return #border[id] > 0 and 1 or 0
end
- error(string.format("invalid floating preview border: %s. :help vim.api.nvim_open_win()", vim.inspect(border)))
+ error(
+ string.format(
+ 'invalid floating preview border: %s. :help vim.api.nvim_open_win()',
+ vim.inspect(border)
+ )
+ )
end
- height = height + border_height(2) -- top
- height = height + border_height(6) -- bottom
- width = width + border_width(4) -- right
- width = width + border_width(8) -- left
+ height = height + border_height(2) -- top
+ height = height + border_height(6) -- bottom
+ width = width + border_width(4) -- right
+ width = width + border_width(8) -- left
end
return { height = height, width = width }
@@ -89,9 +116,15 @@ end
---@param encoding string utf-8|utf-16|utf-32|nil defaults to utf-16
---@return number `encoding` index of `index` in `line`
function M._str_utfindex_enc(line, index, encoding)
- if not encoding then encoding = 'utf-16' end
+ if not encoding then
+ encoding = 'utf-16'
+ end
if encoding == 'utf-8' then
- if index then return index else return #line end
+ if index then
+ return index
+ else
+ return #line
+ end
elseif encoding == 'utf-16' then
local _, col16 = vim.str_utfindex(line, index)
return col16
@@ -99,7 +132,7 @@ function M._str_utfindex_enc(line, index, encoding)
local col32, _ = vim.str_utfindex(line, index)
return col32
else
- error("Invalid encoding: " .. vim.inspect(encoding))
+ error('Invalid encoding: ' .. vim.inspect(encoding))
end
end
@@ -111,15 +144,21 @@ end
---@param encoding string utf-8|utf-16|utf-32|nil defaults to utf-16
---@return number byte (utf-8) index of `encoding` index `index` in `line`
function M._str_byteindex_enc(line, index, encoding)
- if not encoding then encoding = 'utf-16' end
+ if not encoding then
+ encoding = 'utf-16'
+ end
if encoding == 'utf-8' then
- if index then return index else return #line end
+ if index then
+ return index
+ else
+ return #line
+ end
elseif encoding == 'utf-16' then
return vim.str_byteindex(line, index, true)
elseif encoding == 'utf-32' then
return vim.str_byteindex(line, index)
else
- error("Invalid encoding: " .. vim.inspect(encoding))
+ error('Invalid encoding: ' .. vim.inspect(encoding))
end
end
@@ -142,34 +181,38 @@ function M.set_lines(lines, A, B, new_lines)
-- specifying a line number after what we would call the last line.
local i_n = math.min(B[1] + 1, #lines)
if not (i_0 >= 1 and i_0 <= #lines + 1 and i_n >= 1 and i_n <= #lines) then
- error("Invalid range: "..vim.inspect{A = A; B = B; #lines, new_lines})
+ error('Invalid range: ' .. vim.inspect({ A = A, B = B, #lines, new_lines }))
end
- local prefix = ""
- local suffix = lines[i_n]:sub(B[2]+1)
+ local prefix = ''
+ local suffix = lines[i_n]:sub(B[2] + 1)
if A[2] > 0 then
prefix = lines[i_0]:sub(1, A[2])
end
local n = i_n - i_0 + 1
if n ~= #new_lines then
- for _ = 1, n - #new_lines do table.remove(lines, i_0) end
- for _ = 1, #new_lines - n do table.insert(lines, i_0, '') end
+ for _ = 1, n - #new_lines do
+ table.remove(lines, i_0)
+ end
+ for _ = 1, #new_lines - n do
+ table.insert(lines, i_0, '')
+ end
end
for i = 1, #new_lines do
lines[i - 1 + i_0] = new_lines[i]
end
if #suffix > 0 then
local i = i_0 + #new_lines - 1
- lines[i] = lines[i]..suffix
+ lines[i] = lines[i] .. suffix
end
if #prefix > 0 then
- lines[i_0] = prefix..lines[i_0]
+ lines[i_0] = prefix .. lines[i_0]
end
return lines
end
---@private
local function sort_by_key(fn)
- return function(a,b)
+ return function(a, b)
local ka, kb = fn(a), fn(b)
assert(#ka == #kb)
for i = 1, #ka do
@@ -191,18 +234,18 @@ end
---@param rows number[] zero-indexed line numbers
---@return table<number string> a table mapping rows to lines
local function get_lines(bufnr, rows)
- rows = type(rows) == "table" and rows or { rows }
+ rows = type(rows) == 'table' and rows or { rows }
-- This is needed for bufload and bufloaded
if bufnr == 0 then
- bufnr = vim.api.nvim_get_current_buf()
+ bufnr = api.nvim_get_current_buf()
end
---@private
local function buf_lines()
local lines = {}
for _, row in pairs(rows) do
- lines[row] = (vim.api.nvim_buf_get_lines(bufnr, row, row + 1, false) or { "" })[1]
+ lines[row] = (api.nvim_buf_get_lines(bufnr, row, row + 1, false) or { '' })[1]
end
return lines
end
@@ -211,7 +254,7 @@ local function get_lines(bufnr, rows)
-- load the buffer if this is not a file uri
-- Custom language server protocol extensions can result in servers sending URIs with custom schemes. Plugins are able to load these via `BufReadCmd` autocmds.
- if uri:sub(1, 4) ~= "file" then
+ if uri:sub(1, 4) ~= 'file' then
vim.fn.bufload(bufnr)
return buf_lines()
end
@@ -224,8 +267,10 @@ local function get_lines(bufnr, rows)
local filename = api.nvim_buf_get_name(bufnr)
-- get the data from the file
- local fd = uv.fs_open(filename, "r", 438)
- if not fd then return "" end
+ local fd = uv.fs_open(filename, 'r', 438)
+ if not fd then
+ return ''
+ end
local stat = uv.fs_fstat(fd)
local data = uv.fs_read(fd, stat.size, 0)
uv.fs_close(fd)
@@ -242,11 +287,13 @@ local function get_lines(bufnr, rows)
local found = 0
local lnum = 0
- for line in string.gmatch(data, "([^\n]*)\n?") do
+ for line in string.gmatch(data, '([^\n]*)\n?') do
if lines[lnum] == true then
lines[lnum] = line
found = found + 1
- if found == need then break end
+ if found == need then
+ break
+ end
end
lnum = lnum + 1
end
@@ -254,13 +301,12 @@ local function get_lines(bufnr, rows)
-- change any lines we didn't find to the empty string
for i, line in pairs(lines) do
if line == true then
- lines[i] = ""
+ lines[i] = ''
end
end
return lines
end
-
---@private
--- Gets the zero-indexed line from the given buffer.
--- Works on unloaded buffers by reading the file using libuv to bypass buf reading events.
@@ -273,11 +319,10 @@ local function get_line(bufnr, row)
return get_lines(bufnr, { row })[row]
end
-
---@private
--- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position
--- Returns a zero-indexed column, since set_lines() does the conversion to
----@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to utf-16
+---@param offset_encoding string utf-8|utf-16|utf-32
--- 1-indexed
local function get_line_byte_from_position(bufnr, position, offset_encoding)
-- LSP's line and characters are 0-indexed
@@ -286,7 +331,7 @@ local function get_line_byte_from_position(bufnr, position, offset_encoding)
-- When on the first character, we can ignore the difference between byte and
-- character
if col > 0 then
- local line = get_line(bufnr, position.line)
+ local line = get_line(bufnr, position.line) or ''
local ok, result
ok, result = pcall(_str_byteindex_enc, line, col, offset_encoding)
if ok then
@@ -300,54 +345,27 @@ end
--- Process and return progress reports from lsp server
---@private
function M.get_progress_messages()
-
local new_messages = {}
- local msg_remove = {}
local progress_remove = {}
for _, client in ipairs(vim.lsp.get_active_clients()) do
- local messages = client.messages
- local data = messages
- for token, ctx in pairs(data.progress) do
-
- local new_report = {
- name = data.name,
- title = ctx.title or "empty title",
- message = ctx.message,
- percentage = ctx.percentage,
- done = ctx.done,
- progress = true,
- }
- table.insert(new_messages, new_report)
-
- if ctx.done then
- table.insert(progress_remove, {client = client, token = token})
- end
- end
-
- for i, msg in ipairs(data.messages) do
- if msg.show_once then
- msg.shown = msg.shown + 1
- if msg.shown > 1 then
- table.insert(msg_remove, {client = client, idx = i})
- end
- end
-
- table.insert(new_messages, {name = data.name, content = msg.content})
- end
+ local messages = client.messages
+ local data = messages
+ for token, ctx in pairs(data.progress) do
+ local new_report = {
+ name = data.name,
+ title = ctx.title or 'empty title',
+ message = ctx.message,
+ percentage = ctx.percentage,
+ done = ctx.done,
+ progress = true,
+ }
+ table.insert(new_messages, new_report)
- if next(data.status) ~= nil then
- table.insert(new_messages, {
- name = data.name,
- content = data.status.content,
- uri = data.status.uri,
- status = true
- })
+ if ctx.done then
+ table.insert(progress_remove, { client = client, token = token })
end
- for _, item in ipairs(msg_remove) do
- table.remove(client.messages, item.idx)
end
-
end
for _, item in ipairs(progress_remove) do
@@ -360,16 +378,17 @@ end
--- Applies a list of text edits to a buffer.
---@param text_edits table list of `TextEdit` objects
---@param bufnr number Buffer id
----@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to encoding of first client of `bufnr`
+---@param offset_encoding string utf-8|utf-16|utf-32
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textEdit
function M.apply_text_edits(text_edits, bufnr, offset_encoding)
- validate {
- text_edits = { text_edits, 't', false };
- bufnr = { bufnr, 'number', false };
- offset_encoding = { offset_encoding, 'string', true };
- }
- offset_encoding = offset_encoding or M._get_offset_encoding(bufnr)
- if not next(text_edits) then return end
+ validate({
+ text_edits = { text_edits, 't', false },
+ bufnr = { bufnr, 'number', false },
+ offset_encoding = { offset_encoding, 'string', false },
+ })
+ if not next(text_edits) then
+ return
+ end
if not api.nvim_buf_is_loaded(bufnr) then
vim.fn.bufload(bufnr)
end
@@ -381,7 +400,11 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding)
index = index + 1
text_edit._index = index
- if text_edit.range.start.line > text_edit.range['end'].line or text_edit.range.start.line == text_edit.range['end'].line and text_edit.range.start.character > text_edit.range['end'].character then
+ if
+ text_edit.range.start.line > text_edit.range['end'].line
+ or text_edit.range.start.line == text_edit.range['end'].line
+ and text_edit.range.start.character > text_edit.range['end'].character
+ then
local start = text_edit.range.start
text_edit.range.start = text_edit.range['end']
text_edit.range['end'] = start
@@ -402,28 +425,9 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding)
end
end)
- -- Some LSP servers may return +1 range of the buffer content but nvim_buf_set_text can't accept it so we should fix it here.
- local has_eol_text_edit = false
- local max = vim.api.nvim_buf_line_count(bufnr)
- local len = _str_utfindex_enc(vim.api.nvim_buf_get_lines(bufnr, -2, -1, false)[1] or '', nil, offset_encoding)
- text_edits = vim.tbl_map(function(text_edit)
- if max <= text_edit.range.start.line then
- text_edit.range.start.line = max - 1
- text_edit.range.start.character = len
- text_edit.newText = '\n' .. text_edit.newText
- has_eol_text_edit = true
- end
- if max <= text_edit.range['end'].line then
- text_edit.range['end'].line = max - 1
- text_edit.range['end'].character = len
- has_eol_text_edit = true
- end
- return text_edit
- end, text_edits)
-
-- Some LSP servers are depending on the VSCode behavior.
-- The VSCode will re-locate the cursor position after applying TextEdit so we also do it.
- local is_current_buf = vim.api.nvim_get_current_buf() == bufnr
+ local is_current_buf = api.nvim_get_current_buf() == bufnr
local cursor = (function()
if not is_current_buf then
return {
@@ -431,7 +435,7 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding)
col = -1,
}
end
- local cursor = vim.api.nvim_win_get_cursor(0)
+ local cursor = api.nvim_win_get_cursor(0)
return {
row = cursor[1] - 1,
col = cursor[2],
@@ -440,16 +444,38 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding)
-- Apply text edits.
local is_cursor_fixed = false
+ local has_eol_text_edit = false
for _, text_edit in ipairs(text_edits) do
+ -- Normalize line ending
+ text_edit.newText, _ = string.gsub(text_edit.newText, '\r\n?', '\n')
+
+ -- Convert from LSP style ranges to Neovim style ranges.
local e = {
start_row = text_edit.range.start.line,
- start_col = get_line_byte_from_position(bufnr, text_edit.range.start),
+ start_col = get_line_byte_from_position(bufnr, text_edit.range.start, offset_encoding),
end_row = text_edit.range['end'].line,
- end_col = get_line_byte_from_position(bufnr, text_edit.range['end']),
- text = vim.split(text_edit.newText, '\n', true),
+ end_col = get_line_byte_from_position(bufnr, text_edit.range['end'], offset_encoding),
+ text = split(text_edit.newText, '\n', true),
}
- vim.api.nvim_buf_set_text(bufnr, e.start_row, e.start_col, e.end_row, e.end_col, e.text)
+ -- Some LSP servers may return +1 range of the buffer content but nvim_buf_set_text can't accept it so we should fix it here.
+ local max = api.nvim_buf_line_count(bufnr)
+ if max <= e.start_row or max <= e.end_row then
+ local len = #(get_line(bufnr, max - 1) or '')
+ if max <= e.start_row then
+ e.start_row = max - 1
+ e.start_col = len
+ table.insert(e.text, 1, '')
+ end
+ if max <= e.end_row then
+ e.end_row = max - 1
+ e.end_col = len
+ end
+ has_eol_text_edit = true
+ end
+ api.nvim_buf_set_text(bufnr, e.start_row, e.start_col, e.end_row, e.end_col, e.text)
+
+ -- Fix cursor position.
local row_count = (e.end_row - e.start_row) + 1
if e.end_row < cursor.row then
cursor.row = cursor.row + (#e.text - row_count)
@@ -464,21 +490,28 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding)
end
end
+ local max = api.nvim_buf_line_count(bufnr)
+
+ -- Apply fixed cursor position.
if is_cursor_fixed then
local is_valid_cursor = true
- is_valid_cursor = is_valid_cursor and cursor.row < vim.api.nvim_buf_line_count(bufnr)
- is_valid_cursor = is_valid_cursor and cursor.col <= #(vim.api.nvim_buf_get_lines(bufnr, cursor.row, cursor.row + 1, false)[1] or '')
+ is_valid_cursor = is_valid_cursor and cursor.row < max
+ is_valid_cursor = is_valid_cursor and cursor.col <= #(get_line(bufnr, max - 1) or '')
if is_valid_cursor then
- vim.api.nvim_win_set_cursor(0, { cursor.row + 1, cursor.col })
+ api.nvim_win_set_cursor(0, { cursor.row + 1, cursor.col })
end
end
-- Remove final line if needed
local fix_eol = has_eol_text_edit
- fix_eol = fix_eol and api.nvim_buf_get_option(bufnr, 'fixeol')
- fix_eol = fix_eol and (vim.api.nvim_buf_get_lines(bufnr, -2, -1, false)[1] or '') == ''
+ fix_eol = fix_eol
+ and (
+ api.nvim_buf_get_option(bufnr, 'eol')
+ or (api.nvim_buf_get_option(bufnr, 'fixeol') and not api.nvim_buf_get_option(bufnr, 'binary'))
+ )
+ fix_eol = fix_eol and get_line(bufnr, max - 1) == ''
if fix_eol then
- vim.api.nvim_buf_set_lines(bufnr, -2, -1, false, {})
+ api.nvim_buf_set_lines(bufnr, -2, -1, false, {})
end
end
@@ -514,9 +547,15 @@ end
---@param text_document_edit table: a `TextDocumentEdit` object
---@param index number: Optional index of the edit, if from a list of edits (or nil, if not from a list)
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentEdit
-function M.apply_text_document_edit(text_document_edit, index)
+function M.apply_text_document_edit(text_document_edit, index, offset_encoding)
local text_document = text_document_edit.textDocument
local bufnr = vim.uri_to_bufnr(text_document.uri)
+ if offset_encoding == nil then
+ vim.notify_once(
+ 'apply_text_document_edit must be called with valid offset encoding',
+ vim.log.levels.WARN
+ )
+ end
-- For lists of text document edits,
-- do not check the version after the first edit.
@@ -527,15 +566,20 @@ function M.apply_text_document_edit(text_document_edit, index)
-- `VersionedTextDocumentIdentifier`s version may be null
-- https://microsoft.github.io/language-server-protocol/specification#versionedTextDocumentIdentifier
- if should_check_version and (text_document.version
+ if
+ should_check_version
+ and (
+ text_document.version
and text_document.version > 0
and M.buf_versions[bufnr]
- and M.buf_versions[bufnr] > text_document.version) then
- print("Buffer ", text_document.uri, " newer than edits.")
+ and M.buf_versions[bufnr] > text_document.version
+ )
+ then
+ print('Buffer ', text_document.uri, ' newer than edits.')
return
end
- M.apply_text_edits(text_document_edit.edits, bufnr)
+ M.apply_text_edits(text_document_edit.edits, bufnr, offset_encoding)
end
--- Parses snippets in a completion entry.
@@ -567,16 +611,16 @@ end
--- precedence is as follows: textEdit.newText > insertText > label
--see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
local function get_completion_word(item)
- if item.textEdit ~= nil and item.textEdit.newText ~= nil and item.textEdit.newText ~= "" then
+ if item.textEdit ~= nil and item.textEdit.newText ~= nil and item.textEdit.newText ~= '' then
local insert_text_format = protocol.InsertTextFormat[item.insertTextFormat]
- if insert_text_format == "PlainText" or insert_text_format == nil then
+ if insert_text_format == 'PlainText' or insert_text_format == nil then
return item.textEdit.newText
else
return M.parse_snippet(item.textEdit.newText)
end
- elseif item.insertText ~= nil and item.insertText ~= "" then
+ elseif item.insertText ~= nil and item.insertText ~= '' then
local insert_text_format = protocol.InsertTextFormat[item.insertTextFormat]
- if insert_text_format == "PlainText" or insert_text_format == nil then
+ if insert_text_format == 'PlainText' or insert_text_format == nil then
return item.insertText
else
return M.parse_snippet(item.insertText)
@@ -604,7 +648,7 @@ end
---@returns (`vim.lsp.protocol.completionItemKind`)
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
function M._get_completion_item_kind_name(completion_item_kind)
- return protocol.CompletionItemKind[completion_item_kind] or "Unknown"
+ return protocol.CompletionItemKind[completion_item_kind] or 'Unknown'
end
--- Turns the result of a `textDocument/completion` request into vim-compatible
@@ -635,7 +679,7 @@ function M.text_document_completion_list_to_complete_items(result, prefix)
info = documentation
elseif type(documentation) == 'table' and type(documentation.value) == 'string' then
info = documentation.value
- -- else
+ -- else
-- TODO(ashkan) Validation handling here?
end
end
@@ -653,9 +697,9 @@ function M.text_document_completion_list_to_complete_items(result, prefix)
user_data = {
nvim = {
lsp = {
- completion_item = completion_item
- }
- }
+ completion_item = completion_item,
+ },
+ },
},
})
end
@@ -663,6 +707,15 @@ function M.text_document_completion_list_to_complete_items(result, prefix)
return matches
end
+---@private
+--- Like vim.fn.bufwinid except it works across tabpages.
+local function bufwinid(bufnr)
+ for _, win in ipairs(api.nvim_list_wins()) do
+ if api.nvim_win_get_buf(win) == bufnr then
+ return win
+ end
+ end
+end
--- Rename old_fname to new_fname
---
@@ -671,7 +724,7 @@ end
-- ignoreIfExists? bool
function M.rename(old_fname, new_fname, opts)
opts = opts or {}
- local target_exists = vim.loop.fs_stat(new_fname) ~= nil
+ local target_exists = uv.fs_stat(new_fname) ~= nil
if target_exists and not opts.overwrite or opts.ignoreIfExists then
vim.notify('Rename target already exists. Skipping rename.')
return
@@ -688,10 +741,9 @@ function M.rename(old_fname, new_fname, opts)
assert(ok, err)
local newbuf = vim.fn.bufadd(new_fname)
- for _, win in pairs(api.nvim_list_wins()) do
- if api.nvim_win_get_buf(win) == oldbuf then
- api.nvim_win_set_buf(win, newbuf)
- end
+ local win = bufwinid(oldbuf)
+ if win then
+ api.nvim_win_set_buf(win, newbuf)
end
api.nvim_buf_delete(oldbuf, { force = true })
end
@@ -712,11 +764,11 @@ end
local function delete_file(change)
local opts = change.options or {}
local fname = vim.uri_to_fname(change.uri)
- local stat = vim.loop.fs_stat(fname)
+ local stat = uv.fs_stat(fname)
if opts.ignoreIfNotExists and not stat then
return
end
- assert(stat, "Cannot delete not existing file or folder " .. fname)
+ assert(stat, 'Cannot delete not existing file or folder ' .. fname)
local flags
if stat and stat.type == 'directory' then
flags = opts.recursive and 'rf' or 'd'
@@ -729,28 +781,30 @@ local function delete_file(change)
api.nvim_buf_delete(bufnr, { force = true })
end
-
--- Applies a `WorkspaceEdit`.
---
----@param workspace_edit (table) `WorkspaceEdit`
+---@param workspace_edit table `WorkspaceEdit`
+---@param offset_encoding string utf-8|utf-16|utf-32 (required)
--see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit
-function M.apply_workspace_edit(workspace_edit)
+function M.apply_workspace_edit(workspace_edit, offset_encoding)
+ if offset_encoding == nil then
+ vim.notify_once(
+ 'apply_workspace_edit must be called with valid offset encoding',
+ vim.log.levels.WARN
+ )
+ end
if workspace_edit.documentChanges then
for idx, change in ipairs(workspace_edit.documentChanges) do
- if change.kind == "rename" then
- M.rename(
- vim.uri_to_fname(change.oldUri),
- vim.uri_to_fname(change.newUri),
- change.options
- )
+ if change.kind == 'rename' then
+ M.rename(vim.uri_to_fname(change.oldUri), vim.uri_to_fname(change.newUri), change.options)
elseif change.kind == 'create' then
create_file(change)
elseif change.kind == 'delete' then
delete_file(change)
elseif change.kind then
- error(string.format("Unsupported change: %q", vim.inspect(change)))
+ error(string.format('Unsupported change: %q', vim.inspect(change)))
else
- M.apply_text_document_edit(change, idx)
+ M.apply_text_document_edit(change, idx, offset_encoding)
end
end
return
@@ -763,7 +817,7 @@ function M.apply_workspace_edit(workspace_edit)
for uri, changes in pairs(all_changes) do
local bufnr = vim.uri_to_bufnr(uri)
- M.apply_text_edits(changes, bufnr)
+ M.apply_text_edits(changes, bufnr, offset_encoding)
end
end
@@ -782,7 +836,7 @@ function M.convert_input_to_markdown_lines(input, contents)
if type(input) == 'string' then
list_extend(contents, split_lines(input))
else
- assert(type(input) == 'table', "Expected a table for Hover.contents")
+ assert(type(input) == 'table', 'Expected a table for Hover.contents')
-- MarkupContent
if input.kind then
-- The kind can be either plaintext or markdown.
@@ -791,22 +845,22 @@ function M.convert_input_to_markdown_lines(input, contents)
-- Some servers send input.value as empty, so let's ignore this :(
local value = input.value or ''
- if input.kind == "plaintext" then
+ if input.kind == 'plaintext' then
-- wrap this in a <text></text> block so that stylize_markdown
-- can properly process it as plaintext
- value = string.format("<text>\n%s\n</text>", value)
+ value = string.format('<text>\n%s\n</text>', value)
end
-- assert(type(value) == 'string')
list_extend(contents, split_lines(value))
- -- MarkupString variation 2
+ -- MarkupString variation 2
elseif input.language then
-- Some servers send input.value as empty, so let's ignore this :(
-- assert(type(input.value) == 'string')
- table.insert(contents, "```"..input.language)
+ table.insert(contents, '```' .. input.language)
list_extend(contents, split_lines(input.value or ''))
- table.insert(contents, "```")
- -- By deduction, this must be MarkedString[]
+ table.insert(contents, '```')
+ -- By deduction, this must be MarkedString[]
else
-- Use our existing logic to handle MarkedString
for _, marked_string in ipairs(input) do
@@ -839,7 +893,8 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers
local active_hl
local active_signature = signature_help.activeSignature or 0
-- If the activeSignature is not inside the valid range, then clip it.
- if active_signature >= #signature_help.signatures then
+ -- In 3.15 of the protocol, activeSignature was allowed to be negative
+ if active_signature >= #signature_help.signatures or active_signature < 0 then
active_signature = 0
end
local signature = signature_help.signatures[active_signature + 1]
@@ -849,16 +904,16 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers
local label = signature.label
if ft then
-- wrap inside a code block so stylize_markdown can render it properly
- label = ("```%s\n%s\n```"):format(ft, label)
+ label = ('```%s\n%s\n```'):format(ft, label)
end
- vim.list_extend(contents, vim.split(label, '\n', true))
+ list_extend(contents, split(label, '\n', true))
if signature.documentation then
M.convert_input_to_markdown_lines(signature.documentation, contents)
end
if signature.parameters and #signature.parameters > 0 then
local active_parameter = (signature.activeParameter or signature_help.activeParameter or 0)
- if active_parameter < 0
- then active_parameter = 0
+ if active_parameter < 0 then
+ active_parameter = 0
end
-- If the activeParameter is > #parameters, then set it to the last
@@ -888,7 +943,7 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers
}
--]=]
if parameter.label then
- if type(parameter.label) == "table" then
+ if type(parameter.label) == 'table' then
active_hl = parameter.label
else
local offset = 1
@@ -901,9 +956,11 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers
end
for p, param in pairs(signature.parameters) do
offset = signature.label:find(param.label, offset, true)
- if not offset then break end
+ if not offset then
+ break
+ end
if p == active_parameter + 1 then
- active_hl = {offset - 1, offset + #parameter.label - 1}
+ active_hl = { offset - 1, offset + #parameter.label - 1 }
break
end
offset = offset + #param.label + 1
@@ -931,14 +988,14 @@ end
--- - zindex (string or table) override `zindex`, defaults to 50
---@returns (table) Options
function M.make_floating_popup_options(width, height, opts)
- validate {
- opts = { opts, 't', true };
- }
+ validate({
+ opts = { opts, 't', true },
+ })
opts = opts or {}
- validate {
- ["opts.offset_x"] = { opts.offset_x, 'n', true };
- ["opts.offset_y"] = { opts.offset_y, 'n', true };
- }
+ validate({
+ ['opts.offset_x'] = { opts.offset_x, 'n', true },
+ ['opts.offset_y'] = { opts.offset_y, 'n', true },
+ })
local anchor = ''
local row, col
@@ -947,20 +1004,20 @@ function M.make_floating_popup_options(width, height, opts)
local lines_below = vim.fn.winheight(0) - lines_above
if lines_above < lines_below then
- anchor = anchor..'N'
+ anchor = anchor .. 'N'
height = math.min(lines_below, height)
row = 1
else
- anchor = anchor..'S'
+ anchor = anchor .. 'S'
height = math.min(lines_above, height)
row = 0
end
if vim.fn.wincol() + width + (opts.offset_x or 0) <= api.nvim_get_option('columns') then
- anchor = anchor..'W'
+ anchor = anchor .. 'W'
col = 0
else
- anchor = anchor..'E'
+ anchor = anchor .. 'E'
col = 1
end
@@ -980,30 +1037,45 @@ end
--- Jumps to a location.
---
----@param location (`Location`|`LocationLink`)
+---@param location table (`Location`|`LocationLink`)
+---@param offset_encoding string utf-8|utf-16|utf-32 (required)
+---@param reuse_win boolean Jump to existing window if buffer is already opened.
---@returns `true` if the jump succeeded
-function M.jump_to_location(location)
+function M.jump_to_location(location, offset_encoding, reuse_win)
-- location may be Location or LocationLink
local uri = location.uri or location.targetUri
- if uri == nil then return end
+ if uri == nil then
+ return
+ end
+ if offset_encoding == nil then
+ vim.notify_once(
+ 'jump_to_location must be called with valid offset encoding',
+ vim.log.levels.WARN
+ )
+ end
local bufnr = vim.uri_to_bufnr(uri)
-- Save position in jumplist
- vim.cmd "normal! m'"
+ vim.cmd("normal! m'")
-- Push a new item into tagstack
- local from = {vim.fn.bufnr('%'), vim.fn.line('.'), vim.fn.col('.'), 0}
- local items = {{tagname=vim.fn.expand('<cword>'), from=from}}
- vim.fn.settagstack(vim.fn.win_getid(), {items=items}, 't')
+ local from = { vim.fn.bufnr('%'), vim.fn.line('.'), vim.fn.col('.'), 0 }
+ local items = { { tagname = vim.fn.expand('<cword>'), from = from } }
+ vim.fn.settagstack(vim.fn.win_getid(), { items = items }, 't')
--- Jump to new location (adjusting for UTF-16 encoding of characters)
- api.nvim_set_current_buf(bufnr)
- api.nvim_buf_set_option(bufnr, 'buflisted', true)
+ local win = reuse_win and bufwinid(bufnr)
+ if win then
+ api.nvim_set_current_win(win)
+ else
+ api.nvim_buf_set_option(bufnr, 'buflisted', true)
+ api.nvim_set_current_buf(bufnr)
+ end
local range = location.range or location.targetSelectionRange
local row = range.start.line
- local col = get_line_byte_from_position(bufnr, range.start)
- api.nvim_win_set_cursor(0, {row + 1, col})
+ local col = get_line_byte_from_position(bufnr, range.start, offset_encoding)
+ api.nvim_win_set_cursor(0, { row + 1, col })
-- Open folds under the cursor
- vim.cmd("normal! zv")
+ vim.cmd('normal! zv')
return true
end
@@ -1018,22 +1090,24 @@ end
function M.preview_location(location, opts)
-- location may be LocationLink or Location (more useful for the former)
local uri = location.targetUri or location.uri
- if uri == nil then return end
+ if uri == nil then
+ return
+ end
local bufnr = vim.uri_to_bufnr(uri)
if not api.nvim_buf_is_loaded(bufnr) then
vim.fn.bufload(bufnr)
end
local range = location.targetRange or location.range
- local contents = api.nvim_buf_get_lines(bufnr, range.start.line, range["end"].line+1, false)
+ local contents = api.nvim_buf_get_lines(bufnr, range.start.line, range['end'].line + 1, false)
local syntax = api.nvim_buf_get_option(bufnr, 'syntax')
- if syntax == "" then
+ if syntax == '' then
-- When no syntax is set, we use filetype as fallback. This might not result
-- in a valid syntax definition. See also ft detection in stylize_markdown.
-- An empty syntax is more common now with TreeSitter, since TS disables syntax.
syntax = api.nvim_buf_get_option(bufnr, 'filetype')
end
opts = opts or {}
- opts.focus_id = "location"
+ opts.focus_id = 'location'
return M.open_floating_preview(contents, syntax, opts)
end
@@ -1054,20 +1128,20 @@ end
--- - pad_bottom number of lines to pad contents at bottom (default 0)
---@return contents table of trimmed and padded lines
function M._trim(contents, opts)
- validate {
- contents = { contents, 't' };
- opts = { opts, 't', true };
- }
+ validate({
+ contents = { contents, 't' },
+ opts = { opts, 't', true },
+ })
opts = opts or {}
contents = M.trim_empty_lines(contents)
if opts.pad_top then
for _ = 1, opts.pad_top do
- table.insert(contents, 1, "")
+ table.insert(contents, 1, '')
end
end
if opts.pad_bottom then
for _ = 1, opts.pad_bottom do
- table.insert(contents, "")
+ table.insert(contents, '')
end
end
return contents
@@ -1080,7 +1154,7 @@ end
local function get_markdown_fences()
local fences = {}
for _, fence in pairs(vim.g.markdown_fenced_languages or {}) do
- local lang, syntax = fence:match("^(.*)=(.*)$")
+ local lang, syntax = fence:match('^(.*)=(.*)$')
if lang then
fences[lang] = syntax
end
@@ -1109,28 +1183,28 @@ end
--- - separator insert separator after code block
---@returns width,height size of float
function M.stylize_markdown(bufnr, contents, opts)
- validate {
- contents = { contents, 't' };
- opts = { opts, 't', true };
- }
+ validate({
+ contents = { contents, 't' },
+ opts = { opts, 't', true },
+ })
opts = opts or {}
-- table of fence types to {ft, begin, end}
-- when ft is nil, we get the ft from the regex match
local matchers = {
- block = {nil, "```+([a-zA-Z0-9_]*)", "```+"},
- pre = {"", "<pre>", "</pre>"},
- code = {"", "<code>", "</code>"},
- text = {"plaintex", "<text>", "</text>"},
+ block = { nil, '```+([a-zA-Z0-9_]*)', '```+' },
+ pre = { '', '<pre>', '</pre>' },
+ code = { '', '<code>', '</code>' },
+ text = { 'text', '<text>', '</text>' },
}
local match_begin = function(line)
for type, pattern in pairs(matchers) do
- local ret = line:match(string.format("^%%s*%s%%s*$", pattern[2]))
+ local ret = line:match(string.format('^%%s*%s%%s*$', pattern[2]))
if ret then
return {
type = type,
- ft = pattern[1] or ret
+ ft = pattern[1] or ret,
}
end
end
@@ -1138,7 +1212,7 @@ function M.stylize_markdown(bufnr, contents, opts)
local match_end = function(line, match)
local pattern = matchers[match.type]
- return line:match(string.format("^%%s*%s%%s*$", pattern[3]))
+ return line:match(string.format('^%%s*%s%%s*$', pattern[3]))
end
-- Clean up
@@ -1168,25 +1242,34 @@ function M.stylize_markdown(bufnr, contents, opts)
i = i + 1
end
table.insert(highlights, {
- ft = match.ft;
- start = start + 1;
- finish = #stripped;
+ ft = match.ft,
+ start = start + 1,
+ finish = #stripped,
})
-- add a separator, but not on the last line
if add_sep and i < #contents then
- table.insert(stripped, "---")
+ table.insert(stripped, '---')
markdown_lines[#stripped] = true
end
else
-- strip any empty lines or separators prior to this separator in actual markdown
- if line:match("^---+$") then
- while markdown_lines[#stripped] and (stripped[#stripped]:match("^%s*$") or stripped[#stripped]:match("^---+$")) do
+ if line:match('^---+$') then
+ while
+ markdown_lines[#stripped]
+ and (stripped[#stripped]:match('^%s*$') or stripped[#stripped]:match('^---+$'))
+ do
markdown_lines[#stripped] = false
table.remove(stripped, #stripped)
end
end
-- add the line if its not an empty line following a separator
- if not (line:match("^%s*$") and markdown_lines[#stripped] and stripped[#stripped]:match("^---+$")) then
+ if
+ not (
+ line:match('^%s*$')
+ and markdown_lines[#stripped]
+ and stripped[#stripped]:match('^---+$')
+ )
+ then
table.insert(stripped, line)
markdown_lines[#stripped] = true
end
@@ -1196,18 +1279,18 @@ function M.stylize_markdown(bufnr, contents, opts)
end
-- Compute size of float needed to show (wrapped) lines
- opts.wrap_at = opts.wrap_at or (vim.wo["wrap"] and api.nvim_win_get_width(0))
+ opts.wrap_at = opts.wrap_at or (vim.wo['wrap'] and api.nvim_win_get_width(0))
local width = M._make_floating_popup_size(stripped, opts)
- local sep_line = string.rep("─", math.min(width, opts.wrap_at or width))
+ local sep_line = string.rep('─', math.min(width, opts.wrap_at or width))
for l in pairs(markdown_lines) do
- if stripped[l]:match("^---+$") then
+ if stripped[l]:match('^---+$') then
stripped[l] = sep_line
end
end
- vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, stripped)
+ api.nvim_buf_set_lines(bufnr, 0, -1, false, stripped)
local idx = 1
---@private
@@ -1216,24 +1299,38 @@ function M.stylize_markdown(bufnr, contents, opts)
local langs = {}
local fences = get_markdown_fences()
local function apply_syntax_to_region(ft, start, finish)
- if ft == "" then
- vim.cmd(string.format("syntax region markdownCode start=+\\%%%dl+ end=+\\%%%dl+ keepend extend", start, finish + 1))
+ if ft == '' then
+ vim.cmd(
+ string.format(
+ 'syntax region markdownCode start=+\\%%%dl+ end=+\\%%%dl+ keepend extend',
+ start,
+ finish + 1
+ )
+ )
return
end
ft = fences[ft] or ft
- local name = ft..idx
+ local name = ft .. idx
idx = idx + 1
- local lang = "@"..ft:upper()
+ local lang = '@' .. ft:upper()
if not langs[lang] then
-- HACK: reset current_syntax, since some syntax files like markdown won't load if it is already set
- pcall(vim.api.nvim_buf_del_var, bufnr, "current_syntax")
+ pcall(api.nvim_buf_del_var, bufnr, 'current_syntax')
-- TODO(ashkan): better validation before this.
- if not pcall(vim.cmd, string.format("syntax include %s syntax/%s.vim", lang, ft)) then
+ if not pcall(vim.cmd, string.format('syntax include %s syntax/%s.vim', lang, ft)) then
return
end
langs[lang] = true
end
- vim.cmd(string.format("syntax region %s start=+\\%%%dl+ end=+\\%%%dl+ contains=%s keepend", name, start, finish + 1, lang))
+ vim.cmd(
+ string.format(
+ 'syntax region %s start=+\\%%%dl+ end=+\\%%%dl+ contains=%s keepend',
+ name,
+ start,
+ finish + 1,
+ lang
+ )
+ )
end
-- needs to run in the buffer for the regions to work
@@ -1244,13 +1341,13 @@ function M.stylize_markdown(bufnr, contents, opts)
local last = 1
for _, h in ipairs(highlights) do
if last < h.start then
- apply_syntax_to_region("lsp_markdown", last, h.start - 1)
+ apply_syntax_to_region('lsp_markdown', last, h.start - 1)
end
apply_syntax_to_region(h.ft, h.start, h.finish)
last = h.finish + 1
end
if last <= #stripped then
- apply_syntax_to_region("lsp_markdown", last, #stripped)
+ apply_syntax_to_region('lsp_markdown', last, #stripped)
end
end)
@@ -1258,6 +1355,24 @@ function M.stylize_markdown(bufnr, contents, opts)
end
---@private
+--- Closes the preview window
+---
+---@param winnr number window id of preview window
+---@param bufnrs table|nil optional list of ignored buffers
+local function close_preview_window(winnr, bufnrs)
+ vim.schedule(function()
+ -- exit if we are in one of ignored buffers
+ if bufnrs and vim.tbl_contains(bufnrs, api.nvim_get_current_buf()) then
+ return
+ end
+
+ local augroup = 'preview_window_' .. winnr
+ pcall(api.nvim_del_augroup_by_name, augroup)
+ pcall(api.nvim_win_close, winnr, true)
+ end)
+end
+
+---@private
--- Creates autocommands to close a preview window when events happen.
---
---@param events table list of events
@@ -1265,49 +1380,30 @@ end
---@param bufnrs table list of buffers where the preview window will remain visible
---@see |autocmd-events|
local function close_preview_autocmd(events, winnr, bufnrs)
- local augroup = 'preview_window_'..winnr
+ local augroup = api.nvim_create_augroup('preview_window_' .. winnr, {
+ clear = true,
+ })
-- close the preview window when entered a buffer that is not
-- the floating window buffer or the buffer that spawned it
- vim.cmd(string.format([[
- augroup %s
- autocmd!
- autocmd BufEnter * lua vim.lsp.util._close_preview_window(%d, {%s})
- augroup end
- ]], augroup, winnr, table.concat(bufnrs, ',')))
+ api.nvim_create_autocmd('BufEnter', {
+ group = augroup,
+ callback = function()
+ close_preview_window(winnr, bufnrs)
+ end,
+ })
if #events > 0 then
- vim.cmd(string.format([[
- augroup %s
- autocmd %s <buffer> lua vim.lsp.util._close_preview_window(%d)
- augroup end
- ]], augroup, table.concat(events, ','), winnr))
+ api.nvim_create_autocmd(events, {
+ group = augroup,
+ buffer = bufnrs[2],
+ callback = function()
+ close_preview_window(winnr)
+ end,
+ })
end
end
----@private
---- Closes the preview window
----
----@param winnr number window id of preview window
----@param bufnrs table|nil optional list of ignored buffers
-function M._close_preview_window(winnr, bufnrs)
- vim.schedule(function()
- -- exit if we are in one of ignored buffers
- if bufnrs and vim.tbl_contains(bufnrs, api.nvim_get_current_buf()) then
- return
- end
-
- local augroup = 'preview_window_'..winnr
- vim.cmd(string.format([[
- augroup %s
- autocmd!
- augroup end
- augroup! %s
- ]], augroup, augroup))
- pcall(vim.api.nvim_win_close, winnr, true)
- end)
-end
-
---@internal
--- Computes size of float needed to show contents (with optional wrapping)
---
@@ -1320,10 +1416,10 @@ end
--- - max_height maximal height of floating window
---@returns width,height size of float
function M._make_floating_popup_size(contents, opts)
- validate {
- contents = { contents, 't' };
- opts = { opts, 't', true };
- }
+ validate({
+ contents = { contents, 't' },
+ opts = { opts, 't', true },
+ })
opts = opts or {}
local width = opts.width
@@ -1367,11 +1463,11 @@ function M._make_floating_popup_size(contents, opts)
if vim.tbl_isempty(line_widths) then
for _, line in ipairs(contents) do
local line_width = vim.fn.strdisplaywidth(line)
- height = height + math.ceil(line_width/wrap_at)
+ height = height + math.ceil(line_width / wrap_at)
end
else
for i = 1, #contents do
- height = height + math.max(1, math.ceil(line_widths[i]/wrap_at))
+ height = height + math.max(1, math.ceil(line_widths[i] / wrap_at))
end
end
end
@@ -1391,7 +1487,7 @@ end
--- - height: (number) height of floating window
--- - width: (number) width of floating window
--- - wrap: (boolean, default true) wrap long lines
---- - wrap_at: (string) character to wrap at for computing height when wrap is enabled
+--- - wrap_at: (number) character to wrap at for computing height when wrap is enabled
--- - max_width: (number) maximal width of floating window
--- - max_height: (number) maximal height of floating window
--- - pad_top: (number) number of lines to pad contents at top
@@ -1405,16 +1501,16 @@ end
---@returns bufnr,winnr buffer and window number of the newly created floating
---preview window
function M.open_floating_preview(contents, syntax, opts)
- validate {
- contents = { contents, 't' };
- syntax = { syntax, 's', true };
- opts = { opts, 't', true };
- }
+ validate({
+ contents = { contents, 't' },
+ syntax = { syntax, 's', true },
+ opts = { opts, 't', true },
+ })
opts = opts or {}
opts.wrap = opts.wrap ~= false -- wrapping by default
- opts.stylize_markdown = opts.stylize_markdown ~= false
+ opts.stylize_markdown = opts.stylize_markdown ~= false and vim.g.syntax_on ~= nil
opts.focus = opts.focus ~= false
- opts.close_events = opts.close_events or {"CursorMoved", "CursorMovedI", "InsertCharPre"}
+ opts.close_events = opts.close_events or { 'CursorMoved', 'CursorMovedI', 'InsertCharPre' }
local bufnr = api.nvim_get_current_buf()
@@ -1423,7 +1519,7 @@ function M.open_floating_preview(contents, syntax, opts)
-- Go back to previous window if we are in a focusable one
local current_winnr = api.nvim_get_current_win()
if npcall(api.nvim_win_get_var, current_winnr, opts.focus_id) then
- api.nvim_command("wincmd p")
+ api.nvim_command('wincmd p')
return bufnr, current_winnr
end
do
@@ -1431,7 +1527,7 @@ function M.open_floating_preview(contents, syntax, opts)
if win and api.nvim_win_is_valid(win) and vim.fn.pumvisible() == 0 then
-- focus and return the existing buf, win
api.nvim_set_current_win(win)
- api.nvim_command("stopinsert")
+ api.nvim_command('stopinsert')
return api.nvim_win_get_buf(win), win
end
end
@@ -1439,14 +1535,13 @@ function M.open_floating_preview(contents, syntax, opts)
-- check if another floating preview already exists for this buffer
-- and close it if needed
- local existing_float = npcall(api.nvim_buf_get_var, bufnr, "lsp_floating_preview")
+ local existing_float = npcall(api.nvim_buf_get_var, bufnr, 'lsp_floating_preview')
if existing_float and api.nvim_win_is_valid(existing_float) then
api.nvim_win_close(existing_float, true)
end
local floating_bufnr = api.nvim_create_buf(false, true)
- local do_stylize = syntax == "markdown" and opts.stylize_markdown
-
+ local do_stylize = syntax == 'markdown' and opts.stylize_markdown
-- Clean up input: trim empty lines from the end, pad
contents = M._trim(contents, opts)
@@ -1482,26 +1577,32 @@ function M.open_floating_preview(contents, syntax, opts)
api.nvim_buf_set_option(floating_bufnr, 'modifiable', false)
api.nvim_buf_set_option(floating_bufnr, 'bufhidden', 'wipe')
- api.nvim_buf_set_keymap(floating_bufnr, "n", "q", "<cmd>bdelete<cr>", {silent = true, noremap = true, nowait = true})
- close_preview_autocmd(opts.close_events, floating_winnr, {floating_bufnr, bufnr})
+ api.nvim_buf_set_keymap(
+ floating_bufnr,
+ 'n',
+ 'q',
+ '<cmd>bdelete<cr>',
+ { silent = true, noremap = true, nowait = true }
+ )
+ close_preview_autocmd(opts.close_events, floating_winnr, { floating_bufnr, bufnr })
-- save focus_id
if opts.focus_id then
api.nvim_win_set_var(floating_winnr, opts.focus_id, bufnr)
end
- api.nvim_buf_set_var(bufnr, "lsp_floating_preview", floating_winnr)
+ api.nvim_buf_set_var(bufnr, 'lsp_floating_preview', floating_winnr)
return floating_bufnr, floating_winnr
end
do --[[ References ]]
- local reference_ns = api.nvim_create_namespace("vim_lsp_references")
+ local reference_ns = api.nvim_create_namespace('vim_lsp_references')
--- Removes document highlights from a buffer.
---
---@param bufnr number Buffer id
function M.buf_clear_references(bufnr)
- validate { bufnr = {bufnr, 'n', true} }
+ validate({ bufnr = { bufnr, 'n', true } })
api.nvim_buf_clear_namespace(bufnr or 0, reference_ns, 0, -1)
end
@@ -1509,35 +1610,50 @@ do --[[ References ]]
---
---@param bufnr number Buffer id
---@param references table List of `DocumentHighlight` objects to highlight
- ---@param offset_encoding string One of "utf-8", "utf-16", "utf-32", or nil. Defaults to `offset_encoding` of first client of `bufnr`
+ ---@param offset_encoding string One of "utf-8", "utf-16", "utf-32".
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#documentHighlight
function M.buf_highlight_references(bufnr, references, offset_encoding)
- validate { bufnr = {bufnr, 'n', true} }
- offset_encoding = offset_encoding or M._get_offset_encoding(bufnr)
+ validate({
+ bufnr = { bufnr, 'n', true },
+ offset_encoding = { offset_encoding, 'string', false },
+ })
for _, reference in ipairs(references) do
- local start_line, start_char = reference["range"]["start"]["line"], reference["range"]["start"]["character"]
- local end_line, end_char = reference["range"]["end"]["line"], reference["range"]["end"]["character"]
-
- local start_idx = get_line_byte_from_position(bufnr, { line = start_line, character = start_char }, offset_encoding)
- local end_idx = get_line_byte_from_position(bufnr, { line = start_line, character = end_char }, offset_encoding)
+ local start_line, start_char =
+ reference['range']['start']['line'], reference['range']['start']['character']
+ local end_line, end_char =
+ reference['range']['end']['line'], reference['range']['end']['character']
+
+ local start_idx = get_line_byte_from_position(
+ bufnr,
+ { line = start_line, character = start_char },
+ offset_encoding
+ )
+ local end_idx = get_line_byte_from_position(
+ bufnr,
+ { line = start_line, character = end_char },
+ offset_encoding
+ )
local document_highlight_kind = {
- [protocol.DocumentHighlightKind.Text] = "LspReferenceText";
- [protocol.DocumentHighlightKind.Read] = "LspReferenceRead";
- [protocol.DocumentHighlightKind.Write] = "LspReferenceWrite";
+ [protocol.DocumentHighlightKind.Text] = 'LspReferenceText',
+ [protocol.DocumentHighlightKind.Read] = 'LspReferenceRead',
+ [protocol.DocumentHighlightKind.Write] = 'LspReferenceWrite',
}
- local kind = reference["kind"] or protocol.DocumentHighlightKind.Text
- highlight.range(bufnr,
- reference_ns,
- document_highlight_kind[kind],
- { start_line, start_idx },
- { end_line, end_idx })
+ local kind = reference['kind'] or protocol.DocumentHighlightKind.Text
+ highlight.range(
+ bufnr,
+ reference_ns,
+ document_highlight_kind[kind],
+ { start_line, start_idx },
+ { end_line, end_idx },
+ { priority = vim.highlight.priorities.user }
+ )
end
end
end
local position_sort = sort_by_key(function(v)
- return {v.start.line, v.start.character}
+ return { v.start.line, v.start.character }
end)
--- Returns the items with the byte position calculated correctly and in sorted
@@ -1546,25 +1662,32 @@ end)
--- The result can be passed to the {list} argument of |setqflist()| or
--- |setloclist()|.
---
----@param locations (table) list of `Location`s or `LocationLink`s
+---@param locations table list of `Location`s or `LocationLink`s
+---@param offset_encoding string offset_encoding for locations utf-8|utf-16|utf-32
---@returns (table) list of items
-function M.locations_to_items(locations)
+function M.locations_to_items(locations, offset_encoding)
+ if offset_encoding == nil then
+ vim.notify_once(
+ 'locations_to_items must be called with valid offset encoding',
+ vim.log.levels.WARN
+ )
+ end
+
local items = {}
local grouped = setmetatable({}, {
__index = function(t, k)
local v = {}
rawset(t, k, v)
return v
- end;
+ end,
})
for _, d in ipairs(locations) do
-- locations may be Location or LocationLink
local uri = d.uri or d.targetUri
local range = d.range or d.targetSelectionRange
- table.insert(grouped[uri], {start = range.start})
+ table.insert(grouped[uri], { start = range.start })
end
-
local keys = vim.tbl_keys(grouped)
table.sort(keys)
-- TODO(ashkan) I wish we could do this lazily.
@@ -1587,53 +1710,24 @@ function M.locations_to_items(locations)
for _, temp in ipairs(rows) do
local pos = temp.start
local row = pos.line
- local line = lines[row] or ""
- local col = pos.character
+ local line = lines[row] or ''
+ local col = M._str_byteindex_enc(line, pos.character, offset_encoding)
table.insert(items, {
filename = filename,
lnum = row + 1,
- col = col + 1;
- text = line;
+ col = col + 1,
+ text = line,
})
end
end
return items
end
---- Fills target window's location list with given list of items.
---- Can be obtained with e.g. |vim.lsp.util.locations_to_items()|.
---- Defaults to current window.
----
----@deprecated Use |setloclist()|
----
----@param items (table) list of items
-function M.set_loclist(items, win_id)
- vim.api.nvim_echo({{'vim.lsp.util.set_loclist is deprecated. See :h deprecated', 'WarningMsg'}}, true, {})
- vim.fn.setloclist(win_id or 0, {}, ' ', {
- title = 'Language Server';
- items = items;
- })
-end
-
---- Fills quickfix list with given list of items.
---- Can be obtained with e.g. |vim.lsp.util.locations_to_items()|.
----
----@deprecated Use |setqflist()|
----
----@param items (table) list of items
-function M.set_qflist(items)
- vim.api.nvim_echo({{'vim.lsp.util.set_qflist is deprecated. See :h deprecated', 'WarningMsg'}}, true, {})
- vim.fn.setqflist({}, ' ', {
- title = 'Language Server';
- items = items;
- })
-end
-
-- According to LSP spec, if the client set "symbolKind.valueSet",
-- the client must handle it properly even if it receives a value outside the specification.
-- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol
function M._get_symbol_kind_name(symbol_kind)
- return protocol.SymbolKind[symbol_kind] or "Unknown"
+ return protocol.SymbolKind[symbol_kind] or 'Unknown'
end
--- Converts symbols to quickfix list items.
@@ -1651,17 +1745,17 @@ function M.symbols_to_items(symbols, bufnr)
lnum = range.start.line + 1,
col = range.start.character + 1,
kind = kind,
- text = '['..kind..'] '..symbol.name,
+ text = '[' .. kind .. '] ' .. symbol.name,
})
elseif symbol.selectionRange then -- DocumentSymbole type
local kind = M._get_symbol_kind_name(symbol.kind)
table.insert(_items, {
-- bufnr = _bufnr,
- filename = vim.api.nvim_buf_get_name(_bufnr),
+ filename = api.nvim_buf_get_name(_bufnr),
lnum = symbol.selectionRange.start.line + 1,
col = symbol.selectionRange.start.character + 1,
kind = kind,
- text = '['..kind..'] '..symbol.name
+ text = '[' .. kind .. '] ' .. symbol.name,
})
if symbol.children then
for _, v in ipairs(_symbols_to_items(symbol.children, _items, _bufnr)) do
@@ -1695,7 +1789,7 @@ function M.trim_empty_lines(lines)
break
end
end
- return vim.list_extend({}, lines, start, finish)
+ return list_extend({}, lines, start, finish)
end
--- Accepts markdown lines and tries to reduce them to a filetype if they
@@ -1706,12 +1800,12 @@ end
---@param lines (table) list of lines
---@returns (string) filetype or 'markdown' if it was unchanged.
function M.try_trim_markdown_code_blocks(lines)
- local language_id = lines[1]:match("^```(.*)")
+ local language_id = lines[1]:match('^```(.*)')
if language_id then
local has_inner_code_fence = false
for i = 2, (#lines - 1) do
local line = lines[i]
- if line:sub(1,3) == '```' then
+ if line:sub(1, 3) == '```' then
has_inner_code_fence = true
break
end
@@ -1731,18 +1825,18 @@ end
---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of buffer of `window`
local function make_position_param(window, offset_encoding)
window = window or 0
- local buf = vim.api.nvim_win_get_buf(window)
+ local buf = api.nvim_win_get_buf(window)
local row, col = unpack(api.nvim_win_get_cursor(window))
offset_encoding = offset_encoding or M._get_offset_encoding(buf)
row = row - 1
- local line = api.nvim_buf_get_lines(buf, row, row+1, true)[1]
+ local line = api.nvim_buf_get_lines(buf, row, row + 1, true)[1]
if not line then
- return { line = 0; character = 0; }
+ return { line = 0, character = 0 }
end
col = _str_utfindex_enc(line, col, offset_encoding)
- return { line = row; character = col; }
+ return { line = row, character = col }
end
--- Creates a `TextDocumentPositionParams` object for the current buffer and cursor position.
@@ -1753,11 +1847,11 @@ end
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentPositionParams
function M.make_position_params(window, offset_encoding)
window = window or 0
- local buf = vim.api.nvim_win_get_buf(window)
+ local buf = api.nvim_win_get_buf(window)
offset_encoding = offset_encoding or M._get_offset_encoding(buf)
return {
- textDocument = M.make_text_document_params(buf);
- position = make_position_param(window, offset_encoding)
+ textDocument = M.make_text_document_params(buf),
+ position = make_position_param(window, offset_encoding),
}
end
@@ -1765,18 +1859,30 @@ end
---@param bufnr (number) buffer handle or 0 for current, defaults to current
---@returns (string) encoding first client if there is one, nil otherwise
function M._get_offset_encoding(bufnr)
- validate {
- bufnr = {bufnr, 'n', true};
- }
+ validate({
+ bufnr = { bufnr, 'n', true },
+ })
local offset_encoding
for _, client in pairs(vim.lsp.buf_get_clients(bufnr)) do
- local this_offset_encoding = client.offset_encoding or "utf-16"
+ if client.offset_encoding == nil then
+ vim.notify_once(
+ string.format(
+ 'Client (id: %s) offset_encoding is nil. Do not unset offset_encoding.',
+ client.id
+ ),
+ vim.log.levels.ERROR
+ )
+ end
+ local this_offset_encoding = client.offset_encoding
if not offset_encoding then
offset_encoding = this_offset_encoding
elseif offset_encoding ~= this_offset_encoding then
- vim.notify("warning: multiple different client offset_encodings detected for buffer, this is not supported yet", vim.log.levels.WARN)
+ vim.notify(
+ 'warning: multiple different client offset_encodings detected for buffer, this is not supported yet',
+ vim.log.levels.WARN
+ )
end
end
@@ -1793,12 +1899,12 @@ end
---@returns { textDocument = { uri = `current_file_uri` }, range = { start =
---`current_position`, end = `current_position` } }
function M.make_range_params(window, offset_encoding)
- local buf = vim.api.nvim_win_get_buf(window or 0)
+ local buf = api.nvim_win_get_buf(window or 0)
offset_encoding = offset_encoding or M._get_offset_encoding(buf)
local position = make_position_param(window, offset_encoding)
return {
textDocument = M.make_text_document_params(buf),
- range = { start = position; ["end"] = position; }
+ range = { start = position, ['end'] = position },
}
end
@@ -1814,12 +1920,12 @@ end
---@returns { textDocument = { uri = `current_file_uri` }, range = { start =
---`start_position`, end = `end_position` } }
function M.make_given_range_params(start_pos, end_pos, bufnr, offset_encoding)
- validate {
- start_pos = {start_pos, 't', true};
- end_pos = {end_pos, 't', true};
- offset_encoding = {offset_encoding, 's', true};
- }
- bufnr = bufnr or vim.api.nvim_get_current_buf()
+ validate({
+ start_pos = { start_pos, 't', true },
+ end_pos = { end_pos, 't', true },
+ offset_encoding = { offset_encoding, 's', true },
+ })
+ bufnr = bufnr or api.nvim_get_current_buf()
offset_encoding = offset_encoding or M._get_offset_encoding(bufnr)
local A = list_extend({}, start_pos or api.nvim_buf_get_mark(bufnr, '<'))
local B = list_extend({}, end_pos or api.nvim_buf_get_mark(bufnr, '>'))
@@ -1828,10 +1934,10 @@ function M.make_given_range_params(start_pos, end_pos, bufnr, offset_encoding)
B[1] = B[1] - 1
-- account for offset_encoding.
if A[2] > 0 then
- A = {A[1], M.character_offset(bufnr, A[1], A[2], offset_encoding)}
+ A = { A[1], M.character_offset(bufnr, A[1], A[2], offset_encoding) }
end
if B[2] > 0 then
- B = {B[1], M.character_offset(bufnr, B[1], B[2], offset_encoding)}
+ B = { B[1], M.character_offset(bufnr, B[1], B[2], offset_encoding) }
end
-- we need to offset the end character position otherwise we loose the last
-- character of the selection, as LSP end position is exclusive
@@ -1842,9 +1948,9 @@ function M.make_given_range_params(start_pos, end_pos, bufnr, offset_encoding)
return {
textDocument = M.make_text_document_params(bufnr),
range = {
- start = {line = A[1], character = A[2]},
- ['end'] = {line = B[1], character = B[2]}
- }
+ start = { line = A[1], character = A[2] },
+ ['end'] = { line = B[1], character = B[2] },
+ },
}
end
@@ -1861,34 +1967,34 @@ end
---@param added
---@param removed
function M.make_workspace_params(added, removed)
- return { event = { added = added; removed = removed; } }
+ return { event = { added = added, removed = removed } }
end
---- Returns visual width of tabstop.
+--- Returns indentation size.
---
----@see |softtabstop|
----@param bufnr (optional, number): Buffer handle, defaults to current
----@returns (number) tabstop visual width
+---@see |shiftwidth|
+---@param bufnr (number|nil): Buffer handle, defaults to current
+---@returns (number) indentation size
function M.get_effective_tabstop(bufnr)
- validate { bufnr = {bufnr, 'n', true} }
+ validate({ bufnr = { bufnr, 'n', true } })
local bo = bufnr and vim.bo[bufnr] or vim.bo
- local sts = bo.softtabstop
- return (sts > 0 and sts) or (sts < 0 and bo.shiftwidth) or bo.tabstop
+ local sw = bo.shiftwidth
+ return (sw == 0 and bo.tabstop) or sw
end
--- Creates a `DocumentFormattingParams` object for the current buffer and cursor position.
---
----@param options Table with valid `FormattingOptions` entries
+---@param options table|nil with valid `FormattingOptions` entries
---@returns `DocumentFormattingParams` object
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting
function M.make_formatting_params(options)
- validate { options = {options, 't', true} }
+ validate({ options = { options, 't', true } })
options = vim.tbl_extend('keep', options or {}, {
- tabSize = M.get_effective_tabstop();
- insertSpaces = vim.bo.expandtab;
+ tabSize = M.get_effective_tabstop(),
+ insertSpaces = vim.bo.expandtab,
})
return {
- textDocument = { uri = vim.uri_from_bufnr(0) };
- options = options;
+ textDocument = { uri = vim.uri_from_bufnr(0) },
+ options = options,
}
end
@@ -1901,7 +2007,12 @@ end
---@returns (number, number) `offset_encoding` index of the character in line {row} column {col} in buffer {buf}
function M.character_offset(buf, row, col, offset_encoding)
local line = get_line(buf, row)
- offset_encoding = offset_encoding or M._get_offset_encoding(buf)
+ if offset_encoding == nil then
+ vim.notify_once(
+ 'character_offset must be called with valid offset encoding',
+ vim.log.levels.WARN
+ )
+ end
-- If the col is past the EOL, use the line length.
if col > #line then
return _str_utfindex_enc(line, nil, offset_encoding)
@@ -1917,8 +2028,8 @@ end
function M.lookup_section(settings, section)
for part in vim.gsplit(section, '.', true) do
settings = settings[part]
- if not settings then
- return
+ if settings == nil then
+ return vim.NIL
end
end
return settings