aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/lsp
diff options
context:
space:
mode:
authorJosh Rahm <joshuarahm@gmail.com>2023-01-25 18:31:31 +0000
committerJosh Rahm <joshuarahm@gmail.com>2023-01-25 18:31:31 +0000
commit9243becbedbb6a1592208051f8fa2b090dcc5e7d (patch)
tree607c2a862ec3f4399b8766383f6f8e04c4aa43b4 /runtime/lua/vim/lsp
parent9e40b6e9e1bc67f2d856adb837ee64dd0e25b717 (diff)
parent3c48d3c83fc21dbc0841f9210f04bdb073d73cd1 (diff)
downloadrneovim-usermarks.tar.gz
rneovim-usermarks.tar.bz2
rneovim-usermarks.zip
Merge remote-tracking branch 'upstream/master' into usermarksusermarks
Diffstat (limited to 'runtime/lua/vim/lsp')
-rw-r--r--runtime/lua/vim/lsp/buf.lua316
-rw-r--r--runtime/lua/vim/lsp/codelens.lua27
-rw-r--r--runtime/lua/vim/lsp/diagnostic.lua2
-rw-r--r--runtime/lua/vim/lsp/handlers.lua145
-rw-r--r--runtime/lua/vim/lsp/health.lua16
-rw-r--r--runtime/lua/vim/lsp/log.lua17
-rw-r--r--runtime/lua/vim/lsp/protocol.lua97
-rw-r--r--runtime/lua/vim/lsp/rpc.lua741
-rw-r--r--runtime/lua/vim/lsp/semantic_tokens.lua702
-rw-r--r--runtime/lua/vim/lsp/sync.lua2
-rw-r--r--runtime/lua/vim/lsp/util.lua207
11 files changed, 1602 insertions, 670 deletions
diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua
index 6a070928d9..6ac885c78f 100644
--- a/runtime/lua/vim/lsp/buf.lua
+++ b/runtime/lua/vim/lsp/buf.lua
@@ -1,4 +1,3 @@
-local vim = vim
local api = vim.api
local validate = vim.validate
local util = require('vim.lsp.util')
@@ -111,7 +110,7 @@ end
--- about the context in which a completion was triggered (how it was triggered,
--- and by which trigger character, if applicable)
---
----@see |vim.lsp.protocol.constants.CompletionTriggerKind|
+---@see vim.lsp.protocol.constants.CompletionTriggerKind
function M.completion(context)
local params = util.make_position_params()
params.context = context
@@ -119,35 +118,30 @@ function M.completion(context)
end
---@private
---- If there is more than one client that supports the given method,
---- asks the user to select one.
---
----@returns The client that the user selected or nil
-local function select_client(method, on_choice)
- validate({
- on_choice = { on_choice, 'function', false },
- })
- local clients = vim.tbl_values(vim.lsp.buf_get_clients())
- clients = vim.tbl_filter(function(client)
- return client.supports_method(method)
- end, clients)
- -- better UX when choices are always in the same order (between restarts)
- table.sort(clients, function(a, b)
- return a.name < b.name
- end)
-
- if #clients > 1 then
- vim.ui.select(clients, {
- prompt = 'Select a language server:',
- format_item = function(client)
- return client.name
- end,
- }, on_choice)
- elseif #clients < 1 then
- on_choice(nil)
- else
- on_choice(clients[1])
- end
+---@return table {start={row, col}, end={row, col}} using (1, 0) indexing
+local function range_from_selection()
+ -- TODO: Use `vim.region()` instead https://github.com/neovim/neovim/pull/13896
+
+ -- [bufnum, lnum, col, off]; both row and column 1-indexed
+ local start = vim.fn.getpos('v')
+ local end_ = vim.fn.getpos('.')
+ local start_row = start[2]
+ local start_col = start[3]
+ local end_row = end_[2]
+ local end_col = end_[3]
+
+ -- A user can start visual selection at the end and move backwards
+ -- Normalize the range to start < end
+ if start_row == end_row and end_col < start_col then
+ end_col, start_col = start_col, end_col
+ elseif end_row < start_row then
+ start_row, end_row = end_row, start_row
+ start_col, end_col = end_col, start_col
+ end
+ return {
+ ['start'] = { start_row, start_col - 1 },
+ ['end'] = { end_row, end_col - 1 },
+ }
end
--- Formats a buffer using the attached (and optionally filtered) language
@@ -168,11 +162,11 @@ end
--- Predicate used to filter clients. Receives a client as argument and must return a
--- boolean. Clients matching the predicate are included. Example:
---
---- <pre>
---- -- Never request typescript-language-server for formatting
---- vim.lsp.buf.format {
---- filter = function(client) return client.name ~= "tsserver" end
---- }
+--- <pre>lua
+--- -- Never request typescript-language-server for formatting
+--- vim.lsp.buf.format {
+--- filter = function(client) return client.name ~= "tsserver" end
+--- }
--- </pre>
---
--- - async boolean|nil
@@ -184,7 +178,12 @@ end
--- Restrict formatting to the client with ID (client.id) matching this field.
--- - name (string|nil):
--- Restrict formatting to the client with name (client.name) matching this field.
-
+---
+--- - range (table|nil) Range to format.
+--- Table must contain `start` and `end` keys with {row, col} tuples using
+--- (1,0) indexing.
+--- Defaults to current selection in visual mode
+--- Defaults to `nil` in other modes, formatting the full buffer
function M.format(options)
options = options or {}
local bufnr = options.bufnr or api.nvim_get_current_buf()
@@ -198,24 +197,40 @@ function M.format(options)
clients = vim.tbl_filter(options.filter, clients)
end
+ local mode = api.nvim_get_mode().mode
+ local range = options.range
+ if not range and mode == 'v' or mode == 'V' then
+ range = range_from_selection()
+ end
+ local method = range and 'textDocument/rangeFormatting' or 'textDocument/formatting'
+
clients = vim.tbl_filter(function(client)
- return client.supports_method('textDocument/formatting')
+ return client.supports_method(method)
end, clients)
if #clients == 0 then
vim.notify('[LSP] Format request failed, no matching language servers.')
end
+ ---@private
+ local function set_range(client, params)
+ if range then
+ local range_params =
+ util.make_given_range_params(range.start, range['end'], bufnr, client.offset_encoding)
+ params.range = range_params.range
+ end
+ return params
+ end
+
if options.async then
local do_format
do_format = function(idx, client)
if not client then
return
end
- local params = util.make_formatting_params(options.formatting_options)
- client.request('textDocument/formatting', params, function(...)
- local handler = client.handlers['textDocument/formatting']
- or vim.lsp.handlers['textDocument/formatting']
+ local params = set_range(client, util.make_formatting_params(options.formatting_options))
+ client.request(method, params, function(...)
+ local handler = client.handlers[method] or vim.lsp.handlers[method]
handler(...)
do_format(next(clients, idx))
end, bufnr)
@@ -224,8 +239,8 @@ function M.format(options)
else
local timeout_ms = options.timeout_ms or 1000
for _, client in pairs(clients) do
- local params = util.make_formatting_params(options.formatting_options)
- local result, err = client.request_sync('textDocument/formatting', params, timeout_ms, bufnr)
+ local params = set_range(client, util.make_formatting_params(options.formatting_options))
+ local result, err = client.request_sync(method, params, timeout_ms, bufnr)
if result and result.result then
util.apply_text_edits(result.result, bufnr, client.offset_encoding)
elseif err then
@@ -235,138 +250,6 @@ function M.format(options)
end
end
---- Formats the current buffer.
----
----@param options (table|nil) Can be used to specify FormattingOptions.
---- Some unspecified options will be automatically derived from the current
---- Neovim options.
---
----@see https://microsoft.github.io/language-server-protocol/specification#textDocument_formatting
-function M.formatting(options)
- vim.notify_once(
- 'vim.lsp.buf.formatting is deprecated. Use vim.lsp.buf.format { async = true } instead',
- vim.log.levels.WARN
- )
- local params = util.make_formatting_params(options)
- local bufnr = api.nvim_get_current_buf()
- select_client('textDocument/formatting', function(client)
- if client == nil then
- return
- end
-
- return client.request('textDocument/formatting', params, nil, bufnr)
- end)
-end
-
---- Performs |vim.lsp.buf.formatting()| synchronously.
----
---- Useful for running on save, to make sure buffer is formatted prior to being
---- saved. {timeout_ms} is passed on to |vim.lsp.buf_request_sync()|. Example:
----
---- <pre>
---- autocmd BufWritePre <buffer> lua vim.lsp.buf.formatting_sync()
---- </pre>
----
----@param options table|nil with valid `FormattingOptions` entries
----@param timeout_ms (number) Request timeout
----@see |vim.lsp.buf.formatting_seq_sync|
-function M.formatting_sync(options, timeout_ms)
- vim.notify_once(
- 'vim.lsp.buf.formatting_sync is deprecated. Use vim.lsp.buf.format instead',
- vim.log.levels.WARN
- )
- local params = util.make_formatting_params(options)
- local bufnr = api.nvim_get_current_buf()
- select_client('textDocument/formatting', function(client)
- if client == nil then
- return
- end
-
- local result, err = client.request_sync('textDocument/formatting', params, timeout_ms, bufnr)
- if result and result.result then
- util.apply_text_edits(result.result, bufnr, client.offset_encoding)
- elseif err then
- vim.notify('vim.lsp.buf.formatting_sync: ' .. err, vim.log.levels.WARN)
- end
- end)
-end
-
---- Formats the current buffer by sequentially requesting formatting from attached clients.
----
---- Useful when multiple clients with formatting capability are attached.
----
---- Since it's synchronous, can be used for running on save, to make sure buffer is formatted
---- prior to being saved. {timeout_ms} is passed on to the |vim.lsp.client| `request_sync` method.
---- Example:
---- <pre>
---- vim.api.nvim_command[[autocmd BufWritePre <buffer> lua vim.lsp.buf.formatting_seq_sync()]]
---- </pre>
----
----@param options (table|nil) `FormattingOptions` entries
----@param timeout_ms (number|nil) Request timeout
----@param order (table|nil) List of client names. Formatting is requested from clients
----in the following order: first all clients that are not in the `order` list, then
----the remaining clients in the order as they occur in the `order` list.
-function M.formatting_seq_sync(options, timeout_ms, order)
- vim.notify_once(
- 'vim.lsp.buf.formatting_seq_sync is deprecated. Use vim.lsp.buf.format instead',
- vim.log.levels.WARN
- )
- local clients = vim.tbl_values(vim.lsp.buf_get_clients())
- local bufnr = api.nvim_get_current_buf()
-
- -- sort the clients according to `order`
- for _, client_name in pairs(order or {}) do
- -- if the client exists, move to the end of the list
- for i, client in pairs(clients) do
- if client.name == client_name then
- table.insert(clients, table.remove(clients, i))
- break
- end
- end
- end
-
- -- loop through the clients and make synchronous formatting requests
- for _, client in pairs(clients) do
- if vim.tbl_get(client.server_capabilities, 'documentFormattingProvider') then
- local params = util.make_formatting_params(options)
- local result, err = client.request_sync(
- 'textDocument/formatting',
- params,
- timeout_ms,
- api.nvim_get_current_buf()
- )
- if result and result.result then
- util.apply_text_edits(result.result, bufnr, client.offset_encoding)
- elseif err then
- vim.notify(
- string.format('vim.lsp.buf.formatting_seq_sync: (%s) %s', client.name, err),
- vim.log.levels.WARN
- )
- end
- end
- end
-end
-
---- Formats a given range.
----
----@param options Table with valid `FormattingOptions` entries.
----@param start_pos ({number, number}, optional) mark-indexed position.
----Defaults to the start of the last visual selection.
----@param end_pos ({number, number}, optional) mark-indexed position.
----Defaults to the end of the last visual selection.
-function M.range_formatting(options, start_pos, end_pos)
- local params = util.make_given_range_params(start_pos, end_pos)
- params.options = util.make_formatting_params(options).options
- select_client('textDocument/rangeFormatting', function(client)
- if client == nil then
- return
- end
-
- return client.request('textDocument/rangeFormatting', params)
- end)
-end
-
--- Renames all references to the symbol under the cursor.
---
---@param new_name string|nil If not provided, the user will be prompted for a new
@@ -500,7 +383,7 @@ end
--- Lists all the references to the symbol under the cursor in the quickfix window.
---
----@param context (table) Context for the request
+---@param context (table|nil) Context for the request
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references
---@param options table|nil additional options
--- - on_list: (function) handler for list results. See |lsp-on-list-handler|
@@ -565,14 +448,14 @@ end
--- Lists all the call sites of the symbol under the cursor in the
--- |quickfix| window. If the symbol can resolve to multiple
---- items, the user can pick one in the |inputlist|.
+--- items, the user can pick one in the |inputlist()|.
function M.incoming_calls()
call_hierarchy('callHierarchy/incomingCalls')
end
--- Lists all the items that are called by the symbol under the
--- cursor in the |quickfix| window. If the symbol can resolve to
---- multiple items, the user can pick one in the |inputlist|.
+--- multiple items, the user can pick one in the |inputlist()|.
function M.outgoing_calls()
call_hierarchy('callHierarchy/outgoingCalls')
end
@@ -581,7 +464,7 @@ end
---
function M.list_workspace_folders()
local workspace_folders = {}
- for _, client in pairs(vim.lsp.buf_get_clients()) do
+ for _, client in pairs(vim.lsp.get_active_clients({ bufnr = 0 })) do
for _, folder in pairs(client.workspace_folders or {}) do
table.insert(workspace_folders, folder.name)
end
@@ -604,9 +487,9 @@ function M.add_workspace_folder(workspace_folder)
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 _, client in pairs(vim.lsp.get_active_clients({ bufnr = 0 })) do
local found = false
for _, folder in pairs(client.workspace_folders or {}) do
if folder.name == workspace_folder then
@@ -639,7 +522,7 @@ function M.remove_workspace_folder(workspace_folder)
{ {} },
{ { uri = vim.uri_from_fname(workspace_folder), name = workspace_folder } }
)
- for _, client in pairs(vim.lsp.buf_get_clients()) do
+ for _, client in pairs(vim.lsp.get_active_clients({ bufnr = 0 })) do
for idx, folder in pairs(client.workspace_folders) do
if folder.name == workspace_folder then
vim.lsp.buf_notify(0, 'workspace/didChangeWorkspaceFolders', params)
@@ -672,18 +555,17 @@ 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`, e.g.:
----
---- <pre>
---- 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>vim
+--- 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
--- to be defined or you won't be able to see the actual highlights.
---- |LspReferenceText|
---- |LspReferenceRead|
---- |LspReferenceWrite|
+--- |hl-LspReferenceText|
+--- |hl-LspReferenceRead|
+--- |hl-LspReferenceWrite|
function M.document_highlight()
local params = util.make_position_params()
request('textDocument/documentHighlight', params)
@@ -851,6 +733,7 @@ end
--- List of LSP `CodeActionKind`s used to filter the code actions.
--- Most language servers support values like `refactor`
--- or `quickfix`.
+--- - triggerKind (number|nil): The reason why code actions were requested.
--- - filter: (function|nil)
--- Predicate taking an `CodeAction` and returning a boolean.
--- - apply: (boolean|nil)
@@ -864,6 +747,7 @@ end
--- using mark-like indexing. See |api-indexing|
---
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction
+---@see vim.lsp.protocol.constants.CodeActionTriggerKind
function M.code_action(options)
validate({ options = { options, 't', true } })
options = options or {}
@@ -873,6 +757,9 @@ function M.code_action(options)
options = { options = options }
end
local context = options.context or {}
+ if not context.triggerKind then
+ context.triggerKind = vim.lsp.protocol.CodeActionTriggerKind.Invoked
+ end
if not context.diagnostics then
local bufnr = api.nvim_get_current_buf()
context.diagnostics = vim.lsp.diagnostic.get_line_diagnostics(bufnr)
@@ -885,23 +772,8 @@ function M.code_action(options)
local end_ = assert(options.range['end'], 'range must have a `end` property')
params = util.make_given_range_params(start, end_)
elseif mode == 'v' or mode == 'V' then
- -- [bufnum, lnum, col, off]; both row and column 1-indexed
- local start = vim.fn.getpos('v')
- local end_ = vim.fn.getpos('.')
- local start_row = start[2]
- local start_col = start[3]
- local end_row = end_[2]
- local end_col = end_[3]
-
- -- A user can start visual selection at the end and move backwards
- -- Normalize the range to start < end
- if start_row == end_row and end_col < start_col then
- end_col, start_col = start_col, end_col
- elseif end_row < start_row then
- start_row, end_row = end_row, start_row
- start_col, end_col = end_col, start_col
- end
- params = util.make_given_range_params({ start_row, start_col - 1 }, { end_row, end_col - 1 })
+ local range = range_from_selection()
+ params = util.make_given_range_params(range.start, range['end'])
else
params = util.make_range_params()
end
@@ -909,34 +781,6 @@ function M.code_action(options)
code_action_request(params, options)
end
---- Performs |vim.lsp.buf.code_action()| for a given range.
----
----
----@param context table|nil `CodeActionContext` of the LSP specification:
---- - diagnostics: (table|nil)
---- LSP `Diagnostic[]`. Inferred from the current
---- position if not provided.
---- - only: (table|nil)
---- List of LSP `CodeActionKind`s used to filter the code actions.
---- Most language servers support values like `refactor`
---- or `quickfix`.
----@param start_pos ({number, number}, optional) mark-indexed position.
----Defaults to the start of the last visual selection.
----@param end_pos ({number, number}, optional) mark-indexed position.
----Defaults to the end of the last visual selection.
-function M.range_code_action(context, start_pos, end_pos)
- vim.deprecate('vim.lsp.buf.range_code_action', 'vim.lsp.buf.code_action', '0.9.0')
- validate({ context = { context, 't', true } })
- context = context or {}
- if not context.diagnostics then
- local bufnr = api.nvim_get_current_buf()
- context.diagnostics = vim.lsp.diagnostic.get_line_diagnostics(bufnr)
- end
- local params = util.make_given_range_params(start_pos, end_pos)
- params.context = context
- code_action_request(params)
-end
-
--- Executes an LSP server command.
---
---@param command_params table A valid `ExecuteCommandParams` object
diff --git a/runtime/lua/vim/lsp/codelens.lua b/runtime/lua/vim/lsp/codelens.lua
index 4fa02c8db2..17489ed84d 100644
--- a/runtime/lua/vim/lsp/codelens.lua
+++ b/runtime/lua/vim/lsp/codelens.lua
@@ -108,13 +108,36 @@ function M.run()
end
end
+---@private
+local function resolve_bufnr(bufnr)
+ return bufnr == 0 and api.nvim_get_current_buf() or bufnr
+end
+
+--- Clear the lenses
+---
+---@param client_id number|nil filter by client_id. All clients if nil
+---@param bufnr number|nil filter by buffer. All buffers if nil
+function M.clear(client_id, bufnr)
+ local buffers = bufnr and { resolve_bufnr(bufnr) } or vim.tbl_keys(lens_cache_by_buf)
+ for _, iter_bufnr in pairs(buffers) do
+ local client_ids = client_id and { client_id } or vim.tbl_keys(namespaces)
+ for _, iter_client_id in pairs(client_ids) do
+ local ns = namespaces[iter_client_id]
+ lens_cache_by_buf[iter_bufnr][iter_client_id] = {}
+ api.nvim_buf_clear_namespace(iter_bufnr, ns, 0, -1)
+ end
+ 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)
+ local ns = namespaces[client_id]
if not lenses or not next(lenses) then
+ api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
return
end
local lenses_by_lnum = {}
@@ -126,7 +149,6 @@ function M.display(lenses, bufnr, client_id)
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] or {}
@@ -241,7 +263,8 @@ end
---
--- It is recommended to trigger this using an autocmd or via keymap.
---
---- <pre>
+--- Example:
+--- <pre>vim
--- autocmd BufEnter,CursorHold,InsertLeave <buffer> lua vim.lsp.codelens.refresh()
--- </pre>
---
diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua
index 1f9d084e2b..5e2bf75f1b 100644
--- a/runtime/lua/vim/lsp/diagnostic.lua
+++ b/runtime/lua/vim/lsp/diagnostic.lua
@@ -150,7 +150,7 @@ end
---
--- See |vim.diagnostic.config()| for configuration options. Handler-specific
--- configuration can be set using |vim.lsp.with()|:
---- <pre>
+--- <pre>lua
--- vim.lsp.handlers["textDocument/publishDiagnostics"] = vim.lsp.with(
--- vim.lsp.diagnostic.on_publish_diagnostics, {
--- -- Enable underline, use default values
diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua
index 624436bc9b..5096100a60 100644
--- a/runtime/lua/vim/lsp/handlers.lua
+++ b/runtime/lua/vim/lsp/handlers.lua
@@ -1,7 +1,6 @@
local log = require('vim.lsp.log')
local protocol = require('vim.lsp.protocol')
local util = require('vim.lsp.util')
-local vim = vim
local api = vim.api
local M = {}
@@ -82,22 +81,38 @@ M['window/workDoneProgress/create'] = function(_, result, ctx)
end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_showMessageRequest
+---@param result lsp.ShowMessageRequestParams
M['window/showMessageRequest'] = function(_, result)
- local actions = result.actions
- print(result.message)
- local option_strings = { result.message, '\nRequest Actions:' }
- for i, action in ipairs(actions) do
- local title = action.title:gsub('\r\n', '\\r\\n')
- title = title:gsub('\n', '\\n')
- table.insert(option_strings, string.format('%d. %s', i, title))
- end
-
- -- window/showMessageRequest can return either MessageActionItem[] or null.
- local choice = vim.fn.inputlist(option_strings)
- if choice < 1 or choice > #actions then
- return vim.NIL
+ local actions = result.actions or {}
+ local co, is_main = coroutine.running()
+ if co and not is_main then
+ local opts = {
+ prompt = result.message .. ': ',
+ format_item = function(action)
+ return (action.title:gsub('\r\n', '\\r\\n')):gsub('\n', '\\n')
+ end,
+ }
+ vim.ui.select(actions, opts, function(choice)
+ -- schedule to ensure resume doesn't happen _before_ yield with
+ -- default synchronous vim.ui.select
+ vim.schedule(function()
+ coroutine.resume(co, choice or vim.NIL)
+ end)
+ end)
+ return coroutine.yield()
else
- return actions[choice]
+ local option_strings = { result.message, '\nRequest Actions:' }
+ for i, action in ipairs(actions) do
+ local title = action.title:gsub('\r\n', '\\r\\n')
+ title = title:gsub('\n', '\\n')
+ table.insert(option_strings, string.format('%d. %s', i, title))
+ end
+ local choice = vim.fn.inputlist(option_strings)
+ if choice < 1 or choice > #actions then
+ return vim.NIL
+ else
+ return actions[choice]
+ end
end
end
@@ -116,9 +131,10 @@ end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit
M['workspace/applyEdit'] = function(_, workspace_edit, ctx)
- if not workspace_edit then
- return
- end
+ assert(
+ workspace_edit,
+ 'workspace/applyEdit must be called with `ApplyWorkspaceEditParams`. Server is violating the specification'
+ )
-- TODO(ashkan) Do something more with label?
local client_id = ctx.client_id
local client = vim.lsp.get_client_by_id(client_id)
@@ -298,13 +314,15 @@ M['textDocument/completion'] = function(_, result, _, _)
end
--- |lsp-handler| for the method "textDocument/hover"
---- <pre>
---- vim.lsp.handlers["textDocument/hover"] = vim.lsp.with(
---- vim.lsp.handlers.hover, {
---- -- Use a sharp border with `FloatBorder` highlights
---- border = "single"
---- }
---- )
+--- <pre>lua
+--- vim.lsp.handlers["textDocument/hover"] = vim.lsp.with(
+--- vim.lsp.handlers.hover, {
+--- -- Use a sharp border with `FloatBorder` highlights
+--- border = "single",
+--- -- add the title in hover float window
+--- title = "hover"
+--- }
+--- )
--- </pre>
---@param config table Configuration table.
--- - border: (default=nil)
@@ -313,8 +331,14 @@ end
function M.hover(_, result, ctx, config)
config = config or {}
config.focus_id = ctx.method
+ if api.nvim_get_current_buf() ~= ctx.bufnr then
+ -- Ignore result since buffer changed. This happens for slow language servers.
+ return
+ end
if not (result and result.contents) then
- vim.notify('No information available')
+ if config.silent ~= true then
+ vim.notify('No information available')
+ end
return
end
local markdown_lines = util.convert_input_to_markdown_lines(result.contents)
@@ -378,21 +402,25 @@ M['textDocument/implementation'] = location_handler
--- |lsp-handler| for the method "textDocument/signatureHelp".
--- The active parameter is highlighted with |hl-LspSignatureActiveParameter|.
---- <pre>
---- vim.lsp.handlers["textDocument/signatureHelp"] = vim.lsp.with(
---- vim.lsp.handlers.signature_help, {
---- -- Use a sharp border with `FloatBorder` highlights
---- border = "single"
---- }
---- )
+--- <pre>lua
+--- vim.lsp.handlers["textDocument/signatureHelp"] = vim.lsp.with(
+--- vim.lsp.handlers.signature_help, {
+--- -- Use a sharp border with `FloatBorder` highlights
+--- border = "single"
+--- }
+--- )
--- </pre>
---@param config table Configuration table.
--- - border: (default=nil)
--- - Add borders to the floating window
---- - See |vim.api.nvim_open_win()|
+--- - See |nvim_open_win()|
function M.signature_help(_, result, ctx, config)
config = config or {}
config.focus_id = ctx.method
+ if api.nvim_get_current_buf() ~= ctx.bufnr then
+ -- Ignore result since buffer changed. This happens for slow language servers.
+ return
+ end
-- 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
@@ -512,6 +540,55 @@ M['window/showMessage'] = function(_, result, ctx, _)
return result
end
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_showDocument
+M['window/showDocument'] = function(_, result, ctx, _)
+ local uri = result.uri
+
+ if result.external then
+ -- TODO(lvimuser): ask the user for confirmation
+ local cmd
+ if vim.fn.has('win32') == 1 then
+ cmd = { 'cmd.exe', '/c', 'start', '""', uri }
+ elseif vim.fn.has('macunix') == 1 then
+ cmd = { 'open', uri }
+ else
+ cmd = { 'xdg-open', uri }
+ end
+
+ local ret = vim.fn.system(cmd)
+ if vim.v.shell_error ~= 0 then
+ return {
+ success = false,
+ error = {
+ code = protocol.ErrorCodes.UnknownErrorCode,
+ message = ret,
+ },
+ }
+ end
+
+ return { success = true }
+ end
+
+ local client_id = ctx.client_id
+ local client = vim.lsp.get_client_by_id(client_id)
+ local client_name = client and client.name or string.format('id=%d', client_id)
+ if not client then
+ err_message({ 'LSP[', client_name, '] client has shut down after sending ', ctx.method })
+ return vim.NIL
+ end
+
+ local location = {
+ uri = uri,
+ range = result.selection,
+ }
+
+ local success = util.show_document(location, client.offset_encoding, {
+ reuse_win = true,
+ focus = result.takeFocus,
+ })
+ return { success = success or false }
+end
+
-- Add boilerplate error validation and logging for all of these.
for k, fn in pairs(M) do
M[k] = function(err, result, ctx, config)
diff --git a/runtime/lua/vim/lsp/health.lua b/runtime/lua/vim/lsp/health.lua
index ba730e3d6d..987707e661 100644
--- a/runtime/lua/vim/lsp/health.lua
+++ b/runtime/lua/vim/lsp/health.lua
@@ -2,8 +2,8 @@ local M = {}
--- Performs a healthcheck for LSP
function M.check()
- local report_info = vim.fn['health#report_info']
- local report_warn = vim.fn['health#report_warn']
+ local report_info = vim.health.report_info
+ local report_warn = vim.health.report_warn
local log = require('vim.lsp.log')
local current_log_level = log.get_level()
@@ -27,6 +27,18 @@ function M.check()
local report_fn = (log_size / 1000000 > 100 and report_warn or report_info)
report_fn(string.format('Log size: %d KB', log_size / 1000))
+
+ local clients = vim.lsp.get_active_clients()
+ vim.health.report_start('vim.lsp: Active Clients')
+ if next(clients) then
+ for _, client in pairs(clients) do
+ report_info(
+ string.format('%s (id=%s, root_dir=%s)', client.name, client.id, client.config.root_dir)
+ )
+ end
+ else
+ report_info('No active clients')
+ end
end
return M
diff --git a/runtime/lua/vim/lsp/log.lua b/runtime/lua/vim/lsp/log.lua
index 6c6ba0f206..d1a78572aa 100644
--- a/runtime/lua/vim/lsp/log.lua
+++ b/runtime/lua/vim/lsp/log.lua
@@ -20,6 +20,17 @@ local format_func = function(arg)
end
do
+ ---@private
+ local function notify(msg, level)
+ if vim.in_fast_event() then
+ vim.schedule(function()
+ vim.notify(msg, level)
+ end)
+ else
+ vim.notify(msg, level)
+ end
+ end
+
local path_sep = vim.loop.os_uname().version:match('Windows') and '\\' or '/'
---@private
local function path_join(...)
@@ -53,7 +64,7 @@ do
logfile, openerr = io.open(logfilename, 'a+')
if not logfile then
local err_msg = string.format('Failed to open LSP client log file: %s', openerr)
- vim.notify(err_msg, vim.log.levels.ERROR)
+ notify(err_msg, vim.log.levels.ERROR)
return false
end
@@ -64,7 +75,7 @@ do
log_info.size / (1000 * 1000),
logfilename
)
- vim.notify(warn_msg)
+ notify(warn_msg)
end
-- Start message for logging
@@ -130,7 +141,7 @@ end
vim.tbl_add_reverse_lookup(log.levels)
--- Sets the current log level.
----@param level (string or number) One of `vim.lsp.log.levels`
+---@param level (string|number) One of `vim.lsp.log.levels`
function log.set_level(level)
if type(level) == 'string' then
current_log_level =
diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua
index 27da60b4ae..12345b6c8c 100644
--- a/runtime/lua/vim/lsp/protocol.lua
+++ b/runtime/lua/vim/lsp/protocol.lua
@@ -20,6 +20,14 @@ function transform_schema_to_table()
end
--]=]
+---@class lsp.ShowMessageRequestParams
+---@field type lsp.MessageType
+---@field message string
+---@field actions nil|lsp.MessageActionItem[]
+
+---@class lsp.MessageActionItem
+---@field title string
+
local constants = {
DiagnosticSeverity = {
-- Reports an error.
@@ -39,6 +47,7 @@ local constants = {
Deprecated = 2,
},
+ ---@enum lsp.MessageType
MessageType = {
-- An error message.
Error = 1,
@@ -142,6 +151,7 @@ local constants = {
},
-- Represents reasons why a text document is saved.
+ ---@enum lsp.TextDocumentSaveReason
TextDocumentSaveReason = {
-- Manually triggered, e.g. by the user pressing save, by starting debugging,
-- or by an API call.
@@ -294,6 +304,17 @@ local constants = {
-- Base kind for an organize imports source action
SourceOrganizeImports = 'source.organizeImports',
},
+ -- The reason why code actions were requested.
+ ---@enum lsp.CodeActionTriggerKind
+ CodeActionTriggerKind = {
+ -- Code actions were explicitly requested by the user or by an extension.
+ Invoked = 1,
+ -- Code actions were requested automatically.
+ --
+ -- This typically happens when current selection in a file changes, but can
+ -- also be triggered when file content changes.
+ Automatic = 2,
+ },
}
for k, v in pairs(constants) do
@@ -619,14 +640,63 @@ export interface WorkspaceClientCapabilities {
function protocol.make_client_capabilities()
return {
textDocument = {
- synchronization = {
+ semanticTokens = {
dynamicRegistration = false,
+ tokenTypes = {
+ 'namespace',
+ 'type',
+ 'class',
+ 'enum',
+ 'interface',
+ 'struct',
+ 'typeParameter',
+ 'parameter',
+ 'variable',
+ 'property',
+ 'enumMember',
+ 'event',
+ 'function',
+ 'method',
+ 'macro',
+ 'keyword',
+ 'modifier',
+ 'comment',
+ 'string',
+ 'number',
+ 'regexp',
+ 'operator',
+ 'decorator',
+ },
+ tokenModifiers = {
+ 'declaration',
+ 'definition',
+ 'readonly',
+ 'static',
+ 'deprecated',
+ 'abstract',
+ 'async',
+ 'modification',
+ 'documentation',
+ 'defaultLibrary',
+ },
+ formats = { 'relative' },
+ requests = {
+ -- TODO(jdrouhard): Add support for this
+ range = false,
+ full = { delta = true },
+ },
- -- TODO(ashkan) Send textDocument/willSave before saving (BufWritePre)
- willSave = false,
+ overlappingTokenSupport = true,
+ -- TODO(jdrouhard): Add support for this
+ multilineTokenSupport = false,
+ serverCancelSupport = false,
+ augmentsSyntaxTokens = true,
+ },
+ synchronization = {
+ dynamicRegistration = false,
- -- TODO(ashkan) Implement textDocument/willSaveWaitUntil
- willSaveWaitUntil = false,
+ willSave = true,
+ willSaveWaitUntil = true,
-- Send textDocument/didSave after saving (BufWritePost)
didSave = true,
@@ -637,7 +707,7 @@ function protocol.make_client_capabilities()
codeActionLiteralSupport = {
codeActionKind = {
valueSet = (function()
- local res = vim.tbl_values(protocol.CodeActionKind)
+ local res = vim.tbl_values(constants.CodeActionKind)
table.sort(res)
return res
end)(),
@@ -742,6 +812,9 @@ function protocol.make_client_capabilities()
end)(),
},
},
+ callHierarchy = {
+ dynamicRegistration = false,
+ },
},
workspace = {
symbol = {
@@ -765,9 +838,9 @@ function protocol.make_client_capabilities()
workspaceEdit = {
resourceOperations = { 'rename', 'create', 'delete' },
},
- },
- callHierarchy = {
- dynamicRegistration = false,
+ semanticTokens = {
+ refreshSupport = true,
+ },
},
experimental = nil,
window = {
@@ -778,7 +851,7 @@ function protocol.make_client_capabilities()
},
},
showDocument = {
- support = false,
+ support = true,
},
},
}
@@ -861,8 +934,8 @@ function protocol._resolve_capabilities_compat(server_capabilities)
text_document_sync_properties = {
text_document_open_close = if_nil(textDocumentSync.openClose, false),
text_document_did_change = if_nil(textDocumentSync.change, TextDocumentSyncKind.None),
- text_document_will_save = if_nil(textDocumentSync.willSave, false),
- text_document_will_save_wait_until = if_nil(textDocumentSync.willSaveWaitUntil, false),
+ text_document_will_save = if_nil(textDocumentSync.willSave, true),
+ text_document_will_save_wait_until = if_nil(textDocumentSync.willSaveWaitUntil, true),
text_document_save = if_nil(textDocumentSync.save, false),
text_document_save_include_text = if_nil(
type(textDocumentSync.save) == 'table' and textDocumentSync.save.includeText,
diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua
index 0926912066..f1492601ff 100644
--- a/runtime/lua/vim/lsp/rpc.lua
+++ b/runtime/lua/vim/lsp/rpc.lua
@@ -1,4 +1,3 @@
-local vim = vim
local uv = vim.loop
local log = require('vim.lsp.log')
local protocol = require('vim.lsp.protocol')
@@ -241,247 +240,162 @@ function default_dispatchers.on_error(code, err)
local _ = log.error() and log.error('client_error:', client_errors[code], err)
end
---- Starts an LSP server process and create an LSP RPC client object to
---- interact with it. Communication with the server is currently limited to stdio.
----
----@param cmd (string) Command to start the LSP server.
----@param cmd_args (table) List of additional string arguments to pass to {cmd}.
----@param dispatchers table|nil Dispatchers for LSP message types. Valid
----dispatcher names are:
---- - `"notification"`
---- - `"server_request"`
---- - `"on_error"`
---- - `"on_exit"`
----@param extra_spawn_params table|nil Additional context for the LSP
---- server process. May contain:
---- - {cwd} (string) Working directory for the LSP server process
---- - {env} (table) Additional environment variables for LSP server process
----@returns Client RPC object.
----
----@returns Methods:
---- - `notify()` |vim.lsp.rpc.notify()|
---- - `request()` |vim.lsp.rpc.request()|
----
----@returns Members:
---- - {pid} (number) The LSP server's PID.
---- - {handle} A handle for low-level interaction with the LSP server process
---- |vim.loop|.
-local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
- local _ = log.info()
- and log.info('Starting RPC client', { cmd = cmd, args = cmd_args, extra = extra_spawn_params })
- validate({
- cmd = { cmd, 's' },
- cmd_args = { cmd_args, 't' },
- dispatchers = { dispatchers, 't', true },
- })
-
- if extra_spawn_params and extra_spawn_params.cwd then
- assert(is_dir(extra_spawn_params.cwd), 'cwd must be a directory')
- end
- if dispatchers then
- local user_dispatchers = dispatchers
- dispatchers = {}
- for dispatch_name, default_dispatch in pairs(default_dispatchers) do
- local user_dispatcher = user_dispatchers[dispatch_name]
- if user_dispatcher then
- if type(user_dispatcher) ~= 'function' then
- error(string.format('dispatcher.%s must be a function', dispatch_name))
- end
- -- server_request is wrapped elsewhere.
- if
- not (dispatch_name == 'server_request' or dispatch_name == 'on_exit') -- TODO this blocks the loop exiting for some reason.
- then
- user_dispatcher = schedule_wrap(user_dispatcher)
- end
- dispatchers[dispatch_name] = user_dispatcher
- else
- dispatchers[dispatch_name] = default_dispatch
- end
+---@private
+local function create_read_loop(handle_body, on_no_chunk, on_error)
+ local parse_chunk = coroutine.wrap(request_parser_loop)
+ parse_chunk()
+ return function(err, chunk)
+ if err then
+ on_error(err)
+ return
end
- else
- dispatchers = default_dispatchers
- end
-
- local stdin = uv.new_pipe(false)
- local stdout = uv.new_pipe(false)
- local stderr = uv.new_pipe(false)
-
- local message_index = 0
- local message_callbacks = {}
- local notify_reply_callbacks = {}
- local handle, pid
- do
- ---@private
- --- Callback for |vim.loop.spawn()| Closes all streams and runs the `on_exit` dispatcher.
- ---@param code (number) Exit code
- ---@param signal (number) Signal that was used to terminate (if any)
- local function onexit(code, signal)
- stdin:close()
- stdout:close()
- stderr:close()
- handle:close()
- -- Make sure that message_callbacks/notify_reply_callbacks can be gc'd.
- message_callbacks = nil
- notify_reply_callbacks = nil
- dispatchers.on_exit(code, signal)
- end
- local spawn_params = {
- args = cmd_args,
- stdio = { stdin, stdout, stderr },
- detached = not is_win,
- }
- if extra_spawn_params then
- spawn_params.cwd = extra_spawn_params.cwd
- spawn_params.env = env_merge(extra_spawn_params.env)
- if extra_spawn_params.detached ~= nil then
- spawn_params.detached = extra_spawn_params.detached
+ if not chunk then
+ if on_no_chunk then
+ on_no_chunk()
end
+ return
end
- handle, pid = uv.spawn(cmd, spawn_params, onexit)
- if handle == nil then
- stdin:close()
- stdout:close()
- stderr:close()
- local msg = string.format('Spawning language server with cmd: `%s` failed', cmd)
- if string.match(pid, 'ENOENT') then
- msg = msg
- .. '. The language server is either not installed, missing from PATH, or not executable.'
+
+ while true do
+ local headers, body = parse_chunk(chunk)
+ if headers then
+ handle_body(body)
+ chunk = ''
else
- msg = msg .. string.format(' with error message: %s', pid)
+ break
end
- vim.notify(msg, vim.log.levels.WARN)
- return
end
end
+end
- ---@private
- --- Encodes {payload} into a JSON-RPC message and sends it to the remote
- --- process.
- ---
- ---@param payload table
- ---@returns true if the payload could be scheduled, false if the main event-loop is in the process of closing.
- local function encode_and_send(payload)
- local _ = log.debug() and log.debug('rpc.send', payload)
- if handle == nil or handle:is_closing() then
- return false
- end
- local encoded = vim.json.encode(payload)
- stdin:write(format_message_with_content_length(encoded))
- return true
- end
+---@class RpcClient
+---@field message_index number
+---@field message_callbacks table
+---@field notify_reply_callbacks table
+---@field transport table
+---@field dispatchers table
- -- FIXME: DOC: Should be placed on the RPC client object returned by
- -- `start()`
- --
- --- Sends a notification to the LSP server.
- ---@param method (string) The invoked LSP method
- ---@param params (table): Parameters for the invoked LSP method
- ---@returns (bool) `true` if notification could be sent, `false` if not
- local function notify(method, params)
- return encode_and_send({
- jsonrpc = '2.0',
- method = method,
- params = params,
- })
- end
+---@class RpcClient
+local Client = {}
- ---@private
- --- sends an error object to the remote LSP process.
- local function send_response(request_id, err, result)
- return encode_and_send({
- id = request_id,
- jsonrpc = '2.0',
- error = err,
- result = result,
- })
+---@private
+function Client:encode_and_send(payload)
+ local _ = log.debug() and log.debug('rpc.send', payload)
+ if self.transport.is_closing() then
+ return false
end
+ local encoded = vim.json.encode(payload)
+ self.transport.write(format_message_with_content_length(encoded))
+ return true
+end
- -- FIXME: DOC: Should be placed on the RPC client object returned by
- -- `start()`
- --
- --- Sends a request to the LSP server and runs {callback} upon response.
- ---
- ---@param method (string) The invoked LSP method
- ---@param params (table) Parameters for the invoked LSP method
- ---@param callback (function) Callback to invoke
- ---@param notify_reply_callback (function|nil) Callback to invoke as soon as a request is no longer pending
- ---@returns (bool, number) `(true, message_id)` if request could be sent, `false` if not
- local function request(method, params, callback, notify_reply_callback)
- validate({
- callback = { callback, 'f' },
- notify_reply_callback = { notify_reply_callback, 'f', true },
- })
- message_index = message_index + 1
- local message_id = message_index
- local result = encode_and_send({
- id = message_id,
- jsonrpc = '2.0',
- method = method,
- params = params,
- })
- if result then
- if message_callbacks then
- message_callbacks[message_id] = schedule_wrap(callback)
- else
- return false
- end
- if notify_reply_callback and notify_reply_callbacks then
- notify_reply_callbacks[message_id] = schedule_wrap(notify_reply_callback)
- end
- return result, message_id
+---@private
+--- Sends a notification to the LSP server.
+---@param method (string) The invoked LSP method
+---@param params (any): Parameters for the invoked LSP method
+---@returns (bool) `true` if notification could be sent, `false` if not
+function Client:notify(method, params)
+ return self:encode_and_send({
+ jsonrpc = '2.0',
+ method = method,
+ params = params,
+ })
+end
+
+---@private
+--- sends an error object to the remote LSP process.
+function Client:send_response(request_id, err, result)
+ return self:encode_and_send({
+ id = request_id,
+ jsonrpc = '2.0',
+ error = err,
+ result = result,
+ })
+end
+
+---@private
+--- Sends a request to the LSP server and runs {callback} upon response.
+---
+---@param method (string) The invoked LSP method
+---@param params (table|nil) Parameters for the invoked LSP method
+---@param callback (function) Callback to invoke
+---@param notify_reply_callback (function|nil) Callback to invoke as soon as a request is no longer pending
+---@returns (bool, number) `(true, message_id)` if request could be sent, `false` if not
+function Client:request(method, params, callback, notify_reply_callback)
+ validate({
+ callback = { callback, 'f' },
+ notify_reply_callback = { notify_reply_callback, 'f', true },
+ })
+ self.message_index = self.message_index + 1
+ local message_id = self.message_index
+ local result = self:encode_and_send({
+ id = message_id,
+ jsonrpc = '2.0',
+ method = method,
+ params = params,
+ })
+ local message_callbacks = self.message_callbacks
+ local notify_reply_callbacks = self.notify_reply_callbacks
+ if result then
+ if message_callbacks then
+ message_callbacks[message_id] = schedule_wrap(callback)
else
return false
end
+ if notify_reply_callback and notify_reply_callbacks then
+ notify_reply_callbacks[message_id] = schedule_wrap(notify_reply_callback)
+ end
+ return result, message_id
+ else
+ return false
end
+end
- stderr:read_start(function(_, chunk)
- if chunk then
- local _ = log.error() and log.error('rpc', cmd, 'stderr', chunk)
- end
- end)
+---@private
+function Client:on_error(errkind, ...)
+ assert(client_errors[errkind])
+ -- TODO what to do if this fails?
+ pcall(self.dispatchers.on_error, errkind, ...)
+end
- ---@private
- local function on_error(errkind, ...)
- assert(client_errors[errkind])
- -- TODO what to do if this fails?
- pcall(dispatchers.on_error, errkind, ...)
- end
- ---@private
- local function pcall_handler(errkind, status, head, ...)
- if not status then
- on_error(errkind, head, ...)
- return status, head
- end
- return status, head, ...
- end
- ---@private
- local function try_call(errkind, fn, ...)
- return pcall_handler(errkind, pcall(fn, ...))
+---@private
+function Client:pcall_handler(errkind, status, head, ...)
+ if not status then
+ self:on_error(errkind, head, ...)
+ return status, head
end
+ return status, head, ...
+end
- -- TODO periodically check message_callbacks for old requests past a certain
- -- time and log them. This would require storing the timestamp. I could call
- -- them with an error then, perhaps.
+---@private
+function Client:try_call(errkind, fn, ...)
+ return self:pcall_handler(errkind, pcall(fn, ...))
+end
- ---@private
- local function handle_body(body)
- local ok, decoded = pcall(vim.json.decode, body, { luanil = { object = true } })
- if not ok then
- on_error(client_errors.INVALID_SERVER_JSON, decoded)
- return
- end
- local _ = log.debug() and log.debug('rpc.receive', decoded)
+-- TODO periodically check message_callbacks for old requests past a certain
+-- time and log them. This would require storing the timestamp. I could call
+-- them with an error then, perhaps.
- if type(decoded.method) == 'string' and decoded.id then
- local err
- -- Schedule here so that the users functions don't trigger an error and
- -- we can still use the result.
- schedule(function()
+---@private
+function Client:handle_body(body)
+ local ok, decoded = pcall(vim.json.decode, body, { luanil = { object = true } })
+ if not ok then
+ self:on_error(client_errors.INVALID_SERVER_JSON, decoded)
+ return
+ end
+ local _ = log.debug() and log.debug('rpc.receive', decoded)
+
+ if type(decoded.method) == 'string' and decoded.id then
+ local err
+ -- Schedule here so that the users functions don't trigger an error and
+ -- we can still use the result.
+ schedule(function()
+ coroutine.wrap(function()
local status, result
- status, result, err = try_call(
+ status, result, err = self:try_call(
client_errors.SERVER_REQUEST_HANDLER_ERROR,
- dispatchers.server_request,
+ self.dispatchers.server_request,
decoded.method,
decoded.params
)
@@ -491,8 +405,7 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
{ status = status, result = result, err = err }
)
if status then
- if not (result or err) then
- -- TODO this can be a problem if `null` is sent for result. needs vim.NIL
+ if result == nil and err == nil then
error(
string.format(
'method %q: either a result or an error must be sent to the server in response',
@@ -516,120 +429,328 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
err = rpc_response_error(protocol.ErrorCodes.InternalError, result)
result = nil
end
- send_response(decoded.id, err, result)
- end)
- -- This works because we are expecting vim.NIL here
- elseif decoded.id and (decoded.result ~= vim.NIL or decoded.error ~= vim.NIL) then
- -- We sent a number, so we expect a number.
- local result_id = assert(tonumber(decoded.id), 'response id must be a number')
-
- -- Notify the user that a response was received for the request
- local notify_reply_callback = notify_reply_callbacks and notify_reply_callbacks[result_id]
- if notify_reply_callback then
- validate({
- notify_reply_callback = { notify_reply_callback, 'f' },
- })
- notify_reply_callback(result_id)
- notify_reply_callbacks[result_id] = nil
- end
+ self:send_response(decoded.id, err, result)
+ end)()
+ end)
+ -- This works because we are expecting vim.NIL here
+ elseif decoded.id and (decoded.result ~= vim.NIL or decoded.error ~= vim.NIL) then
+ -- We sent a number, so we expect a number.
+ local result_id = assert(tonumber(decoded.id), 'response id must be a number')
+
+ -- Notify the user that a response was received for the request
+ local notify_reply_callbacks = self.notify_reply_callbacks
+ local notify_reply_callback = notify_reply_callbacks and notify_reply_callbacks[result_id]
+ if notify_reply_callback then
+ validate({
+ notify_reply_callback = { notify_reply_callback, 'f' },
+ })
+ notify_reply_callback(result_id)
+ notify_reply_callbacks[result_id] = nil
+ end
- -- Do not surface RequestCancelled to users, it is RPC-internal.
- if decoded.error then
- local mute_error = false
- if decoded.error.code == protocol.ErrorCodes.RequestCancelled then
- local _ = log.debug() and log.debug('Received cancellation ack', decoded)
- mute_error = true
- end
+ local message_callbacks = self.message_callbacks
- if mute_error then
- -- Clear any callback since this is cancelled now.
- -- This is safe to do assuming that these conditions hold:
- -- - The server will not send a result callback after this cancellation.
- -- - If the server sent this cancellation ACK after sending the result, the user of this RPC
- -- client will ignore the result themselves.
- if result_id and message_callbacks then
- message_callbacks[result_id] = nil
- end
- return
+ -- Do not surface RequestCancelled to users, it is RPC-internal.
+ if decoded.error then
+ local mute_error = false
+ if decoded.error.code == protocol.ErrorCodes.RequestCancelled then
+ local _ = log.debug() and log.debug('Received cancellation ack', decoded)
+ mute_error = true
+ end
+
+ if mute_error then
+ -- Clear any callback since this is cancelled now.
+ -- This is safe to do assuming that these conditions hold:
+ -- - The server will not send a result callback after this cancellation.
+ -- - If the server sent this cancellation ACK after sending the result, the user of this RPC
+ -- client will ignore the result themselves.
+ if result_id and message_callbacks then
+ message_callbacks[result_id] = nil
end
+ return
end
+ end
- local callback = message_callbacks and message_callbacks[result_id]
- if callback then
- message_callbacks[result_id] = nil
- validate({
- callback = { callback, 'f' },
+ local callback = message_callbacks and message_callbacks[result_id]
+ if callback then
+ message_callbacks[result_id] = nil
+ validate({
+ callback = { callback, 'f' },
+ })
+ if decoded.error then
+ decoded.error = setmetatable(decoded.error, {
+ __tostring = format_rpc_error,
})
- if decoded.error then
- decoded.error = setmetatable(decoded.error, {
- __tostring = format_rpc_error,
- })
- end
- try_call(
- client_errors.SERVER_RESULT_CALLBACK_ERROR,
- callback,
- decoded.error,
- decoded.result
- )
- else
- on_error(client_errors.NO_RESULT_CALLBACK_FOUND, decoded)
- local _ = log.error()
- and log.error('No callback found for server response id ' .. result_id)
end
- elseif type(decoded.method) == 'string' then
- -- Notification
- try_call(
- client_errors.NOTIFICATION_HANDLER_ERROR,
- dispatchers.notification,
- decoded.method,
- decoded.params
+ self:try_call(
+ client_errors.SERVER_RESULT_CALLBACK_ERROR,
+ callback,
+ decoded.error,
+ decoded.result
)
else
- -- Invalid server message
- on_error(client_errors.INVALID_SERVER_MESSAGE, decoded)
+ self:on_error(client_errors.NO_RESULT_CALLBACK_FOUND, decoded)
+ local _ = log.error() and log.error('No callback found for server response id ' .. result_id)
end
+ elseif type(decoded.method) == 'string' then
+ -- Notification
+ self:try_call(
+ client_errors.NOTIFICATION_HANDLER_ERROR,
+ self.dispatchers.notification,
+ decoded.method,
+ decoded.params
+ )
+ else
+ -- Invalid server message
+ self:on_error(client_errors.INVALID_SERVER_MESSAGE, decoded)
end
+end
- local request_parser = coroutine.wrap(request_parser_loop)
- request_parser()
- stdout:read_start(function(err, chunk)
- if err then
- -- TODO better handling. Can these be intermittent errors?
- on_error(client_errors.READ_ERROR, err)
- return
- end
- -- This should signal that we are done reading from the client.
- if not chunk then
- return
- end
- -- Flush anything in the parser by looping until we don't get a result
- -- anymore.
- while true do
- local headers, body = request_parser(chunk)
- -- If we successfully parsed, then handle the response.
- if headers then
- handle_body(body)
- -- Set chunk to empty so that we can call request_parser to get
- -- anything existing in the parser to flush.
- chunk = ''
+---@private
+---@return RpcClient
+local function new_client(dispatchers, transport)
+ local state = {
+ message_index = 0,
+ message_callbacks = {},
+ notify_reply_callbacks = {},
+ transport = transport,
+ dispatchers = dispatchers,
+ }
+ return setmetatable(state, { __index = Client })
+end
+
+---@private
+---@param client RpcClient
+local function public_client(client)
+ local result = {}
+
+ ---@private
+ function result.is_closing()
+ return client.transport.is_closing()
+ end
+
+ ---@private
+ function result.terminate()
+ client.transport.terminate()
+ end
+
+ --- Sends a request to the LSP server and runs {callback} upon response.
+ ---
+ ---@param method (string) The invoked LSP method
+ ---@param params (table|nil) Parameters for the invoked LSP method
+ ---@param callback (function) Callback to invoke
+ ---@param notify_reply_callback (function|nil) Callback to invoke as soon as a request is no longer pending
+ ---@returns (bool, number) `(true, message_id)` if request could be sent, `false` if not
+ function result.request(method, params, callback, notify_reply_callback)
+ return client:request(method, params, callback, notify_reply_callback)
+ end
+
+ --- Sends a notification to the LSP server.
+ ---@param method (string) The invoked LSP method
+ ---@param params (table|nil): Parameters for the invoked LSP method
+ ---@returns (bool) `true` if notification could be sent, `false` if not
+ function result.notify(method, params)
+ return client:notify(method, params)
+ end
+
+ return result
+end
+
+---@private
+local function merge_dispatchers(dispatchers)
+ if dispatchers then
+ local user_dispatchers = dispatchers
+ dispatchers = {}
+ for dispatch_name, default_dispatch in pairs(default_dispatchers) do
+ local user_dispatcher = user_dispatchers[dispatch_name]
+ if user_dispatcher then
+ if type(user_dispatcher) ~= 'function' then
+ error(string.format('dispatcher.%s must be a function', dispatch_name))
+ end
+ -- server_request is wrapped elsewhere.
+ if
+ not (dispatch_name == 'server_request' or dispatch_name == 'on_exit') -- TODO this blocks the loop exiting for some reason.
+ then
+ user_dispatcher = schedule_wrap(user_dispatcher)
+ end
+ dispatchers[dispatch_name] = user_dispatcher
else
- break
+ dispatchers[dispatch_name] = default_dispatch
end
end
- end)
+ else
+ dispatchers = default_dispatchers
+ end
+ return dispatchers
+end
+
+--- Create a LSP RPC client factory that connects via TCP to the given host
+--- and port
+---
+---@param host string
+---@param port number
+---@return function
+local function connect(host, port)
+ return function(dispatchers)
+ dispatchers = merge_dispatchers(dispatchers)
+ local tcp = uv.new_tcp()
+ local closing = false
+ local transport = {
+ write = function(msg)
+ tcp:write(msg)
+ end,
+ is_closing = function()
+ return closing
+ end,
+ terminate = function()
+ if not closing then
+ closing = true
+ tcp:shutdown()
+ tcp:close()
+ dispatchers.on_exit(0, 0)
+ end
+ end,
+ }
+ local client = new_client(dispatchers, transport)
+ tcp:connect(host, port, function(err)
+ if err then
+ vim.schedule(function()
+ vim.notify(
+ string.format('Could not connect to %s:%s, reason: %s', host, port, vim.inspect(err)),
+ vim.log.levels.WARN
+ )
+ end)
+ return
+ end
+ local handle_body = function(body)
+ client:handle_body(body)
+ end
+ tcp:read_start(create_read_loop(handle_body, transport.terminate, function(read_err)
+ client:on_error(client_errors.READ_ERROR, read_err)
+ end))
+ end)
- return {
- pid = pid,
- handle = handle,
- request = request,
- notify = notify,
+ return public_client(client)
+ end
+end
+
+--- Starts an LSP server process and create an LSP RPC client object to
+--- interact with it. Communication with the spawned process happens via stdio. For
+--- communication via TCP, spawn a process manually and use |vim.lsp.rpc.connect()|
+---
+---@param cmd (string) Command to start the LSP server.
+---@param cmd_args (table) List of additional string arguments to pass to {cmd}.
+---@param dispatchers table|nil Dispatchers for LSP message types. Valid
+---dispatcher names are:
+--- - `"notification"`
+--- - `"server_request"`
+--- - `"on_error"`
+--- - `"on_exit"`
+---@param extra_spawn_params table|nil Additional context for the LSP
+--- server process. May contain:
+--- - {cwd} (string) Working directory for the LSP server process
+--- - {env} (table) Additional environment variables for LSP server process
+---@returns Client RPC object.
+---
+---@returns Methods:
+--- - `notify()` |vim.lsp.rpc.notify()|
+--- - `request()` |vim.lsp.rpc.request()|
+--- - `is_closing()` returns a boolean indicating if the RPC is closing.
+--- - `terminate()` terminates the RPC client.
+local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
+ local _ = log.info()
+ and log.info('Starting RPC client', { cmd = cmd, args = cmd_args, extra = extra_spawn_params })
+ validate({
+ cmd = { cmd, 's' },
+ cmd_args = { cmd_args, 't' },
+ dispatchers = { dispatchers, 't', true },
+ })
+
+ if extra_spawn_params and extra_spawn_params.cwd then
+ assert(is_dir(extra_spawn_params.cwd), 'cwd must be a directory')
+ end
+
+ dispatchers = merge_dispatchers(dispatchers)
+ local stdin = uv.new_pipe(false)
+ local stdout = uv.new_pipe(false)
+ local stderr = uv.new_pipe(false)
+ local handle, pid
+
+ local client = new_client(dispatchers, {
+ write = function(msg)
+ stdin:write(msg)
+ end,
+ is_closing = function()
+ return handle == nil or handle:is_closing()
+ end,
+ terminate = function()
+ if handle then
+ handle:kill(15)
+ end
+ end,
+ })
+
+ ---@private
+ --- Callback for |vim.loop.spawn()| Closes all streams and runs the `on_exit` dispatcher.
+ ---@param code (number) Exit code
+ ---@param signal (number) Signal that was used to terminate (if any)
+ local function onexit(code, signal)
+ stdin:close()
+ stdout:close()
+ stderr:close()
+ handle:close()
+ dispatchers.on_exit(code, signal)
+ end
+ local spawn_params = {
+ args = cmd_args,
+ stdio = { stdin, stdout, stderr },
+ detached = not is_win,
}
+ if extra_spawn_params then
+ spawn_params.cwd = extra_spawn_params.cwd
+ spawn_params.env = env_merge(extra_spawn_params.env)
+ if extra_spawn_params.detached ~= nil then
+ spawn_params.detached = extra_spawn_params.detached
+ end
+ end
+ handle, pid = uv.spawn(cmd, spawn_params, onexit)
+ if handle == nil then
+ stdin:close()
+ stdout:close()
+ stderr:close()
+ local msg = string.format('Spawning language server with cmd: `%s` failed', cmd)
+ if string.match(pid, 'ENOENT') then
+ msg = msg
+ .. '. The language server is either not installed, missing from PATH, or not executable.'
+ else
+ msg = msg .. string.format(' with error message: %s', pid)
+ end
+ vim.notify(msg, vim.log.levels.WARN)
+ return
+ end
+
+ stderr:read_start(function(_, chunk)
+ if chunk then
+ local _ = log.error() and log.error('rpc', cmd, 'stderr', chunk)
+ end
+ end)
+
+ local handle_body = function(body)
+ client:handle_body(body)
+ end
+ stdout:read_start(create_read_loop(handle_body, nil, function(err)
+ client:on_error(client_errors.READ_ERROR, err)
+ end))
+
+ return public_client(client)
end
return {
start = start,
+ connect = connect,
rpc_response_error = rpc_response_error,
format_rpc_error = format_rpc_error,
client_errors = client_errors,
+ create_read_loop = create_read_loop,
}
-- vim:sw=2 ts=2 et
diff --git a/runtime/lua/vim/lsp/semantic_tokens.lua b/runtime/lua/vim/lsp/semantic_tokens.lua
new file mode 100644
index 0000000000..b1bc48dac6
--- /dev/null
+++ b/runtime/lua/vim/lsp/semantic_tokens.lua
@@ -0,0 +1,702 @@
+local api = vim.api
+local handlers = require('vim.lsp.handlers')
+local util = require('vim.lsp.util')
+
+--- @class STTokenRange
+--- @field line number line number 0-based
+--- @field start_col number start column 0-based
+--- @field end_col number end column 0-based
+--- @field type string token type as string
+--- @field modifiers string[] token modifiers as strings
+--- @field extmark_added boolean whether this extmark has been added to the buffer yet
+---
+--- @class STCurrentResult
+--- @field version number document version associated with this result
+--- @field result_id string resultId from the server; used with delta requests
+--- @field highlights STTokenRange[] cache of highlight ranges for this document version
+--- @field tokens number[] raw token array as received by the server. used for calculating delta responses
+--- @field namespace_cleared boolean whether the namespace was cleared for this result yet
+---
+--- @class STActiveRequest
+--- @field request_id number the LSP request ID of the most recent request sent to the server
+--- @field version number the document version associated with the most recent request
+---
+--- @class STClientState
+--- @field namespace number
+--- @field active_request STActiveRequest
+--- @field current_result STCurrentResult
+
+---@class STHighlighter
+---@field active table<number, STHighlighter>
+---@field bufnr number
+---@field augroup number augroup for buffer events
+---@field debounce number milliseconds to debounce requests for new tokens
+---@field timer table uv_timer for debouncing requests for new tokens
+---@field client_state table<number, STClientState>
+local STHighlighter = { active = {} }
+
+---@private
+local function binary_search(tokens, line)
+ local lo = 1
+ local hi = #tokens
+ while lo < hi do
+ local mid = math.floor((lo + hi) / 2)
+ if tokens[mid].line < line then
+ lo = mid + 1
+ else
+ hi = mid
+ end
+ end
+ return lo
+end
+
+--- Extracts modifier strings from the encoded number in the token array
+---
+---@private
+---@return string[]
+local function modifiers_from_number(x, modifiers_table)
+ local modifiers = {}
+ local idx = 1
+ while x > 0 do
+ if _G.bit then
+ if _G.bit.band(x, 1) == 1 then
+ modifiers[#modifiers + 1] = modifiers_table[idx]
+ end
+ x = _G.bit.rshift(x, 1)
+ else
+ --TODO(jdrouhard): remove this branch once `bit` module is available for non-LuaJIT (#21222)
+ if x % 2 == 1 then
+ modifiers[#modifiers + 1] = modifiers_table[idx]
+ end
+ x = math.floor(x / 2)
+ end
+ idx = idx + 1
+ end
+
+ return modifiers
+end
+
+--- Converts a raw token list to a list of highlight ranges used by the on_win callback
+---
+---@private
+---@return STTokenRange[]
+local function tokens_to_ranges(data, bufnr, client)
+ local legend = client.server_capabilities.semanticTokensProvider.legend
+ local token_types = legend.tokenTypes
+ local token_modifiers = legend.tokenModifiers
+ local ranges = {}
+
+ local line
+ local start_char = 0
+ for i = 1, #data, 5 do
+ local delta_line = data[i]
+ line = line and line + delta_line or delta_line
+ local delta_start = data[i + 1]
+ start_char = delta_line == 0 and start_char + delta_start or delta_start
+
+ -- data[i+3] +1 because Lua tables are 1-indexed
+ local token_type = token_types[data[i + 3] + 1]
+ local modifiers = modifiers_from_number(data[i + 4], token_modifiers)
+
+ ---@private
+ local function _get_byte_pos(char_pos)
+ return util._get_line_byte_from_position(bufnr, {
+ line = line,
+ character = char_pos,
+ }, client.offset_encoding)
+ end
+
+ local start_col = _get_byte_pos(start_char)
+ local end_col = _get_byte_pos(start_char + data[i + 2])
+
+ if token_type then
+ ranges[#ranges + 1] = {
+ line = line,
+ start_col = start_col,
+ end_col = end_col,
+ type = token_type,
+ modifiers = modifiers,
+ extmark_added = false,
+ }
+ end
+ end
+
+ return ranges
+end
+
+--- Construct a new STHighlighter for the buffer
+---
+---@private
+---@param bufnr number
+function STHighlighter.new(bufnr)
+ local self = setmetatable({}, { __index = STHighlighter })
+
+ self.bufnr = bufnr
+ self.augroup = api.nvim_create_augroup('vim_lsp_semantic_tokens:' .. bufnr, { clear = true })
+ self.client_state = {}
+
+ STHighlighter.active[bufnr] = self
+
+ api.nvim_buf_attach(bufnr, false, {
+ on_lines = function(_, buf)
+ local highlighter = STHighlighter.active[buf]
+ if not highlighter then
+ return true
+ end
+ highlighter:on_change()
+ end,
+ on_reload = function(_, buf)
+ local highlighter = STHighlighter.active[buf]
+ if highlighter then
+ highlighter:reset()
+ highlighter:send_request()
+ end
+ end,
+ on_detach = function(_, buf)
+ local highlighter = STHighlighter.active[buf]
+ if highlighter then
+ highlighter:destroy()
+ end
+ end,
+ })
+
+ api.nvim_create_autocmd({ 'BufWinEnter', 'InsertLeave' }, {
+ buffer = self.bufnr,
+ group = self.augroup,
+ callback = function()
+ self:send_request()
+ end,
+ })
+
+ api.nvim_create_autocmd('LspDetach', {
+ buffer = self.bufnr,
+ group = self.augroup,
+ callback = function(args)
+ self:detach(args.data.client_id)
+ if vim.tbl_isempty(self.client_state) then
+ self:destroy()
+ end
+ end,
+ })
+
+ return self
+end
+
+---@private
+function STHighlighter:destroy()
+ for client_id, _ in pairs(self.client_state) do
+ self:detach(client_id)
+ end
+
+ api.nvim_del_augroup_by_id(self.augroup)
+ STHighlighter.active[self.bufnr] = nil
+end
+
+---@private
+function STHighlighter:attach(client_id)
+ local state = self.client_state[client_id]
+ if not state then
+ state = {
+ namespace = api.nvim_create_namespace('vim_lsp_semantic_tokens:' .. client_id),
+ active_request = {},
+ current_result = {},
+ }
+ self.client_state[client_id] = state
+ end
+end
+
+---@private
+function STHighlighter:detach(client_id)
+ local state = self.client_state[client_id]
+ if state then
+ --TODO: delete namespace if/when that becomes possible
+ api.nvim_buf_clear_namespace(self.bufnr, state.namespace, 0, -1)
+ self.client_state[client_id] = nil
+ end
+end
+
+--- This is the entry point for getting all the tokens in a buffer.
+---
+--- For the given clients (or all attached, if not provided), this sends a request
+--- to ask for semantic tokens. If the server supports delta requests, that will
+--- be prioritized if we have a previous requestId and token array.
+---
+--- This function will skip servers where there is an already an active request in
+--- flight for the same version. If there is a stale request in flight, that is
+--- cancelled prior to sending a new one.
+---
+--- Finally, if the request was successful, the requestId and document version
+--- are saved to facilitate document synchronization in the response.
+---
+---@private
+function STHighlighter:send_request()
+ local version = util.buf_versions[self.bufnr]
+
+ self:reset_timer()
+
+ for client_id, state in pairs(self.client_state) do
+ local client = vim.lsp.get_client_by_id(client_id)
+
+ local current_result = state.current_result
+ local active_request = state.active_request
+
+ -- Only send a request for this client if the current result is out of date and
+ -- there isn't a current a request in flight for this version
+ if client and current_result.version ~= version and active_request.version ~= version then
+ -- cancel stale in-flight request
+ if active_request.request_id then
+ client.cancel_request(active_request.request_id)
+ active_request = {}
+ state.active_request = active_request
+ end
+
+ local spec = client.server_capabilities.semanticTokensProvider.full
+ local hasEditProvider = type(spec) == 'table' and spec.delta
+
+ local params = { textDocument = util.make_text_document_params(self.bufnr) }
+ local method = 'textDocument/semanticTokens/full'
+
+ if hasEditProvider and current_result.result_id then
+ method = method .. '/delta'
+ params.previousResultId = current_result.result_id
+ end
+ local success, request_id = client.request(method, params, function(err, response, ctx)
+ -- look client up again using ctx.client_id instead of using a captured
+ -- client object
+ local c = vim.lsp.get_client_by_id(ctx.client_id)
+ local highlighter = STHighlighter.active[ctx.bufnr]
+ if not err and c and highlighter then
+ highlighter:process_response(response, c, version)
+ end
+ end, self.bufnr)
+
+ if success then
+ active_request.request_id = request_id
+ active_request.version = version
+ end
+ end
+ end
+end
+
+--- This function will parse the semantic token responses and set up the cache
+--- (current_result). It also performs document synchronization by checking the
+--- version of the document associated with the resulting request_id and only
+--- performing work if the response is not out-of-date.
+---
+--- Delta edits are applied if necessary, and new highlight ranges are calculated
+--- and stored in the buffer state.
+---
+--- Finally, a redraw command is issued to force nvim to redraw the screen to
+--- pick up changed highlight tokens.
+---
+---@private
+function STHighlighter:process_response(response, client, version)
+ local state = self.client_state[client.id]
+ if not state then
+ return
+ end
+
+ -- ignore stale responses
+ if state.active_request.version and version ~= state.active_request.version then
+ return
+ end
+
+ -- reset active request
+ state.active_request = {}
+
+ -- skip nil responses
+ if response == nil then
+ return
+ end
+
+ -- if we have a response to a delta request, update the state of our tokens
+ -- appropriately. if it's a full response, just use that
+ local tokens
+ local token_edits = response.edits
+ if token_edits then
+ table.sort(token_edits, function(a, b)
+ return a.start < b.start
+ end)
+
+ tokens = {}
+ local old_tokens = state.current_result.tokens
+ local idx = 1
+ for _, token_edit in ipairs(token_edits) do
+ vim.list_extend(tokens, old_tokens, idx, token_edit.start)
+ if token_edit.data then
+ vim.list_extend(tokens, token_edit.data)
+ end
+ idx = token_edit.start + token_edit.deleteCount + 1
+ end
+ vim.list_extend(tokens, old_tokens, idx)
+ else
+ tokens = response.data
+ end
+
+ -- Update the state with the new results
+ local current_result = state.current_result
+ current_result.version = version
+ current_result.result_id = response.resultId
+ current_result.tokens = tokens
+ current_result.highlights = tokens_to_ranges(tokens, self.bufnr, client)
+ current_result.namespace_cleared = false
+
+ api.nvim_command('redraw!')
+end
+
+--- on_win handler for the decoration provider (see |nvim_set_decoration_provider|)
+---
+--- If there is a current result for the buffer and the version matches the
+--- current document version, then the tokens are valid and can be applied. As
+--- the buffer is drawn, this function will add extmark highlights for every
+--- token in the range of visible lines. Once a highlight has been added, it
+--- sticks around until the document changes and there's a new set of matching
+--- highlight tokens available.
+---
+--- If this is the first time a buffer is being drawn with a new set of
+--- highlights for the current document version, the namespace is cleared to
+--- remove extmarks from the last version. It's done here instead of the response
+--- handler to avoid the "blink" that occurs due to the timing between the
+--- response handler and the actual redraw.
+---
+---@private
+function STHighlighter:on_win(topline, botline)
+ for _, state in pairs(self.client_state) do
+ local current_result = state.current_result
+ if current_result.version and current_result.version == util.buf_versions[self.bufnr] then
+ if not current_result.namespace_cleared then
+ api.nvim_buf_clear_namespace(self.bufnr, state.namespace, 0, -1)
+ current_result.namespace_cleared = true
+ end
+
+ -- We can't use ephemeral extmarks because the buffer updates are not in
+ -- sync with the list of semantic tokens. There's a delay between the
+ -- buffer changing and when the LSP server can respond with updated
+ -- tokens, and we don't want to "blink" the token highlights while
+ -- updates are in flight, and we don't want to use stale tokens because
+ -- they likely won't line up right with the actual buffer.
+ --
+ -- Instead, we have to use normal extmarks that can attach to locations
+ -- in the buffer and are persisted between redraws.
+ local highlights = current_result.highlights
+ local idx = binary_search(highlights, topline)
+
+ for i = idx, #highlights do
+ local token = highlights[i]
+
+ if token.line > botline then
+ break
+ end
+
+ if not token.extmark_added then
+ -- `strict = false` is necessary here for the 1% of cases where the
+ -- current result doesn't actually match the buffer contents. Some
+ -- LSP servers can respond with stale tokens on requests if they are
+ -- still processing changes from a didChange notification.
+ --
+ -- LSP servers that do this _should_ follow up known stale responses
+ -- with a refresh notification once they've finished processing the
+ -- didChange notification, which would re-synchronize the tokens from
+ -- our end.
+ --
+ -- The server I know of that does this is clangd when the preamble of
+ -- a file changes and the token request is processed with a stale
+ -- preamble while the new one is still being built. Once the preamble
+ -- finishes, clangd sends a refresh request which lets the client
+ -- re-synchronize the tokens.
+ api.nvim_buf_set_extmark(self.bufnr, state.namespace, token.line, token.start_col, {
+ hl_group = '@' .. token.type,
+ end_col = token.end_col,
+ priority = vim.highlight.priorities.semantic_tokens,
+ strict = false,
+ })
+
+ -- TODO(bfredl) use single extmark when hl_group supports table
+ if #token.modifiers > 0 then
+ for _, modifier in pairs(token.modifiers) do
+ api.nvim_buf_set_extmark(self.bufnr, state.namespace, token.line, token.start_col, {
+ hl_group = '@' .. modifier,
+ end_col = token.end_col,
+ priority = vim.highlight.priorities.semantic_tokens + 1,
+ strict = false,
+ })
+ end
+ end
+
+ token.extmark_added = true
+ end
+ end
+ end
+ end
+end
+
+--- Reset the buffer's highlighting state and clears the extmark highlights.
+---
+---@private
+function STHighlighter:reset()
+ for client_id, state in pairs(self.client_state) do
+ api.nvim_buf_clear_namespace(self.bufnr, state.namespace, 0, -1)
+ state.current_result = {}
+ if state.active_request.request_id then
+ local client = vim.lsp.get_client_by_id(client_id)
+ assert(client)
+ client.cancel_request(state.active_request.request_id)
+ state.active_request = {}
+ end
+ end
+end
+
+--- Mark a client's results as dirty. This method will cancel any active
+--- requests to the server and pause new highlights from being added
+--- in the on_win callback. The rest of the current results are saved
+--- in case the server supports delta requests.
+---
+---@private
+---@param client_id number
+function STHighlighter:mark_dirty(client_id)
+ local state = self.client_state[client_id]
+ assert(state)
+
+ -- if we clear the version from current_result, it'll cause the
+ -- next request to be sent and will also pause new highlights
+ -- from being added in on_win until a new result comes from
+ -- the server
+ if state.current_result then
+ state.current_result.version = nil
+ end
+
+ if state.active_request.request_id then
+ local client = vim.lsp.get_client_by_id(client_id)
+ assert(client)
+ client.cancel_request(state.active_request.request_id)
+ state.active_request = {}
+ end
+end
+
+---@private
+function STHighlighter:on_change()
+ self:reset_timer()
+ if self.debounce > 0 then
+ self.timer = vim.defer_fn(function()
+ self:send_request()
+ end, self.debounce)
+ else
+ self:send_request()
+ end
+end
+
+---@private
+function STHighlighter:reset_timer()
+ local timer = self.timer
+ if timer then
+ self.timer = nil
+ if not timer:is_closing() then
+ timer:stop()
+ timer:close()
+ end
+ end
+end
+
+local M = {}
+
+--- Start the semantic token highlighting engine for the given buffer with the
+--- given client. The client must already be attached to the buffer.
+---
+--- NOTE: This is currently called automatically by |vim.lsp.buf_attach_client()|. To
+--- opt-out of semantic highlighting with a server that supports it, you can
+--- delete the semanticTokensProvider table from the {server_capabilities} of
+--- your client in your |LspAttach| callback or your configuration's
+--- `on_attach` callback:
+--- <pre>lua
+--- client.server_capabilities.semanticTokensProvider = nil
+--- </pre>
+---
+---@param bufnr number
+---@param client_id number
+---@param opts (nil|table) Optional keyword arguments
+--- - debounce (number, default: 200): Debounce token requests
+--- to the server by the given number in milliseconds
+function M.start(bufnr, client_id, opts)
+ vim.validate({
+ bufnr = { bufnr, 'n', false },
+ client_id = { client_id, 'n', false },
+ })
+
+ opts = opts or {}
+ assert(
+ (not opts.debounce or type(opts.debounce) == 'number'),
+ 'opts.debounce must be a number with the debounce time in milliseconds'
+ )
+
+ local client = vim.lsp.get_client_by_id(client_id)
+ if not client then
+ vim.notify('[LSP] No client with id ' .. client_id, vim.log.levels.ERROR)
+ return
+ end
+
+ if not vim.lsp.buf_is_attached(bufnr, client_id) then
+ vim.notify(
+ '[LSP] Client with id ' .. client_id .. ' not attached to buffer ' .. bufnr,
+ vim.log.levels.WARN
+ )
+ return
+ end
+
+ if not vim.tbl_get(client.server_capabilities, 'semanticTokensProvider', 'full') then
+ vim.notify('[LSP] Server does not support semantic tokens', vim.log.levels.WARN)
+ return
+ end
+
+ local highlighter = STHighlighter.active[bufnr]
+
+ if not highlighter then
+ highlighter = STHighlighter.new(bufnr)
+ highlighter.debounce = opts.debounce or 200
+ else
+ highlighter.debounce = math.max(highlighter.debounce, opts.debounce or 200)
+ end
+
+ highlighter:attach(client_id)
+ highlighter:send_request()
+end
+
+--- Stop the semantic token highlighting engine for the given buffer with the
+--- given client.
+---
+--- NOTE: This is automatically called by a |LspDetach| autocmd that is set up as part
+--- of `start()`, so you should only need this function to manually disengage the semantic
+--- token engine without fully detaching the LSP client from the buffer.
+---
+---@param bufnr number
+---@param client_id number
+function M.stop(bufnr, client_id)
+ vim.validate({
+ bufnr = { bufnr, 'n', false },
+ client_id = { client_id, 'n', false },
+ })
+
+ local highlighter = STHighlighter.active[bufnr]
+ if not highlighter then
+ return
+ end
+
+ highlighter:detach(client_id)
+
+ if vim.tbl_isempty(highlighter.client_state) then
+ highlighter:destroy()
+ end
+end
+
+--- Return the semantic token(s) at the given position.
+--- If called without arguments, returns the token under the cursor.
+---
+---@param bufnr number|nil Buffer number (0 for current buffer, default)
+---@param row number|nil Position row (default cursor position)
+---@param col number|nil Position column (default cursor position)
+---
+---@return table|nil (table|nil) List of tokens at position
+function M.get_at_pos(bufnr, row, col)
+ if bufnr == nil or bufnr == 0 then
+ bufnr = api.nvim_get_current_buf()
+ end
+
+ local highlighter = STHighlighter.active[bufnr]
+ if not highlighter then
+ return
+ end
+
+ if row == nil or col == nil then
+ local cursor = api.nvim_win_get_cursor(0)
+ row, col = cursor[1] - 1, cursor[2]
+ end
+
+ local tokens = {}
+ for client_id, client in pairs(highlighter.client_state) do
+ local highlights = client.current_result.highlights
+ if highlights then
+ local idx = binary_search(highlights, row)
+ for i = idx, #highlights do
+ local token = highlights[i]
+
+ if token.line > row then
+ break
+ end
+
+ if token.start_col <= col and token.end_col > col then
+ token.client_id = client_id
+ tokens[#tokens + 1] = token
+ end
+ end
+ end
+ end
+ return tokens
+end
+
+--- Force a refresh of all semantic tokens
+---
+--- Only has an effect if the buffer is currently active for semantic token
+--- highlighting (|vim.lsp.semantic_tokens.start()| has been called for it)
+---
+---@param bufnr (nil|number) default: current buffer
+function M.force_refresh(bufnr)
+ vim.validate({
+ bufnr = { bufnr, 'n', true },
+ })
+
+ if bufnr == nil or bufnr == 0 then
+ bufnr = api.nvim_get_current_buf()
+ end
+
+ local highlighter = STHighlighter.active[bufnr]
+ if not highlighter then
+ return
+ end
+
+ highlighter:reset()
+ highlighter:send_request()
+end
+
+--- |lsp-handler| for the method `workspace/semanticTokens/refresh`
+---
+--- Refresh requests are sent by the server to indicate a project-wide change
+--- that requires all tokens to be re-requested by the client. This handler will
+--- invalidate the current results of all buffers and automatically kick off a
+--- new request for buffers that are displayed in a window. For those that aren't, a
+--- the BufWinEnter event should take care of it next time it's displayed.
+---
+---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#semanticTokens_refreshRequest
+handlers['workspace/semanticTokens/refresh'] = function(err, _, ctx)
+ if err then
+ return vim.NIL
+ end
+
+ for _, bufnr in ipairs(vim.lsp.get_buffers_by_client_id(ctx.client_id)) do
+ local highlighter = STHighlighter.active[bufnr]
+ if highlighter and highlighter.client_state[ctx.client_id] then
+ highlighter:mark_dirty(ctx.client_id)
+
+ if not vim.tbl_isempty(vim.fn.win_findbuf(bufnr)) then
+ highlighter:send_request()
+ end
+ end
+ end
+
+ return vim.NIL
+end
+
+local namespace = api.nvim_create_namespace('vim_lsp_semantic_tokens')
+api.nvim_set_decoration_provider(namespace, {
+ on_win = function(_, _, bufnr, topline, botline)
+ local highlighter = STHighlighter.active[bufnr]
+ if highlighter then
+ highlighter:on_win(topline, botline)
+ end
+ end,
+})
+
+--- for testing only! there is no guarantee of API stability with this!
+---
+---@private
+M.__STHighlighter = STHighlighter
+
+return M
diff --git a/runtime/lua/vim/lsp/sync.lua b/runtime/lua/vim/lsp/sync.lua
index 0d65e86b55..826352f036 100644
--- a/runtime/lua/vim/lsp/sync.lua
+++ b/runtime/lua/vim/lsp/sync.lua
@@ -392,7 +392,7 @@ end
---@param lastline number line to begin search in old_lines for last difference
---@param new_lastline number line to begin search in new_lines for last difference
---@param offset_encoding string encoding requested by language server
----@returns table TextDocumentContentChangeEvent see https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocumentContentChangeEvent
+---@returns table TextDocumentContentChangeEvent see https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentContentChangeEvent
function M.compute_diff(
prev_lines,
curr_lines,
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index 283099bbcf..38051e6410 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -1,6 +1,5 @@
local protocol = require('vim.lsp.protocol')
local snippet = require('vim.lsp._snippet')
-local vim = vim
local validate = vim.validate
local api = vim.api
local list_extend = vim.list_extend
@@ -110,6 +109,15 @@ local function split_lines(value)
return split(value, '\n', true)
end
+---@private
+local function create_window_without_focus()
+ local prev = vim.api.nvim_get_current_win()
+ vim.cmd.new()
+ local new = vim.api.nvim_get_current_win()
+ vim.api.nvim_set_current_win(prev)
+ return new
+end
+
--- Convert byte index to `encoding` index.
--- Convenience wrapper around vim.str_utfindex
---@param line string line to be indexed
@@ -459,35 +467,52 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding)
text = split(text_edit.newText, '\n', true),
}
- -- Some LSP servers may return +1 range of the buffer content but nvim_buf_set_text can't accept it so we should fix it here.
local max = api.nvim_buf_line_count(bufnr)
- if max <= e.start_row or max <= e.end_row then
- local len = #(get_line(bufnr, max - 1) or '')
- if max <= e.start_row then
- e.start_row = max - 1
- e.start_col = len
- table.insert(e.text, 1, '')
- end
+ -- If the whole edit is after the lines in the buffer we can simply add the new text to the end
+ -- of the buffer.
+ if max <= e.start_row then
+ api.nvim_buf_set_lines(bufnr, max, max, false, e.text)
+ else
+ local last_line_len = #(get_line(bufnr, math.min(e.end_row, max - 1)) or '')
+ -- Some LSP servers may return +1 range of the buffer content but nvim_buf_set_text can't
+ -- accept it so we should fix it here.
if max <= e.end_row then
e.end_row = max - 1
- e.end_col = len
+ e.end_col = last_line_len
+ has_eol_text_edit = true
+ else
+ -- If the replacement is over the end of a line (i.e. e.end_col is out of bounds and the
+ -- replacement text ends with a newline We can likely assume that the replacement is assumed
+ -- to be meant to replace the newline with another newline and we need to make sure this
+ -- doesn't add an extra empty line. E.g. when the last line to be replaced contains a '\r'
+ -- in the file some servers (clangd on windows) will include that character in the line
+ -- while nvim_buf_set_text doesn't count it as part of the line.
+ if
+ e.end_col > last_line_len
+ and #text_edit.newText > 0
+ and string.sub(text_edit.newText, -1) == '\n'
+ then
+ table.remove(e.text, #e.text)
+ end
end
- has_eol_text_edit = true
- end
- api.nvim_buf_set_text(bufnr, e.start_row, e.start_col, e.end_row, e.end_col, e.text)
-
- -- Fix cursor position.
- local row_count = (e.end_row - e.start_row) + 1
- if e.end_row < cursor.row then
- cursor.row = cursor.row + (#e.text - row_count)
- is_cursor_fixed = true
- elseif e.end_row == cursor.row and e.end_col <= cursor.col then
- cursor.row = cursor.row + (#e.text - row_count)
- cursor.col = #e.text[#e.text] + (cursor.col - e.end_col)
- if #e.text == 1 then
- cursor.col = cursor.col + e.start_col
+ -- Make sure we don't go out of bounds for e.end_col
+ e.end_col = math.min(last_line_len, e.end_col)
+
+ api.nvim_buf_set_text(bufnr, e.start_row, e.start_col, e.end_row, e.end_col, e.text)
+
+ -- Fix cursor position.
+ local row_count = (e.end_row - e.start_row) + 1
+ if e.end_row < cursor.row then
+ cursor.row = cursor.row + (#e.text - row_count)
+ is_cursor_fixed = true
+ elseif e.end_row == cursor.row and e.end_col <= cursor.col then
+ cursor.row = cursor.row + (#e.text - row_count)
+ cursor.col = #e.text[#e.text] + (cursor.col - e.end_col)
+ if #e.text == 1 then
+ cursor.col = cursor.col + e.start_col
+ end
+ is_cursor_fixed = true
end
- is_cursor_fixed = true
end
end
@@ -755,8 +780,11 @@ local function create_file(change)
-- from spec: Overwrite wins over `ignoreIfExists`
local fname = vim.uri_to_fname(change.uri)
if not opts.ignoreIfExists or opts.overwrite then
+ vim.fn.mkdir(vim.fs.dirname(fname), 'p')
local file = io.open(fname, 'w')
- file:close()
+ if file then
+ file:close()
+ end
end
vim.fn.bufadd(fname)
end
@@ -828,7 +856,7 @@ end
--- `textDocument/signatureHelp`, and potentially others.
---
---@param input (`MarkedString` | `MarkedString[]` | `MarkupContent`)
----@param contents (table, optional, default `{}`) List of strings to extend with converted lines
+---@param contents (table|nil) List of strings to extend with converted lines. Defaults to {}.
---@returns {contents}, extended with lines of converted markdown.
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover
function M.convert_input_to_markdown_lines(input, contents)
@@ -887,8 +915,8 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers
return
end
--The active signature. If omitted or the value lies outside the range of
- --`signatures` the value defaults to zero or is ignored if `signatures.length
- --=== 0`. Whenever possible implementors should make an active decision about
+ --`signatures` the value defaults to zero or is ignored if `signatures.length == 0`.
+ --Whenever possible implementors should make an active decision about
--the active signature and shouldn't rely on a default value.
local contents = {}
local active_hl
@@ -987,6 +1015,7 @@ end
--- - border (string or table) override `border`
--- - focusable (string or table) override `focusable`
--- - zindex (string or table) override `zindex`, defaults to 50
+--- - relative ("mouse"|"cursor") defaults to "cursor"
---@returns (table) Options
function M.make_floating_popup_options(width, height, opts)
validate({
@@ -1001,7 +1030,8 @@ function M.make_floating_popup_options(width, height, opts)
local anchor = ''
local row, col
- local lines_above = vim.fn.winline() - 1
+ local lines_above = opts.relative == 'mouse' and vim.fn.getmousepos().line - 1
+ or vim.fn.winline() - 1
local lines_below = vim.fn.winheight(0) - lines_above
if lines_above < lines_below then
@@ -1014,7 +1044,9 @@ function M.make_floating_popup_options(width, height, opts)
row = 0
end
- if vim.fn.wincol() + width + (opts.offset_x or 0) <= api.nvim_get_option('columns') then
+ local wincol = opts.relative == 'mouse' and vim.fn.getmousepos().column or vim.fn.wincol()
+
+ if wincol + width + (opts.offset_x or 0) <= api.nvim_get_option('columns') then
anchor = anchor .. 'W'
col = 0
else
@@ -1022,64 +1054,103 @@ function M.make_floating_popup_options(width, height, opts)
col = 1
end
+ local title = (opts.border and opts.title) and opts.title or nil
+ local title_pos
+
+ if title then
+ title_pos = opts.title_pos or 'center'
+ end
+
return {
anchor = anchor,
col = col + (opts.offset_x or 0),
height = height,
focusable = opts.focusable,
- relative = 'cursor',
+ relative = opts.relative == 'mouse' and 'mouse' or 'cursor',
row = row + (opts.offset_y or 0),
style = 'minimal',
width = width,
border = opts.border or default_border,
zindex = opts.zindex or 50,
+ title = title,
+ title_pos = title_pos,
}
end
---- Jumps to a location.
+--- Shows document and optionally jumps to the location.
---
---@param location table (`Location`|`LocationLink`)
----@param offset_encoding string utf-8|utf-16|utf-32 (required)
----@param reuse_win boolean Jump to existing window if buffer is already opened.
----@returns `true` if the jump succeeded
-function M.jump_to_location(location, offset_encoding, reuse_win)
+---@param offset_encoding "utf-8" | "utf-16" | "utf-32"
+---@param opts table|nil options
+--- - reuse_win (boolean) Jump to existing window if buffer is already open.
+--- - focus (boolean) Whether to focus/jump to location if possible. Defaults to true.
+---@return boolean `true` if succeeded
+function M.show_document(location, offset_encoding, opts)
-- location may be Location or LocationLink
local uri = location.uri or location.targetUri
if uri == nil then
- return
+ return false
end
if offset_encoding == nil then
- vim.notify_once(
- 'jump_to_location must be called with valid offset encoding',
- vim.log.levels.WARN
- )
+ vim.notify_once('show_document must be called with valid offset encoding', vim.log.levels.WARN)
end
local bufnr = vim.uri_to_bufnr(uri)
- -- Save position in jumplist
- vim.cmd("normal! m'")
- -- Push a new item into tagstack
- local from = { vim.fn.bufnr('%'), vim.fn.line('.'), vim.fn.col('.'), 0 }
- local items = { { tagname = vim.fn.expand('<cword>'), from = from } }
- vim.fn.settagstack(vim.fn.win_getid(), { items = items }, 't')
+ opts = opts or {}
+ local focus = vim.F.if_nil(opts.focus, true)
+ if focus then
+ -- Save position in jumplist
+ vim.cmd("normal! m'")
- --- Jump to new location (adjusting for UTF-16 encoding of characters)
- local win = reuse_win and bufwinid(bufnr)
- if win then
+ -- Push a new item into tagstack
+ local from = { vim.fn.bufnr('%'), vim.fn.line('.'), vim.fn.col('.'), 0 }
+ local items = { { tagname = vim.fn.expand('<cword>'), from = from } }
+ vim.fn.settagstack(vim.fn.win_getid(), { items = items }, 't')
+ end
+
+ local win = opts.reuse_win and bufwinid(bufnr)
+ or focus and api.nvim_get_current_win()
+ or create_window_without_focus()
+
+ api.nvim_buf_set_option(bufnr, 'buflisted', true)
+ api.nvim_win_set_buf(win, bufnr)
+ if focus then
api.nvim_set_current_win(win)
- else
- api.nvim_buf_set_option(bufnr, 'buflisted', true)
- api.nvim_set_current_buf(bufnr)
end
+
+ -- location may be Location or LocationLink
local range = location.range or location.targetSelectionRange
- local row = range.start.line
- local col = get_line_byte_from_position(bufnr, range.start, offset_encoding)
- api.nvim_win_set_cursor(0, { row + 1, col })
- -- Open folds under the cursor
- vim.cmd('normal! zv')
+ if range then
+ --- Jump to new location (adjusting for encoding of characters)
+ local row = range.start.line
+ local col = get_line_byte_from_position(bufnr, range.start, offset_encoding)
+ api.nvim_win_set_cursor(win, { row + 1, col })
+ api.nvim_win_call(win, function()
+ -- Open folds under the cursor
+ vim.cmd('normal! zv')
+ end)
+ end
+
return true
end
+--- Jumps to a location.
+---
+---@param location table (`Location`|`LocationLink`)
+---@param offset_encoding "utf-8" | "utf-16" | "utf-32"
+---@param reuse_win boolean|nil Jump to existing window if buffer is already open.
+---@return boolean `true` if the jump succeeded
+function M.jump_to_location(location, offset_encoding, reuse_win)
+ if offset_encoding == nil then
+ vim.notify_once(
+ 'jump_to_location must be called with valid offset encoding',
+ vim.log.levels.WARN
+ )
+ end
+
+ return M.show_document(location, offset_encoding, { reuse_win = reuse_win, focus = true })
+end
+
--- Previews a location in a floating window
---
--- behavior depends on type of location:
@@ -1194,7 +1265,7 @@ function M.stylize_markdown(bufnr, contents, opts)
-- when ft is nil, we get the ft from the regex match
local matchers = {
block = { nil, '```+([a-zA-Z0-9_]*)', '```+' },
- pre = { '', '<pre>', '</pre>' },
+ pre = { nil, '<pre>([a-z0-9]*)', '</pre>' },
code = { '', '<code>', '</code>' },
text = { 'text', '<text>', '</text>' },
}
@@ -1219,8 +1290,6 @@ function M.stylize_markdown(bufnr, contents, opts)
-- Clean up
contents = M._trim(contents, opts)
- -- Insert blank line separator after code block?
- local add_sep = opts.separator == nil and true or opts.separator
local stripped = {}
local highlights = {}
-- keep track of lnums that contain markdown
@@ -1248,7 +1317,7 @@ function M.stylize_markdown(bufnr, contents, opts)
finish = #stripped,
})
-- add a separator, but not on the last line
- if add_sep and i < #contents then
+ if opts.separator and i < #contents then
table.insert(stripped, '---')
markdown_lines[#stripped] = true
end
@@ -1484,7 +1553,7 @@ end
---
---@param contents table of lines to show in window
---@param syntax string of syntax to set for opened buffer
----@param opts table with optional fields (additional keys are passed on to |vim.api.nvim_open_win()|)
+---@param opts table with optional fields (additional keys are passed on to |nvim_open_win()|)
--- - height: (number) height of floating window
--- - width: (number) width of floating window
--- - wrap: (boolean, default true) wrap long lines
@@ -1612,7 +1681,7 @@ do --[[ References ]]
---@param bufnr number Buffer id
---@param references table List of `DocumentHighlight` objects to highlight
---@param offset_encoding string One of "utf-8", "utf-16", "utf-32".
- ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#documentHighlight
+ ---@see https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentContentChangeEvent
function M.buf_highlight_references(bufnr, references, offset_encoding)
validate({
bufnr = { bufnr, 'n', true },
@@ -1799,7 +1868,7 @@ end
--- CAUTION: Modifies the input in-place!
---
---@param lines (table) list of lines
----@returns (string) filetype or 'markdown' if it was unchanged.
+---@returns (string) filetype or "markdown" if it was unchanged.
function M.try_trim_markdown_code_blocks(lines)
local language_id = lines[1]:match('^```(.*)')
if language_id then
@@ -1843,7 +1912,7 @@ end
--- Creates a `TextDocumentPositionParams` object for the current buffer and cursor position.
---
---@param window number|nil: 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`
+---@param offset_encoding string|nil 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(window, offset_encoding)
@@ -1866,7 +1935,7 @@ function M._get_offset_encoding(bufnr)
local offset_encoding
- for _, client in pairs(vim.lsp.buf_get_clients(bufnr)) do
+ for _, client in pairs(vim.lsp.get_active_clients({ bufnr = bufnr })) do
if client.offset_encoding == nil then
vim.notify_once(
string.format(
@@ -1972,7 +2041,7 @@ function M.make_workspace_params(added, removed)
end
--- Returns indentation size.
---
----@see |shiftwidth|
+---@see 'shiftwidth'
---@param bufnr (number|nil): Buffer handle, defaults to current
---@returns (number) indentation size
function M.get_effective_tabstop(bufnr)