aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/lsp
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua/vim/lsp')
-rw-r--r--runtime/lua/vim/lsp/buf.lua128
-rw-r--r--runtime/lua/vim/lsp/diagnostic.lua89
-rw-r--r--runtime/lua/vim/lsp/handlers.lua111
-rw-r--r--runtime/lua/vim/lsp/log.lua5
-rw-r--r--runtime/lua/vim/lsp/protocol.lua144
-rw-r--r--runtime/lua/vim/lsp/rpc.lua18
-rw-r--r--runtime/lua/vim/lsp/sync.lua43
-rw-r--r--runtime/lua/vim/lsp/util.lua607
8 files changed, 636 insertions, 509 deletions
diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua
index 747d761730..eb7ec579f1 100644
--- a/runtime/lua/vim/lsp/buf.lua
+++ b/runtime/lua/vim/lsp/buf.lua
@@ -116,31 +116,30 @@ end
--- asks the user to select one.
--
---@returns The client that the user selected or nil
-local function select_client(method)
- local clients = vim.tbl_values(vim.lsp.buf_get_clients());
- clients = vim.tbl_filter(function (client)
+local function select_client(method, on_choice)
+ validate {
+ on_choice = { on_choice, 'function', false },
+ }
+ local clients = vim.tbl_values(vim.lsp.buf_get_clients())
+ clients = vim.tbl_filter(function(client)
return client.supports_method(method)
end, clients)
-- better UX when choices are always in the same order (between restarts)
- table.sort(clients, function (a, b) return a.name < b.name end)
+ table.sort(clients, function(a, b)
+ return a.name < b.name
+ end)
if #clients > 1 then
- local choices = {}
- for k,v in pairs(clients) do
- table.insert(choices, string.format("%d %s", k, v.name))
- end
- local user_choice = vim.fn.confirm(
- "Select a language server:",
- table.concat(choices, "\n"),
- 0,
- "Question"
- )
- if user_choice == 0 then return nil end
- return clients[user_choice]
+ vim.ui.select(clients, {
+ prompt = 'Select a language server:',
+ format_item = function(client)
+ return client.name
+ end,
+ }, on_choice)
elseif #clients < 1 then
- return nil
+ on_choice(nil)
else
- return clients[1]
+ on_choice(clients[1])
end
end
@@ -152,11 +151,15 @@ end
--
---@see https://microsoft.github.io/language-server-protocol/specification#textDocument_formatting
function M.formatting(options)
- local client = select_client("textDocument/formatting")
- if client == nil then return end
-
local params = util.make_formatting_params(options)
- return client.request("textDocument/formatting", params, nil, vim.api.nvim_get_current_buf())
+ local bufnr = vim.api.nvim_get_current_buf()
+ select_client('textDocument/formatting', function(client)
+ if client == nil then
+ return
+ end
+
+ return client.request('textDocument/formatting', params, nil, bufnr)
+ end)
end
--- Performs |vim.lsp.buf.formatting()| synchronously.
@@ -165,23 +168,27 @@ end
--- saved. {timeout_ms} is passed on to |vim.lsp.buf_request_sync()|. Example:
---
--- <pre>
---- vim.api.nvim_command[[autocmd BufWritePre <buffer> lua vim.lsp.buf.formatting_sync()]]
+--- autocmd BufWritePre <buffer> lua vim.lsp.buf.formatting_sync()
--- </pre>
---
---@param options Table with valid `FormattingOptions` entries
---@param timeout_ms (number) Request timeout
---@see |vim.lsp.buf.formatting_seq_sync|
function M.formatting_sync(options, timeout_ms)
- local client = select_client("textDocument/formatting")
- if client == nil then return end
-
local params = util.make_formatting_params(options)
- local result, err = client.request_sync("textDocument/formatting", params, timeout_ms, vim.api.nvim_get_current_buf())
- if result and result.result then
- util.apply_text_edits(result.result)
- elseif err then
- vim.notify("vim.lsp.buf.formatting_sync: " .. err, vim.log.levels.WARN)
- end
+ local bufnr = vim.api.nvim_get_current_buf()
+ select_client('textDocument/formatting', function(client)
+ if client == nil then
+ return
+ end
+
+ local result, err = client.request_sync('textDocument/formatting', params, timeout_ms, bufnr)
+ if result and result.result then
+ util.apply_text_edits(result.result, bufnr, client.offset_encoding)
+ elseif err then
+ vim.notify('vim.lsp.buf.formatting_sync: ' .. err, vim.log.levels.WARN)
+ end
+ end)
end
--- Formats the current buffer by sequentially requesting formatting from attached clients.
@@ -202,6 +209,7 @@ end
---the remaining clients in the order as they occur in the `order` list.
function M.formatting_seq_sync(options, timeout_ms, order)
local clients = vim.tbl_values(vim.lsp.buf_get_clients());
+ local bufnr = vim.api.nvim_get_current_buf()
-- sort the clients according to `order`
for _, client_name in pairs(order or {}) do
@@ -220,7 +228,7 @@ function M.formatting_seq_sync(options, timeout_ms, order)
local params = util.make_formatting_params(options)
local result, err = client.request_sync("textDocument/formatting", params, timeout_ms, vim.api.nvim_get_current_buf())
if result and result.result then
- util.apply_text_edits(result.result)
+ util.apply_text_edits(result.result, bufnr, client.offset_encoding)
elseif err then
vim.notify(string.format("vim.lsp.buf.formatting_seq_sync: (%s) %s", client.name, err), vim.log.levels.WARN)
end
@@ -236,12 +244,15 @@ end
---@param end_pos ({number, number}, optional) mark-indexed position.
---Defaults to the end of the last visual selection.
function M.range_formatting(options, start_pos, end_pos)
- local client = select_client("textDocument/rangeFormatting")
- if client == nil then return end
-
local params = util.make_given_range_params(start_pos, end_pos)
params.options = util.make_formatting_params(options).options
- return client.request("textDocument/rangeFormatting", params)
+ select_client('textDocument/rangeFormatting', function(client)
+ if client == nil then
+ return
+ end
+
+ return client.request('textDocument/rangeFormatting', params)
+ end)
end
--- Renames all references to the symbol under the cursor.
@@ -261,6 +272,7 @@ function M.rename(new_name)
request('textDocument/rename', params)
end
+ ---@private
local function prepare_rename(err, result)
if err == nil and result == nil then
vim.notify('nothing to rename', vim.log.levels.INFO)
@@ -369,7 +381,7 @@ end
function M.list_workspace_folders()
local workspace_folders = {}
for _, client in pairs(vim.lsp.buf_get_clients()) do
- for _, folder in pairs(client.workspaceFolders or {}) do
+ for _, folder in pairs(client.workspace_folders or {}) do
table.insert(workspace_folders, folder.name)
end
end
@@ -389,7 +401,7 @@ function M.add_workspace_folder(workspace_folder)
local params = util.make_workspace_params({{uri = vim.uri_from_fname(workspace_folder); name = workspace_folder}}, {{}})
for _, client in pairs(vim.lsp.buf_get_clients()) do
local found = false
- for _, folder in pairs(client.workspaceFolders or {}) do
+ for _, folder in pairs(client.workspace_folders or {}) do
if folder.name == workspace_folder then
found = true
print(workspace_folder, "is already part of this workspace")
@@ -398,10 +410,10 @@ function M.add_workspace_folder(workspace_folder)
end
if not found then
vim.lsp.buf_notify(0, 'workspace/didChangeWorkspaceFolders', params)
- if not client.workspaceFolders then
- client.workspaceFolders = {}
+ if not client.workspace_folders then
+ client.workspace_folders = {}
end
- table.insert(client.workspaceFolders, params.event.added[1])
+ table.insert(client.workspace_folders, params.event.added[1])
end
end
end
@@ -415,10 +427,10 @@ function M.remove_workspace_folder(workspace_folder)
if not (workspace_folder and #workspace_folder > 0) then return end
local params = util.make_workspace_params({{}}, {{uri = vim.uri_from_fname(workspace_folder); name = workspace_folder}})
for _, client in pairs(vim.lsp.buf_get_clients()) do
- for idx, folder in pairs(client.workspaceFolders) do
+ for idx, folder in pairs(client.workspace_folders) do
if folder.name == workspace_folder then
vim.lsp.buf_notify(0, 'workspace/didChangeWorkspaceFolders', params)
- client.workspaceFolders[idx] = nil
+ client.workspace_folders[idx] = nil
return
end
end
@@ -435,18 +447,21 @@ end
---@param query (string, optional)
function M.workspace_symbol(query)
query = query or npcall(vfn.input, "Query: ")
+ if query == nil then
+ return
+ end
local params = {query = query}
request('workspace/symbol', params)
end
--- Send request to the server to resolve document highlights for the current
--- text document position. This request can be triggered by a key mapping or
---- by events such as `CursorHold`, eg:
+--- by events such as `CursorHold`, e.g.:
---
--- <pre>
---- vim.api.nvim_command [[autocmd CursorHold <buffer> lua vim.lsp.buf.document_highlight()]]
---- vim.api.nvim_command [[autocmd CursorHoldI <buffer> lua vim.lsp.buf.document_highlight()]]
---- vim.api.nvim_command [[autocmd CursorMoved <buffer> lua vim.lsp.buf.clear_references()]]
+--- autocmd CursorHold <buffer> lua vim.lsp.buf.document_highlight()
+--- autocmd CursorHoldI <buffer> lua vim.lsp.buf.document_highlight()
+--- autocmd CursorMoved <buffer> lua vim.lsp.buf.clear_references()
--- </pre>
---
--- Note: Usage of |vim.lsp.buf.document_highlight()| requires the following highlight groups
@@ -491,7 +506,7 @@ local function on_code_action_results(results, ctx)
---@private
local function apply_action(action, client)
if action.edit then
- util.apply_workspace_edit(action.edit)
+ util.apply_workspace_edit(action.edit, client.offset_encoding)
end
if action.command then
local command = type(action.command) == 'table' and action.command or action
@@ -615,14 +630,19 @@ end
--- Executes an LSP server command.
---
----@param command A valid `ExecuteCommandParams` object
+---@param command_params table A valid `ExecuteCommandParams` object
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand
-function M.execute_command(command)
+function M.execute_command(command_params)
validate {
- command = { command.command, 's' },
- arguments = { command.arguments, 't', true }
+ command = { command_params.command, 's' },
+ arguments = { command_params.arguments, 't', true }
+ }
+ command_params = {
+ command=command_params.command,
+ arguments=command_params.arguments,
+ workDoneToken=command_params.workDoneToken,
}
- request('workspace/executeCommand', command)
+ request('workspace/executeCommand', command_params )
end
return M
diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua
index 1e6f83c1ba..614d83f565 100644
--- a/runtime/lua/vim/lsp/diagnostic.lua
+++ b/runtime/lua/vim/lsp/diagnostic.lua
@@ -104,8 +104,10 @@ local function diagnostic_lsp_to_vim(diagnostics, bufnr, client_id)
severity = severity_lsp_to_vim(diagnostic.severity),
message = diagnostic.message,
source = diagnostic.source,
+ code = diagnostic.code,
user_data = {
lsp = {
+ -- usage of user_data.lsp.code is deprecated in favor of the top-level code field
code = diagnostic.code,
codeDescription = diagnostic.codeDescription,
tags = diagnostic.tags,
@@ -120,7 +122,8 @@ end
---@private
local function diagnostic_vim_to_lsp(diagnostics)
return vim.tbl_map(function(diagnostic)
- return vim.tbl_extend("error", {
+ return vim.tbl_extend("keep", {
+ -- "keep" the below fields over any duplicate fields in diagnostic.user_data.lsp
range = {
start = {
line = diagnostic.lnum,
@@ -134,6 +137,7 @@ local function diagnostic_vim_to_lsp(diagnostics)
severity = severity_vim_to_lsp(diagnostic.severity),
message = diagnostic.message,
source = diagnostic.source,
+ code = diagnostic.code,
}, diagnostic.user_data and (diagnostic.user_data.lsp or {}) or {})
end, diagnostics)
end
@@ -153,19 +157,6 @@ function M.get_namespace(client_id)
return _client_namespaces[client_id]
end
---- Save diagnostics to the current buffer.
----
---- Handles saving diagnostics from multiple clients in the same buffer.
----@param diagnostics Diagnostic[]
----@param bufnr number
----@param client_id number
----@private
-function M.save(diagnostics, bufnr, client_id)
- local namespace = M.get_namespace(client_id)
- vim.diagnostic.set(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id))
-end
--- }}}
-
--- |lsp-handler| for the method "textDocument/publishDiagnostics"
---
--- See |vim.diagnostic.config()| for configuration options. Handler-specific
@@ -181,8 +172,8 @@ end
--- },
--- -- Use a function to dynamically turn signs off
--- -- and on, using buffer local variables
---- signs = function(bufnr, client_id)
---- return vim.bo[bufnr].show_signs == false
+--- signs = function(namespace, bufnr)
+--- return vim.b[bufnr].show_signs == true
--- end,
--- -- Disable a feature
--- update_in_insert = false,
@@ -220,12 +211,9 @@ function M.on_publish_diagnostics(_, result, ctx, config)
end
vim.diagnostic.set(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id))
-
- -- Keep old autocmd for back compat. This should eventually be removed.
- vim.api.nvim_command("doautocmd <nomodeline> User LspDiagnosticsChanged")
end
---- Clear diagnotics and diagnostic cache.
+--- Clear diagnostics and diagnostic cache.
---
--- Diagnostic producers should prefer |vim.diagnostic.reset()|. However,
--- this method signature is still used internally in some parts of the LSP
@@ -248,6 +236,23 @@ end
-- Deprecated Functions {{{
+
+--- Save diagnostics to the current buffer.
+---
+---@deprecated Prefer |vim.diagnostic.set()|
+---
+--- Handles saving diagnostics from multiple clients in the same buffer.
+---@param diagnostics Diagnostic[]
+---@param bufnr number
+---@param client_id number
+---@private
+function M.save(diagnostics, bufnr, client_id)
+ vim.notify_once('vim.lsp.diagnostic.save is deprecated. See :h deprecated', vim.log.levels.WARN)
+ local namespace = M.get_namespace(client_id)
+ vim.diagnostic.set(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id))
+end
+-- }}}
+
--- Get all diagnostics for clients
---
---@deprecated Prefer |vim.diagnostic.get()|
@@ -256,6 +261,7 @@ end
--- If nil, diagnostics of all clients are included.
---@return table with diagnostics grouped by bufnr (bufnr: Diagnostic[])
function M.get_all(client_id)
+ vim.notify_once('vim.lsp.diagnostic.get_all is deprecated. See :h deprecated', vim.log.levels.WARN)
local result = {}
local namespace
if client_id then
@@ -277,6 +283,7 @@ end
--- Else, return just the diagnostics associated with the client_id.
---@param predicate function|nil Optional function for filtering diagnostics
function M.get(bufnr, client_id, predicate)
+ vim.notify_once('vim.lsp.diagnostic.get is deprecated. See :h deprecated', vim.log.levels.WARN)
predicate = predicate or function() return true end
if client_id == nil then
local all_diagnostics = {}
@@ -338,6 +345,7 @@ end
---@param severity DiagnosticSeverity
---@param client_id number the client id
function M.get_count(bufnr, severity, client_id)
+ vim.notify_once('vim.lsp.diagnostic.get_count is deprecated. See :h deprecated', vim.log.levels.WARN)
severity = severity_lsp_to_vim(severity)
local opts = { severity = severity }
if client_id ~= nil then
@@ -354,6 +362,7 @@ end
---@param opts table See |vim.lsp.diagnostic.goto_next()|
---@return table Previous diagnostic
function M.get_prev(opts)
+ vim.notify_once('vim.lsp.diagnostic.get_prev is deprecated. See :h deprecated', vim.log.levels.WARN)
if opts then
if opts.severity then
opts.severity = severity_lsp_to_vim(opts.severity)
@@ -371,6 +380,7 @@ end
---@param opts table See |vim.lsp.diagnostic.goto_next()|
---@return table Previous diagnostic position
function M.get_prev_pos(opts)
+ vim.notify_once('vim.lsp.diagnostic.get_prev_pos is deprecated. See :h deprecated', vim.log.levels.WARN)
if opts then
if opts.severity then
opts.severity = severity_lsp_to_vim(opts.severity)
@@ -387,6 +397,7 @@ end
---
---@param opts table See |vim.lsp.diagnostic.goto_next()|
function M.goto_prev(opts)
+ vim.notify_once('vim.lsp.diagnostic.goto_prev is deprecated. See :h deprecated', vim.log.levels.WARN)
if opts then
if opts.severity then
opts.severity = severity_lsp_to_vim(opts.severity)
@@ -404,6 +415,7 @@ end
---@param opts table See |vim.lsp.diagnostic.goto_next()|
---@return table Next diagnostic
function M.get_next(opts)
+ vim.notify_once('vim.lsp.diagnostic.get_next is deprecated. See :h deprecated', vim.log.levels.WARN)
if opts then
if opts.severity then
opts.severity = severity_lsp_to_vim(opts.severity)
@@ -421,6 +433,7 @@ end
---@param opts table See |vim.lsp.diagnostic.goto_next()|
---@return table Next diagnostic position
function M.get_next_pos(opts)
+ vim.notify_once('vim.lsp.diagnostic.get_next_pos is deprecated. See :h deprecated', vim.log.levels.WARN)
if opts then
if opts.severity then
opts.severity = severity_lsp_to_vim(opts.severity)
@@ -434,25 +447,8 @@ end
--- Move to the next diagnostic
---
---@deprecated Prefer |vim.diagnostic.goto_next()|
----
----@param opts table|nil Configuration table. Keys:
---- - {client_id}: (number)
---- - If nil, will consider all clients attached to buffer.
---- - {cursor_position}: (Position, default current position)
---- - See |nvim_win_get_cursor()|
---- - {wrap}: (boolean, default true)
---- - Whether to loop around file or not. Similar to 'wrapscan'
---- - {severity}: (DiagnosticSeverity)
---- - Exclusive severity to consider. Overrides {severity_limit}
---- - {severity_limit}: (DiagnosticSeverity)
---- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
---- - {enable_popup}: (boolean, default true)
---- - Call |vim.lsp.diagnostic.show_line_diagnostics()| on jump
---- - {popup_opts}: (table)
---- - Table to pass as {opts} parameter to |vim.lsp.diagnostic.show_line_diagnostics()|
---- - {win_id}: (number, default 0)
---- - Window ID
function M.goto_next(opts)
+ vim.notify_once('vim.lsp.diagnostic.goto_next is deprecated. See :h deprecated', vim.log.levels.WARN)
if opts then
if opts.severity then
opts.severity = severity_lsp_to_vim(opts.severity)
@@ -476,6 +472,7 @@ end
--- - severity_limit (DiagnosticSeverity):
--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
function M.set_signs(diagnostics, bufnr, client_id, _, opts)
+ vim.notify_once('vim.lsp.diagnostic.set_signs is deprecated. See :h deprecated', vim.log.levels.WARN)
local namespace = M.get_namespace(client_id)
if opts and not opts.severity and opts.severity_limit then
opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
@@ -496,6 +493,7 @@ end
--- - severity_limit (DiagnosticSeverity):
--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
function M.set_underline(diagnostics, bufnr, client_id, _, opts)
+ vim.notify_once('vim.lsp.diagnostic.set_underline is deprecated. See :h deprecated', vim.log.levels.WARN)
local namespace = M.get_namespace(client_id)
if opts and not opts.severity and opts.severity_limit then
opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
@@ -517,6 +515,7 @@ end
--- - severity_limit (DiagnosticSeverity):
--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
function M.set_virtual_text(diagnostics, bufnr, client_id, _, opts)
+ vim.notify_once('vim.lsp.diagnostic.set_virtual_text is deprecated. See :h deprecated', vim.log.levels.WARN)
local namespace = M.get_namespace(client_id)
if opts and not opts.severity and opts.severity_limit then
opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
@@ -535,6 +534,7 @@ end
---@return an array of [text, hl_group] arrays. This can be passed directly to
--- the {virt_text} option of |nvim_buf_set_extmark()|.
function M.get_virtual_text_chunks_for_line(bufnr, _, line_diags, opts)
+ vim.notify_once('vim.lsp.diagnostic.get_virtual_text_chunks_for_line is deprecated. See :h deprecated', vim.log.levels.WARN)
return vim.diagnostic._get_virt_text_chunks(diagnostic_lsp_to_vim(line_diags, bufnr), opts)
end
@@ -552,6 +552,7 @@ end
---@param position table|nil The (0,0)-indexed position
---@return table {popup_bufnr, win_id}
function M.show_position_diagnostics(opts, buf_nr, position)
+ vim.notify_once('vim.lsp.diagnostic.show_position_diagnostics is deprecated. See :h deprecated', vim.log.levels.WARN)
opts = opts or {}
opts.scope = "cursor"
opts.pos = position
@@ -565,7 +566,7 @@ end
--- Open a floating window with the diagnostics from {line_nr}
---
----@deprecated Prefer |vim.diagnostic.show_line_diagnostics()|
+---@deprecated Prefer |vim.diagnostic.open_float()|
---
---@param opts table Configuration table
--- - all opts for |vim.lsp.diagnostic.get_line_diagnostics()| and
@@ -575,6 +576,7 @@ end
---@param client_id number|nil the client id
---@return table {popup_bufnr, win_id}
function M.show_line_diagnostics(opts, buf_nr, line_nr, client_id)
+ vim.notify_once('vim.lsp.diagnostic.show_line_diagnostics is deprecated. See :h deprecated', vim.log.levels.WARN)
opts = opts or {}
opts.scope = "line"
opts.pos = line_nr
@@ -586,7 +588,7 @@ end
--- Redraw diagnostics for the given buffer and client
---
----@deprecated Prefer |vim.diagnostic.redraw()|
+---@deprecated Prefer |vim.diagnostic.show()|
---
--- This calls the "textDocument/publishDiagnostics" handler manually using
--- the cached diagnostics already received from the server. This can be useful
@@ -598,6 +600,7 @@ end
--- client. The default is to redraw diagnostics for all attached
--- clients.
function M.redraw(bufnr, client_id)
+ vim.notify_once('vim.lsp.diagnostic.redraw is deprecated. See :h deprecated', vim.log.levels.WARN)
bufnr = get_bufnr(bufnr)
if not client_id then
return vim.lsp.for_each_buffer_client(bufnr, function(client)
@@ -625,6 +628,7 @@ end
--- - {workspace}: (boolean, default true)
--- - Set the list with workspace diagnostics
function M.set_qflist(opts)
+ vim.notify_once('vim.lsp.diagnostic.set_qflist is deprecated. See :h deprecated', vim.log.levels.WARN)
opts = opts or {}
if opts.severity then
opts.severity = severity_lsp_to_vim(opts.severity)
@@ -656,6 +660,7 @@ end
--- - {workspace}: (boolean, default false)
--- - Set the list with workspace diagnostics
function M.set_loclist(opts)
+ vim.notify_once('vim.lsp.diagnostic.set_loclist is deprecated. See :h deprecated', vim.log.levels.WARN)
opts = opts or {}
if opts.severity then
opts.severity = severity_lsp_to_vim(opts.severity)
@@ -683,6 +688,7 @@ end
-- send diagnostic information and the client will still process it. The
-- diagnostics are simply not displayed to the user.
function M.disable(bufnr, client_id)
+ vim.notify_once('vim.lsp.diagnostic.disable is deprecated. See :h deprecated', vim.log.levels.WARN)
if not client_id then
return vim.lsp.for_each_buffer_client(bufnr, function(client)
M.disable(bufnr, client.id)
@@ -703,6 +709,7 @@ end
--- client. The default is to enable diagnostics for all attached
--- clients.
function M.enable(bufnr, client_id)
+ vim.notify_once('vim.lsp.diagnostic.enable is deprecated. See :h deprecated', vim.log.levels.WARN)
if not client_id then
return vim.lsp.for_each_buffer_client(bufnr, function(client)
M.enable(bufnr, client.id)
@@ -717,5 +724,3 @@ end
-- }}}
return M
-
--- vim: fdm=marker
diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua
index fa4f2b22a5..f5aefd4402 100644
--- a/runtime/lua/vim/lsp/handlers.lua
+++ b/runtime/lua/vim/lsp/handlers.lua
@@ -28,7 +28,7 @@ local function progress_handler(_, result, ctx, _)
local client_name = client and client.name or string.format("id=%d", client_id)
if not client then
err_message("LSP[", client_name, "] client has shut down after sending the message")
- return
+ return vim.NIL
end
local val = result.value -- unspecified yet
local token = result.token -- string or number
@@ -70,6 +70,7 @@ M['window/workDoneProgress/create'] = function(_, result, ctx)
local client_name = client and client.name or string.format("id=%d", client_id)
if not client then
err_message("LSP[", client_name, "] client has shut down after sending the message")
+ return vim.NIL
end
client.messages.progress[token] = {}
return vim.NIL
@@ -110,13 +111,15 @@ M['client/registerCapability'] = function(_, _, ctx)
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit
-M['workspace/applyEdit'] = function(_, workspace_edit)
+M['workspace/applyEdit'] = function(_, workspace_edit, ctx)
if not workspace_edit then return end
-- TODO(ashkan) Do something more with label?
+ local client_id = ctx.client_id
+ local client = vim.lsp.get_client_by_id(client_id)
if workspace_edit.label then
print("Workspace edit", workspace_edit.label)
end
- local status, result = pcall(util.apply_workspace_edit, workspace_edit.edit)
+ local status, result = pcall(util.apply_workspace_edit, workspace_edit.edit, client.offset_encoding)
return {
applied = status;
failureReason = result;
@@ -149,6 +152,17 @@ M['workspace/configuration'] = function(_, result, ctx)
return response
end
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_workspaceFolders
+M['workspace/workspaceFolders'] = function(_, _, ctx)
+ local client_id = ctx.client_id
+ local client = vim.lsp.get_client_by_id(client_id)
+ if not client then
+ err_message("LSP[id=", client_id, "] client has shut down after sending the message")
+ return
+ end
+ return client.workspace_folders or vim.NIL
+end
+
M['textDocument/publishDiagnostics'] = function(...)
return require('vim.lsp.diagnostic').on_publish_diagnostics(...)
end
@@ -158,6 +172,31 @@ M['textDocument/codeLens'] = function(...)
end
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references
+M['textDocument/references'] = function(_, result, ctx, config)
+ if not result or vim.tbl_isempty(result) then
+ vim.notify('No references found')
+ else
+ local client = vim.lsp.get_client_by_id(ctx.client_id)
+ config = config or {}
+ if config.loclist then
+ vim.fn.setloclist(0, {}, ' ', {
+ title = 'References';
+ items = util.locations_to_items(result, client.offset_encoding);
+ context = ctx;
+ })
+ api.nvim_command("lopen")
+ else
+ vim.fn.setqflist({}, ' ', {
+ title = 'References';
+ items = util.locations_to_items(result, client.offset_encoding);
+ context = ctx;
+ })
+ api.nvim_command("botright copen")
+ end
+ end
+end
+
---@private
--- Return a function that converts LSP responses to list items and opens the list
@@ -168,23 +207,26 @@ end
--- loclist: (boolean) use the location list (default is to use the quickfix list)
---
---@param map_result function `((resp, bufnr) -> list)` to convert the response
----@param entity name of the resource used in a `not found` error message
-local function response_to_list(map_result, entity)
- return function(_,result, ctx, config)
+---@param entity string name of the resource used in a `not found` error message
+---@param title_fn function Function to call to generate list title
+local function response_to_list(map_result, entity, title_fn)
+ return function(_, result, ctx, config)
if not result or vim.tbl_isempty(result) then
vim.notify('No ' .. entity .. ' found')
else
config = config or {}
if config.loclist then
vim.fn.setloclist(0, {}, ' ', {
- title = 'Language Server';
+ title = title_fn(ctx);
items = map_result(result, ctx.bufnr);
+ context = ctx;
})
api.nvim_command("lopen")
else
vim.fn.setqflist({}, ' ', {
- title = 'Language Server';
+ title = title_fn(ctx);
items = map_result(result, ctx.bufnr);
+ context = ctx;
})
api.nvim_command("botright copen")
end
@@ -193,31 +235,36 @@ local function response_to_list(map_result, entity)
end
---see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references
-M['textDocument/references'] = response_to_list(util.locations_to_items, 'references')
-
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol
-M['textDocument/documentSymbol'] = response_to_list(util.symbols_to_items, 'document symbols')
+M['textDocument/documentSymbol'] = response_to_list(util.symbols_to_items, 'document symbols', function(ctx)
+ local fname = vim.fn.fnamemodify(vim.uri_to_fname(ctx.params.textDocument.uri), ":.")
+ return string.format('Symbols in %s', fname)
+end)
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_symbol
-M['workspace/symbol'] = response_to_list(util.symbols_to_items, 'symbols')
+M['workspace/symbol'] = response_to_list(util.symbols_to_items, 'symbols', function(ctx)
+ return string.format("Symbols matching '%s'", ctx.params.query)
+end)
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rename
-M['textDocument/rename'] = function(_, result, _)
+M['textDocument/rename'] = function(_, result, ctx, _)
if not result then return end
- util.apply_workspace_edit(result)
+ local client = vim.lsp.get_client_by_id(ctx.client_id)
+ util.apply_workspace_edit(result, client.offset_encoding)
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rangeFormatting
M['textDocument/rangeFormatting'] = function(_, result, ctx, _)
if not result then return end
- util.apply_text_edits(result, ctx.bufnr)
+ local client = vim.lsp.get_client_by_id(ctx.client_id)
+ util.apply_text_edits(result, ctx.bufnr, client.offset_encoding)
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting
M['textDocument/formatting'] = function(_, result, ctx, _)
if not result then return end
- util.apply_text_edits(result, ctx.bufnr)
+ local client = vim.lsp.get_client_by_id(ctx.client_id)
+ util.apply_text_edits(result, ctx.bufnr, client.offset_encoding)
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
@@ -245,7 +292,7 @@ end
---@param config table Configuration table.
--- - border: (default=nil)
--- - Add borders to the floating window
---- - See |vim.api.nvim_open_win()|
+--- - See |nvim_open_win()|
function M.hover(_, result, ctx, config)
config = config or {}
config.focus_id = ctx.method
@@ -276,19 +323,23 @@ local function location_handler(_, result, ctx, _)
local _ = log.info() and log.info(ctx.method, 'No location found')
return nil
end
+ local client = vim.lsp.get_client_by_id(ctx.client_id)
-- textDocument/definition can return Location or Location[]
-- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition
if vim.tbl_islist(result) then
- util.jump_to_location(result[1])
+ util.jump_to_location(result[1], client.offset_encoding)
if #result > 1 then
- util.set_qflist(util.locations_to_items(result))
- api.nvim_command("copen")
+ vim.fn.setqflist({}, ' ', {
+ title = 'LSP locations',
+ items = util.locations_to_items(result, client.offset_encoding)
+ })
+ api.nvim_command("botright copen")
end
else
- util.jump_to_location(result)
+ util.jump_to_location(result, client.offset_encoding)
end
end
@@ -378,8 +429,8 @@ local make_call_hierarchy_handler = function(direction)
})
end
end
- util.set_qflist(items)
- api.nvim_command("copen")
+ vim.fn.setqflist({}, ' ', {title = 'LSP call hierarchy', items = items})
+ api.nvim_command("botright copen")
end
end
@@ -438,14 +489,20 @@ for k, fn in pairs(M) do
})
if err then
- local client = vim.lsp.get_client_by_id(ctx.client_id)
- local client_name = client and client.name or string.format("client_id=%d", ctx.client_id)
-- LSP spec:
-- interface ResponseError:
-- code: integer;
-- message: string;
-- data?: string | number | boolean | array | object | null;
- return err_message(client_name .. ': ' .. tostring(err.code) .. ': ' .. err.message)
+
+ -- Per LSP, don't show ContentModified error to the user.
+ if err.code ~= protocol.ErrorCodes.ContentModified then
+ local client = vim.lsp.get_client_by_id(ctx.client_id)
+ local client_name = client and client.name or string.format("client_id=%d", ctx.client_id)
+
+ err_message(client_name .. ': ' .. tostring(err.code) .. ': ' .. err.message)
+ end
+ return
end
return fn(err, result, ctx, config)
diff --git a/runtime/lua/vim/lsp/log.lua b/runtime/lua/vim/lsp/log.lua
index 4597f1919a..e0b5653587 100644
--- a/runtime/lua/vim/lsp/log.lua
+++ b/runtime/lua/vim/lsp/log.lua
@@ -8,8 +8,8 @@ local log = {}
-- Log level dictionary with reverse lookup as well.
--
-- Can be used to lookup the number from the name or the name from the number.
--- Levels by name: 'trace', 'debug', 'info', 'warn', 'error'
--- Level numbers begin with 'trace' at 0
+-- Levels by name: "TRACE", "DEBUG", "INFO", "WARN", "ERROR"
+-- Level numbers begin with "TRACE" at 0
log.levels = vim.deepcopy(vim.log.levels)
-- Default log level is warn.
@@ -101,6 +101,7 @@ function log.set_level(level)
end
--- Gets the current log level.
+---@return string current log level
function log.get_level()
return current_log_level
end
diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua
index b3aa8b934f..86c9e2fd58 100644
--- a/runtime/lua/vim/lsp/protocol.lua
+++ b/runtime/lua/vim/lsp/protocol.lua
@@ -776,149 +776,9 @@ function protocol.make_client_capabilities()
}
end
---[=[
-export interface DocumentFilter {
- --A language id, like `typescript`.
- language?: string;
- --A Uri [scheme](#Uri.scheme), like `file` or `untitled`.
- scheme?: string;
- --A glob pattern, like `*.{ts,js}`.
- --
- --Glob patterns can have the following syntax:
- --- `*` to match one or more characters in a path segment
- --- `?` to match on one character in a path segment
- --- `**` to match any number of path segments, including none
- --- `{}` to group conditions (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files)
- --- `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
- --- `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`)
- pattern?: string;
-}
---]=]
-
---[[
---Static registration options to be returned in the initialize request.
-interface StaticRegistrationOptions {
- --The id used to register the request. The id can be used to deregister
- --the request again. See also Registration#id.
- id?: string;
-}
-
-export interface DocumentFilter {
- --A language id, like `typescript`.
- language?: string;
- --A Uri [scheme](#Uri.scheme), like `file` or `untitled`.
- scheme?: string;
- --A glob pattern, like `*.{ts,js}`.
- --
- --Glob patterns can have the following syntax:
- --- `*` to match one or more characters in a path segment
- --- `?` to match on one character in a path segment
- --- `**` to match any number of path segments, including none
- --- `{}` to group conditions (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files)
- --- `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
- --- `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`)
- pattern?: string;
-}
-export type DocumentSelector = DocumentFilter[];
-export interface TextDocumentRegistrationOptions {
- --A document selector to identify the scope of the registration. If set to null
- --the document selector provided on the client side will be used.
- documentSelector: DocumentSelector | null;
-}
-
---Code Action options.
-export interface CodeActionOptions {
- --CodeActionKinds that this server may return.
- --
- --The list of kinds may be generic, such as `CodeActionKind.Refactor`, or the server
- --may list out every specific kind they provide.
- codeActionKinds?: CodeActionKind[];
-}
-
-interface ServerCapabilities {
- --Defines how text documents are synced. Is either a detailed structure defining each notification or
- --for backwards compatibility the TextDocumentSyncKind number. If omitted it defaults to `TextDocumentSyncKind.None`.
- textDocumentSync?: TextDocumentSyncOptions | number;
- --The server provides hover support.
- hoverProvider?: boolean;
- --The server provides completion support.
- completionProvider?: CompletionOptions;
- --The server provides signature help support.
- signatureHelpProvider?: SignatureHelpOptions;
- --The server provides goto definition support.
- definitionProvider?: boolean;
- --The server provides Goto Type Definition support.
- --
- --Since 3.6.0
- typeDefinitionProvider?: boolean | (TextDocumentRegistrationOptions & StaticRegistrationOptions);
- --The server provides Goto Implementation support.
- --
- --Since 3.6.0
- implementationProvider?: boolean | (TextDocumentRegistrationOptions & StaticRegistrationOptions);
- --The server provides find references support.
- referencesProvider?: boolean;
- --The server provides document highlight support.
- documentHighlightProvider?: boolean;
- --The server provides document symbol support.
- documentSymbolProvider?: boolean;
- --The server provides workspace symbol support.
- workspaceSymbolProvider?: boolean;
- --The server provides code actions. The `CodeActionOptions` return type is only
- --valid if the client signals code action literal support via the property
- --`textDocument.codeAction.codeActionLiteralSupport`.
- codeActionProvider?: boolean | CodeActionOptions;
- --The server provides code lens.
- codeLensProvider?: CodeLensOptions;
- --The server provides document formatting.
- documentFormattingProvider?: boolean;
- --The server provides document range formatting.
- documentRangeFormattingProvider?: boolean;
- --The server provides document formatting on typing.
- documentOnTypeFormattingProvider?: DocumentOnTypeFormattingOptions;
- --The server provides rename support. RenameOptions may only be
- --specified if the client states that it supports
- --`prepareSupport` in its initial `initialize` request.
- renameProvider?: boolean | RenameOptions;
- --The server provides document link support.
- documentLinkProvider?: DocumentLinkOptions;
- --The server provides color provider support.
- --
- --Since 3.6.0
- colorProvider?: boolean | ColorProviderOptions | (ColorProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions);
- --The server provides folding provider support.
- --
- --Since 3.10.0
- foldingRangeProvider?: boolean | FoldingRangeProviderOptions | (FoldingRangeProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions);
- --The server provides go to declaration support.
- --
- --Since 3.14.0
- declarationProvider?: boolean | (TextDocumentRegistrationOptions & StaticRegistrationOptions);
- --The server provides execute command support.
- executeCommandProvider?: ExecuteCommandOptions;
- --Workspace specific server capabilities
- workspace?: {
- --The server supports workspace folder.
- --
- --Since 3.6.0
- workspaceFolders?: {
- * The server has support for workspace folders
- supported?: boolean;
- * Whether the server wants to receive workspace folder
- * change notifications.
- *
- * If a strings is provided the string is treated as a ID
- * under which the notification is registered on the client
- * side. The ID can be used to unregister for these events
- * using the `client/unregisterCapability` request.
- changeNotifications?: string | boolean;
- }
- }
- --Experimental server capabilities.
- experimental?: any;
-}
---]]
-
--- Creates a normalized object describing LSP server capabilities.
+---@param server_capabilities table Table of capabilities supported by the server
+---@return table Normalized table of capabilities
function protocol.resolve_capabilities(server_capabilities)
local general_properties = {}
local text_document_sync_properties
diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua
index bce1e9f35d..1ecac50df4 100644
--- a/runtime/lua/vim/lsp/rpc.lua
+++ b/runtime/lua/vim/lsp/rpc.lua
@@ -133,7 +133,8 @@ local function request_parser_loop()
end
end
-local client_errors = vim.tbl_add_reverse_lookup {
+--- Mapping of error codes used by the client
+local client_errors = {
INVALID_SERVER_MESSAGE = 1;
INVALID_SERVER_JSON = 2;
NO_RESULT_CALLBACK_FOUND = 3;
@@ -143,6 +144,8 @@ local client_errors = vim.tbl_add_reverse_lookup {
SERVER_RESULT_CALLBACK_ERROR = 7;
}
+client_errors = vim.tbl_add_reverse_lookup(client_errors)
+
--- Constructs an error message from an LSP error object.
---
---@param err (table) The error object
@@ -230,7 +233,7 @@ function default_dispatchers.on_error(code, err)
end
--- Starts an LSP server process and create an LSP RPC client object to
---- interact with it.
+--- interact with it. Communication with the server is currently limited to stdio.
---
---@param cmd (string) Command to start the LSP server.
---@param cmd_args (table) List of additional string arguments to pass to {cmd}.
@@ -264,8 +267,6 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
if extra_spawn_params and extra_spawn_params.cwd then
assert(is_dir(extra_spawn_params.cwd), "cwd must be a directory")
- elseif not (vim.fn.executable(cmd) == 1) then
- error(string.format("The given command %q is not executable.", cmd))
end
if dispatchers then
local user_dispatchers = dispatchers
@@ -325,7 +326,14 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
end
handle, pid = uv.spawn(cmd, spawn_params, onexit)
if handle == nil then
- error(string.format("start `%s` failed: %s", cmd, pid))
+ local msg = string.format("Spawning language server with cmd: `%s` failed", cmd)
+ if string.match(pid, "ENOENT") then
+ msg = msg .. ". The language server is either not installed, missing from PATH, or not executable."
+ else
+ msg = msg .. string.format(" with error message: %s", pid)
+ end
+ vim.notify(msg, vim.log.levels.WARN)
+ return
end
end
diff --git a/runtime/lua/vim/lsp/sync.lua b/runtime/lua/vim/lsp/sync.lua
index f185e3973f..9955fff3e2 100644
--- a/runtime/lua/vim/lsp/sync.lua
+++ b/runtime/lua/vim/lsp/sync.lua
@@ -74,6 +74,7 @@ local function byte_to_utf(line, byte, offset_encoding)
return utf_idx + 1
end
+---@private
local function compute_line_length(line, offset_encoding)
local length
local _
@@ -104,15 +105,16 @@ local function align_end_position(line, byte, offset_encoding)
char = compute_line_length(line, offset_encoding) + 1
else
-- Modifying line, find the nearest utf codepoint
- local offset = str_utf_end(line, byte)
+ local offset = str_utf_start(line, byte)
-- If the byte does not fall on the start of the character, then
-- align to the start of the next character.
- if offset > 0 then
- char = byte_to_utf(line, byte, offset_encoding) + 1
- byte = byte + offset
- else
+ if offset < 0 then
+ byte = byte + str_utf_end(line, byte) + 1
+ end
+ if byte <= #line then
char = byte_to_utf(line, byte, offset_encoding)
- byte = byte + offset
+ else
+ char = compute_line_length(line, offset_encoding) + 1
end
-- Extending line, find the nearest utf codepoint for the last valid character
end
@@ -129,13 +131,30 @@ end
---@param offset_encoding string utf-8|utf-16|utf-32|nil (fallback to utf-8)
---@returns table<int, int> line_idx, byte_idx, and char_idx of first change position
local function compute_start_range(prev_lines, curr_lines, firstline, lastline, new_lastline, offset_encoding)
+ local char_idx
+ local byte_idx
-- If firstline == lastline, no existing text is changed. All edit operations
-- occur on a new line pointed to by lastline. This occurs during insertion of
-- new lines(O), the new newline is inserted at the line indicated by
-- new_lastline.
- -- If firstline == new_lastline, the first change occured on a line that was deleted.
+ if firstline == lastline then
+ local line_idx
+ local line = prev_lines[firstline - 1]
+ if line then
+ line_idx = firstline - 1
+ byte_idx = #line + 1
+ char_idx = compute_line_length(line, offset_encoding) + 1
+ else
+ line_idx = firstline
+ byte_idx = 1
+ char_idx = 1
+ end
+ return { line_idx = line_idx, byte_idx = byte_idx, char_idx = char_idx }
+ end
+
+ -- If firstline == new_lastline, the first change occurred on a line that was deleted.
-- In this case, the first byte change is also at the first byte of firstline
- if firstline == new_lastline or firstline == lastline then
+ if firstline == new_lastline then
return { line_idx = firstline, byte_idx = 1, char_idx = 1 }
end
@@ -156,8 +175,6 @@ local function compute_start_range(prev_lines, curr_lines, firstline, lastline,
end
-- Convert byte to codepoint if applicable
- local char_idx
- local byte_idx
if start_byte_idx == 1 or (#prev_line == 0 and start_byte_idx == 1)then
byte_idx = start_byte_idx
char_idx = 1
@@ -166,7 +183,7 @@ local function compute_start_range(prev_lines, curr_lines, firstline, lastline,
char_idx = compute_line_length(prev_line, offset_encoding) + 1
else
byte_idx = start_byte_idx + str_utf_start(prev_line, start_byte_idx)
- char_idx = byte_to_utf(prev_line, start_byte_idx, offset_encoding)
+ char_idx = byte_to_utf(prev_line, byte_idx, offset_encoding)
end
-- Return the start difference (shared for new and prev lines)
@@ -187,7 +204,7 @@ end
---@param offset_encoding string
---@returns (int, int) end_line_idx and end_col_idx of range
local function compute_end_range(prev_lines, curr_lines, start_range, firstline, lastline, new_lastline, offset_encoding)
- -- If firstline == new_lastline, the first change occured on a line that was deleted.
+ -- If firstline == new_lastline, the first change occurred on a line that was deleted.
-- In this case, the last_byte...
if firstline == new_lastline then
return { line_idx = (lastline - new_lastline + firstline), byte_idx = 1, char_idx = 1 }, { line_idx = firstline, byte_idx = 1, char_idx = 1 }
@@ -296,7 +313,7 @@ end
---@private
-- rangelength depends on the offset encoding
--- bytes for utf-8 (clangd with extenion)
+-- bytes for utf-8 (clangd with extension)
-- codepoints for utf-16
-- codeunits for utf-32
-- Line endings count here as 2 chars for \r\n (dos), 1 char for \n (unix), and 1 char for \r (mac)
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index ba5c20ef9f..401dac9acd 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -10,14 +10,6 @@ local uv = vim.loop
local npcall = vim.F.npcall
local split = vim.split
-local _warned = {}
-local warn_once = function(message)
- if not _warned[message] then
- vim.api.nvim_err_writeln(message)
- _warned[message] = true
- end
-end
-
local M = {}
local default_border = {
@@ -90,6 +82,49 @@ local function split_lines(value)
return split(value, '\n', true)
end
+--- Convert byte index to `encoding` index.
+--- Convenience wrapper around vim.str_utfindex
+---@param line string line to be indexed
+---@param index number byte index (utf-8), or `nil` for length
+---@param encoding string utf-8|utf-16|utf-32|nil defaults to utf-16
+---@return number `encoding` index of `index` in `line`
+function M._str_utfindex_enc(line, index, encoding)
+ if not encoding then encoding = 'utf-16' end
+ if encoding == 'utf-8' then
+ if index then return index else return #line end
+ elseif encoding == 'utf-16' then
+ local _, col16 = vim.str_utfindex(line, index)
+ return col16
+ elseif encoding == 'utf-32' then
+ local col32, _ = vim.str_utfindex(line, index)
+ return col32
+ else
+ error("Invalid encoding: " .. vim.inspect(encoding))
+ end
+end
+
+--- Convert UTF index to `encoding` index.
+--- Convenience wrapper around vim.str_byteindex
+---Alternative to vim.str_byteindex that takes an encoding.
+---@param line string line to be indexed
+---@param index number UTF index
+---@param encoding string utf-8|utf-16|utf-32|nil defaults to utf-16
+---@return number byte (utf-8) index of `encoding` index `index` in `line`
+function M._str_byteindex_enc(line, index, encoding)
+ if not encoding then encoding = 'utf-16' end
+ if encoding == 'utf-8' then
+ if index then return index else return #line end
+ elseif encoding == 'utf-16' then
+ return vim.str_byteindex(line, index, true)
+ elseif encoding == 'utf-32' then
+ return vim.str_byteindex(line, index)
+ else
+ error("Invalid encoding: " .. vim.inspect(encoding))
+ end
+end
+
+local _str_utfindex_enc = M._str_utfindex_enc
+local _str_byteindex_enc = M._str_byteindex_enc
--- Replaces text in a range with new text.
---
--- CAUTION: Changes in-place!
@@ -148,8 +183,101 @@ local function sort_by_key(fn)
end
---@private
+--- Gets the zero-indexed lines from the given buffer.
+--- Works on unloaded buffers by reading the file using libuv to bypass buf reading events.
+--- Falls back to loading the buffer and nvim_buf_get_lines for buffers with non-file URI.
+---
+---@param bufnr number bufnr to get the lines from
+---@param rows number[] zero-indexed line numbers
+---@return table<number string> a table mapping rows to lines
+local function get_lines(bufnr, rows)
+ rows = type(rows) == "table" and rows or { rows }
+
+ -- This is needed for bufload and bufloaded
+ if bufnr == 0 then
+ bufnr = vim.api.nvim_get_current_buf()
+ end
+
+ ---@private
+ local function buf_lines()
+ local lines = {}
+ for _, row in pairs(rows) do
+ lines[row] = (vim.api.nvim_buf_get_lines(bufnr, row, row + 1, false) or { "" })[1]
+ end
+ return lines
+ end
+
+ local uri = vim.uri_from_bufnr(bufnr)
+
+ -- load the buffer if this is not a file uri
+ -- Custom language server protocol extensions can result in servers sending URIs with custom schemes. Plugins are able to load these via `BufReadCmd` autocmds.
+ if uri:sub(1, 4) ~= "file" then
+ vim.fn.bufload(bufnr)
+ return buf_lines()
+ end
+
+ -- use loaded buffers if available
+ if vim.fn.bufloaded(bufnr) == 1 then
+ return buf_lines()
+ end
+
+ local filename = api.nvim_buf_get_name(bufnr)
+
+ -- get the data from the file
+ local fd = uv.fs_open(filename, "r", 438)
+ if not fd then return "" end
+ local stat = uv.fs_fstat(fd)
+ local data = uv.fs_read(fd, stat.size, 0)
+ uv.fs_close(fd)
+
+ local lines = {} -- rows we need to retrieve
+ local need = 0 -- keep track of how many unique rows we need
+ for _, row in pairs(rows) do
+ if not lines[row] then
+ need = need + 1
+ end
+ lines[row] = true
+ end
+
+ local found = 0
+ local lnum = 0
+
+ for line in string.gmatch(data, "([^\n]*)\n?") do
+ if lines[lnum] == true then
+ lines[lnum] = line
+ found = found + 1
+ if found == need then break end
+ end
+ lnum = lnum + 1
+ end
+
+ -- change any lines we didn't find to the empty string
+ for i, line in pairs(lines) do
+ if line == true then
+ lines[i] = ""
+ end
+ end
+ return lines
+end
+
+
+---@private
+--- Gets the zero-indexed line from the given buffer.
+--- Works on unloaded buffers by reading the file using libuv to bypass buf reading events.
+--- Falls back to loading the buffer and nvim_buf_get_lines for buffers with non-file URI.
+---
+---@param bufnr number
+---@param row number zero-indexed line number
+---@return string the line at row in filename
+local function get_line(bufnr, row)
+ return get_lines(bufnr, { row })[row]
+end
+
+
+---@private
--- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position
--- Returns a zero-indexed column, since set_lines() does the conversion to
+---@param offset_encoding string utf-8|utf-16|utf-32
--- 1-indexed
local function get_line_byte_from_position(bufnr, position, offset_encoding)
-- LSP's line and characters are 0-indexed
@@ -158,31 +286,19 @@ local function get_line_byte_from_position(bufnr, position, offset_encoding)
-- When on the first character, we can ignore the difference between byte and
-- character
if col > 0 then
- if not api.nvim_buf_is_loaded(bufnr) then
- vim.fn.bufload(bufnr)
- end
-
- local line = position.line
- local lines = api.nvim_buf_get_lines(bufnr, line, line + 1, false)
- if #lines > 0 then
- local ok, result
-
- if offset_encoding == "utf-16" or not offset_encoding then
- ok, result = pcall(vim.str_byteindex, lines[1], col, true)
- elseif offset_encoding == "utf-32" then
- ok, result = pcall(vim.str_byteindex, lines[1], col, false)
- end
-
- if ok then
- return result
- end
- return math.min(#lines[1], col)
+ local line = get_line(bufnr, position.line) or ''
+ local ok, result
+ ok, result = pcall(_str_byteindex_enc, line, col, offset_encoding)
+ if ok then
+ return result
end
+ return math.min(#line, col)
end
return col
end
--- Process and return progress reports from lsp server
+---@private
function M.get_progress_messages()
local new_messages = {}
@@ -244,8 +360,14 @@ end
--- Applies a list of text edits to a buffer.
---@param text_edits table list of `TextEdit` objects
---@param bufnr number Buffer id
+---@param offset_encoding string utf-8|utf-16|utf-32
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textEdit
-function M.apply_text_edits(text_edits, bufnr)
+function M.apply_text_edits(text_edits, bufnr, offset_encoding)
+ validate {
+ text_edits = { text_edits, 't', false };
+ bufnr = { bufnr, 'number', false };
+ offset_encoding = { offset_encoding, 'string', false };
+ }
if not next(text_edits) then return end
if not api.nvim_buf_is_loaded(bufnr) then
vim.fn.bufload(bufnr)
@@ -279,26 +401,6 @@ function M.apply_text_edits(text_edits, bufnr)
end
end)
- -- Some LSP servers may return +1 range of the buffer content but nvim_buf_set_text can't accept it so we should fix it here.
- local has_eol_text_edit = false
- local max = vim.api.nvim_buf_line_count(bufnr)
- -- TODO handle offset_encoding
- local _, len = vim.str_utfindex(vim.api.nvim_buf_get_lines(bufnr, -2, -1, false)[1] or '')
- text_edits = vim.tbl_map(function(text_edit)
- if max <= text_edit.range.start.line then
- text_edit.range.start.line = max - 1
- text_edit.range.start.character = len
- text_edit.newText = '\n' .. text_edit.newText
- has_eol_text_edit = true
- end
- if max <= text_edit.range['end'].line then
- text_edit.range['end'].line = max - 1
- text_edit.range['end'].character = len
- has_eol_text_edit = true
- end
- return text_edit
- end, text_edits)
-
-- Some LSP servers are depending on the VSCode behavior.
-- The VSCode will re-locate the cursor position after applying TextEdit so we also do it.
local is_current_buf = vim.api.nvim_get_current_buf() == bufnr
@@ -318,16 +420,38 @@ function M.apply_text_edits(text_edits, bufnr)
-- Apply text edits.
local is_cursor_fixed = false
+ local has_eol_text_edit = false
for _, text_edit in ipairs(text_edits) do
+ -- Normalize line ending
+ text_edit.newText, _ = string.gsub(text_edit.newText, '\r\n?', '\n')
+
+ -- Convert from LSP style ranges to Neovim style ranges.
local e = {
start_row = text_edit.range.start.line,
- start_col = get_line_byte_from_position(bufnr, text_edit.range.start),
+ start_col = get_line_byte_from_position(bufnr, text_edit.range.start, offset_encoding),
end_row = text_edit.range['end'].line,
- end_col = get_line_byte_from_position(bufnr, text_edit.range['end']),
+ end_col = get_line_byte_from_position(bufnr, text_edit.range['end'], offset_encoding),
text = vim.split(text_edit.newText, '\n', true),
}
+
+ -- Some LSP servers may return +1 range of the buffer content but nvim_buf_set_text can't accept it so we should fix it here.
+ local max = vim.api.nvim_buf_line_count(bufnr)
+ if max <= e.start_row or max <= e.end_row then
+ local len = #(get_line(bufnr, max - 1) or '')
+ if max <= e.start_row then
+ e.start_row = max - 1
+ e.start_col = len
+ table.insert(e.text, 1, '')
+ end
+ if max <= e.end_row then
+ e.end_row = max - 1
+ e.end_col = len
+ end
+ has_eol_text_edit = true
+ end
vim.api.nvim_buf_set_text(bufnr, e.start_row, e.start_col, e.end_row, e.end_col, e.text)
+ -- Fix cursor position.
local row_count = (e.end_row - e.start_row) + 1
if e.end_row < cursor.row then
cursor.row = cursor.row + (#e.text - row_count)
@@ -342,10 +466,13 @@ function M.apply_text_edits(text_edits, bufnr)
end
end
+ local max = vim.api.nvim_buf_line_count(bufnr)
+
+ -- Apply fixed cursor position.
if is_cursor_fixed then
local is_valid_cursor = true
- is_valid_cursor = is_valid_cursor and cursor.row < vim.api.nvim_buf_line_count(bufnr)
- is_valid_cursor = is_valid_cursor and cursor.col <= #(vim.api.nvim_buf_get_lines(bufnr, cursor.row, cursor.row + 1, false)[1] or '')
+ is_valid_cursor = is_valid_cursor and cursor.row < max
+ is_valid_cursor = is_valid_cursor and cursor.col <= #(get_line(bufnr, max - 1) or '')
if is_valid_cursor then
vim.api.nvim_win_set_cursor(0, { cursor.row + 1, cursor.col })
end
@@ -353,8 +480,8 @@ function M.apply_text_edits(text_edits, bufnr)
-- Remove final line if needed
local fix_eol = has_eol_text_edit
- fix_eol = fix_eol and api.nvim_buf_get_option(bufnr, 'fixeol')
- fix_eol = fix_eol and (vim.api.nvim_buf_get_lines(bufnr, -2, -1, false)[1] or '') == ''
+ fix_eol = fix_eol and (api.nvim_buf_get_option(bufnr, 'eol') or (api.nvim_buf_get_option(bufnr, 'fixeol') and not api.nvim_buf_get_option('binary')))
+ fix_eol = fix_eol and get_line(bufnr, max - 1) == ''
if fix_eol then
vim.api.nvim_buf_set_lines(bufnr, -2, -1, false, {})
end
@@ -392,9 +519,12 @@ end
---@param text_document_edit table: a `TextDocumentEdit` object
---@param index number: Optional index of the edit, if from a list of edits (or nil, if not from a list)
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentEdit
-function M.apply_text_document_edit(text_document_edit, index)
+function M.apply_text_document_edit(text_document_edit, index, offset_encoding)
local text_document = text_document_edit.textDocument
local bufnr = vim.uri_to_bufnr(text_document.uri)
+ if offset_encoding == nil then
+ vim.notify_once("apply_text_document_edit must be called with valid offset encoding", vim.log.levels.WARN)
+ end
-- For lists of text document edits,
-- do not check the version after the first edit.
@@ -413,7 +543,7 @@ function M.apply_text_document_edit(text_document_edit, index)
return
end
- M.apply_text_edits(text_document_edit.edits, bufnr)
+ M.apply_text_edits(text_document_edit.edits, bufnr, offset_encoding)
end
--- Parses snippets in a completion entry.
@@ -474,7 +604,7 @@ local function remove_unmatch_completion_items(items, prefix)
end, items)
end
---- Acording to LSP spec, if the client set `completionItemKind.valueSet`,
+--- According to LSP spec, if the client set `completionItemKind.valueSet`,
--- the client must handle it properly even if it receives a value outside the
--- specification.
---
@@ -574,7 +704,7 @@ function M.rename(old_fname, new_fname, opts)
api.nvim_buf_delete(oldbuf, { force = true })
end
-
+---@private
local function create_file(change)
local opts = change.options or {}
-- from spec: Overwrite wins over `ignoreIfExists`
@@ -586,7 +716,7 @@ local function create_file(change)
vim.fn.bufadd(fname)
end
-
+---@private
local function delete_file(change)
local opts = change.options or {}
local fname = vim.uri_to_fname(change.uri)
@@ -610,9 +740,13 @@ end
--- Applies a `WorkspaceEdit`.
---
----@param workspace_edit (table) `WorkspaceEdit`
+---@param workspace_edit table `WorkspaceEdit`
+---@param offset_encoding string utf-8|utf-16|utf-32 (required)
--see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit
-function M.apply_workspace_edit(workspace_edit)
+function M.apply_workspace_edit(workspace_edit, offset_encoding)
+ if offset_encoding == nil then
+ vim.notify_once("apply_workspace_edit must be called with valid offset encoding", vim.log.levels.WARN)
+ end
if workspace_edit.documentChanges then
for idx, change in ipairs(workspace_edit.documentChanges) do
if change.kind == "rename" then
@@ -628,7 +762,7 @@ function M.apply_workspace_edit(workspace_edit)
elseif change.kind then
error(string.format("Unsupported change: %q", vim.inspect(change)))
else
- M.apply_text_document_edit(change, idx)
+ M.apply_text_document_edit(change, idx, offset_encoding)
end
end
return
@@ -641,7 +775,7 @@ function M.apply_workspace_edit(workspace_edit)
for uri, changes in pairs(all_changes) do
local bufnr = vim.uri_to_bufnr(uri)
- M.apply_text_edits(changes, bufnr)
+ M.apply_text_edits(changes, bufnr, offset_encoding)
end
end
@@ -717,7 +851,8 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers
local active_hl
local active_signature = signature_help.activeSignature or 0
-- If the activeSignature is not inside the valid range, then clip it.
- if active_signature >= #signature_help.signatures then
+ -- In 3.15 of the protocol, activeSignature was allowed to be negative
+ if active_signature >= #signature_help.signatures or active_signature < 0 then
active_signature = 0
end
local signature = signature_help.signatures[active_signature + 1]
@@ -858,12 +993,16 @@ end
--- Jumps to a location.
---
----@param location (`Location`|`LocationLink`)
+---@param location table (`Location`|`LocationLink`)
+---@param offset_encoding string utf-8|utf-16|utf-32 (required)
---@returns `true` if the jump succeeded
-function M.jump_to_location(location)
+function M.jump_to_location(location, offset_encoding)
-- location may be Location or LocationLink
local uri = location.uri or location.targetUri
if uri == nil then return end
+ if offset_encoding == nil then
+ vim.notify_once("jump_to_location must be called with valid offset encoding", vim.log.levels.WARN)
+ end
local bufnr = vim.uri_to_bufnr(uri)
-- Save position in jumplist
vim.cmd "normal! m'"
@@ -875,11 +1014,13 @@ function M.jump_to_location(location)
--- Jump to new location (adjusting for UTF-16 encoding of characters)
api.nvim_set_current_buf(bufnr)
- api.nvim_buf_set_option(0, 'buflisted', true)
+ api.nvim_buf_set_option(bufnr, 'buflisted', true)
local range = location.range or location.targetSelectionRange
local row = range.start.line
- local col = get_line_byte_from_position(0, range.start)
+ local col = get_line_byte_from_position(bufnr, range.start, offset_encoding)
api.nvim_win_set_cursor(0, {row + 1, col})
+ -- Open folds under the cursor
+ vim.cmd("normal! zv")
return true
end
@@ -997,7 +1138,7 @@ function M.stylize_markdown(bufnr, contents, opts)
block = {nil, "```+([a-zA-Z0-9_]*)", "```+"},
pre = {"", "<pre>", "</pre>"},
code = {"", "<code>", "</code>"},
- text = {"plaintex", "<text>", "</text>"},
+ text = {"text", "<text>", "</text>"},
}
local match_begin = function(line)
@@ -1054,7 +1195,7 @@ function M.stylize_markdown(bufnr, contents, opts)
markdown_lines[#stripped] = true
end
else
- -- strip any emty lines or separators prior to this separator in actual markdown
+ -- strip any empty lines or separators prior to this separator in actual markdown
if line:match("^---+$") then
while markdown_lines[#stripped] and (stripped[#stripped]:match("^%s*$") or stripped[#stripped]:match("^---+$")) do
markdown_lines[#stripped] = false
@@ -1087,7 +1228,7 @@ function M.stylize_markdown(bufnr, contents, opts)
local idx = 1
---@private
- -- keep track of syntaxes we already inlcuded.
+ -- keep track of syntaxes we already included.
-- no need to include the same syntax more than once
local langs = {}
local fences = get_markdown_fences()
@@ -1133,17 +1274,57 @@ function M.stylize_markdown(bufnr, contents, opts)
return stripped
end
+---@private
--- Creates autocommands to close a preview window when events happen.
---
----@param events (table) list of events
----@param winnr (number) window id of preview window
+---@param events table list of events
+---@param winnr number window id of preview window
+---@param bufnrs table list of buffers where the preview window will remain visible
---@see |autocmd-events|
-function M.close_preview_autocmd(events, winnr)
+local function close_preview_autocmd(events, winnr, bufnrs)
+ local augroup = 'preview_window_'..winnr
+
+ -- close the preview window when entered a buffer that is not
+ -- the floating window buffer or the buffer that spawned it
+ vim.cmd(string.format([[
+ augroup %s
+ autocmd!
+ autocmd BufEnter * lua vim.lsp.util._close_preview_window(%d, {%s})
+ augroup end
+ ]], augroup, winnr, table.concat(bufnrs, ',')))
+
if #events > 0 then
- api.nvim_command("autocmd "..table.concat(events, ',').." <buffer> ++once lua pcall(vim.api.nvim_win_close, "..winnr..", true)")
+ vim.cmd(string.format([[
+ augroup %s
+ autocmd %s <buffer> lua vim.lsp.util._close_preview_window(%d)
+ augroup end
+ ]], augroup, table.concat(events, ','), winnr))
end
end
+---@private
+--- Closes the preview window
+---
+---@param winnr number window id of preview window
+---@param bufnrs table|nil optional list of ignored buffers
+function M._close_preview_window(winnr, bufnrs)
+ vim.schedule(function()
+ -- exit if we are in one of ignored buffers
+ if bufnrs and vim.tbl_contains(bufnrs, api.nvim_get_current_buf()) then
+ return
+ end
+
+ local augroup = 'preview_window_'..winnr
+ vim.cmd(string.format([[
+ augroup %s
+ autocmd!
+ augroup end
+ augroup! %s
+ ]], augroup, augroup))
+ pcall(vim.api.nvim_win_close, winnr, true)
+ end)
+end
+
---@internal
--- Computes size of float needed to show contents (with optional wrapping)
---
@@ -1223,18 +1404,21 @@ end
---
---@param contents table of lines to show in window
---@param syntax string of syntax to set for opened buffer
----@param opts dictionary with optional fields
---- - height of floating window
---- - width of floating window
---- - wrap boolean enable wrapping of long lines (defaults to true)
---- - wrap_at character to wrap at for computing height when wrap is enabled
---- - max_width maximal width of floating window
---- - max_height maximal height of floating window
---- - pad_top number of lines to pad contents at top
---- - pad_bottom number of lines to pad contents at bottom
---- - focus_id if a popup with this id is opened, then focus it
---- - close_events list of events that closes the floating window
---- - focusable (boolean, default true): Make float focusable
+---@param opts table with optional fields (additional keys are passed on to |vim.api.nvim_open_win()|)
+--- - height: (number) height of floating window
+--- - width: (number) width of floating window
+--- - wrap: (boolean, default true) wrap long lines
+--- - wrap_at: (string) character to wrap at for computing height when wrap is enabled
+--- - max_width: (number) maximal width of floating window
+--- - max_height: (number) maximal height of floating window
+--- - pad_top: (number) number of lines to pad contents at top
+--- - pad_bottom: (number) number of lines to pad contents at bottom
+--- - focus_id: (string) if a popup with this id is opened, then focus it
+--- - close_events: (table) list of events that closes the floating window
+--- - focusable: (boolean, default true) Make float focusable
+--- - focus: (boolean, default true) If `true`, and if {focusable}
+--- is also `true`, focus an existing floating window with the same
+--- {focus_id}
---@returns bufnr,winnr buffer and window number of the newly created floating
---preview window
function M.open_floating_preview(contents, syntax, opts)
@@ -1246,12 +1430,13 @@ function M.open_floating_preview(contents, syntax, opts)
opts = opts or {}
opts.wrap = opts.wrap ~= false -- wrapping by default
opts.stylize_markdown = opts.stylize_markdown ~= false
- opts.close_events = opts.close_events or {"CursorMoved", "CursorMovedI", "BufHidden", "InsertCharPre"}
+ opts.focus = opts.focus ~= false
+ opts.close_events = opts.close_events or {"CursorMoved", "CursorMovedI", "InsertCharPre"}
local bufnr = api.nvim_get_current_buf()
-- check if this popup is focusable and we need to focus
- if opts.focus_id and opts.focusable ~= false then
+ if opts.focus_id and opts.focusable ~= false and opts.focus then
-- Go back to previous window if we are in a focusable one
local current_winnr = api.nvim_get_current_win()
if npcall(api.nvim_win_get_var, current_winnr, opts.focus_id) then
@@ -1314,8 +1499,8 @@ function M.open_floating_preview(contents, syntax, opts)
api.nvim_buf_set_option(floating_bufnr, 'modifiable', false)
api.nvim_buf_set_option(floating_bufnr, 'bufhidden', 'wipe')
- api.nvim_buf_set_keymap(floating_bufnr, "n", "q", "<cmd>bdelete<cr>", {silent = true, noremap = true})
- M.close_preview_autocmd(opts.close_events, floating_winnr)
+ api.nvim_buf_set_keymap(floating_bufnr, "n", "q", "<cmd>bdelete<cr>", {silent = true, noremap = true, nowait = true})
+ close_preview_autocmd(opts.close_events, floating_winnr, {floating_bufnr, bufnr})
-- save focus_id
if opts.focus_id then
@@ -1331,21 +1516,23 @@ do --[[ References ]]
--- Removes document highlights from a buffer.
---
- ---@param bufnr buffer id
+ ---@param bufnr number Buffer id
function M.buf_clear_references(bufnr)
validate { bufnr = {bufnr, 'n', true} }
- api.nvim_buf_clear_namespace(bufnr, reference_ns, 0, -1)
+ api.nvim_buf_clear_namespace(bufnr or 0, reference_ns, 0, -1)
end
--- Shows a list of document highlights for a certain buffer.
---
- ---@param bufnr buffer id
- ---@param references List of `DocumentHighlight` objects to highlight
- ---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to utf-16
+ ---@param bufnr number Buffer id
+ ---@param references table List of `DocumentHighlight` objects to highlight
+ ---@param offset_encoding string One of "utf-8", "utf-16", "utf-32".
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#documentHighlight
function M.buf_highlight_references(bufnr, references, offset_encoding)
- validate { bufnr = {bufnr, 'n', true} }
- offset_encoding = offset_encoding or 'utf-16'
+ validate {
+ bufnr = {bufnr, 'n', true},
+ offset_encoding = { offset_encoding, 'string', false };
+ }
for _, reference in ipairs(references) do
local start_line, start_char = reference["range"]["start"]["line"], reference["range"]["start"]["character"]
local end_line, end_char = reference["range"]["end"]["line"], reference["range"]["end"]["character"]
@@ -1363,7 +1550,8 @@ do --[[ References ]]
reference_ns,
document_highlight_kind[kind],
{ start_line, start_idx },
- { end_line, end_idx })
+ { end_line, end_idx },
+ { priority = vim.highlight.priorities.user })
end
end
end
@@ -1372,97 +1560,20 @@ local position_sort = sort_by_key(function(v)
return {v.start.line, v.start.character}
end)
---- Gets the zero-indexed line from the given uri.
----@param uri string uri of the resource to get the line from
----@param row number zero-indexed line number
----@return string the line at row in filename
--- For non-file uris, we load the buffer and get the line.
--- If a loaded buffer exists, then that is used.
--- Otherwise we get the line using libuv which is a lot faster than loading the buffer.
-function M.get_line(uri, row)
- return M.get_lines(uri, { row })[row]
-end
-
---- Gets the zero-indexed lines from the given uri.
----@param uri string uri of the resource to get the lines from
----@param rows number[] zero-indexed line numbers
----@return table<number string> a table mapping rows to lines
--- For non-file uris, we load the buffer and get the lines.
--- If a loaded buffer exists, then that is used.
--- Otherwise we get the lines using libuv which is a lot faster than loading the buffer.
-function M.get_lines(uri, rows)
- rows = type(rows) == "table" and rows or { rows }
-
- local function buf_lines(bufnr)
- local lines = {}
- for _, row in pairs(rows) do
- lines[row] = (vim.api.nvim_buf_get_lines(bufnr, row, row + 1, false) or { "" })[1]
- end
- return lines
- end
-
- -- load the buffer if this is not a file uri
- -- Custom language server protocol extensions can result in servers sending URIs with custom schemes. Plugins are able to load these via `BufReadCmd` autocmds.
- if uri:sub(1, 4) ~= "file" then
- local bufnr = vim.uri_to_bufnr(uri)
- vim.fn.bufload(bufnr)
- return buf_lines(bufnr)
- end
-
- local filename = vim.uri_to_fname(uri)
-
- -- use loaded buffers if available
- if vim.fn.bufloaded(filename) == 1 then
- local bufnr = vim.fn.bufnr(filename, false)
- return buf_lines(bufnr)
- end
-
- -- get the data from the file
- local fd = uv.fs_open(filename, "r", 438)
- if not fd then return "" end
- local stat = uv.fs_fstat(fd)
- local data = uv.fs_read(fd, stat.size, 0)
- uv.fs_close(fd)
-
- local lines = {} -- rows we need to retrieve
- local need = 0 -- keep track of how many unique rows we need
- for _, row in pairs(rows) do
- if not lines[row] then
- need = need + 1
- end
- lines[row] = true
- end
-
- local found = 0
- local lnum = 0
-
- for line in string.gmatch(data, "([^\n]*)\n?") do
- if lines[lnum] == true then
- lines[lnum] = line
- found = found + 1
- if found == need then break end
- end
- lnum = lnum + 1
- end
-
- -- change any lines we didn't find to the empty string
- for i, line in pairs(lines) do
- if line == true then
- lines[i] = ""
- end
- end
- return lines
-end
-
--- Returns the items with the byte position calculated correctly and in sorted
--- order, for display in quickfix and location lists.
---
--- The result can be passed to the {list} argument of |setqflist()| or
--- |setloclist()|.
---
----@param locations (table) list of `Location`s or `LocationLink`s
+---@param locations table list of `Location`s or `LocationLink`s
+---@param offset_encoding string offset_encoding for locations utf-8|utf-16|utf-32
---@returns (table) list of items
-function M.locations_to_items(locations)
+function M.locations_to_items(locations, offset_encoding)
+ if offset_encoding == nil then
+ vim.notify_once("locations_to_items must be called with valid offset encoding", vim.log.levels.WARN)
+ end
+
local items = {}
local grouped = setmetatable({}, {
__index = function(t, k)
@@ -1496,13 +1607,13 @@ function M.locations_to_items(locations)
end
-- get all the lines for this uri
- local lines = M.get_lines(uri, uri_rows)
+ local lines = get_lines(vim.uri_to_bufnr(uri), uri_rows)
for _, temp in ipairs(rows) do
local pos = temp.start
local row = pos.line
local line = lines[row] or ""
- local col = pos.character
+ local col = M._str_byteindex_enc(line, pos.character, offset_encoding)
table.insert(items, {
filename = filename,
lnum = row + 1,
@@ -1522,6 +1633,7 @@ end
---
---@param items (table) list of items
function M.set_loclist(items, win_id)
+ vim.api.nvim_echo({{'vim.lsp.util.set_loclist is deprecated. See :h deprecated', 'WarningMsg'}}, true, {})
vim.fn.setloclist(win_id or 0, {}, ' ', {
title = 'Language Server';
items = items;
@@ -1535,13 +1647,14 @@ end
---
---@param items (table) list of items
function M.set_qflist(items)
+ vim.api.nvim_echo({{'vim.lsp.util.set_qflist is deprecated. See :h deprecated', 'WarningMsg'}}, true, {})
vim.fn.setqflist({}, ' ', {
title = 'Language Server';
items = items;
})
end
--- Acording to LSP spec, if the client set "symbolKind.valueSet",
+-- According to LSP spec, if the client set "symbolKind.valueSet",
-- the client must handle it properly even if it receives a value outside the specification.
-- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol
function M._get_symbol_kind_name(symbol_kind)
@@ -1586,7 +1699,7 @@ function M.symbols_to_items(symbols, bufnr)
end
return _items
end
- return _symbols_to_items(symbols, {}, bufnr)
+ return _symbols_to_items(symbols, {}, bufnr or 0)
end
--- Removes empty lines from the beginning and end.
@@ -1638,43 +1751,84 @@ function M.try_trim_markdown_code_blocks(lines)
return 'markdown'
end
-local str_utfindex = vim.str_utfindex
---@private
-local function make_position_param()
- local row, col = unpack(api.nvim_win_get_cursor(0))
+---@param window (optional, number): window handle or 0 for current, defaults to current
+---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of buffer of `window`
+local function make_position_param(window, offset_encoding)
+ window = window or 0
+ local buf = vim.api.nvim_win_get_buf(window)
+ local row, col = unpack(api.nvim_win_get_cursor(window))
+ offset_encoding = offset_encoding or M._get_offset_encoding(buf)
row = row - 1
- local line = api.nvim_buf_get_lines(0, row, row+1, true)[1]
+ local line = api.nvim_buf_get_lines(buf, row, row+1, true)[1]
if not line then
return { line = 0; character = 0; }
end
- -- TODO handle offset_encoding
- local _
- _, col = str_utfindex(line, col)
+
+ col = _str_utfindex_enc(line, col, offset_encoding)
+
return { line = row; character = col; }
end
--- Creates a `TextDocumentPositionParams` object for the current buffer and cursor position.
---
+---@param window (optional, number): window handle or 0 for current, defaults to current
+---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of buffer of `window`
---@returns `TextDocumentPositionParams` object
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentPositionParams
-function M.make_position_params()
+function M.make_position_params(window, offset_encoding)
+ window = window or 0
+ local buf = vim.api.nvim_win_get_buf(window)
+ offset_encoding = offset_encoding or M._get_offset_encoding(buf)
return {
- textDocument = M.make_text_document_params();
- position = make_position_param()
+ textDocument = M.make_text_document_params(buf);
+ position = make_position_param(window, offset_encoding)
}
end
+--- Utility function for getting the encoding of the first LSP client on the given buffer.
+---@param bufnr (number) buffer handle or 0 for current, defaults to current
+---@returns (string) encoding first client if there is one, nil otherwise
+function M._get_offset_encoding(bufnr)
+ validate {
+ bufnr = {bufnr, 'n', true};
+ }
+
+ local offset_encoding
+
+ for _, client in pairs(vim.lsp.buf_get_clients(bufnr)) do
+ if client.offset_encoding == nil then
+ vim.notify_once(
+ string.format("Client (id: %s) offset_encoding is nil. Do not unset offset_encoding.", client.id),
+ vim.log.levels.ERROR
+ )
+ end
+ local this_offset_encoding = client.offset_encoding
+ if not offset_encoding then
+ offset_encoding = this_offset_encoding
+ elseif offset_encoding ~= this_offset_encoding then
+ vim.notify("warning: multiple different client offset_encodings detected for buffer, this is not supported yet", vim.log.levels.WARN)
+ end
+ end
+
+ return offset_encoding
+end
+
--- Using the current position in the current buffer, creates an object that
--- can be used as a building block for several LSP requests, such as
--- `textDocument/codeAction`, `textDocument/colorPresentation`,
--- `textDocument/rangeFormatting`.
---
+---@param window (optional, number): window handle or 0 for current, defaults to current
+---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of buffer of `window`
---@returns { textDocument = { uri = `current_file_uri` }, range = { start =
---`current_position`, end = `current_position` } }
-function M.make_range_params()
- local position = make_position_param()
+function M.make_range_params(window, offset_encoding)
+ local buf = vim.api.nvim_win_get_buf(window or 0)
+ offset_encoding = offset_encoding or M._get_offset_encoding(buf)
+ local position = make_position_param(window, offset_encoding)
return {
- textDocument = M.make_text_document_params(),
+ textDocument = M.make_text_document_params(buf),
range = { start = position; ["end"] = position; }
}
end
@@ -1686,27 +1840,29 @@ end
---Defaults to the start of the last visual selection.
---@param end_pos ({number, number}, optional) mark-indexed position.
---Defaults to the end of the last visual selection.
+---@param bufnr (optional, number): buffer handle or 0 for current, defaults to current
+---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of `bufnr`
---@returns { textDocument = { uri = `current_file_uri` }, range = { start =
---`start_position`, end = `end_position` } }
-function M.make_given_range_params(start_pos, end_pos)
+function M.make_given_range_params(start_pos, end_pos, bufnr, offset_encoding)
validate {
start_pos = {start_pos, 't', true};
end_pos = {end_pos, 't', true};
+ offset_encoding = {offset_encoding, 's', true};
}
- local A = list_extend({}, start_pos or api.nvim_buf_get_mark(0, '<'))
- local B = list_extend({}, end_pos or api.nvim_buf_get_mark(0, '>'))
+ bufnr = bufnr or vim.api.nvim_get_current_buf()
+ offset_encoding = offset_encoding or M._get_offset_encoding(bufnr)
+ local A = list_extend({}, start_pos or api.nvim_buf_get_mark(bufnr, '<'))
+ local B = list_extend({}, end_pos or api.nvim_buf_get_mark(bufnr, '>'))
-- convert to 0-index
A[1] = A[1] - 1
B[1] = B[1] - 1
- -- account for encoding.
- -- TODO handle offset_encoding
+ -- account for offset_encoding.
if A[2] > 0 then
- local _, char = M.character_offset(0, A[1], A[2])
- A = {A[1], char}
+ A = {A[1], M.character_offset(bufnr, A[1], A[2], offset_encoding)}
end
if B[2] > 0 then
- local _, char = M.character_offset(0, B[1], B[2])
- B = {B[1], char}
+ B = {B[1], M.character_offset(bufnr, B[1], B[2], offset_encoding)}
end
-- we need to offset the end character position otherwise we loose the last
-- character of the selection, as LSP end position is exclusive
@@ -1715,7 +1871,7 @@ function M.make_given_range_params(start_pos, end_pos)
B[2] = B[2] + 1
end
return {
- textDocument = M.make_text_document_params(),
+ textDocument = M.make_text_document_params(bufnr),
range = {
start = {line = A[1], character = A[2]},
['end'] = {line = B[1], character = B[2]}
@@ -1725,10 +1881,11 @@ end
--- Creates a `TextDocumentIdentifier` object for the current buffer.
---
+---@param bufnr (optional, number): Buffer handle, defaults to current
---@returns `TextDocumentIdentifier`
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentIdentifier
-function M.make_text_document_params()
- return { uri = vim.uri_from_bufnr(0) }
+function M.make_text_document_params(bufnr)
+ return { uri = vim.uri_from_bufnr(bufnr or 0) }
end
--- Create the workspace params
@@ -1737,16 +1894,16 @@ end
function M.make_workspace_params(added, removed)
return { event = { added = added; removed = removed; } }
end
---- Returns visual width of tabstop.
+--- Returns indentation size.
---
----@see |softtabstop|
+---@see |shiftwidth|
---@param bufnr (optional, number): Buffer handle, defaults to current
----@returns (number) tabstop visual width
+---@returns (number) indentation size
function M.get_effective_tabstop(bufnr)
validate { bufnr = {bufnr, 'n', true} }
local bo = bufnr and vim.bo[bufnr] or vim.bo
- local sts = bo.softtabstop
- return (sts > 0 and sts) or (sts < 0 and bo.shiftwidth) or bo.tabstop
+ local sw = bo.shiftwidth
+ return (sw == 0 and bo.tabstop) or sw
end
--- Creates a `DocumentFormattingParams` object for the current buffer and cursor position.
@@ -1771,15 +1928,18 @@ end
---@param buf buffer id (0 for current)
---@param row 0-indexed line
---@param col 0-indexed byte offset in line
----@returns (number, number) UTF-32 and UTF-16 index of the character in line {row} column {col} in buffer {buf}
-function M.character_offset(bufnr, row, col)
- local uri = vim.uri_from_bufnr(bufnr)
- local line = M.get_line(uri, row)
+---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of `buf`
+---@returns (number, number) `offset_encoding` index of the character in line {row} column {col} in buffer {buf}
+function M.character_offset(buf, row, col, offset_encoding)
+ local line = get_line(buf, row)
+ if offset_encoding == nil then
+ vim.notify_once("character_offset must be called with valid offset encoding", vim.log.levels.WARN)
+ end
-- If the col is past the EOL, use the line length.
if col > #line then
- return str_utfindex(line)
+ return _str_utfindex_enc(line, nil, offset_encoding)
end
- return str_utfindex(line, col)
+ return _str_utfindex_enc(line, col, offset_encoding)
end
--- Helper function to return nested values in language server settings
@@ -1798,7 +1958,6 @@ function M.lookup_section(settings, section)
end
M._get_line_byte_from_position = get_line_byte_from_position
-M._warn_once = warn_once
M.buf_versions = {}