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.lua136
-rw-r--r--runtime/lua/vim/lsp/codelens.lua231
-rw-r--r--runtime/lua/vim/lsp/diagnostic.lua87
-rw-r--r--runtime/lua/vim/lsp/handlers.lua44
-rw-r--r--runtime/lua/vim/lsp/log.lua2
-rw-r--r--runtime/lua/vim/lsp/rpc.lua30
-rw-r--r--runtime/lua/vim/lsp/util.lua550
7 files changed, 835 insertions, 245 deletions
diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua
index 31116985e2..c63b947c99 100644
--- a/runtime/lua/vim/lsp/buf.lua
+++ b/runtime/lua/vim/lsp/buf.lua
@@ -111,6 +111,39 @@ function M.completion(context)
return request('textDocument/completion', params)
end
+--@private
+--- If there is more than one client that supports the given method,
+--- asks the user to select one.
+--
+--@returns The client that the user selected or nil
+local function select_client(method)
+ local clients = vim.tbl_values(vim.lsp.buf_get_clients());
+ clients = vim.tbl_filter(function (client)
+ return client.supports_method(method)
+ end, clients)
+ -- better UX when choices are always in the same order (between restarts)
+ table.sort(clients, function (a, b) return a.name < b.name end)
+
+ if #clients > 1 then
+ local choices = {}
+ for k,v in ipairs(clients) do
+ table.insert(choices, string.format("%d %s", k, v.name))
+ end
+ local user_choice = vim.fn.confirm(
+ "Select a language server:",
+ table.concat(choices, "\n"),
+ 0,
+ "Question"
+ )
+ if user_choice == 0 then return nil end
+ return clients[user_choice]
+ elseif #clients < 1 then
+ return nil
+ else
+ return clients[1]
+ end
+end
+
--- Formats the current buffer.
---
--@param options (optional, table) Can be used to specify FormattingOptions.
@@ -119,8 +152,11 @@ end
--
--@see https://microsoft.github.io/language-server-protocol/specification#textDocument_formatting
function M.formatting(options)
+ local client = select_client("textDocument/formatting")
+ if client == nil then return end
+
local params = util.make_formatting_params(options)
- return request('textDocument/formatting', params)
+ return client.request("textDocument/formatting", params)
end
--- Performs |vim.lsp.buf.formatting()| synchronously.
@@ -134,14 +170,62 @@ end
---
--@param options Table with valid `FormattingOptions` entries
--@param timeout_ms (number) Request timeout
+--@see |vim.lsp.buf.formatting_seq_sync|
function M.formatting_sync(options, timeout_ms)
+ local client = select_client("textDocument/formatting")
+ if client == nil then return end
+
local params = util.make_formatting_params(options)
- local result = vim.lsp.buf_request_sync(0, "textDocument/formatting", params, timeout_ms)
- if not result or vim.tbl_isempty(result) then return end
- local _, formatting_result = next(result)
- result = formatting_result.result
- if not result then return end
- vim.lsp.util.apply_text_edits(result)
+ local result, err = client.request_sync("textDocument/formatting", params, timeout_ms)
+ if result and result.result then
+ util.apply_text_edits(result.result)
+ elseif err then
+ vim.notify("vim.lsp.buf.formatting_sync: " .. err, vim.log.levels.WARN)
+ end
+end
+
+--- Formats the current buffer by sequentially requesting formatting from attached clients.
+---
+--- Useful when multiple clients with formatting capability are attached.
+---
+--- Since it's synchronous, can be used for running on save, to make sure buffer is formatted
+--- prior to being saved. {timeout_ms} is passed on to the |vim.lsp.client| `request_sync` method.
+--- Example:
+--- <pre>
+--- vim.api.nvim_command[[autocmd BufWritePre <buffer> lua vim.lsp.buf.formatting_seq_sync()]]
+--- </pre>
+---
+--@param options (optional, table) `FormattingOptions` entries
+--@param timeout_ms (optional, number) Request timeout
+--@param order (optional, table) List of client names. Formatting is requested from clients
+---in the following order: first all clients that are not in the `order` list, then
+---the remaining clients in the order as they occur in the `order` list.
+function M.formatting_seq_sync(options, timeout_ms, order)
+ local clients = vim.tbl_values(vim.lsp.buf_get_clients());
+
+ -- sort the clients according to `order`
+ for _, client_name in ipairs(order or {}) do
+ -- if the client exists, move to the end of the list
+ for i, client in ipairs(clients) do
+ if client.name == client_name then
+ table.insert(clients, table.remove(clients, i))
+ break
+ end
+ end
+ end
+
+ -- loop through the clients and make synchronous formatting requests
+ for _, client in ipairs(clients) do
+ if client.resolved_capabilities.document_formatting then
+ local params = util.make_formatting_params(options)
+ local result, err = client.request_sync("textDocument/formatting", params, timeout_ms)
+ if result and result.result then
+ util.apply_text_edits(result.result)
+ elseif err then
+ vim.notify(string.format("vim.lsp.buf.formatting_seq_sync: (%s) %s", client.name, err), vim.log.levels.WARN)
+ end
+ end
+ end
end
--- Formats a given range.
@@ -152,15 +236,12 @@ end
--@param end_pos ({number, number}, optional) mark-indexed position.
---Defaults to the end of the last visual selection.
function M.range_formatting(options, start_pos, end_pos)
- validate { options = {options, 't', true} }
- local sts = vim.bo.softtabstop;
- options = vim.tbl_extend('keep', options or {}, {
- tabSize = (sts > 0 and sts) or (sts < 0 and vim.bo.shiftwidth) or vim.bo.tabstop;
- insertSpaces = vim.bo.expandtab;
- })
+ local client = select_client("textDocument/rangeFormatting")
+ if client == nil then return end
+
local params = util.make_given_range_params(start_pos, end_pos)
- params.options = options
- return request('textDocument/rangeFormatting', params)
+ params.options = util.make_formatting_params(options).options
+ return client.request("textDocument/rangeFormatting", params)
end
--- Renames all references to the symbol under the cursor.
@@ -216,26 +297,31 @@ local function pick_call_hierarchy_item(call_hierarchy_items)
return choice
end
+--@private
+local function call_hierarchy(method)
+ local params = util.make_position_params()
+ request('textDocument/prepareCallHierarchy', params, function(err, _, result)
+ if err then
+ vim.notify(err.message, vim.log.levels.WARN)
+ return
+ end
+ local call_hierarchy_item = pick_call_hierarchy_item(result)
+ vim.lsp.buf_request(0, method, { item = call_hierarchy_item })
+ end)
+end
+
--- Lists all the call sites of the symbol under the cursor in the
--- |quickfix| window. If the symbol can resolve to multiple
--- items, the user can pick one in the |inputlist|.
function M.incoming_calls()
- local params = util.make_position_params()
- request('textDocument/prepareCallHierarchy', params, function(_, _, result)
- local call_hierarchy_item = pick_call_hierarchy_item(result)
- vim.lsp.buf_request(0, 'callHierarchy/incomingCalls', { item = call_hierarchy_item })
- end)
+ call_hierarchy('callHierarchy/incomingCalls')
end
--- Lists all the items that are called by the symbol under the
--- cursor in the |quickfix| window. If the symbol can resolve to
--- multiple items, the user can pick one in the |inputlist|.
function M.outgoing_calls()
- local params = util.make_position_params()
- request('textDocument/prepareCallHierarchy', params, function(_, _, result)
- local call_hierarchy_item = pick_call_hierarchy_item(result)
- vim.lsp.buf_request(0, 'callHierarchy/outgoingCalls', { item = call_hierarchy_item })
- end)
+ call_hierarchy('callHierarchy/outgoingCalls')
end
--- List workspace folders.
diff --git a/runtime/lua/vim/lsp/codelens.lua b/runtime/lua/vim/lsp/codelens.lua
new file mode 100644
index 0000000000..fbd37e3830
--- /dev/null
+++ b/runtime/lua/vim/lsp/codelens.lua
@@ -0,0 +1,231 @@
+local util = require('vim.lsp.util')
+local api = vim.api
+local M = {}
+
+--- bufnr → true|nil
+--- to throttle refreshes to at most one at a time
+local active_refreshes = {}
+
+--- bufnr -> client_id -> lenses
+local lens_cache_by_buf = setmetatable({}, {
+ __index = function(t, b)
+ local key = b > 0 and b or api.nvim_get_current_buf()
+ return rawget(t, key)
+ end
+})
+
+local namespaces = setmetatable({}, {
+ __index = function(t, key)
+ local value = api.nvim_create_namespace('vim_lsp_codelens:' .. key)
+ rawset(t, key, value)
+ return value
+ end;
+})
+
+--@private
+M.__namespaces = namespaces
+
+
+--@private
+local function execute_lens(lens, bufnr, client_id)
+ local line = lens.range.start.line
+ api.nvim_buf_clear_namespace(bufnr, namespaces[client_id], line, line + 1)
+
+ -- Need to use the client that returned the lens → must not use buf_request
+ local client = vim.lsp.get_client_by_id(client_id)
+ assert(client, 'Client is required to execute lens, client_id=' .. client_id)
+ client.request('workspace/executeCommand', lens.command, function(...)
+ local result = vim.lsp.handlers['workspace/executeCommand'](...)
+ M.refresh()
+ return result
+ end, bufnr)
+end
+
+
+--- Return all lenses for the given buffer
+---
+---@return table (`CodeLens[]`)
+function M.get(bufnr)
+ local lenses_by_client = lens_cache_by_buf[bufnr]
+ if not lenses_by_client then return {} end
+ local lenses = {}
+ for _, client_lenses in pairs(lenses_by_client) do
+ vim.list_extend(lenses, client_lenses)
+ end
+ return lenses
+end
+
+
+--- Run the code lens in the current line
+---
+function M.run()
+ local line = api.nvim_win_get_cursor(0)[1]
+ local bufnr = api.nvim_get_current_buf()
+ local options = {}
+ local lenses_by_client = lens_cache_by_buf[bufnr] or {}
+ for client, lenses in pairs(lenses_by_client) do
+ for _, lens in pairs(lenses) do
+ if lens.range.start.line == (line - 1) then
+ table.insert(options, {client=client, lens=lens})
+ end
+ end
+ end
+ if #options == 0 then
+ vim.notify('No executable codelens found at current line')
+ elseif #options == 1 then
+ local option = options[1]
+ execute_lens(option.lens, bufnr, option.client)
+ else
+ local options_strings = {"Code lenses:"}
+ for i, option in ipairs(options) do
+ table.insert(options_strings, string.format('%d. %s', i, option.lens.command.title))
+ end
+ local choice = vim.fn.inputlist(options_strings)
+ if choice < 1 or choice > #options then
+ return
+ end
+ local option = options[choice]
+ execute_lens(option.lens, bufnr, option.client)
+ end
+end
+
+
+--- Display the lenses using virtual text
+---
+---@param lenses table of lenses to display (`CodeLens[] | null`)
+---@param bufnr number
+---@param client_id number
+function M.display(lenses, bufnr, client_id)
+ if not lenses or not next(lenses) then
+ return
+ end
+ local lenses_by_lnum = {}
+ for _, lens in pairs(lenses) do
+ local line_lenses = lenses_by_lnum[lens.range.start.line]
+ if not line_lenses then
+ line_lenses = {}
+ lenses_by_lnum[lens.range.start.line] = line_lenses
+ end
+ table.insert(line_lenses, lens)
+ end
+ local ns = namespaces[client_id]
+ local num_lines = api.nvim_buf_line_count(bufnr)
+ for i = 0, num_lines do
+ local line_lenses = lenses_by_lnum[i]
+ api.nvim_buf_clear_namespace(bufnr, ns, i, i + 1)
+ local chunks = {}
+ for _, lens in pairs(line_lenses or {}) do
+ local text = lens.command and lens.command.title or 'Unresolved lens ...'
+ table.insert(chunks, {text, 'LspCodeLens' })
+ end
+ if #chunks > 0 then
+ api.nvim_buf_set_virtual_text(bufnr, ns, i, chunks, {})
+ end
+ end
+end
+
+
+--- Store lenses for a specific buffer and client
+---
+---@param lenses table of lenses to store (`CodeLens[] | null`)
+---@param bufnr number
+---@param client_id number
+function M.save(lenses, bufnr, client_id)
+ local lenses_by_client = lens_cache_by_buf[bufnr]
+ if not lenses_by_client then
+ lenses_by_client = {}
+ lens_cache_by_buf[bufnr] = lenses_by_client
+ local ns = namespaces[client_id]
+ api.nvim_buf_attach(bufnr, false, {
+ on_detach = function(b) lens_cache_by_buf[b] = nil end,
+ on_lines = function(_, b, _, first_lnum, last_lnum)
+ api.nvim_buf_clear_namespace(b, ns, first_lnum, last_lnum)
+ end
+ })
+ end
+ lenses_by_client[client_id] = lenses
+end
+
+
+--@private
+local function resolve_lenses(lenses, bufnr, client_id, callback)
+ lenses = lenses or {}
+ local num_lens = vim.tbl_count(lenses)
+ if num_lens == 0 then
+ callback()
+ return
+ end
+
+ --@private
+ local function countdown()
+ num_lens = num_lens - 1
+ if num_lens == 0 then
+ callback()
+ end
+ end
+ local ns = namespaces[client_id]
+ local client = vim.lsp.get_client_by_id(client_id)
+ for _, lens in pairs(lenses or {}) do
+ if lens.command then
+ countdown()
+ else
+ client.request('codeLens/resolve', lens, function(_, _, result)
+ if result and result.command then
+ lens.command = result.command
+ -- Eager display to have some sort of incremental feedback
+ -- Once all lenses got resolved there will be a full redraw for all lenses
+ -- So that multiple lens per line are properly displayed
+ api.nvim_buf_set_virtual_text(
+ bufnr,
+ ns,
+ lens.range.start.line,
+ {{ lens.command.title, 'LspCodeLens' },},
+ {}
+ )
+ end
+ countdown()
+ end, bufnr)
+ end
+ end
+end
+
+
+--- |lsp-handler| for the method `textDocument/codeLens`
+---
+function M.on_codelens(err, _, result, client_id, bufnr)
+ assert(not err, vim.inspect(err))
+
+ M.save(result, bufnr, client_id)
+
+ -- Eager display for any resolved (and unresolved) lenses and refresh them
+ -- once resolved.
+ M.display(result, bufnr, client_id)
+ resolve_lenses(result, bufnr, client_id, function()
+ M.display(result, bufnr, client_id)
+ active_refreshes[bufnr] = nil
+ end)
+end
+
+
+--- Refresh the codelens for the current buffer
+---
+--- It is recommended to trigger this using an autocmd or via keymap.
+---
+--- <pre>
+--- autocmd BufEnter,CursorHold,InsertLeave <buffer> lua vim.lsp.codelens.refresh()
+--- </pre>
+---
+function M.refresh()
+ local params = {
+ textDocument = util.make_text_document_params()
+ }
+ local bufnr = api.nvim_get_current_buf()
+ if active_refreshes[bufnr] then
+ return
+ end
+ active_refreshes[bufnr] = true
+ vim.lsp.buf_request(0, 'textDocument/codeLens', params)
+end
+
+
+return M
diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua
index 6f2f846a3b..d4580885db 100644
--- a/runtime/lua/vim/lsp/diagnostic.lua
+++ b/runtime/lua/vim/lsp/diagnostic.lua
@@ -271,8 +271,12 @@ local function set_diagnostic_cache(diagnostics, bufnr, client_id)
end
-- Account for servers that place diagnostics on terminating newline
if buf_line_count > 0 then
- local start = diagnostic.range.start
- start.line = math.min(start.line, buf_line_count - 1)
+ diagnostic.range.start.line = math.max(math.min(
+ diagnostic.range.start.line, buf_line_count - 1
+ ), 0)
+ diagnostic.range["end"].line = math.max(math.min(
+ diagnostic.range["end"].line, buf_line_count - 1
+ ), 0)
end
end
@@ -317,9 +321,9 @@ function M.save(diagnostics, bufnr, client_id)
-- Clean up our data when the buffer unloads.
api.nvim_buf_attach(bufnr, false, {
- on_detach = function(b)
+ on_detach = function(_, b)
clear_diagnostic_cache(b, client_id)
- _diagnostic_cleanup[bufnr][client_id] = nil
+ _diagnostic_cleanup[b][client_id] = nil
end
})
end
@@ -330,15 +334,19 @@ end
-- Diagnostic Retrieval {{{
---- Get all diagnostics for all clients
+--- Get all diagnostics for clients
---
----@return {bufnr: Diagnostic[]}
-function M.get_all()
+---@param client_id number Restrict included diagnostics to the client
+--- If nil, diagnostics of all clients are included.
+---@return table with diagnostics grouped by bufnr (bufnr: Diagnostic[])
+function M.get_all(client_id)
local diagnostics_by_bufnr = {}
for bufnr, buf_diagnostics in pairs(diagnostic_cache) do
diagnostics_by_bufnr[bufnr] = {}
- for _, client_diagnostics in pairs(buf_diagnostics) do
- vim.list_extend(diagnostics_by_bufnr[bufnr], client_diagnostics)
+ for cid, client_diagnostics in pairs(buf_diagnostics) do
+ if client_id == nil or cid == client_id then
+ vim.list_extend(diagnostics_by_bufnr[bufnr], client_diagnostics)
+ end
end
end
return diagnostics_by_bufnr
@@ -1151,6 +1159,7 @@ function M.show_line_diagnostics(opts, bufnr, line_nr, client_id)
end
end
+ opts.focus_id = "line_diagnostics"
local popup_bufnr, winnr = util.open_floating_preview(lines, 'plaintext', opts)
for i, hi in ipairs(highlights) do
local prefixlen, hiname = unpack(hi)
@@ -1161,13 +1170,6 @@ function M.show_line_diagnostics(opts, bufnr, line_nr, client_id)
return popup_bufnr, winnr
end
-local loclist_type_map = {
- [DiagnosticSeverity.Error] = 'E',
- [DiagnosticSeverity.Warning] = 'W',
- [DiagnosticSeverity.Information] = 'I',
- [DiagnosticSeverity.Hint] = 'I',
-}
-
--- Clear diagnotics and diagnostic cache
---
@@ -1196,44 +1198,29 @@ end
--- - Exclusive severity to consider. Overrides {severity_limit}
--- - {severity_limit}: (DiagnosticSeverity)
--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
+--- - {workspace}: (boolean, default false)
+--- - Set the list with workspace diagnostics
function M.set_loclist(opts)
opts = opts or {}
-
local open_loclist = if_nil(opts.open_loclist, true)
-
- local bufnr = vim.api.nvim_get_current_buf()
- local buffer_diags = M.get(bufnr, opts.client_id)
-
- if opts.severity then
- buffer_diags = filter_to_severity_limit(opts.severity, buffer_diags)
- elseif opts.severity_limit then
- buffer_diags = filter_by_severity_limit(opts.severity_limit, buffer_diags)
- end
-
- local items = {}
- local insert_diag = function(diag)
- local pos = diag.range.start
- local row = pos.line
- local col = util.character_offset(bufnr, row, pos.character)
-
- local line = (api.nvim_buf_get_lines(bufnr, row, row + 1, false) or {""})[1]
-
- table.insert(items, {
- bufnr = bufnr,
- lnum = row + 1,
- col = col + 1,
- text = line .. " | " .. diag.message,
- type = loclist_type_map[diag.severity or DiagnosticSeverity.Error] or 'E',
- })
- end
-
- for _, diag in ipairs(buffer_diags) do
- insert_diag(diag)
+ local current_bufnr = api.nvim_get_current_buf()
+ local diags = opts.workspace and M.get_all(opts.client_id) or {
+ [current_bufnr] = M.get(current_bufnr, opts.client_id)
+ }
+ local predicate = function(d)
+ local severity = to_severity(opts.severity)
+ if severity then
+ return d.severity == severity
+ end
+ severity = to_severity(opts.severity_limit)
+ if severity then
+ return d.severity == severity
+ end
+ return true
end
-
- table.sort(items, function(a, b) return a.lnum < b.lnum end)
-
- util.set_loclist(items)
+ local items = util.diagnostics_to_items(diags, predicate)
+ local win_id = vim.api.nvim_get_current_win()
+ util.set_loclist(items, win_id)
if open_loclist then
vim.cmd [[lopen]]
end
diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua
index 525ec4ce5b..5d38150fc0 100644
--- a/runtime/lua/vim/lsp/handlers.lua
+++ b/runtime/lua/vim/lsp/handlers.lua
@@ -187,6 +187,10 @@ M['textDocument/publishDiagnostics'] = function(...)
return require('vim.lsp.diagnostic').on_publish_diagnostics(...)
end
+M['textDocument/codeLens'] = function(...)
+ return require('vim.lsp.codelens').on_codelens(...)
+end
+
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references
M['textDocument/references'] = function(_, _, result)
if not result then return end
@@ -260,24 +264,18 @@ end
--- - See |vim.api.nvim_open_win()|
function M.hover(_, method, result, _, _, config)
config = config or {}
- local bufnr, winnr = util.focusable_float(method, function()
- if not (result and result.contents) then
- -- return { 'No information available' }
- return
- end
- local markdown_lines = util.convert_input_to_markdown_lines(result.contents)
- markdown_lines = util.trim_empty_lines(markdown_lines)
- if vim.tbl_isempty(markdown_lines) then
- -- return { 'No information available' }
- return
- end
- local bufnr, winnr = util.fancy_floating_markdown(markdown_lines, {
- border = config.border
- })
- util.close_preview_autocmd({"CursorMoved", "BufHidden", "InsertCharPre"}, winnr)
- return bufnr, winnr
- end)
- return bufnr, winnr
+ config.focus_id = method
+ if not (result and result.contents) then
+ -- return { 'No information available' }
+ return
+ end
+ local markdown_lines = util.convert_input_to_markdown_lines(result.contents)
+ markdown_lines = util.trim_empty_lines(markdown_lines)
+ if vim.tbl_isempty(markdown_lines) then
+ -- return { 'No information available' }
+ return
+ end
+ return util.open_floating_preview(markdown_lines, "markdown", config)
end
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover
@@ -335,23 +333,21 @@ M['textDocument/implementation'] = location_handler
--- - See |vim.api.nvim_open_win()|
function M.signature_help(_, method, result, _, bufnr, config)
config = config or {}
+ config.focus_id = method
-- When use `autocmd CompleteDone <silent><buffer> lua vim.lsp.buf.signature_help()` to call signatureHelp handler
-- If the completion item doesn't have signatures It will make noise. Change to use `print` that can use `<silent>` to ignore
if not (result and result.signatures and result.signatures[1]) then
print('No signature help available')
return
end
- local lines = util.convert_signature_help_to_markdown_lines(result)
+ local ft = api.nvim_buf_get_option(bufnr, 'filetype')
+ local lines = util.convert_signature_help_to_markdown_lines(result, ft)
lines = util.trim_empty_lines(lines)
if vim.tbl_isempty(lines) then
print('No signature help available')
return
end
- local syntax = api.nvim_buf_get_option(bufnr, 'syntax')
- local p_bufnr, _ = util.focusable_preview(method, function()
- return lines, util.try_trim_markdown_code_blocks(lines), config
- end)
- api.nvim_buf_set_option(p_bufnr, 'syntax', syntax)
+ return util.open_floating_preview(lines, "markdown", config)
end
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp
diff --git a/runtime/lua/vim/lsp/log.lua b/runtime/lua/vim/lsp/log.lua
index 331e980e67..471a311c16 100644
--- a/runtime/lua/vim/lsp/log.lua
+++ b/runtime/lua/vim/lsp/log.lua
@@ -10,7 +10,7 @@ local log = {}
-- Can be used to lookup the number from the name or the name from the number.
-- Levels by name: 'trace', 'debug', 'info', 'warn', 'error'
-- Level numbers begin with 'trace' at 0
-log.levels = vim.log.levels
+log.levels = vim.deepcopy(vim.log.levels)
-- Default log level is warn.
local current_log_level = log.levels.WARN
diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua
index 1aa8326514..98835d6708 100644
--- a/runtime/lua/vim/lsp/rpc.lua
+++ b/runtime/lua/vim/lsp/rpc.lua
@@ -518,32 +518,38 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
send_response(decoded.id, err, result)
end)
-- This works because we are expecting vim.NIL here
- elseif decoded.id and (decoded.result or decoded.error) then
+ elseif decoded.id and (decoded.result ~= vim.NIL or decoded.error ~= vim.NIL) then
-- Server Result
decoded.error = convert_NIL(decoded.error)
decoded.result = convert_NIL(decoded.result)
+ -- We sent a number, so we expect a number.
+ local result_id = tonumber(decoded.id)
+
-- Do not surface RequestCancelled or ContentModified to users, it is RPC-internal.
if decoded.error then
+ local mute_error = false
if decoded.error.code == protocol.ErrorCodes.RequestCancelled then
local _ = log.debug() and log.debug("Received cancellation ack", decoded)
+ mute_error = true
elseif decoded.error.code == protocol.ErrorCodes.ContentModified then
local _ = log.debug() and log.debug("Received content modified ack", decoded)
+ mute_error = true
end
- local result_id = tonumber(decoded.id)
- -- Clear any callback since this is cancelled now.
- -- This is safe to do assuming that these conditions hold:
- -- - The server will not send a result callback after this cancellation.
- -- - If the server sent this cancellation ACK after sending the result, the user of this RPC
- -- client will ignore the result themselves.
- if result_id then
- message_callbacks[result_id] = nil
+
+ if mute_error then
+ -- Clear any callback since this is cancelled now.
+ -- This is safe to do assuming that these conditions hold:
+ -- - The server will not send a result callback after this cancellation.
+ -- - If the server sent this cancellation ACK after sending the result, the user of this RPC
+ -- client will ignore the result themselves.
+ if result_id then
+ message_callbacks[result_id] = nil
+ end
+ return
end
- return
end
- -- We sent a number, so we expect a number.
- local result_id = tonumber(decoded.id)
local callback = message_callbacks[result_id]
if callback then
message_callbacks[result_id] = nil
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index 92ec447b55..326005dac4 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -4,6 +4,7 @@ local validate = vim.validate
local api = vim.api
local list_extend = vim.list_extend
local highlight = require 'vim.highlight'
+local uv = vim.loop
local npcall = vim.F.npcall
local split = vim.split
@@ -29,6 +30,16 @@ local default_border = {
{" ", "NormalFloat"},
}
+
+local DiagnosticSeverity = protocol.DiagnosticSeverity
+local loclist_type_map = {
+ [DiagnosticSeverity.Error] = 'E',
+ [DiagnosticSeverity.Warning] = 'W',
+ [DiagnosticSeverity.Information] = 'I',
+ [DiagnosticSeverity.Hint] = 'I',
+}
+
+
--@private
-- Check the border given by opts or the default border for the additional
-- size it adds to a float.
@@ -39,14 +50,37 @@ local function get_border_size(opts)
local width = 0
if type(border) == 'string' then
- -- 'single', 'double', etc.
- height = 2
- width = 2
+ local border_size = {none = {0, 0}, single = {2, 2}, double = {2, 2}, rounded = {2, 2}, solid = {2, 2}, shadow = {1, 1}}
+ if border_size[border] == nil then
+ error("floating preview border is not correct. Please refer to the docs |vim.api.nvim_open_win()|"
+ .. vim.inspect(border))
+ end
+ height, width = unpack(border_size[border])
else
- height = height + vim.fn.strdisplaywidth(border[2][1]) -- top
- height = height + vim.fn.strdisplaywidth(border[6][1]) -- bottom
- width = width + vim.fn.strdisplaywidth(border[4][1]) -- right
- width = width + vim.fn.strdisplaywidth(border[8][1]) -- left
+ local function border_width(id)
+ if type(border[id]) == "table" then
+ -- border specified as a table of <character, highlight group>
+ return vim.fn.strdisplaywidth(border[id][1])
+ elseif type(border[id]) == "string" then
+ -- border specified as a list of border characters
+ return vim.fn.strdisplaywidth(border[id])
+ end
+ error("floating preview border is not correct. Please refer to the docs |vim.api.nvim_open_win()|" .. vim.inspect(border))
+ end
+ local function border_height(id)
+ if type(border[id]) == "table" then
+ -- border specified as a table of <character, highlight group>
+ return #border[id][1] > 0 and 1 or 0
+ elseif type(border[id]) == "string" then
+ -- border specified as a list of border characters
+ return #border[id] > 0 and 1 or 0
+ end
+ error("floating preview border is not correct. Please refer to the docs |vim.api.nvim_open_win()|" .. vim.inspect(border))
+ end
+ height = height + border_height(2) -- top
+ height = height + border_height(6) -- bottom
+ width = width + border_width(4) -- right
+ width = width + border_width(8) -- left
end
return { height = height, width = width }
@@ -73,7 +107,7 @@ function M.set_lines(lines, A, B, new_lines)
-- way the LSP describes the range including the last newline is by
-- specifying a line number after what we would call the last line.
local i_n = math.min(B[1] + 1, #lines)
- if not (i_0 >= 1 and i_0 <= #lines and i_n >= 1 and i_n <= #lines) then
+ if not (i_0 >= 1 and i_0 <= #lines + 1 and i_n >= 1 and i_n <= #lines) then
error("Invalid range: "..vim.inspect{A = A; B = B; #lines, new_lines})
end
local prefix = ""
@@ -566,13 +600,15 @@ end
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
local function get_completion_word(item)
if item.textEdit ~= nil and item.textEdit.newText ~= nil and item.textEdit.newText ~= "" then
- if protocol.InsertTextFormat[item.insertTextFormat] == "PlainText" then
+ local insert_text_format = protocol.InsertTextFormat[item.insertTextFormat]
+ if insert_text_format == "PlainText" or insert_text_format == nil then
return item.textEdit.newText
else
return M.parse_snippet(item.textEdit.newText)
end
elseif item.insertText ~= nil and item.insertText ~= "" then
- if protocol.InsertTextFormat[item.insertTextFormat] == "PlainText" then
+ local insert_text_format = protocol.InsertTextFormat[item.insertTextFormat]
+ if insert_text_format == "PlainText" or insert_text_format == nil then
return item.insertText
else
return M.parse_snippet(item.insertText)
@@ -770,14 +806,20 @@ function M.convert_input_to_markdown_lines(input, contents)
assert(type(input) == 'table', "Expected a table for Hover.contents")
-- MarkupContent
if input.kind then
- -- The kind can be either plaintext or markdown. However, either way we
- -- will just be rendering markdown, so we handle them both the same way.
- -- TODO these can have escaped/sanitized html codes in markdown. We
- -- should make sure we handle this correctly.
+ -- The kind can be either plaintext or markdown.
+ -- If it's plaintext, then wrap it in a <text></text> block
-- Some servers send input.value as empty, so let's ignore this :(
+ input.value = input.value or ''
+
+ if input.kind == "plaintext" then
+ -- wrap this in a <text></text> block so that stylize_markdown
+ -- can properly process it as plaintext
+ input.value = string.format("<text>\n%s\n</text>", input.value or "")
+ end
+
-- assert(type(input.value) == 'string')
- list_extend(contents, split_lines(input.value or ''))
+ list_extend(contents, split_lines(input.value))
-- MarkupString variation 2
elseif input.language then
-- Some servers send input.value as empty, so let's ignore this :(
@@ -802,9 +844,10 @@ end
--- Converts `textDocument/SignatureHelp` response to markdown lines.
---
--@param signature_help Response of `textDocument/SignatureHelp`
+--@param ft optional filetype that will be use as the `lang` for the label markdown code block
--@returns list of lines of converted markdown.
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp
-function M.convert_signature_help_to_markdown_lines(signature_help)
+function M.convert_signature_help_to_markdown_lines(signature_help, ft)
if not signature_help.signatures then
return
end
@@ -822,7 +865,12 @@ function M.convert_signature_help_to_markdown_lines(signature_help)
if not signature then
return
end
- vim.list_extend(contents, vim.split(signature.label, '\n', true))
+ local label = signature.label
+ if ft then
+ -- wrap inside a code block so fancy_markdown can render it properly
+ label = ("```%s\n%s\n```"):format(ft, label)
+ end
+ vim.list_extend(contents, vim.split(label, '\n', true))
if signature.documentation then
M.convert_input_to_markdown_lines(signature.documentation, contents)
end
@@ -906,6 +954,7 @@ function M.make_floating_popup_options(width, height, opts)
anchor = anchor,
col = col + (opts.offset_x or 0),
height = height,
+ focusable = opts.focusable,
relative = 'cursor',
row = row + (opts.offset_y or 0),
style = 'minimal',
@@ -914,23 +963,6 @@ function M.make_floating_popup_options(width, height, opts)
}
end
-local function _should_add_to_tagstack(new_item)
- local stack = vim.fn.gettagstack()
-
- -- Check if we're at the bottom of the tagstack.
- if stack.curidx <= 1 then return true end
-
- local top_item = stack.items[stack.curidx-1]
-
- -- Check if the item at the top of the tagstack is exactly the
- -- same as the one we want to push.
- if top_item.tagname ~= new_item.tagname then return true end
- for i, v in ipairs(top_item.from) do
- if v ~= new_item.from[i] then return true end
- end
- return false
-end
-
--- Jumps to a location.
---
--@param location (`Location`|`LocationLink`)
@@ -939,33 +971,22 @@ function M.jump_to_location(location)
-- location may be Location or LocationLink
local uri = location.uri or location.targetUri
if uri == nil then return end
+ local bufnr = vim.uri_to_bufnr(uri)
+ -- Save position in jumplist
+ vim.cmd "normal! m'"
- local from_bufnr = vim.fn.bufnr('%')
- local from = {from_bufnr, vim.fn.line('.'), vim.fn.col('.'), 0}
- local item = {tagname=vim.fn.expand('<cword>'), from=from}
+ -- Push a new item into tagstack
+ local from = {vim.fn.bufnr('%'), vim.fn.line('.'), vim.fn.col('.'), 0}
+ local items = {{tagname=vim.fn.expand('<cword>'), from=from}}
+ vim.fn.settagstack(vim.fn.win_getid(), {items=items}, 't')
--- Jump to new location (adjusting for UTF-16 encoding of characters)
- local bufnr = vim.uri_to_bufnr(uri)
api.nvim_set_current_buf(bufnr)
api.nvim_buf_set_option(0, 'buflisted', true)
local range = location.range or location.targetSelectionRange
local row = range.start.line
local col = get_line_byte_from_position(0, range.start)
- -- This prevents the tagstack to be filled with items that provide
- -- no motion when CTRL-T is pressed because they're both the source
- -- and the destination.
- local motionless =
- bufnr == from_bufnr and
- row+1 == from[2] and col+1 == from[3]
- if not motionless and _should_add_to_tagstack(item) then
- local winid = vim.fn.win_getid()
- local items = {item}
- vim.fn.settagstack(winid, {items=items}, 't')
- end
-
- -- Jump to new location
api.nvim_win_set_cursor(0, {row + 1, col})
-
return true
end
@@ -977,7 +998,7 @@ end
---
--@param location a single `Location` or `LocationLink`
--@returns (bufnr,winnr) buffer and window number of floating window or nil
-function M.preview_location(location)
+function M.preview_location(location, opts)
-- location may be LocationLink or Location (more useful for the former)
local uri = location.targetUri or location.uri
if uri == nil then return end
@@ -988,7 +1009,15 @@ function M.preview_location(location)
local range = location.targetRange or location.range
local contents = api.nvim_buf_get_lines(bufnr, range.start.line, range["end"].line+1, false)
local syntax = api.nvim_buf_get_option(bufnr, 'syntax')
- return M.open_floating_preview(contents, syntax)
+ if syntax == "" then
+ -- When no syntax is set, we use filetype as fallback. This might not result
+ -- in a valid syntax definition. See also ft detection in fancy_floating_win.
+ -- An empty syntax is more common now with TreeSitter, since TS disables syntax.
+ syntax = api.nvim_buf_get_option(bufnr, 'filetype')
+ end
+ opts = opts or {}
+ opts.focus_id = "location"
+ return M.open_floating_preview(contents, syntax, opts)
end
--@private
@@ -1010,7 +1039,9 @@ end
---buffer id, the newly created window will be the new focus associated with
---the current buffer via the tag `unique_name`.
--@returns (pbufnr, pwinnr) if `fn()` has created a new window; nil otherwise
+---@deprecated please use open_floating_preview directly
function M.focusable_float(unique_name, fn)
+ vim.notify("focusable_float is deprecated. Please use open_floating_preview and pass focus_id = [unique_name] instead", vim.log.levels.WARN)
-- Go back to previous window if we are in a focusable one
if npcall(api.nvim_win_get_var, 0, unique_name) then
return api.nvim_command("wincmd p")
@@ -1039,10 +1070,10 @@ end
--@param fn (function) The return values of this function will be passed
---directly to |vim.lsp.util.open_floating_preview()|, in the case that a new
---floating window should be created
+---@deprecated please use open_floating_preview directly
function M.focusable_preview(unique_name, fn)
- return M.focusable_float(unique_name, function()
- return M.open_floating_preview(fn())
- end)
+ vim.notify("focusable_preview is deprecated. Please use open_floating_preview and pass focus_id = [unique_name] instead", vim.log.levels.WARN)
+ return M.open_floating_preview(fn(), {focus_id = unique_name})
end
--- Trims empty lines from input and pad top and bottom with empty lines
@@ -1074,13 +1105,20 @@ end
--- TODO: refactor to separate stripping/converting and make use of open_floating_preview
---
+--- @deprecated please use open_floating_preview directly
+function M.fancy_floating_markdown(contents, opts)
+ vim.notify("fancy_floating_markdown is deprecated. Please use open_floating_preview and pass focus_id = [unique_name] instead", vim.log.levels.WARN)
+ return M.open_floating_preview(contents, "markdown", opts)
+end
+
--- Converts markdown into syntax highlighted regions by stripping the code
--- blocks and converting them into highlighted code.
--- This will by default insert a blank line separator after those code block
--- regions to improve readability.
---- The result is shown in a floating preview.
+---
+--- This method configures the given buffer and returns the lines to set.
+---
+--- If you want to open a popup with fancy markdown, use `open_floating_preview` instead
---
---@param contents table of lines to show in window
---@param opts dictionary with optional fields
@@ -1095,29 +1133,57 @@ end
--- - pad_bottom number of lines to pad contents at bottom
--- - separator insert separator after code block
---@returns width,height size of float
-function M.fancy_floating_markdown(contents, opts)
+function M.stylize_markdown(bufnr, contents, opts)
validate {
contents = { contents, 't' };
opts = { opts, 't', true };
}
opts = opts or {}
+ -- table of fence types to {ft, begin, end}
+ -- when ft is nil, we get the ft from the regex match
+ local matchers = {
+ block = {nil, "```+([a-zA-Z0-9_]*)", "```+"},
+ pre = {"", "<pre>", "</pre>"},
+ code = {"", "<code>", "</code>"},
+ text = {"plaintex", "<text>", "</text>"},
+ }
+
+ local match_begin = function(line)
+ for type, pattern in pairs(matchers) do
+ local ret = line:match(string.format("^%%s*%s%%s*$", pattern[2]))
+ if ret then
+ return {
+ type = type,
+ ft = pattern[1] or ret
+ }
+ end
+ end
+ end
+
+ local match_end = function(line, match)
+ local pattern = matchers[match.type]
+ return line:match(string.format("^%%s*%s%%s*$", pattern[3]))
+ end
+
+ -- Clean up
+ contents = M._trim(contents, opts)
+
local stripped = {}
local highlights = {}
+ -- keep track of lnums that contain markdown
+ local markdown_lines = {}
do
local i = 1
while i <= #contents do
local line = contents[i]
- -- TODO(ashkan): use a more strict regex for filetype?
- local ft = line:match("^```([a-zA-Z0-9_]*)$")
- -- local ft = line:match("^```(.*)$")
- -- TODO(ashkan): validate the filetype here.
- if ft then
+ local match = match_begin(line)
+ if match then
local start = #stripped
i = i + 1
while i <= #contents do
line = contents[i]
- if line == "```" then
+ if match_end(line, match) then
i = i + 1
break
end
@@ -1125,76 +1191,96 @@ function M.fancy_floating_markdown(contents, opts)
i = i + 1
end
table.insert(highlights, {
- ft = ft;
+ ft = match.ft;
start = start + 1;
finish = #stripped + 1 - 1;
})
else
table.insert(stripped, line)
+ markdown_lines[#stripped] = true
i = i + 1
end
end
end
- -- Clean up
- stripped = M._trim(stripped, opts)
-- Compute size of float needed to show (wrapped) lines
opts.wrap_at = opts.wrap_at or (vim.wo["wrap"] and api.nvim_win_get_width(0))
local width, height = M._make_floating_popup_size(stripped, opts)
+ local sep_line = string.rep("─", math.min(width, opts.wrap_at or width))
+
+ for l in pairs(markdown_lines) do
+ if stripped[l]:match("^---+$") then
+ stripped[l] = sep_line
+ end
+ end
+
-- Insert blank line separator after code block
local insert_separator = opts.separator
if insert_separator == nil then insert_separator = true end
if insert_separator then
- for i, h in ipairs(highlights) do
- h.start = h.start + i - 1
- h.finish = h.finish + i - 1
+ local offset = 0
+ for _, h in ipairs(highlights) do
+ h.start = h.start + offset
+ h.finish = h.finish + offset
+ -- check if a seperator already exists and use that one instead of creating a new one
if h.finish + 1 <= #stripped then
- table.insert(stripped, h.finish + 1, string.rep("─", math.min(width, opts.wrap_at or width)))
- height = height + 1
+ if stripped[h.finish + 1] ~= sep_line then
+ table.insert(stripped, h.finish + 1, sep_line)
+ offset = offset + 1
+ height = height + 1
+ end
end
end
end
- -- Make the floating window.
- local bufnr = api.nvim_create_buf(false, true)
- local winnr = api.nvim_open_win(bufnr, false, M.make_floating_popup_options(width, height, opts))
- vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, stripped)
- api.nvim_buf_set_option(bufnr, 'modifiable', false)
- -- Switch to the floating window to apply the syntax highlighting.
- -- This is because the syntax command doesn't accept a target.
- local cwin = vim.api.nvim_get_current_win()
- vim.api.nvim_set_current_win(winnr)
- api.nvim_win_set_option(winnr, 'conceallevel', 2)
- api.nvim_win_set_option(winnr, 'concealcursor', 'n')
+ vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, stripped)
- vim.cmd("ownsyntax lsp_markdown")
local idx = 1
--@private
+ -- keep track of syntaxes we already inlcuded.
+ -- no need to include the same syntax more than once
+ local langs = {}
local function apply_syntax_to_region(ft, start, finish)
- if ft == '' then return end
+ if ft == "" then
+ vim.cmd(string.format("syntax region markdownCode start=+\\%%%dl+ end=+\\%%%dl+ keepend extend", start, finish + 1))
+ return
+ end
local name = ft..idx
idx = idx + 1
local lang = "@"..ft:upper()
- -- TODO(ashkan): better validation before this.
- if not pcall(vim.cmd, string.format("syntax include %s syntax/%s.vim", lang, ft)) then
- return
+ if not langs[lang] then
+ -- HACK: reset current_syntax, since some syntax files like markdown won't load if it is already set
+ pcall(vim.api.nvim_buf_del_var, bufnr, "current_syntax")
+ -- TODO(ashkan): better validation before this.
+ if not pcall(vim.cmd, string.format("syntax include %s syntax/%s.vim", lang, ft)) then
+ return
+ end
+ langs[lang] = true
end
- vim.cmd(string.format("syntax region %s start=+\\%%%dl+ end=+\\%%%dl+ contains=%s", name, start, finish + 1, lang))
- end
- -- Previous highlight region.
- -- TODO(ashkan): this wasn't working for some reason, but I would like to
- -- make sure that regions between code blocks are definitely markdown.
- -- local ph = {start = 0; finish = 1;}
- for _, h in ipairs(highlights) do
- -- apply_syntax_to_region('markdown', ph.finish, h.start)
- apply_syntax_to_region(h.ft, h.start, h.finish)
- -- ph = h
+ vim.cmd(string.format("syntax region %s start=+\\%%%dl+ end=+\\%%%dl+ contains=%s keepend", name, start, finish + 1, lang))
end
- vim.api.nvim_set_current_win(cwin)
- return bufnr, winnr
+ -- needs to run in the buffer for the regions to work
+ api.nvim_buf_call(bufnr, function()
+ -- we need to apply lsp_markdown regions speperately, since otherwise
+ -- markdown regions can "bleed" through the other syntax regions
+ -- and mess up the formatting
+ local last = 1
+ for _, h in ipairs(highlights) do
+ if last < h.start then
+ apply_syntax_to_region("lsp_markdown", last, h.start - 1)
+ end
+ apply_syntax_to_region(h.ft, h.start, h.finish)
+ last = h.finish + 1
+ end
+ if last < #stripped then
+ apply_syntax_to_region("lsp_markdown", last, #stripped)
+ end
+ end)
+
+ return stripped
end
--- Creates autocommands to close a preview window when events happen.
@@ -1203,7 +1289,9 @@ end
--@param winnr (number) window id of preview window
--@see |autocmd-events|
function M.close_preview_autocmd(events, winnr)
- api.nvim_command("autocmd "..table.concat(events, ',').." <buffer> ++once lua pcall(vim.api.nvim_win_close, "..winnr..", true)")
+ if #events > 0 then
+ api.nvim_command("autocmd "..table.concat(events, ',').." <buffer> ++once lua pcall(vim.api.nvim_win_close, "..winnr..", true)")
+ end
end
--@internal
@@ -1286,15 +1374,19 @@ end
--@param contents table of lines to show in window
--@param syntax string of syntax to set for opened buffer
--@param opts dictionary with optional fields
--- - height of floating window
--- - width of floating window
--- - wrap_at character to wrap at for computing height
--- - max_width maximal width of floating window
--- - max_height maximal height of floating window
--- - pad_left number of columns to pad contents at left
--- - pad_right number of columns to pad contents at right
--- - pad_top number of lines to pad contents at top
--- - pad_bottom number of lines to pad contents at bottom
+--- - height of floating window
+--- - width of floating window
+--- - wrap boolean enable wrapping of long lines (defaults to true)
+--- - wrap_at character to wrap at for computing height when wrap is enabled
+--- - max_width maximal width of floating window
+--- - max_height maximal height of floating window
+--- - pad_left number of columns to pad contents at left
+--- - pad_right number of columns to pad contents at right
+--- - pad_top number of lines to pad contents at top
+--- - pad_bottom number of lines to pad contents at bottom
+--- - focus_id if a popup with this id is opened, then focus it
+--- - close_events list of events that closes the floating window
+--- - focusable (boolean, default true): Make float focusable
--@returns bufnr,winnr buffer and window number of the newly created floating
---preview window
function M.open_floating_preview(contents, syntax, opts)
@@ -1304,27 +1396,85 @@ function M.open_floating_preview(contents, syntax, opts)
opts = { opts, 't', true };
}
opts = opts or {}
+ opts.wrap = opts.wrap ~= false -- wrapping by default
+ opts.stylize_markdown = opts.stylize_markdown ~= false
+ opts.close_events = opts.close_events or {"CursorMoved", "CursorMovedI", "BufHidden", "InsertCharPre"}
+
+ local bufnr = api.nvim_get_current_buf()
+
+ -- check if this popup is focusable and we need to focus
+ if opts.focus_id and opts.focusable ~= false then
+ -- Go back to previous window if we are in a focusable one
+ local current_winnr = api.nvim_get_current_win()
+ if npcall(api.nvim_win_get_var, current_winnr, opts.focus_id) then
+ api.nvim_command("wincmd p")
+ return bufnr, current_winnr
+ end
+ do
+ local win = find_window_by_var(opts.focus_id, bufnr)
+ if win and api.nvim_win_is_valid(win) and vim.fn.pumvisible() == 0 then
+ -- focus and return the existing buf, win
+ api.nvim_set_current_win(win)
+ api.nvim_command("stopinsert")
+ return api.nvim_win_get_buf(win), win
+ end
+ end
+ end
+
+ -- check if another floating preview already exists for this buffer
+ -- and close it if needed
+ local existing_float = npcall(api.nvim_buf_get_var, bufnr, "lsp_floating_preview")
+ if existing_float and api.nvim_win_is_valid(existing_float) then
+ api.nvim_win_close(existing_float, true)
+ end
+
+ local floating_bufnr = api.nvim_create_buf(false, true)
+ local do_stylize = syntax == "markdown" and opts.stylize_markdown
+
-- Clean up input: trim empty lines from the end, pad
contents = M._trim(contents, opts)
+ if do_stylize then
+ -- applies the syntax and sets the lines to the buffer
+ contents = M.stylize_markdown(floating_bufnr, contents, opts)
+ else
+ if syntax then
+ api.nvim_buf_set_option(floating_bufnr, 'syntax', syntax)
+ end
+ api.nvim_buf_set_lines(floating_bufnr, 0, -1, true, contents)
+ end
+
-- Compute size of float needed to show (wrapped) lines
- opts.wrap_at = opts.wrap_at or (vim.wo["wrap"] and api.nvim_win_get_width(0))
+ if opts.wrap then
+ opts.wrap_at = opts.wrap_at or api.nvim_win_get_width(0)
+ else
+ opts.wrap_at = nil
+ end
local width, height = M._make_floating_popup_size(contents, opts)
- local floating_bufnr = api.nvim_create_buf(false, true)
- if syntax then
- api.nvim_buf_set_option(floating_bufnr, 'syntax', syntax)
- end
local float_option = M.make_floating_popup_options(width, height, opts)
local floating_winnr = api.nvim_open_win(floating_bufnr, false, float_option)
- if syntax == 'markdown' then
+ if do_stylize then
api.nvim_win_set_option(floating_winnr, 'conceallevel', 2)
+ api.nvim_win_set_option(floating_winnr, 'concealcursor', 'n')
end
- api.nvim_buf_set_lines(floating_bufnr, 0, -1, true, contents)
+ -- disable folding
+ api.nvim_win_set_option(floating_winnr, 'foldenable', false)
+ -- soft wrapping
+ api.nvim_win_set_option(floating_winnr, 'wrap', opts.wrap)
+
api.nvim_buf_set_option(floating_bufnr, 'modifiable', false)
api.nvim_buf_set_option(floating_bufnr, 'bufhidden', 'wipe')
- M.close_preview_autocmd({"CursorMoved", "CursorMovedI", "BufHidden", "BufLeave"}, floating_winnr)
+ api.nvim_buf_set_keymap(floating_bufnr, "n", "q", "<cmd>bdelete<cr>", {silent = true, noremap = true})
+ M.close_preview_autocmd(opts.close_events, floating_winnr)
+
+ -- save focus_id
+ if opts.focus_id then
+ api.nvim_win_set_var(floating_winnr, opts.focus_id, bufnr)
+ end
+ api.nvim_buf_set_var(bufnr, "lsp_floating_preview", floating_winnr)
+
return floating_bufnr, floating_winnr
end
@@ -1363,6 +1513,88 @@ local position_sort = sort_by_key(function(v)
return {v.start.line, v.start.character}
end)
+-- Gets the zero-indexed line from the given uri.
+-- For non-file uris, we load the buffer and get the line.
+-- If a loaded buffer exists, then that is used.
+-- Otherwise we get the line using libuv which is a lot faster than loading the buffer.
+--@param uri string uri of the resource to get the line from
+--@param row number zero-indexed line number
+--@return string the line at row in filename
+function M.get_line(uri, row)
+ return M.get_lines(uri, { row })[row]
+end
+
+-- Gets the zero-indexed lines from the given uri.
+-- For non-file uris, we load the buffer and get the lines.
+-- If a loaded buffer exists, then that is used.
+-- Otherwise we get the lines using libuv which is a lot faster than loading the buffer.
+--@param uri string uri of the resource to get the lines from
+--@param rows number[] zero-indexed line numbers
+--@return table<number string> a table mapping rows to lines
+function M.get_lines(uri, rows)
+ rows = type(rows) == "table" and rows or { rows }
+
+ local function buf_lines(bufnr)
+ local lines = {}
+ for _, row in pairs(rows) do
+ lines[row] = (vim.api.nvim_buf_get_lines(bufnr, row, row + 1, false) or { "" })[1]
+ end
+ return lines
+ end
+
+ -- load the buffer if this is not a file uri
+ -- Custom language server protocol extensions can result in servers sending URIs with custom schemes. Plugins are able to load these via `BufReadCmd` autocmds.
+ if uri:sub(1, 4) ~= "file" then
+ local bufnr = vim.uri_to_bufnr(uri)
+ vim.fn.bufload(bufnr)
+ return buf_lines(bufnr)
+ end
+
+ local filename = vim.uri_to_fname(uri)
+
+ -- use loaded buffers if available
+ if vim.fn.bufloaded(filename) == 1 then
+ local bufnr = vim.fn.bufnr(filename, false)
+ return buf_lines(bufnr)
+ end
+
+ -- get the data from the file
+ local fd = uv.fs_open(filename, "r", 438)
+ if not fd then return "" end
+ local stat = uv.fs_fstat(fd)
+ local data = uv.fs_read(fd, stat.size, 0)
+ uv.fs_close(fd)
+
+ local lines = {} -- rows we need to retrieve
+ local need = 0 -- keep track of how many unique rows we need
+ for _, row in pairs(rows) do
+ if not lines[row] then
+ need = need + 1
+ end
+ lines[row] = true
+ end
+
+ local found = 0
+ local lnum = 0
+
+ for line in string.gmatch(data, "([^\n]*)\n?") do
+ if lines[lnum] == true then
+ lines[lnum] = line
+ found = found + 1
+ if found == need then break end
+ end
+ lnum = lnum + 1
+ end
+
+ -- change any lines we didn't find to the empty string
+ for i, line in pairs(lines) do
+ if line == true then
+ lines[i] = ""
+ end
+ end
+ return lines
+end
+
--- Returns the items with the byte position calculated correctly and in sorted
--- order, for display in quickfix and location lists.
---
@@ -1391,14 +1623,24 @@ function M.locations_to_items(locations)
for _, uri in ipairs(keys) do
local rows = grouped[uri]
table.sort(rows, position_sort)
- local bufnr = vim.uri_to_bufnr(uri)
- vim.fn.bufload(bufnr)
local filename = vim.uri_to_fname(uri)
+
+ -- list of row numbers
+ local uri_rows = {}
for _, temp in ipairs(rows) do
local pos = temp.start
local row = pos.line
- local line = (api.nvim_buf_get_lines(bufnr, row, row + 1, false) or {""})[1]
- local col = M.character_offset(bufnr, row, pos.character)
+ table.insert(uri_rows, row)
+ end
+
+ -- get all the lines for this uri
+ local lines = M.get_lines(uri, uri_rows)
+
+ for _, temp in ipairs(rows) do
+ local pos = temp.start
+ local row = pos.line
+ local line = lines[row] or ""
+ local col = pos.character
table.insert(items, {
filename = filename,
lnum = row + 1,
@@ -1410,12 +1652,13 @@ function M.locations_to_items(locations)
return items
end
---- Fills current window's location list with given list of items.
+--- Fills target window's location list with given list of items.
--- Can be obtained with e.g. |vim.lsp.util.locations_to_items()|.
+--- Defaults to current window.
---
--@param items (table) list of items
-function M.set_loclist(items)
- vim.fn.setloclist(0, {}, ' ', {
+function M.set_loclist(items, win_id)
+ vim.fn.setloclist(win_id or 0, {}, ' ', {
title = 'Language Server';
items = items;
})
@@ -1456,13 +1699,13 @@ function M.symbols_to_items(symbols, bufnr)
kind = kind,
text = '['..kind..'] '..symbol.name,
})
- elseif symbol.range then -- DocumentSymbole type
+ elseif symbol.selectionRange then -- DocumentSymbole type
local kind = M._get_symbol_kind_name(symbol.kind)
table.insert(_items, {
-- bufnr = _bufnr,
filename = vim.api.nvim_buf_get_name(_bufnr),
- lnum = symbol.range.start.line + 1,
- col = symbol.range.start.character + 1,
+ lnum = symbol.selectionRange.start.line + 1,
+ col = symbol.selectionRange.start.character + 1,
kind = kind,
text = '['..kind..'] '..symbol.name
})
@@ -1592,6 +1835,12 @@ function M.make_given_range_params(start_pos, end_pos)
if B[2] > 0 then
B = {B[1], M.character_offset(0, B[1], B[2])}
end
+ -- we need to offset the end character position otherwise we loose the last
+ -- character of the selection, as LSP end position is exclusive
+ -- see https://microsoft.github.io/language-server-protocol/specification#range
+ if vim.o.selection ~= 'exclusive' then
+ B[2] = B[2] + 1
+ end
return {
textDocument = M.make_text_document_params(),
range = {
@@ -1650,8 +1899,9 @@ end
--@param row 0-indexed line
--@param col 0-indexed byte offset in line
--@returns (number, number) UTF-32 and UTF-16 index of the character in line {row} column {col} in buffer {buf}
-function M.character_offset(buf, row, col)
- local line = api.nvim_buf_get_lines(buf, row, row+1, true)[1]
+function M.character_offset(bufnr, row, col)
+ local uri = vim.uri_from_bufnr(bufnr)
+ local line = M.get_line(uri, row)
-- If the col is past the EOL, use the line length.
if col > #line then
return str_utfindex(line)
@@ -1674,6 +1924,40 @@ function M.lookup_section(settings, section)
return settings
end
+
+--- Convert diagnostics grouped by bufnr to a list of items for use in the
+--- quickfix or location list.
+---
+--@param diagnostics_by_bufnr table bufnr -> Diagnostic[]
+--@param predicate an optional function to filter the diagnostics.
+-- If present, only diagnostic items matching will be included.
+--@return table (A list of items)
+function M.diagnostics_to_items(diagnostics_by_bufnr, predicate)
+ local items = {}
+ for bufnr, diagnostics in pairs(diagnostics_by_bufnr or {}) do
+ for _, d in pairs(diagnostics) do
+ if not predicate or predicate(d) then
+ table.insert(items, {
+ bufnr = bufnr,
+ lnum = d.range.start.line + 1,
+ col = d.range.start.character + 1,
+ text = d.message,
+ type = loclist_type_map[d.severity or DiagnosticSeverity.Error] or 'E'
+ })
+ end
+ end
+ end
+ table.sort(items, function(a, b)
+ if a.bufnr == b.bufnr then
+ return a.lnum < b.lnum
+ else
+ return a.bufnr < b.bufnr
+ end
+ end)
+ return items
+end
+
+
M._get_line_byte_from_position = get_line_byte_from_position
M._warn_once = warn_once