aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/lsp/util.lua
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua/vim/lsp/util.lua')
-rw-r--r--runtime/lua/vim/lsp/util.lua1101
1 files changed, 507 insertions, 594 deletions
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index 882ec22ca6..6eab0f3da4 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -2,12 +2,8 @@ local protocol = require('vim.lsp.protocol')
local validate = vim.validate
local api = vim.api
local list_extend = vim.list_extend
-local highlight = vim.highlight
local uv = vim.uv
-local npcall = vim.F.npcall
-local split = vim.split
-
local M = {}
local default_border = {
@@ -21,82 +17,73 @@ local default_border = {
{ ' ', 'NormalFloat' },
}
+--- @param border string|(string|[string,string])[]
+local function border_error(border)
+ error(
+ string.format(
+ 'invalid floating preview border: %s. :help vim.api.nvim_open_win()',
+ vim.inspect(border)
+ ),
+ 2
+ )
+end
+
+local border_size = {
+ none = { 0, 0 },
+ single = { 2, 2 },
+ double = { 2, 2 },
+ rounded = { 2, 2 },
+ solid = { 2, 2 },
+ shadow = { 1, 1 },
+}
+
--- Check the border given by opts or the default border for the additional
--- size it adds to a float.
----@param opts table optional options for the floating window
---- - border (string or table) the border
----@return table size of border in the form of { height = height, width = width }
+--- @param opts? {border:string|(string|[string,string])[]}
+--- @return integer height
+--- @return integer width
local function get_border_size(opts)
local border = opts and opts.border or default_border
- local height = 0
- local width = 0
if type(border) == 'string' then
- local border_size = {
- none = { 0, 0 },
- single = { 2, 2 },
- double = { 2, 2 },
- rounded = { 2, 2 },
- solid = { 2, 2 },
- shadow = { 1, 1 },
- }
- if border_size[border] == nil then
- error(
- string.format(
- 'invalid floating preview border: %s. :help vim.api.nvim_open_win()',
- vim.inspect(border)
- )
- )
+ if not border_size[border] then
+ border_error(border)
end
- height, width = unpack(border_size[border])
- else
- if 8 % #border ~= 0 then
- error(
- string.format(
- 'invalid floating preview border: %s. :help vim.api.nvim_open_win()',
- vim.inspect(border)
- )
- )
- end
- local function border_width(id)
- id = (id - 1) % #border + 1
- if type(border[id]) == 'table' then
- -- border specified as a table of <character, highlight group>
- return vim.fn.strdisplaywidth(border[id][1])
- elseif type(border[id]) == 'string' then
- -- border specified as a list of border characters
- return vim.fn.strdisplaywidth(border[id])
- end
- error(
- string.format(
- 'invalid floating preview border: %s. :help vim.api.nvim_open_win()',
- vim.inspect(border)
- )
- )
- end
- local function border_height(id)
- id = (id - 1) % #border + 1
- if type(border[id]) == 'table' then
- -- border specified as a table of <character, highlight group>
- return #border[id][1] > 0 and 1 or 0
- elseif type(border[id]) == 'string' then
- -- border specified as a list of border characters
- return #border[id] > 0 and 1 or 0
- end
- error(
- string.format(
- 'invalid floating preview border: %s. :help vim.api.nvim_open_win()',
- vim.inspect(border)
- )
- )
+ return unpack(border_size[border])
+ end
+
+ if 8 % #border ~= 0 then
+ border_error(border)
+ end
+
+ --- @param id integer
+ --- @return string
+ local function elem(id)
+ id = (id - 1) % #border + 1
+ local e = border[id]
+ if type(e) == 'table' then
+ -- border specified as a table of <character, highlight group>
+ return e[1]
+ elseif type(e) == 'string' then
+ -- border specified as a list of border characters
+ return e
end
- height = height + border_height(2) -- top
- height = height + border_height(6) -- bottom
- width = width + border_width(4) -- right
- width = width + border_width(8) -- left
+ --- @diagnostic disable-next-line:missing-return
+ border_error(border)
+ end
+
+ --- @param e string
+ local function border_height(e)
+ return #e > 0 and 1 or 0
end
- return { height = height, width = width }
+ local top, bottom = elem(2), elem(6)
+ local height = border_height(top) + border_height(bottom)
+
+ local right, left = elem(4), elem(8)
+ local width = vim.fn.strdisplaywidth(right) + vim.fn.strdisplaywidth(left)
+
+ return height, width
end
--- Splits string at newlines, optionally removing unwanted blank lines.
@@ -122,79 +109,13 @@ local function split_lines(s, no_blank)
end
local function create_window_without_focus()
- local prev = vim.api.nvim_get_current_win()
+ local prev = api.nvim_get_current_win()
vim.cmd.new()
- local new = vim.api.nvim_get_current_win()
- vim.api.nvim_set_current_win(prev)
+ local new = api.nvim_get_current_win()
+ 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
----@param index integer|nil byte index (utf-8), or `nil` for length
----@param encoding 'utf-8'|'utf-16'|'utf-32'|nil defaults to utf-16
----@return integer `encoding` index of `index` in `line`
-function M._str_utfindex_enc(line, index, encoding)
- local len32, len16 = vim.str_utfindex(line)
- if not encoding then
- encoding = 'utf-16'
- end
- if encoding == 'utf-8' then
- if index then
- return index
- else
- return #line
- end
- elseif encoding == 'utf-16' then
- if not index or index > len16 then
- return len16
- end
- local _, col16 = vim.str_utfindex(line, index)
- return col16
- elseif encoding == 'utf-32' then
- if not index or index > len32 then
- return len32
- end
- local col32, _ = vim.str_utfindex(line, index)
- return col32
- else
- error('Invalid encoding: ' .. vim.inspect(encoding))
- end
-end
-
---- Convert UTF index to `encoding` index.
---- Convenience wrapper around vim.str_byteindex
----Alternative to vim.str_byteindex that takes an encoding.
----@param line string line to be indexed
----@param index integer UTF index
----@param encoding string utf-8|utf-16|utf-32| defaults to utf-16
----@return integer byte (utf-8) index of `encoding` index `index` in `line`
-function M._str_byteindex_enc(line, index, encoding)
- local len = #line
- if index > len then
- -- LSP spec: if character > line length, default to the line length.
- -- https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#position
- return len
- end
- if not encoding then
- encoding = 'utf-16'
- end
- if encoding == 'utf-8' then
- if index then
- return index
- else
- return len
- end
- elseif encoding == 'utf-16' then
- return vim.str_byteindex(line, index, true)
- elseif encoding == 'utf-32' then
- return vim.str_byteindex(line, index)
- else
- error('Invalid encoding: ' .. vim.inspect(encoding))
- end
-end
-
--- Replaces text in a range with new text.
---
--- CAUTION: Changes in-place!
@@ -243,6 +164,8 @@ function M.set_lines(lines, A, B, new_lines)
return lines
end
+--- @param fn fun(x:any):any[]
+--- @return function
local function sort_by_key(fn)
return function(a, b)
local ka, kb = fn(a), fn(b)
@@ -354,7 +277,7 @@ end
--- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position
---@param position lsp.Position
----@param offset_encoding? string utf-8|utf-16|utf-32
+---@param offset_encoding 'utf-8'|'utf-16'|'utf-32'
---@return integer
local function get_line_byte_from_position(bufnr, position, offset_encoding)
-- LSP's line and characters are 0-indexed
@@ -364,7 +287,7 @@ local function get_line_byte_from_position(bufnr, position, offset_encoding)
-- character
if col > 0 then
local line = get_line(bufnr, position.line) or ''
- return M._str_byteindex_enc(line, col, offset_encoding or 'utf-16')
+ return vim.str_byteindex(line, offset_encoding, col, false)
end
return col
end
@@ -372,14 +295,13 @@ end
--- Applies a list of text edits to a buffer.
---@param text_edits lsp.TextEdit[]
---@param bufnr integer Buffer id
----@param offset_encoding string utf-8|utf-16|utf-32
+---@param offset_encoding 'utf-8'|'utf-16'|'utf-32'
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textEdit
function M.apply_text_edits(text_edits, bufnr, offset_encoding)
- validate({
- text_edits = { text_edits, 't', false },
- bufnr = { bufnr, 'number', false },
- offset_encoding = { offset_encoding, 'string', false },
- })
+ validate('text_edits', text_edits, 'table', false)
+ validate('bufnr', bufnr, 'number', false)
+ validate('offset_encoding', offset_encoding, 'string', false)
+
if not next(text_edits) then
return
end
@@ -392,10 +314,8 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding)
vim.bo[bufnr].buflisted = true
-- Fix reversed range and indexing each text_edits
- local index = 0
- --- @param text_edit lsp.TextEdit
- text_edits = vim.tbl_map(function(text_edit)
- index = index + 1
+ for index, text_edit in ipairs(text_edits) do
+ --- @cast text_edit lsp.TextEdit|{_index: integer}
text_edit._index = index
if
@@ -407,8 +327,7 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding)
text_edit.range.start = text_edit.range['end']
text_edit.range['end'] = start
end
- return text_edit
- end, text_edits)
+ end
-- Sort text_edits
---@param a lsp.TextEdit | { _index: integer }
@@ -439,47 +358,45 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding)
text_edit.newText, _ = string.gsub(text_edit.newText, '\r\n?', '\n')
-- Convert from LSP style ranges to Neovim style ranges.
- local e = {
- start_row = text_edit.range.start.line,
- start_col = get_line_byte_from_position(bufnr, text_edit.range.start, offset_encoding),
- end_row = text_edit.range['end'].line,
- end_col = get_line_byte_from_position(bufnr, text_edit.range['end'], offset_encoding),
- text = split(text_edit.newText, '\n', { plain = true }),
- }
+ local start_row = text_edit.range.start.line
+ local start_col = get_line_byte_from_position(bufnr, text_edit.range.start, offset_encoding)
+ local end_row = text_edit.range['end'].line
+ local end_col = get_line_byte_from_position(bufnr, text_edit.range['end'], offset_encoding)
+ local text = vim.split(text_edit.newText, '\n', { plain = true })
local max = api.nvim_buf_line_count(bufnr)
-- 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)
+ if max <= start_row then
+ api.nvim_buf_set_lines(bufnr, max, max, false, text)
else
- local last_line_len = #(get_line(bufnr, math.min(e.end_row, max - 1)) or '')
+ local last_line_len = #(get_line(bufnr, math.min(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 = last_line_len
+ if max <= end_row then
+ end_row = max - 1
+ 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 equal to the line length and the
+ -- If the replacement is over the end of a line (i.e. end_col is equal to the line length 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.range['end'].character > e.end_col
+ end_col >= last_line_len
+ and text_edit.range['end'].character > end_col
and #text_edit.newText > 0
and string.sub(text_edit.newText, -1) == '\n'
then
- table.remove(e.text, #e.text)
+ table.remove(text, #text)
end
end
- -- Make sure we don't go out of bounds for e.end_col
- e.end_col = math.min(last_line_len, e.end_col)
+ -- Make sure we don't go out of bounds for end_col
+ end_col = math.min(last_line_len, end_col)
- api.nvim_buf_set_text(bufnr, e.start_row, e.start_col, e.end_row, e.end_col, e.text)
+ api.nvim_buf_set_text(bufnr, start_row, start_col, end_row, end_col, text)
end
end
@@ -495,7 +412,7 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding)
-- make sure we don't go out of bounds
pos[1] = math.min(pos[1], max)
pos[2] = math.min(pos[2], #(get_line(bufnr, pos[1] - 1) or ''))
- vim.api.nvim_buf_set_mark(bufnr or 0, mark, pos[1], pos[2], {})
+ api.nvim_buf_set_mark(bufnr or 0, mark, pos[1], pos[2], {})
end
end
@@ -513,7 +430,7 @@ end
---
---@param text_document_edit lsp.TextDocumentEdit
---@param index? integer: Optional index of the edit, if from a list of edits (or nil, if not from a list)
----@param offset_encoding? string
+---@param offset_encoding? 'utf-8'|'utf-16'|'utf-32'
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentEdit
function M.apply_text_document_edit(text_document_edit, index, offset_encoding)
local text_document = text_document_edit.textDocument
@@ -523,19 +440,15 @@ function M.apply_text_document_edit(text_document_edit, index, offset_encoding)
'apply_text_document_edit must be called with valid offset encoding',
vim.log.levels.WARN
)
- end
-
- -- For lists of text document edits,
- -- do not check the version after the first edit.
- local should_check_version = true
- if index and index > 1 then
- should_check_version = false
+ return
end
-- `VersionedTextDocumentIdentifier`s version may be null
-- https://microsoft.github.io/language-server-protocol/specification#versionedTextDocumentIdentifier
if
- should_check_version
+ -- For lists of text document edits,
+ -- do not check the version after the first edit.
+ not (index and index > 1)
and (
text_document.version
and text_document.version > 0
@@ -553,6 +466,9 @@ local function path_components(path)
return vim.split(path, '/', { plain = true })
end
+--- @param path string[]
+--- @param prefix string[]
+--- @return boolean
local function path_under_prefix(path, prefix)
for i, c in ipairs(prefix) do
if c ~= path[i] then
@@ -562,17 +478,24 @@ local function path_under_prefix(path, prefix)
return true
end
---- Get list of buffers whose filename matches the given path prefix (normalized full path)
+--- Get list of loaded writable buffers whose filename matches the given path
+--- prefix (normalized full path).
---@param prefix string
---@return integer[]
-local function get_bufs_with_prefix(prefix)
- prefix = path_components(prefix)
- local buffers = {}
- for _, v in ipairs(vim.api.nvim_list_bufs()) do
- local bname = vim.api.nvim_buf_get_name(v)
- local path = path_components(vim.fs.normalize(bname, { expand_env = false }))
- if path_under_prefix(path, prefix) then
- table.insert(buffers, v)
+local function get_writable_bufs(prefix)
+ local prefix_parts = path_components(prefix)
+ local buffers = {} --- @type integer[]
+ for _, buf in ipairs(api.nvim_list_bufs()) do
+ -- No need to care about unloaded or nofile buffers. Also :saveas won't work for them.
+ if
+ api.nvim_buf_is_loaded(buf)
+ and not vim.list_contains({ 'nofile', 'nowrite' }, vim.bo[buf].buftype)
+ then
+ local bname = api.nvim_buf_get_name(buf)
+ local path = path_components(vim.fs.normalize(bname, { expand_env = false }))
+ if path_under_prefix(path, prefix_parts) then
+ buffers[#buffers + 1] = buf
+ end
end
end
return buffers
@@ -616,19 +539,13 @@ function M.rename(old_fname, new_fname, opts)
local buf_rename = {} ---@type table<integer, {from: string, to: string}>
local old_fname_pat = '^' .. vim.pesc(old_fname_full)
- for b in
- vim.iter(get_bufs_with_prefix(old_fname_full)):filter(function(b)
- -- No need to care about unloaded or nofile buffers. Also :saveas won't work for them.
- return api.nvim_buf_is_loaded(b)
- and not vim.list_contains({ 'nofile', 'nowrite' }, vim.bo[b].buftype)
- end)
- do
+ for _, b in ipairs(get_writable_bufs(old_fname_full)) do
-- Renaming a buffer may conflict with another buffer that happens to have the same name. In
-- most cases, this would have been already detected by the file conflict check above, but the
-- conflicting buffer may not be associated with a file. For example, 'buftype' can be "nofile"
-- or "nowrite", or the buffer can be a normal buffer but has not been written to the file yet.
-- Renaming should fail in such cases to avoid losing the contents of the conflicting buffer.
- local old_bname = vim.api.nvim_buf_get_name(b)
+ local old_bname = api.nvim_buf_get_name(b)
local new_bname = old_bname:gsub(old_fname_pat, escape_gsub_repl(new_fname))
if vim.fn.bufexists(new_bname) == 1 then
local existing_buf = vim.fn.bufnr(new_bname)
@@ -702,7 +619,7 @@ end
--- Applies a `WorkspaceEdit`.
---
---@param workspace_edit lsp.WorkspaceEdit
----@param offset_encoding string utf-8|utf-16|utf-32 (required)
+---@param offset_encoding 'utf-8'|'utf-16'|'utf-32' (required)
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit
function M.apply_workspace_edit(workspace_edit, offset_encoding)
if offset_encoding == nil then
@@ -710,16 +627,18 @@ function M.apply_workspace_edit(workspace_edit, offset_encoding)
'apply_workspace_edit must be called with valid offset encoding',
vim.log.levels.WARN
)
+ return
end
if workspace_edit.documentChanges then
for idx, change in ipairs(workspace_edit.documentChanges) do
if change.kind == 'rename' then
- M.rename(vim.uri_to_fname(change.oldUri), vim.uri_to_fname(change.newUri), change.options)
+ local options = change.options --[[@as vim.lsp.util.rename.Opts]]
+ M.rename(vim.uri_to_fname(change.oldUri), vim.uri_to_fname(change.newUri), options)
elseif change.kind == 'create' then
create_file(change)
elseif change.kind == 'delete' then
delete_file(change)
- elseif change.kind then
+ elseif change.kind then --- @diagnostic disable-line:undefined-field
error(string.format('Unsupported change: %q', vim.inspect(change)))
else
M.apply_text_document_edit(change, idx, offset_encoding)
@@ -748,7 +667,7 @@ end
--- then the corresponding value is returned without further modifications.
---
---@param input lsp.MarkedString|lsp.MarkedString[]|lsp.MarkupContent
----@param contents string[]|nil List of strings to extend with converted lines. Defaults to {}.
+---@param contents string[]? List of strings to extend with converted lines. Defaults to {}.
---@return string[] 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)
@@ -781,111 +700,117 @@ function M.convert_input_to_markdown_lines(input, contents)
return contents
end
+--- Returns the line/column-based position in `contents` at the given offset.
+---
+---@param offset integer
+---@param contents string[]
+---@return { [1]: integer, [2]: integer }?
+local function get_pos_from_offset(offset, contents)
+ local i = 0
+ for l, line in ipairs(contents) do
+ if offset >= i and offset < i + #line then
+ return { l - 1, offset - i + 1 }
+ else
+ i = i + #line + 1
+ end
+ end
+end
+
--- Converts `textDocument/signatureHelp` response to markdown lines.
---
---@param signature_help lsp.SignatureHelp Response of `textDocument/SignatureHelp`
----@param ft string|nil filetype that will be use as the `lang` for the label markdown code block
----@param triggers table|nil list of trigger characters from the lsp server. used to better determine parameter offsets
----@return string[]|nil table list of lines of converted markdown.
----@return number[]|nil table of active hl
+---@param ft string? filetype that will be use as the `lang` for the label markdown code block
+---@param triggers string[]? list of trigger characters from the lsp server. used to better determine parameter offsets
+---@return string[]? # lines of converted markdown.
+---@return Range4? # highlight range for the active parameter
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp
function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers)
- if not signature_help.signatures then
- 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
--the active signature and shouldn't rely on a default value.
- local contents = {}
- local active_hl
+ local contents = {} --- @type string[]
+ local active_offset ---@type [integer, integer]?
local active_signature = signature_help.activeSignature or 0
-- If the activeSignature is not inside the valid range, then clip it.
-- In 3.15 of the protocol, activeSignature was allowed to be negative
if active_signature >= #signature_help.signatures or active_signature < 0 then
active_signature = 0
end
- local signature = signature_help.signatures[active_signature + 1]
- if not signature then
- return
- end
+ local signature = vim.deepcopy(signature_help.signatures[active_signature + 1])
local label = signature.label
if ft then
-- wrap inside a code block for proper rendering
label = ('```%s\n%s\n```'):format(ft, label)
end
- list_extend(contents, split(label, '\n', { plain = true, trimempty = true }))
- if signature.documentation then
+ list_extend(contents, vim.split(label, '\n', { plain = true, trimempty = true }))
+ local doc = signature.documentation
+ if doc then
-- if LSP returns plain string, we treat it as plaintext. This avoids
-- special characters like underscore or similar from being interpreted
-- as markdown font modifiers
- if type(signature.documentation) == 'string' then
- signature.documentation = { kind = 'plaintext', value = signature.documentation }
+ if type(doc) == 'string' then
+ signature.documentation = { kind = 'plaintext', value = doc }
end
M.convert_input_to_markdown_lines(signature.documentation, contents)
end
if signature.parameters and #signature.parameters > 0 then
- local active_parameter = (signature.activeParameter or signature_help.activeParameter or 0)
- if active_parameter < 0 then
- active_parameter = 0
- end
+ -- First check if the signature has an activeParameter. If it doesn't check if the response
+ -- had that property instead. Else just default to 0.
+ local active_parameter =
+ math.max(signature.activeParameter or signature_help.activeParameter or 0, 0)
-- If the activeParameter is > #parameters, then set it to the last
-- NOTE: this is not fully according to the spec, but a client-side interpretation
- if active_parameter >= #signature.parameters then
- active_parameter = #signature.parameters - 1
- end
+ active_parameter = math.min(active_parameter, #signature.parameters - 1)
local parameter = signature.parameters[active_parameter + 1]
- if parameter then
- --[=[
- --Represents a parameter of a callable-signature. A parameter can
- --have a label and a doc-comment.
- interface ParameterInformation {
- --The label of this parameter information.
- --
- --Either a string or an inclusive start and exclusive end offsets within its containing
- --signature label. (see SignatureInformation.label). The offsets are based on a UTF-16
- --string representation as `Position` and `Range` does.
- --
- --*Note*: a label of type string should be a substring of its containing signature label.
- --Its intended use case is to highlight the parameter label part in the `SignatureInformation.label`.
- label: string | [number, number];
- --The human-readable doc-comment of this parameter. Will be shown
- --in the UI but can be omitted.
- documentation?: string | MarkupContent;
- }
- --]=]
- if parameter.label then
- if type(parameter.label) == 'table' then
- active_hl = parameter.label
- else
- local offset = 1
- -- try to set the initial offset to the first found trigger character
- for _, t in ipairs(triggers or {}) do
- local trigger_offset = signature.label:find(t, 1, true)
- if trigger_offset and (offset == 1 or trigger_offset < offset) then
- offset = trigger_offset
- end
- end
- for p, param in pairs(signature.parameters) do
- offset = signature.label:find(param.label, offset, true)
- if not offset then
- break
- end
- if p == active_parameter + 1 then
- active_hl = { offset - 1, offset + #parameter.label - 1 }
- break
- end
- offset = offset + #param.label + 1
- end
+ local parameter_label = parameter.label
+ if type(parameter_label) == 'table' then
+ active_offset = parameter_label
+ else
+ local offset = 1 ---@type integer?
+ -- try to set the initial offset to the first found trigger character
+ for _, t in ipairs(triggers or {}) do
+ local trigger_offset = signature.label:find(t, 1, true)
+ if trigger_offset and (offset == 1 or trigger_offset < offset) then
+ offset = trigger_offset
end
end
- if parameter.documentation then
- M.convert_input_to_markdown_lines(parameter.documentation, contents)
+ for p, param in pairs(signature.parameters) do
+ local plabel = param.label
+ assert(type(plabel) == 'string', 'Expected label to be a string')
+ offset = signature.label:find(plabel, offset, true)
+ if not offset then
+ break
+ end
+ if p == active_parameter + 1 then
+ active_offset = { offset - 1, offset + #parameter_label - 1 }
+ break
+ end
+ offset = offset + #param.label + 1
end
end
+ if parameter.documentation then
+ M.convert_input_to_markdown_lines(parameter.documentation, contents)
+ end
end
+
+ local active_hl = nil
+ if active_offset then
+ -- Account for the start of the markdown block.
+ if ft then
+ active_offset[1] = active_offset[1] + #contents[1]
+ active_offset[2] = active_offset[2] + #contents[1]
+ end
+
+ local a_start = get_pos_from_offset(active_offset[1], contents)
+ local a_end = get_pos_from_offset(active_offset[2], contents)
+ if a_start and a_end then
+ active_hl = { a_start[1], a_start[2], a_end[1], a_end[2] }
+ end
+ end
+
return contents, active_hl
end
@@ -894,32 +819,15 @@ end
---
---@param width integer window width (in character cells)
---@param height integer window height (in character cells)
----@param opts table optional
---- - offset_x (integer) offset to add to `col`
---- - offset_y (integer) offset to add to `row`
---- - 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"
---- - anchor_bias ("auto"|"above"|"below") defaults to "auto"
---- - "auto": place window based on which side of the cursor has more lines
---- - "above": place the window above the cursor unless there are not enough lines
---- to display the full window height.
---- - "below": place the window below the cursor unless there are not enough lines
---- to display the full window height.
----@return table Options
+---@param opts? vim.lsp.util.open_floating_preview.Opts
+---@return vim.api.keyset.win_config
function M.make_floating_popup_options(width, height, opts)
- validate({
- opts = { opts, 't', true },
- })
+ validate('opts', opts, 'table', true)
opts = opts or {}
- validate({
- ['opts.offset_x'] = { opts.offset_x, 'n', true },
- ['opts.offset_y'] = { opts.offset_y, 'n', true },
- })
+ validate('opts.offset_x', opts.offset_x, 'number', true)
+ validate('opts.offset_y', opts.offset_y, 'number', true)
local anchor = ''
- local row, col
local lines_above = opts.relative == 'mouse' and vim.fn.getmousepos().line - 1
or vim.fn.winline() - 1
@@ -927,7 +835,7 @@ function M.make_floating_popup_options(width, height, opts)
local anchor_bias = opts.anchor_bias or 'auto'
- local anchor_below
+ local anchor_below --- @type boolean?
if anchor_bias == 'below' then
anchor_below = (lines_below > lines_above) or (height <= lines_below)
@@ -938,7 +846,8 @@ function M.make_floating_popup_options(width, height, opts)
anchor_below = lines_below > lines_above
end
- local border_height = get_border_size(opts).height
+ local border_height = get_border_size(opts)
+ local row, col --- @type integer?, integer?
if anchor_below then
anchor = anchor .. 'N'
height = math.max(math.min(lines_below - border_height, height), 0)
@@ -960,7 +869,7 @@ function M.make_floating_popup_options(width, height, opts)
end
local title = (opts.border and opts.title) and opts.title or nil
- local title_pos
+ local title_pos --- @type 'left'|'center'|'right'?
if title then
title_pos = opts.title_pos or 'center'
@@ -982,13 +891,21 @@ function M.make_floating_popup_options(width, height, opts)
}
end
+--- @class vim.lsp.util.show_document.Opts
+--- @inlinedoc
+---
+--- Jump to existing window if buffer is already open.
+--- @field reuse_win? boolean
+---
+--- Whether to focus/jump to location if possible.
+--- (defaults: true)
+--- @field focus? boolean
+
--- Shows document and optionally jumps to the location.
---
---@param location lsp.Location|lsp.LocationLink
----@param offset_encoding string|nil 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.
+---@param offset_encoding 'utf-8'|'utf-16'|'utf-32'?
+---@param opts? vim.lsp.util.show_document.Opts
---@return boolean `true` if succeeded
function M.show_document(location, offset_encoding, opts)
-- location may be Location or LocationLink
@@ -998,6 +915,7 @@ function M.show_document(location, offset_encoding, opts)
end
if offset_encoding == nil then
vim.notify_once('show_document must be called with valid offset encoding', vim.log.levels.WARN)
+ return false
end
local bufnr = vim.uri_to_bufnr(uri)
@@ -1041,18 +959,13 @@ end
--- Jumps to a location.
---
+---@deprecated use `vim.lsp.util.show_document` with `{focus=true}` instead
---@param location lsp.Location|lsp.LocationLink
----@param offset_encoding string|nil utf-8|utf-16|utf-32
----@param reuse_win boolean|nil Jump to existing window if buffer is already open.
+---@param offset_encoding 'utf-8'|'utf-16'|'utf-32'?
+---@param reuse_win boolean? 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
-
+ vim.deprecate('vim.lsp.util.jump_to_location', nil, '0.12')
return M.show_document(location, offset_encoding, { reuse_win = reuse_win, focus = true })
end
@@ -1063,9 +976,9 @@ end
--- - for LocationLink, targetRange is shown (e.g., body of function definition)
---
---@param location lsp.Location|lsp.LocationLink
----@param opts table
----@return integer|nil buffer id of float window
----@return integer|nil window id of float window
+---@param opts? vim.lsp.util.open_floating_preview.Opts
+---@return integer? buffer id of float window
+---@return integer? window id of float window
function M.preview_location(location, opts)
-- location may be LocationLink or Location (more useful for the former)
local uri = location.targetUri or location.uri
@@ -1092,7 +1005,7 @@ end
local function find_window_by_var(name, value)
for _, win in ipairs(api.nvim_list_wins()) do
- if npcall(api.nvim_win_get_var, win, name) == value then
+ if vim.w[win][name] == value then
return win
end
end
@@ -1158,8 +1071,10 @@ local function collapse_blank_lines(contents)
end
local function get_markdown_fences()
- local fences = {}
- for _, fence in pairs(vim.g.markdown_fenced_languages or {}) do
+ local fences = {} --- @type table<string,string>
+ for _, fence in
+ pairs(vim.g.markdown_fenced_languages or {} --[[@as string[] ]])
+ do
local lang, syntax = fence:match('^(.*)=(.*)$')
if lang then
fences[lang] = syntax
@@ -1179,7 +1094,7 @@ end
---
---@param bufnr integer
---@param contents string[] of lines to show in window
----@param opts table with optional fields
+---@param opts? table with optional fields
--- - height of floating window
--- - width of floating window
--- - wrap_at character to wrap at for computing height
@@ -1188,10 +1103,8 @@ end
--- - separator insert separator after code block
---@return table stripped content
function M.stylize_markdown(bufnr, contents, opts)
- validate({
- contents = { contents, 't' },
- opts = { opts, 't', true },
- })
+ validate('contents', contents, 'table')
+ validate('opts', opts, 'table', true)
opts = opts or {}
-- table of fence types to {ft, begin, end}
@@ -1203,8 +1116,11 @@ function M.stylize_markdown(bufnr, contents, opts)
text = { 'text', '<text>', '</text>' },
}
- local match_begin = function(line)
+ --- @param line string
+ --- @return {type:string,ft:string}?
+ local function match_begin(line)
for type, pattern in pairs(matchers) do
+ --- @type string?
local ret = line:match(string.format('^%%s*%s%%s*$', pattern[2]))
if ret then
return {
@@ -1215,7 +1131,10 @@ function M.stylize_markdown(bufnr, contents, opts)
end
end
- local match_end = function(line, match)
+ --- @param line string
+ --- @param match {type:string,ft:string}
+ --- @return string
+ local function match_end(line, match)
local pattern = matchers[match.type]
return line:match(string.format('^%%s*%s%%s*$', pattern[3]))
end
@@ -1224,76 +1143,80 @@ function M.stylize_markdown(bufnr, contents, opts)
contents = vim.split(table.concat(contents, '\n'), '\n', { trimempty = true })
local stripped = {}
- local highlights = {}
+ local highlights = {} --- @type {ft:string,start:integer,finish:integer}[]
-- keep track of lnums that contain markdown
- local markdown_lines = {}
- do
- local i = 1
- while i <= #contents do
- local line = contents[i]
- local match = match_begin(line)
- if match then
- local start = #stripped
- i = i + 1
- while i <= #contents do
- line = contents[i]
- if match_end(line, match) then
- i = i + 1
- break
- end
- table.insert(stripped, line)
+ local markdown_lines = {} --- @type table<integer,boolean>
+
+ local i = 1
+ while i <= #contents do
+ local line = contents[i]
+ local match = match_begin(line)
+ if match then
+ local start = #stripped
+ i = i + 1
+ while i <= #contents do
+ line = contents[i]
+ if match_end(line, match) then
i = i + 1
+ break
end
- table.insert(highlights, {
- ft = match.ft,
- start = start + 1,
- finish = #stripped,
- })
- -- add a separator, but not on the last line
- if opts.separator and i < #contents then
- table.insert(stripped, '---')
- markdown_lines[#stripped] = true
- end
- else
- -- strip any empty lines or separators prior to this separator in actual markdown
- if line:match('^---+$') then
- while
- markdown_lines[#stripped]
- and (stripped[#stripped]:match('^%s*$') or stripped[#stripped]:match('^---+$'))
- do
- markdown_lines[#stripped] = false
- table.remove(stripped, #stripped)
- end
- end
- -- add the line if its not an empty line following a separator
- if
- not (
- line:match('^%s*$')
- and markdown_lines[#stripped]
- and stripped[#stripped]:match('^---+$')
- )
- then
- table.insert(stripped, line)
- markdown_lines[#stripped] = true
- end
+ table.insert(stripped, line)
i = i + 1
end
+ table.insert(highlights, {
+ ft = match.ft,
+ start = start + 1,
+ finish = #stripped,
+ })
+ -- add a separator, but not on the last line
+ if opts.separator and i < #contents then
+ table.insert(stripped, '---')
+ markdown_lines[#stripped] = true
+ end
+ else
+ -- strip any empty lines or separators prior to this separator in actual markdown
+ if line:match('^---+$') then
+ while
+ markdown_lines[#stripped]
+ and (stripped[#stripped]:match('^%s*$') or stripped[#stripped]:match('^---+$'))
+ do
+ markdown_lines[#stripped] = false
+ table.remove(stripped, #stripped)
+ end
+ end
+ -- add the line if its not an empty line following a separator
+ if
+ not (
+ line:match('^%s*$')
+ and markdown_lines[#stripped]
+ and stripped[#stripped]:match('^---+$')
+ )
+ then
+ table.insert(stripped, line)
+ markdown_lines[#stripped] = true
+ end
+ i = i + 1
end
end
-- Handle some common html escape sequences
- stripped = vim.tbl_map(function(line)
- local escapes = {
- ['&gt;'] = '>',
- ['&lt;'] = '<',
- ['&quot;'] = '"',
- ['&apos;'] = "'",
- ['&ensp;'] = ' ',
- ['&emsp;'] = ' ',
- ['&amp;'] = '&',
- }
- return (string.gsub(line, '&[^ ;]+;', escapes))
- end, stripped)
+ --- @type string[]
+ stripped = vim.tbl_map(
+ --- @param line string
+ function(line)
+ local escapes = {
+ ['&gt;'] = '>',
+ ['&lt;'] = '<',
+ ['&quot;'] = '"',
+ ['&apos;'] = "'",
+ ['&ensp;'] = ' ',
+ ['&emsp;'] = ' ',
+ ['&amp;'] = '&',
+ }
+ return (line:gsub('&[^ ;]+;', escapes))
+ end,
+ stripped
+ )
-- Compute size of float needed to show (wrapped) lines
opts.wrap_at = opts.wrap_at or (vim.wo['wrap'] and api.nvim_win_get_width(0))
@@ -1312,7 +1235,7 @@ function M.stylize_markdown(bufnr, contents, opts)
local idx = 1
-- keep track of syntaxes we already included.
-- no need to include the same syntax more than once
- local langs = {}
+ local langs = {} --- @type table<string,boolean>
local fences = get_markdown_fences()
local function apply_syntax_to_region(ft, start, finish)
if ft == '' then
@@ -1335,6 +1258,7 @@ function M.stylize_markdown(bufnr, contents, opts)
if #api.nvim_get_runtime_file(('syntax/%s.vim'):format(ft), true) == 0 then
return
end
+ --- @diagnostic disable-next-line:param-type-mismatch
pcall(vim.cmd, string.format('syntax include %s syntax/%s.vim', lang, ft))
langs[lang] = true
end
@@ -1390,10 +1314,8 @@ end
---@return string[] table of lines containing normalized Markdown
---@see https://github.github.com/gfm
function M._normalize_markdown(contents, opts)
- validate({
- contents = { contents, 't' },
- opts = { opts, 't', true },
- })
+ validate('contents', contents, 'table')
+ validate('opts', opts, 'table', true)
opts = opts or {}
-- 1. Carriage returns are removed
@@ -1412,7 +1334,7 @@ end
--- Closes the preview window
---
---@param winnr integer window id of preview window
----@param bufnrs table|nil optional list of ignored buffers
+---@param bufnrs table? optional list of ignored buffers
local function close_preview_window(winnr, bufnrs)
vim.schedule(function()
-- exit if we are in one of ignored buffers
@@ -1460,20 +1382,13 @@ end
---@private
--- Computes size of float needed to show contents (with optional wrapping)
---
----@param contents table of lines to show in window
----@param opts? table with optional fields
---- - height of floating window
---- - width of floating window
---- - wrap_at character to wrap at for computing height
---- - max_width maximal width of floating window
---- - max_height maximal height of floating window
+---@param contents string[] of lines to show in window
+---@param opts? vim.lsp.util.open_floating_preview.Opts
---@return integer width size of float
---@return integer height size of float
function M._make_floating_popup_size(contents, opts)
- validate({
- contents = { contents, 't' },
- opts = { opts, 't', true },
- })
+ validate('contents', contents, 'table')
+ validate('opts', opts, 'table', true)
opts = opts or {}
local width = opts.width
@@ -1481,7 +1396,7 @@ function M._make_floating_popup_size(contents, opts)
local wrap_at = opts.wrap_at
local max_width = opts.max_width
local max_height = opts.max_height
- local line_widths = {}
+ local line_widths = {} --- @type table<integer,integer>
if not width then
width = 0
@@ -1492,17 +1407,15 @@ function M._make_floating_popup_size(contents, opts)
end
end
- local border_width = get_border_size(opts).width
+ local _, border_width = get_border_size(opts)
local screen_width = api.nvim_win_get_width(0)
width = math.min(width, screen_width)
-- make sure borders are always inside the screen
- if width + border_width > screen_width then
- width = width - (width + border_width - screen_width)
- end
+ width = math.min(width, screen_width - border_width)
- if wrap_at and wrap_at > width then
- wrap_at = width
+ if wrap_at then
+ wrap_at = math.min(wrap_at, width)
end
if max_width then
@@ -1534,7 +1447,6 @@ function M._make_floating_popup_size(contents, opts)
end
--- @class vim.lsp.util.open_floating_preview.Opts
---- @inlinedoc
---
--- Height of floating window
--- @field height? integer
@@ -1569,6 +1481,29 @@ end
--- window with the same {focus_id}
--- (default: `true`)
--- @field focus? boolean
+---
+--- offset to add to `col`
+--- @field offset_x? integer
+---
+--- offset to add to `row`
+--- @field offset_y? integer
+--- @field border? string|(string|[string,string])[] override `border`
+--- @field zindex? integer override `zindex`, defaults to 50
+--- @field title? string
+--- @field title_pos? 'left'|'center'|'right'
+---
+--- (default: `'cursor'`)
+--- @field relative? 'mouse'|'cursor'
+---
+--- - "auto": place window based on which side of the cursor has more lines
+--- - "above": place the window above the cursor unless there are not enough lines
+--- to display the full window height.
+--- - "below": place the window below the cursor unless there are not enough lines
+--- to display the full window height.
+--- (default: `'auto'`)
+--- @field anchor_bias? 'auto'|'above'|'below'
+---
+--- @field _update_win? integer
--- Shows contents in a floating window.
---
@@ -1580,11 +1515,9 @@ end
---@return integer bufnr of newly created float window
---@return integer winid of newly created float window preview window
function M.open_floating_preview(contents, syntax, opts)
- validate({
- contents = { contents, 't' },
- syntax = { syntax, 's', true },
- opts = { opts, 't', true },
- })
+ validate('contents', contents, 'table')
+ validate('syntax', syntax, 'string', true)
+ validate('opts', opts, 'table', true)
opts = opts or {}
opts.wrap = opts.wrap ~= false -- wrapping by default
opts.focus = opts.focus ~= false
@@ -1592,43 +1525,49 @@ function M.open_floating_preview(contents, syntax, opts)
local bufnr = api.nvim_get_current_buf()
- -- check if this popup is focusable and we need to focus
- if opts.focus_id and opts.focusable ~= false and opts.focus then
- -- Go back to previous window if we are in a focusable one
- local current_winnr = api.nvim_get_current_win()
- if npcall(api.nvim_win_get_var, current_winnr, opts.focus_id) then
- api.nvim_command('wincmd p')
- return bufnr, current_winnr
- end
- do
- local win = find_window_by_var(opts.focus_id, bufnr)
- if win and api.nvim_win_is_valid(win) and vim.fn.pumvisible() == 0 then
- -- focus and return the existing buf, win
- api.nvim_set_current_win(win)
- api.nvim_command('stopinsert')
- return api.nvim_win_get_buf(win), win
+ local floating_winnr = opts._update_win
+
+ -- Create/get the buffer
+ local floating_bufnr --- @type integer
+ if floating_winnr then
+ floating_bufnr = api.nvim_win_get_buf(floating_winnr)
+ else
+ -- check if this popup is focusable and we need to focus
+ if opts.focus_id and opts.focusable ~= false and opts.focus then
+ -- Go back to previous window if we are in a focusable one
+ local current_winnr = api.nvim_get_current_win()
+ if vim.w[current_winnr][opts.focus_id] then
+ api.nvim_command('wincmd p')
+ return bufnr, current_winnr
+ end
+ do
+ local win = find_window_by_var(opts.focus_id, bufnr)
+ if win and api.nvim_win_is_valid(win) and vim.fn.pumvisible() == 0 then
+ -- focus and return the existing buf, win
+ api.nvim_set_current_win(win)
+ api.nvim_command('stopinsert')
+ return api.nvim_win_get_buf(win), win
+ end
end
end
- end
- -- check if another floating preview already exists for this buffer
- -- and close it if needed
- local existing_float = npcall(api.nvim_buf_get_var, bufnr, 'lsp_floating_preview')
- if existing_float and api.nvim_win_is_valid(existing_float) then
- api.nvim_win_close(existing_float, true)
+ -- check if another floating preview already exists for this buffer
+ -- and close it if needed
+ local existing_float = vim.b[bufnr].lsp_floating_preview
+ if existing_float and api.nvim_win_is_valid(existing_float) then
+ api.nvim_win_close(existing_float, true)
+ end
+ floating_bufnr = api.nvim_create_buf(false, true)
end
- -- Create the buffer
- local floating_bufnr = api.nvim_create_buf(false, true)
-
-- Set up the contents, using treesitter for markdown
local do_stylize = syntax == 'markdown' and vim.g.syntax_on ~= nil
+
if do_stylize then
local width = M._make_floating_popup_size(contents, opts)
contents = M._normalize_markdown(contents, { width = width })
vim.bo[floating_bufnr].filetype = 'markdown'
vim.treesitter.start(floating_bufnr)
- api.nvim_buf_set_lines(floating_bufnr, 0, -1, false, contents)
else
-- Clean up input: trim empty lines
contents = vim.split(table.concat(contents, '\n'), '\n', { trimempty = true })
@@ -1636,19 +1575,47 @@ function M.open_floating_preview(contents, syntax, opts)
if syntax then
vim.bo[floating_bufnr].syntax = syntax
end
- api.nvim_buf_set_lines(floating_bufnr, 0, -1, true, contents)
end
- -- Compute size of float needed to show (wrapped) lines
- if opts.wrap then
- opts.wrap_at = opts.wrap_at or api.nvim_win_get_width(0)
+ vim.bo[floating_bufnr].modifiable = true
+ api.nvim_buf_set_lines(floating_bufnr, 0, -1, false, contents)
+
+ if floating_winnr then
+ api.nvim_win_set_config(floating_winnr, {
+ border = opts.border,
+ title = opts.title,
+ })
else
- opts.wrap_at = nil
- end
- local width, height = M._make_floating_popup_size(contents, opts)
+ -- Compute size of float needed to show (wrapped) lines
+ if opts.wrap then
+ opts.wrap_at = opts.wrap_at or api.nvim_win_get_width(0)
+ else
+ opts.wrap_at = nil
+ end
- local float_option = M.make_floating_popup_options(width, height, opts)
- local floating_winnr = api.nvim_open_win(floating_bufnr, false, float_option)
+ -- TODO(lewis6991): These function assume the current window to determine options,
+ -- therefore it won't work for opts._update_win and the current window if the floating
+ -- window
+ local width, height = M._make_floating_popup_size(contents, opts)
+ local float_option = M.make_floating_popup_options(width, height, opts)
+
+ floating_winnr = api.nvim_open_win(floating_bufnr, false, float_option)
+
+ api.nvim_buf_set_keymap(
+ floating_bufnr,
+ 'n',
+ 'q',
+ '<cmd>bdelete<cr>',
+ { silent = true, noremap = true, nowait = true }
+ )
+ close_preview_autocmd(opts.close_events, floating_winnr, { floating_bufnr, bufnr })
+
+ -- save focus_id
+ if opts.focus_id then
+ api.nvim_win_set_var(floating_winnr, opts.focus_id, bufnr)
+ end
+ api.nvim_buf_set_var(bufnr, 'lsp_floating_preview', floating_winnr)
+ end
if do_stylize then
vim.wo[floating_winnr].conceallevel = 2
@@ -1656,25 +1623,11 @@ function M.open_floating_preview(contents, syntax, opts)
vim.wo[floating_winnr].foldenable = false -- Disable folding.
vim.wo[floating_winnr].wrap = opts.wrap -- Soft wrapping.
vim.wo[floating_winnr].breakindent = true -- Slightly better list presentation.
+ vim.wo[floating_winnr].smoothscroll = true -- Scroll by screen-line instead of buffer-line.
vim.bo[floating_bufnr].modifiable = false
vim.bo[floating_bufnr].bufhidden = 'wipe'
- api.nvim_buf_set_keymap(
- floating_bufnr,
- 'n',
- 'q',
- '<cmd>bdelete<cr>',
- { silent = true, noremap = true, nowait = true }
- )
- close_preview_autocmd(opts.close_events, floating_winnr, { floating_bufnr, bufnr })
-
- -- save focus_id
- if opts.focus_id then
- api.nvim_win_set_var(floating_winnr, opts.focus_id, bufnr)
- end
- api.nvim_buf_set_var(bufnr, 'lsp_floating_preview', floating_winnr)
-
return floating_bufnr, floating_winnr
end
@@ -1683,9 +1636,8 @@ do --[[ References ]]
--- Removes document highlights from a buffer.
---
- ---@param bufnr integer|nil Buffer id
+ ---@param bufnr integer? Buffer id
function M.buf_clear_references(bufnr)
- validate({ bufnr = { bufnr, { 'n' }, true } })
api.nvim_buf_clear_namespace(bufnr or 0, reference_ns, 0, -1)
end
@@ -1693,29 +1645,18 @@ do --[[ References ]]
---
---@param bufnr integer Buffer id
---@param references lsp.DocumentHighlight[] objects to highlight
- ---@param offset_encoding string One of "utf-8", "utf-16", "utf-32".
+ ---@param offset_encoding 'utf-8'|'utf-16'|'utf-32'
---@see https://microsoft.github.io/language-server-protocol/specification/#textDocumentContentChangeEvent
function M.buf_highlight_references(bufnr, references, offset_encoding)
- validate({
- bufnr = { bufnr, 'n', true },
- offset_encoding = { offset_encoding, 'string', false },
- })
+ validate('bufnr', bufnr, 'number', true)
+ validate('offset_encoding', offset_encoding, 'string', false)
for _, reference in ipairs(references) do
- local start_line, start_char =
- reference['range']['start']['line'], reference['range']['start']['character']
- local end_line, end_char =
- reference['range']['end']['line'], reference['range']['end']['character']
+ local range = reference.range
+ local start_line = range.start.line
+ local end_line = range['end'].line
- local start_idx = get_line_byte_from_position(
- bufnr,
- { line = start_line, character = start_char },
- offset_encoding
- )
- local end_idx = get_line_byte_from_position(
- bufnr,
- { line = start_line, character = end_char },
- offset_encoding
- )
+ local start_idx = get_line_byte_from_position(bufnr, range.start, offset_encoding)
+ local end_idx = get_line_byte_from_position(bufnr, range['end'], offset_encoding)
local document_highlight_kind = {
[protocol.DocumentHighlightKind.Text] = 'LspReferenceText',
@@ -1723,13 +1664,13 @@ do --[[ References ]]
[protocol.DocumentHighlightKind.Write] = 'LspReferenceWrite',
}
local kind = reference['kind'] or protocol.DocumentHighlightKind.Text
- highlight.range(
+ vim.hl.range(
bufnr,
reference_ns,
document_highlight_kind[kind],
{ start_line, start_idx },
{ end_line, end_idx },
- { priority = vim.highlight.priorities.user }
+ { priority = vim.hl.priorities.user }
)
end
end
@@ -1739,16 +1680,6 @@ local position_sort = sort_by_key(function(v)
return { v.start.line, v.start.character }
end)
----@class vim.lsp.util.locations_to_items.ret
----@inlinedoc
----@field filename string
----@field lnum integer 1-indexed line number
----@field end_lnum integer 1-indexed end line number
----@field col integer 1-indexed column
----@field end_col integer 1-indexed end column
----@field text string
----@field user_data lsp.Location|lsp.LocationLink
-
--- Returns the items with the byte position calculated correctly and in sorted
--- order, for display in quickfix and location lists.
---
@@ -1759,9 +1690,9 @@ end)
--- |setloclist()|.
---
---@param locations lsp.Location[]|lsp.LocationLink[]
----@param offset_encoding string offset_encoding for locations utf-8|utf-16|utf-32
---- default to first client of buffer
----@return vim.lsp.util.locations_to_items.ret[]
+---@param offset_encoding? 'utf-8'|'utf-16'|'utf-32'
+--- default to first client of buffer
+---@return vim.quickfix.entry[] # See |setqflist()| for the format
function M.locations_to_items(locations, offset_encoding)
if offset_encoding == nil then
vim.notify_once(
@@ -1771,28 +1702,19 @@ function M.locations_to_items(locations, offset_encoding)
offset_encoding = vim.lsp.get_clients({ bufnr = 0 })[1].offset_encoding
end
- local items = {}
+ local items = {} --- @type vim.quickfix.entry[]
+
---@type table<string, {start: lsp.Position, end: lsp.Position, location: lsp.Location|lsp.LocationLink}[]>
- local grouped = setmetatable({}, {
- __index = function(t, k)
- local v = {}
- rawset(t, k, v)
- return v
- end,
- })
+ local grouped = {}
for _, d in ipairs(locations) do
-- locations may be Location or LocationLink
local uri = d.uri or d.targetUri
local range = d.range or d.targetSelectionRange
+ grouped[uri] = grouped[uri] or {}
table.insert(grouped[uri], { start = range.start, ['end'] = range['end'], location = d })
end
- ---@type string[]
- local keys = vim.tbl_keys(grouped)
- table.sort(keys)
- -- TODO(ashkan) I wish we could do this lazily.
- for _, uri in ipairs(keys) do
- local rows = grouped[uri]
+ for uri, rows in vim.spairs(grouped) do
table.sort(rows, position_sort)
local filename = vim.uri_to_fname(uri)
@@ -1814,10 +1736,10 @@ function M.locations_to_items(locations, offset_encoding)
local end_row = end_pos.line
local line = lines[row] or ''
local end_line = lines[end_row] or ''
- local col = M._str_byteindex_enc(line, pos.character, offset_encoding)
- local end_col = M._str_byteindex_enc(end_line, end_pos.character, offset_encoding)
+ local col = vim.str_byteindex(line, offset_encoding, pos.character, false)
+ local end_col = vim.str_byteindex(end_line, offset_encoding, end_pos.character, false)
- table.insert(items, {
+ items[#items + 1] = {
filename = filename,
lnum = row + 1,
end_lnum = end_row + 1,
@@ -1825,58 +1747,51 @@ function M.locations_to_items(locations, offset_encoding)
end_col = end_col + 1,
text = line,
user_data = temp.location,
- })
+ }
end
end
return items
end
--- According to LSP spec, if the client set "symbolKind.valueSet",
--- the client must handle it properly even if it receives a value outside the specification.
--- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol
-function M._get_symbol_kind_name(symbol_kind)
- return protocol.SymbolKind[symbol_kind] or 'Unknown'
-end
-
--- Converts symbols to quickfix list items.
---
----@param symbols table DocumentSymbol[] or SymbolInformation[]
+---@param symbols lsp.DocumentSymbol[]|lsp.SymbolInformation[]
---@param bufnr? integer
+---@return vim.quickfix.entry[] # See |setqflist()| for the format
function M.symbols_to_items(symbols, bufnr)
- local function _symbols_to_items(_symbols, _items, _bufnr)
- for _, symbol in ipairs(_symbols) do
- if symbol.location then -- SymbolInformation type
- local range = symbol.location.range
- local kind = M._get_symbol_kind_name(symbol.kind)
- table.insert(_items, {
- filename = vim.uri_to_fname(symbol.location.uri),
- lnum = range.start.line + 1,
- col = range.start.character + 1,
- kind = kind,
- text = '[' .. kind .. '] ' .. symbol.name,
- })
- elseif symbol.selectionRange then -- DocumentSymbole type
- local kind = M._get_symbol_kind_name(symbol.kind)
- table.insert(_items, {
- -- bufnr = _bufnr,
- filename = api.nvim_buf_get_name(_bufnr),
- lnum = symbol.selectionRange.start.line + 1,
- col = symbol.selectionRange.start.character + 1,
- kind = kind,
- text = '[' .. kind .. '] ' .. symbol.name,
- })
- if symbol.children then
- for _, v in ipairs(_symbols_to_items(symbol.children, _items, _bufnr)) do
- for _, s in ipairs(v) do
- table.insert(_items, s)
- end
- end
- end
- end
+ bufnr = bufnr or 0
+ local items = {} --- @type vim.quickfix.entry[]
+ for _, symbol in ipairs(symbols) do
+ --- @type string?, lsp.Position?
+ local filename, pos
+
+ if symbol.location then
+ --- @cast symbol lsp.SymbolInformation
+ filename = vim.uri_to_fname(symbol.location.uri)
+ pos = symbol.location.range.start
+ elseif symbol.selectionRange then
+ --- @cast symbol lsp.DocumentSymbol
+ filename = api.nvim_buf_get_name(bufnr)
+ pos = symbol.selectionRange.start
+ end
+
+ if filename and pos then
+ local kind = protocol.SymbolKind[symbol.kind] or 'Unknown'
+ items[#items + 1] = {
+ filename = filename,
+ lnum = pos.line + 1,
+ col = pos.character + 1,
+ kind = kind,
+ text = '[' .. kind .. '] ' .. symbol.name,
+ }
+ end
+
+ if symbol.children then
+ list_extend(items, M.symbols_to_items(symbol.children, bufnr))
end
- return _items
end
- return _symbols_to_items(symbols, {}, bufnr or 0)
+
+ return items
end
--- Removes empty lines from the beginning and end.
@@ -1899,7 +1814,7 @@ function M.trim_empty_lines(lines)
break
end
end
- return list_extend({}, lines, start, finish)
+ return vim.list_slice(lines, start, finish)
end
--- Accepts markdown lines and tries to reduce them to a filetype if they
@@ -1932,8 +1847,8 @@ function M.try_trim_markdown_code_blocks(lines)
return 'markdown'
end
----@param window integer|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 window integer?: window handle or 0 for current, defaults to current
+---@param offset_encoding? 'utf-8'|'utf-16'|'utf-32'? defaults to `offset_encoding` of first client of buffer of `window`
local function make_position_param(window, offset_encoding)
window = window or 0
local buf = api.nvim_win_get_buf(window)
@@ -1945,15 +1860,15 @@ local function make_position_param(window, offset_encoding)
return { line = 0, character = 0 }
end
- col = M._str_utfindex_enc(line, col, offset_encoding)
+ col = vim.str_utfindex(line, offset_encoding, col, false)
return { line = row, character = col }
end
--- Creates a `TextDocumentPositionParams` object for the current buffer and cursor position.
---
----@param window integer|nil: window handle or 0 for current, defaults to current
----@param offset_encoding string|nil utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of buffer of `window`
+---@param window integer?: window handle or 0 for current, defaults to current
+---@param offset_encoding 'utf-8'|'utf-16'|'utf-32'? defaults to `offset_encoding` of first client of buffer of `window`
---@return lsp.TextDocumentPositionParams
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentPositionParams
function M.make_position_params(window, offset_encoding)
@@ -1970,11 +1885,9 @@ end
---@param bufnr integer buffer handle or 0 for current, defaults to current
---@return string encoding first client if there is one, nil otherwise
function M._get_offset_encoding(bufnr)
- validate({
- bufnr = { bufnr, 'n', true },
- })
+ validate('bufnr', bufnr, 'number', true)
- local offset_encoding
+ local offset_encoding --- @type 'utf-8'|'utf-16'|'utf-32'?
for _, client in pairs(vim.lsp.get_clients({ bufnr = bufnr })) do
if client.offset_encoding == nil then
@@ -2005,8 +1918,8 @@ end
--- `textDocument/codeAction`, `textDocument/colorPresentation`,
--- `textDocument/rangeFormatting`.
---
----@param window integer|nil: window handle or 0 for current, defaults to current
----@param offset_encoding "utf-8"|"utf-16"|"utf-32"|nil defaults to `offset_encoding` of first client of buffer of `window`
+---@param window integer? window handle or 0 for current, defaults to current
+---@param offset_encoding "utf-8"|"utf-16"|"utf-32"? defaults to `offset_encoding` of first client of buffer of `window`
---@return table { textDocument = { uri = `current_file_uri` }, range = { start =
---`current_position`, end = `current_position` } }
function M.make_range_params(window, offset_encoding)
@@ -2022,33 +1935,33 @@ end
--- Using the given range in the current buffer, creates an object that
--- is similar to |vim.lsp.util.make_range_params()|.
---
----@param start_pos integer[]|nil {row,col} mark-indexed position.
+---@param start_pos [integer,integer]? {row,col} mark-indexed position.
--- Defaults to the start of the last visual selection.
----@param end_pos integer[]|nil {row,col} mark-indexed position.
+---@param end_pos [integer,integer]? {row,col} mark-indexed position.
--- Defaults to the end of the last visual selection.
----@param bufnr integer|nil buffer handle or 0 for current, defaults to current
----@param offset_encoding "utf-8"|"utf-16"|"utf-32"|nil defaults to `offset_encoding` of first client of `bufnr`
+---@param bufnr integer? buffer handle or 0 for current, defaults to current
+---@param offset_encoding 'utf-8'|'utf-16'|'utf-32'? defaults to `offset_encoding` of first client of `bufnr`
---@return table { textDocument = { uri = `current_file_uri` }, range = { start =
---`start_position`, end = `end_position` } }
function M.make_given_range_params(start_pos, end_pos, bufnr, offset_encoding)
- validate({
- start_pos = { start_pos, 't', true },
- end_pos = { end_pos, 't', true },
- offset_encoding = { offset_encoding, 's', true },
- })
+ validate('start_pos', start_pos, 'table', true)
+ validate('end_pos', end_pos, 'table', true)
+ validate('offset_encoding', offset_encoding, 'string', true)
bufnr = bufnr or api.nvim_get_current_buf()
offset_encoding = offset_encoding or M._get_offset_encoding(bufnr)
- local A = list_extend({}, start_pos or api.nvim_buf_get_mark(bufnr, '<'))
- local B = list_extend({}, end_pos or api.nvim_buf_get_mark(bufnr, '>'))
+ --- @type [integer, integer]
+ local A = { unpack(start_pos or api.nvim_buf_get_mark(bufnr, '<')) }
+ --- @type [integer, integer]
+ local B = { unpack(end_pos or api.nvim_buf_get_mark(bufnr, '>')) }
-- convert to 0-index
A[1] = A[1] - 1
B[1] = B[1] - 1
-- account for offset_encoding.
if A[2] > 0 then
- A = { A[1], M.character_offset(bufnr, A[1], A[2], offset_encoding) }
+ A[2] = M.character_offset(bufnr, A[1], A[2], offset_encoding)
end
if B[2] > 0 then
- B = { B[1], M.character_offset(bufnr, B[1], B[2], offset_encoding) }
+ B[2] = M.character_offset(bufnr, B[1], B[2], offset_encoding)
end
-- we need to offset the end character position otherwise we loose the last
-- character of the selection, as LSP end position is exclusive
@@ -2067,7 +1980,7 @@ end
--- Creates a `TextDocumentIdentifier` object for the current buffer.
---
----@param bufnr integer|nil: Buffer handle, defaults to current
+---@param bufnr integer?: Buffer handle, defaults to current
---@return lsp.TextDocumentIdentifier
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentIdentifier
function M.make_text_document_params(bufnr)
@@ -2085,10 +1998,10 @@ end
--- Returns indentation size.
---
---@see 'shiftwidth'
----@param bufnr integer|nil: Buffer handle, defaults to current
+---@param bufnr integer?: Buffer handle, defaults to current
---@return integer indentation size
function M.get_effective_tabstop(bufnr)
- validate({ bufnr = { bufnr, 'n', true } })
+ validate('bufnr', bufnr, 'number', true)
local bo = bufnr and vim.bo[bufnr] or vim.bo
local sw = bo.shiftwidth
return (sw == 0 and bo.tabstop) or sw
@@ -2096,11 +2009,11 @@ end
--- Creates a `DocumentFormattingParams` object for the current buffer and cursor position.
---
----@param options lsp.FormattingOptions|nil with valid `FormattingOptions` entries
+---@param options lsp.FormattingOptions? with valid `FormattingOptions` entries
---@return lsp.DocumentFormattingParams object
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting
function M.make_formatting_params(options)
- validate({ options = { options, 't', true } })
+ validate('options', options, 'table', true)
options = vim.tbl_extend('keep', options or {}, {
tabSize = M.get_effective_tabstop(),
insertSpaces = vim.bo.expandtab,
@@ -2116,7 +2029,8 @@ end
---@param buf integer buffer number (0 for current)
---@param row integer 0-indexed line
---@param col integer 0-indexed byte offset in line
----@param offset_encoding string utf-8|utf-16|utf-32 defaults to `offset_encoding` of first client of `buf`
+---@param offset_encoding? 'utf-8'|'utf-16'|'utf-32'
+--- defaults to `offset_encoding` of first client of `buf`
---@return integer `offset_encoding` index of the character in line {row} column {col} in buffer {buf}
function M.character_offset(buf, row, col, offset_encoding)
local line = get_line(buf, row)
@@ -2127,7 +2041,7 @@ function M.character_offset(buf, row, col, offset_encoding)
)
offset_encoding = vim.lsp.get_clients({ bufnr = buf })[1].offset_encoding
end
- return M._str_utfindex_enc(line, col, offset_encoding)
+ return vim.str_utfindex(line, offset_encoding, col, false)
end
--- Helper function to return nested values in language server settings
@@ -2139,6 +2053,7 @@ end
function M.lookup_section(settings, section)
vim.deprecate('vim.lsp.util.lookup_section()', 'vim.tbl_get() with `vim.split`', '0.12')
for part in vim.gsplit(section, '.', { plain = true }) do
+ --- @diagnostic disable-next-line:no-unknown
settings = settings[part]
if settings == nil then
return vim.NIL
@@ -2153,7 +2068,7 @@ end
---@param bufnr integer
---@param start_line integer
---@param end_line integer
----@param offset_encoding lsp.PositionEncodingKind
+---@param offset_encoding 'utf-8'|'utf-16'|'utf-32'
---@return lsp.Range
local function make_line_range_params(bufnr, start_line, end_line, offset_encoding)
local last_line = api.nvim_buf_line_count(bufnr) - 1
@@ -2161,7 +2076,7 @@ local function make_line_range_params(bufnr, start_line, end_line, offset_encodi
---@type lsp.Position
local end_pos
- if end_line == last_line and not vim.api.nvim_get_option_value('endofline', { buf = bufnr }) then
+ if end_line == last_line and not vim.bo[bufnr].endofline then
end_pos = {
line = end_line,
character = M.character_offset(bufnr, end_line, #get_line(bufnr, end_line), offset_encoding),
@@ -2201,9 +2116,7 @@ function M._refresh(method, opts)
local textDocument = M.make_text_document_params(bufnr)
- local only_visible = opts.only_visible or false
-
- if only_visible then
+ if opts.only_visible then
for _, window in ipairs(api.nvim_list_wins()) do
if api.nvim_win_get_buf(window) == bufnr then
local first = vim.fn.line('w0', window)