aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim
diff options
context:
space:
mode:
authorLewis Russell <lewis6991@gmail.com>2024-10-17 13:35:02 +0100
committerGitHub <noreply@github.com>2024-10-17 13:35:02 +0100
commitfa6ab0d90958516d0bc1ed62839d85405ad08fa8 (patch)
tree310a1c9bd3fcb6d176f823ebb81e7cfd595ff5ec /runtime/lua/vim
parentce678043e3461d45f1251979cf047dd529b117ea (diff)
parenta18fa2f11c3a66be8addc303efb5c8033a47f41e (diff)
downloadrneovim-fa6ab0d90958516d0bc1ed62839d85405ad08fa8.tar.gz
rneovim-fa6ab0d90958516d0bc1ed62839d85405ad08fa8.tar.bz2
rneovim-fa6ab0d90958516d0bc1ed62839d85405ad08fa8.zip
Merge pull request #30825 from lewis6991/refactor/lsputil
Diffstat (limited to 'runtime/lua/vim')
-rw-r--r--runtime/lua/vim/_meta/builtin_types.lua91
-rw-r--r--runtime/lua/vim/_meta/vimfn.lua4
-rw-r--r--runtime/lua/vim/lsp/_tagfunc.lua7
-rw-r--r--runtime/lua/vim/lsp/buf.lua2
-rw-r--r--runtime/lua/vim/lsp/util.lua813
5 files changed, 481 insertions, 436 deletions
diff --git a/runtime/lua/vim/_meta/builtin_types.lua b/runtime/lua/vim/_meta/builtin_types.lua
index aca6649957..eae76d80d7 100644
--- a/runtime/lua/vim/_meta/builtin_types.lua
+++ b/runtime/lua/vim/_meta/builtin_types.lua
@@ -66,6 +66,97 @@
--- @field winnr integer
--- @field winrow integer
+--- @class vim.quickfix.entry
+--- buffer number; must be the number of a valid buffer
+--- @field bufnr? integer
+---
+--- name of a file; only used when "bufnr" is not
+--- present or it is invalid.
+--- @field filename? string
+---
+--- name of a module; if given it will be used in
+--- quickfix error window instead of the filename.
+--- @field module? string
+---
+--- line number in the file
+--- @field lnum? integer
+---
+--- end of lines, if the item spans multiple lines
+--- @field end_lnum? integer
+---
+--- search pattern used to locate the error
+--- @field pattern? string
+---
+--- column number
+--- @field col? integer
+---
+--- when non-zero: "col" is visual column
+--- when zero: "col" is byte index
+--- @field vcol? integer
+---
+--- end column, if the item spans multiple columns
+--- @field end_col? integer
+---
+--- error number
+--- @field nr? integer
+---
+--- description of the error
+--- @field text? string
+---
+--- single-character error type, 'E', 'W', etc.
+--- @field type? string
+---
+--- recognized error message
+--- @field valid? boolean
+---
+--- custom data associated with the item, can be
+--- any type.
+--- @field user_data? any
+
+--- @class vim.fn.setqflist.what
+---
+--- quickfix list context. See |quickfix-context|
+--- @field context? table
+---
+--- errorformat to use when parsing text from
+--- "lines". If this is not present, then the
+--- 'errorformat' option value is used.
+--- See |quickfix-parse|
+--- @field efm? string
+---
+--- quickfix list identifier |quickfix-ID|
+--- @field id? integer
+--- index of the current entry in the quickfix
+--- list specified by "id" or "nr". If set to '$',
+--- then the last entry in the list is set as the
+--- current entry. See |quickfix-index|
+--- @field idx? integer
+---
+--- list of quickfix entries. Same as the {list}
+--- argument.
+--- @field items? vim.quickfix.entry[]
+---
+--- use 'errorformat' to parse a list of lines and
+--- add the resulting entries to the quickfix list
+--- {nr} or {id}. Only a |List| value is supported.
+--- See |quickfix-parse|
+--- @field lines? string[]
+---
+--- list number in the quickfix stack; zero
+--- means the current quickfix list and "$" means
+--- the last quickfix list.
+--- @field nr? integer
+---
+--- function to get the text to display in the
+--- quickfix window. The value can be the name of
+--- a function or a funcref or a lambda. Refer
+--- to |quickfix-window-function| for an explanation
+--- of how to write the function and an example.
+--- @field quickfixtextfunc? function
+---
+--- quickfix list title text. See |quickfix-title|
+--- @field title? string
+
--- @class vim.fn.sign_define.dict
--- @field text string
--- @field icon? string
diff --git a/runtime/lua/vim/_meta/vimfn.lua b/runtime/lua/vim/_meta/vimfn.lua
index d00bbe4770..f40a94a442 100644
--- a/runtime/lua/vim/_meta/vimfn.lua
+++ b/runtime/lua/vim/_meta/vimfn.lua
@@ -8286,9 +8286,9 @@ function vim.fn.setpos(expr, list) end
--- independent of the 'errorformat' setting. Use a command like
--- `:cc 1` to jump to the first position.
---
---- @param list any[]
+--- @param list vim.quickfix.entry[]
--- @param action? string
---- @param what? table
+--- @param what? vim.fn.setqflist.what
--- @return any
function vim.fn.setqflist(list, action, what) end
diff --git a/runtime/lua/vim/lsp/_tagfunc.lua b/runtime/lua/vim/lsp/_tagfunc.lua
index 4ad50e4a58..70917de48d 100644
--- a/runtime/lua/vim/lsp/_tagfunc.lua
+++ b/runtime/lua/vim/lsp/_tagfunc.lua
@@ -27,15 +27,20 @@ local function query_definition(pattern)
return {}
end
local results = {}
+
+ --- @param range lsp.Range
+ --- @param uri string
+ --- @param offset_encoding string
local add = function(range, uri, offset_encoding)
table.insert(results, mk_tag_item(pattern, range, uri, offset_encoding))
end
+
for client_id, lsp_results in pairs(assert(results_by_client)) do
local client = lsp.get_client_by_id(client_id)
local offset_encoding = client and client.offset_encoding or 'utf-16'
local result = lsp_results.result or {}
if result.range then -- Location
- add(result.range, result.uri)
+ add(result.range, result.uri, offset_encoding)
else
result = result --[[@as (lsp.Location[]|lsp.LocationLink[])]]
for _, item in pairs(result) do
diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua
index 984222efbe..ccef314d86 100644
--- a/runtime/lua/vim/lsp/buf.lua
+++ b/runtime/lua/vim/lsp/buf.lua
@@ -338,6 +338,8 @@ function M.rename(new_name, opts)
-- Compute early to account for cursor movements after going async
local cword = vim.fn.expand('<cword>')
+ --- @param range lsp.Range
+ --- @param offset_encoding string
local function get_text_at_range(range, offset_encoding)
return api.nvim_buf_get_text(
bufnr,
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index 2b9e734c18..e3eef8901b 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -5,9 +5,6 @@ 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 +18,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)
- )
- )
- 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)
- )
- )
+ if not border_size[border] then
+ border_error(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
- return { height = height, width = width }
+ --- @param e string
+ local function border_height(e)
+ return #e > 0 and 1 or 0
+ end
+
+ 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,18 +110,18 @@ 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
+---@param index integer? byte index (utf-8), or `nil` for length
+---@param encoding 'utf-8'|'utf-16'|'utf-32'? 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)
@@ -242,6 +230,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)
@@ -353,7 +343,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
@@ -363,7 +353,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 M._str_byteindex_enc(line, col, offset_encoding)
end
return col
end
@@ -371,14 +361,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
@@ -391,10 +380,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
@@ -406,8 +393,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 }
@@ -438,47 +424,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
@@ -494,7 +478,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
@@ -512,7 +496,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
@@ -522,19 +506,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
@@ -552,6 +532,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
@@ -561,17 +544,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
@@ -615,19 +605,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)
@@ -701,7 +685,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
@@ -709,16 +693,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)
@@ -747,7 +733,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)
@@ -784,7 +770,7 @@ end
---
---@param offset integer
---@param contents string[]
----@return { [1]: integer, [2]: integer }
+---@return { [1]: integer, [2]: integer }?
local function get_pos_from_offset(offset, contents)
local i = 0
for l, line in ipairs(contents) do
@@ -799,18 +785,18 @@ 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 Range4|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)
--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_offset ---@type [integer, integer]|nil
+ 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
@@ -823,54 +809,33 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers
-- 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
-- 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 = (signature.activeParameter or signature_help.activeParameter or 0)
- if active_parameter < 0 then
- active_parameter = 0
- end
+ 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]
local parameter_label = parameter.label
- --[=[
- --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 type(parameter_label) == 'table' then
active_offset = parameter_label
else
- local offset = 1 ---@type integer|nil
+ 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)
@@ -879,7 +844,9 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers
end
end
for p, param in pairs(signature.parameters) do
- offset = signature.label:find(param.label, offset, true)
+ 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
@@ -897,15 +864,15 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers
local active_hl = nil
if active_offset then
- active_hl = {}
-- Account for the start of the markdown block.
if ft then
- active_offset[1], active_offset[2] =
- active_offset[1] + #contents[1], active_offset[2] + #contents[1]
+ active_offset[1] = active_offset[1] + #contents[1]
+ active_offset[2] = active_offset[2] + #contents[1]
end
- list_extend(active_hl, get_pos_from_offset(active_offset[1], contents))
- list_extend(active_hl, get_pos_from_offset(active_offset[2], contents))
+ active_hl = {}
+ list_extend(active_hl, get_pos_from_offset(active_offset[1], contents) or {})
+ list_extend(active_hl, get_pos_from_offset(active_offset[2], contents) or {})
end
return contents, active_hl
@@ -916,32 +883,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.
+---@param opts? vim.lsp.util.open_floating_preview.Opts
---@return table Options
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, 'n', true)
+ validate('opts.offset_y', opts.offset_y, 'n', true)
local anchor = ''
- local row, col
local lines_above = opts.relative == 'mouse' and vim.fn.getmousepos().line - 1
or vim.fn.winline() - 1
@@ -949,7 +899,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)
@@ -960,7 +910,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)
@@ -982,7 +933,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'
@@ -1004,13 +955,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
@@ -1020,6 +979,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)
@@ -1064,8 +1024,8 @@ end
--- Jumps to a location.
---
---@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
@@ -1085,9 +1045,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
@@ -1114,7 +1074,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
@@ -1180,8 +1140,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
@@ -1201,7 +1163,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
@@ -1210,10 +1172,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}
@@ -1225,8 +1185,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 {
@@ -1237,7 +1200,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
@@ -1246,76 +1212,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))
@@ -1334,7 +1304,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
@@ -1357,6 +1327,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
@@ -1412,10 +1383,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
@@ -1434,7 +1403,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
@@ -1482,20 +1451,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
@@ -1503,7 +1465,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
@@ -1514,17 +1476,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
@@ -1556,7 +1516,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
@@ -1591,6 +1550,27 @@ 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])[] 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'
--- Shows contents in a floating window.
---
@@ -1602,11 +1582,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
@@ -1618,7 +1596,7 @@ function M.open_floating_preview(contents, syntax, opts)
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
+ if vim.w[current_winnr][opts.focus_id] then
api.nvim_command('wincmd p')
return bufnr, current_winnr
end
@@ -1635,7 +1613,7 @@ function M.open_floating_preview(contents, syntax, opts)
-- check if another floating preview already exists for this buffer
-- and close it if needed
- local existing_float = npcall(api.nvim_buf_get_var, bufnr, 'lsp_floating_preview')
+ local existing_float = 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
@@ -1706,9 +1684,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
@@ -1716,18 +1693,16 @@ 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 start_line = reference.range.start.line
+ local start_char = reference.range.start.character
+ local end_line = reference.range['end'].line
+ local end_char = reference.range['end'].character
local start_idx = get_line_byte_from_position(
bufnr,
@@ -1762,16 +1737,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.
---
@@ -1782,9 +1747,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(
@@ -1794,28 +1759,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)
@@ -1840,7 +1796,7 @@ function M.locations_to_items(locations, offset_encoding)
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)
- table.insert(items, {
+ items[#items + 1] = {
filename = filename,
lnum = row + 1,
end_lnum = end_row + 1,
@@ -1848,58 +1804,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.
@@ -1922,7 +1871,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
@@ -1955,8 +1904,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)
@@ -1975,8 +1924,8 @@ 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)
@@ -1993,11 +1942,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
@@ -2028,8 +1975,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)
@@ -2045,33 +1992,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
@@ -2090,7 +2037,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)
@@ -2108,10 +2055,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
@@ -2119,11 +2066,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,
@@ -2139,7 +2086,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)
@@ -2162,6 +2110,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
@@ -2176,7 +2125,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
@@ -2184,7 +2133,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),
@@ -2224,9 +2173,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)