aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/lsp/util.lua
diff options
context:
space:
mode:
authorJosh Rahm <joshuarahm@gmail.com>2023-11-30 20:35:25 +0000
committerJosh Rahm <joshuarahm@gmail.com>2023-11-30 20:35:25 +0000
commit1b7b916b7631ddf73c38e3a0070d64e4636cb2f3 (patch)
treecd08258054db80bb9a11b1061bb091c70b76926a /runtime/lua/vim/lsp/util.lua
parenteaa89c11d0f8aefbb512de769c6c82f61a8baca3 (diff)
parent4a8bf24ac690004aedf5540fa440e788459e5e34 (diff)
downloadrneovim-1b7b916b7631ddf73c38e3a0070d64e4636cb2f3.tar.gz
rneovim-1b7b916b7631ddf73c38e3a0070d64e4636cb2f3.tar.bz2
rneovim-1b7b916b7631ddf73c38e3a0070d64e4636cb2f3.zip
Merge remote-tracking branch 'upstream/master' into aucmd_textputpostaucmd_textputpost
Diffstat (limited to 'runtime/lua/vim/lsp/util.lua')
-rw-r--r--runtime/lua/vim/lsp/util.lua834
1 files changed, 477 insertions, 357 deletions
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index 38051e6410..32b220746f 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -1,10 +1,10 @@
local protocol = require('vim.lsp.protocol')
-local snippet = require('vim.lsp._snippet')
+local snippet = require('vim.lsp._snippet_grammar')
local validate = vim.validate
local api = vim.api
local list_extend = vim.list_extend
local highlight = require('vim.highlight')
-local uv = vim.loop
+local uv = vim.uv
local npcall = vim.F.npcall
local split = vim.split
@@ -22,12 +22,11 @@ local default_border = {
{ ' ', 'NormalFloat' },
}
----@private
--- 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
+---@param opts table optional options for the floating window
--- - border (string or table) the border
----@returns (table) size of border in the form of { height = height, width = width }
+---@return table size of border in the form of { height = height, width = width }
local function get_border_size(opts)
local border = opts and opts.border or default_border
local height = 0
@@ -60,7 +59,6 @@ local function get_border_size(opts)
)
)
end
- ---@private
local function border_width(id)
id = (id - 1) % #border + 1
if type(border[id]) == 'table' then
@@ -77,7 +75,6 @@ local function get_border_size(opts)
)
)
end
- ---@private
local function border_height(id)
id = (id - 1) % #border + 1
if type(border[id]) == 'table' then
@@ -103,13 +100,11 @@ local function get_border_size(opts)
return { height = height, width = width }
end
----@private
local function split_lines(value)
value = string.gsub(value, '\r\n?', '\n')
- return split(value, '\n', true)
+ return split(value, '\n', { plain = true, trimempty = true })
end
----@private
local function create_window_without_focus()
local prev = vim.api.nvim_get_current_win()
vim.cmd.new()
@@ -121,9 +116,9 @@ end
--- Convert byte index to `encoding` index.
--- Convenience wrapper around vim.str_utfindex
---@param line string line to be indexed
----@param index number|nil byte index (utf-8), or `nil` for length
----@param encoding string utf-8|utf-16|utf-32|nil defaults to utf-16
----@return number `encoding` index of `index` in `line`
+---@param index integer|nil byte index (utf-8), or `nil` for length
+---@param encoding string|nil 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)
if not encoding then
encoding = 'utf-16'
@@ -149,9 +144,9 @@ end
--- Convenience wrapper around vim.str_byteindex
---Alternative to vim.str_byteindex that takes an encoding.
---@param line string line to be indexed
----@param index number UTF index
----@param encoding string utf-8|utf-16|utf-32|nil defaults to utf-16
----@return number byte (utf-8) index of `encoding` index `index` in `line`
+---@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)
if not encoding then
encoding = 'utf-16'
@@ -173,15 +168,17 @@ end
local _str_utfindex_enc = M._str_utfindex_enc
local _str_byteindex_enc = M._str_byteindex_enc
+
--- Replaces text in a range with new text.
---
--- CAUTION: Changes in-place!
---
+---@deprecated
---@param lines (table) Original list of strings
----@param A (table) Start position; a 2-tuple of {line, col} numbers
----@param B (table) End position; a 2-tuple of {line, col} numbers
----@param new_lines A list of strings to replace the original
----@returns (table) The modified {lines} object
+---@param A (table) Start position; a 2-tuple of {line,col} numbers
+---@param B (table) End position; a 2-tuple of {line,col} numbers
+---@param new_lines (table) list of strings to replace the original
+---@return table The modified {lines} object
function M.set_lines(lines, A, B, new_lines)
-- 0-indexing to 1-indexing
local i_0 = A[1] + 1
@@ -219,7 +216,6 @@ function M.set_lines(lines, A, B, new_lines)
return lines
end
----@private
local function sort_by_key(fn)
return function(a, b)
local ka, kb = fn(a), fn(b)
@@ -234,14 +230,13 @@ local function sort_by_key(fn)
end
end
----@private
--- Gets the zero-indexed lines from the given buffer.
--- Works on unloaded buffers by reading the file using libuv to bypass buf reading events.
--- Falls back to loading the buffer and nvim_buf_get_lines for buffers with non-file URI.
---
----@param bufnr number bufnr to get the lines from
----@param rows number[] zero-indexed line numbers
----@return table<number string> a table mapping rows to lines
+---@param bufnr integer bufnr to get the lines from
+---@param rows integer[] zero-indexed line numbers
+---@return table<integer, string>|string a table mapping rows to lines
local function get_lines(bufnr, rows)
rows = type(rows) == 'table' and rows or { rows }
@@ -250,15 +245,19 @@ local function get_lines(bufnr, rows)
bufnr = api.nvim_get_current_buf()
end
- ---@private
local function buf_lines()
local lines = {}
- for _, row in pairs(rows) do
+ for _, row in ipairs(rows) do
lines[row] = (api.nvim_buf_get_lines(bufnr, row, row + 1, false) or { '' })[1]
end
return lines
end
+ -- use loaded buffers if available
+ if vim.fn.bufloaded(bufnr) == 1 then
+ return buf_lines()
+ end
+
local uri = vim.uri_from_bufnr(bufnr)
-- load the buffer if this is not a file uri
@@ -268,11 +267,6 @@ local function get_lines(bufnr, rows)
return buf_lines()
end
- -- use loaded buffers if available
- if vim.fn.bufloaded(bufnr) == 1 then
- return buf_lines()
- end
-
local filename = api.nvim_buf_get_name(bufnr)
-- get the data from the file
@@ -316,23 +310,20 @@ local function get_lines(bufnr, rows)
return lines
end
----@private
--- Gets the zero-indexed line from the given buffer.
--- Works on unloaded buffers by reading the file using libuv to bypass buf reading events.
--- Falls back to loading the buffer and nvim_buf_get_lines for buffers with non-file URI.
---
----@param bufnr number
----@param row number zero-indexed line number
+---@param bufnr integer
+---@param row integer zero-indexed line number
---@return string the line at row in filename
local function get_line(bufnr, row)
return get_lines(bufnr, { row })[row]
end
----@private
--- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position
---- Returns a zero-indexed column, since set_lines() does the conversion to
----@param offset_encoding string utf-8|utf-16|utf-32
---- 1-indexed
+---@param offset_encoding string|nil 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
-- Vim's line and columns are 1-indexed
@@ -353,11 +344,40 @@ end
--- Process and return progress reports from lsp server
---@private
+---@deprecated Use vim.lsp.status() or access client.progress directly
function M.get_progress_messages()
+ vim.deprecate('vim.lsp.util.get_progress_messages', 'vim.lsp.status', '0.11.0')
local new_messages = {}
local progress_remove = {}
- for _, client in ipairs(vim.lsp.get_active_clients()) do
+ for _, client in ipairs(vim.lsp.get_clients()) do
+ local groups = {}
+ for progress in client.progress do
+ local value = progress.value
+ if type(value) == 'table' and value.kind then
+ local group = groups[progress.token]
+ if not group then
+ group = {
+ done = false,
+ progress = true,
+ title = 'empty title',
+ }
+ groups[progress.token] = group
+ end
+ group.title = value.title or group.title
+ group.cancellable = value.cancellable or group.cancellable
+ if value.kind == 'end' then
+ group.done = true
+ end
+ group.message = value.message or group.message
+ group.percentage = value.percentage or group.percentage
+ end
+ end
+
+ for _, group in pairs(groups) do
+ table.insert(new_messages, group)
+ end
+
local messages = client.messages
local data = messages
for token, ctx in pairs(data.progress) do
@@ -386,7 +406,7 @@ end
--- Applies a list of text edits to a buffer.
---@param text_edits table list of `TextEdit` objects
----@param bufnr number Buffer id
+---@param bufnr integer Buffer id
---@param offset_encoding string utf-8|utf-16|utf-32
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textEdit
function M.apply_text_edits(text_edits, bufnr, offset_encoding)
@@ -401,7 +421,7 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding)
if not api.nvim_buf_is_loaded(bufnr) then
vim.fn.bufload(bufnr)
end
- api.nvim_buf_set_option(bufnr, 'buflisted', true)
+ vim.bo[bufnr].buflisted = true
-- Fix reversed range and indexing each text_edits
local index = 0
@@ -434,25 +454,15 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding)
end
end)
- -- Some LSP servers are depending on the VSCode behavior.
- -- The VSCode will re-locate the cursor position after applying TextEdit so we also do it.
- local is_current_buf = api.nvim_get_current_buf() == bufnr
- local cursor = (function()
- if not is_current_buf then
- return {
- row = -1,
- col = -1,
- }
+ -- save and restore local marks since they get deleted by nvim_buf_set_lines
+ local marks = {}
+ for _, m in pairs(vim.fn.getmarklist(bufnr or vim.api.nvim_get_current_buf())) do
+ if m.mark:match("^'[a-z]$") then
+ marks[m.mark:sub(2, 2)] = { m.pos[2], m.pos[3] - 1 } -- api-indexed
end
- local cursor = api.nvim_win_get_cursor(0)
- return {
- row = cursor[1] - 1,
- col = cursor[2],
- }
- end)()
+ end
-- Apply text edits.
- local is_cursor_fixed = false
local has_eol_text_edit = false
for _, text_edit in ipairs(text_edits) do
-- Normalize line ending
@@ -464,7 +474,7 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding)
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', true),
+ text = split(text_edit.newText, '\n', { plain = true }),
}
local max = api.nvim_buf_line_count(bufnr)
@@ -499,42 +509,28 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding)
e.end_col = math.min(last_line_len, e.end_col)
api.nvim_buf_set_text(bufnr, e.start_row, e.start_col, e.end_row, e.end_col, e.text)
-
- -- Fix cursor position.
- local row_count = (e.end_row - e.start_row) + 1
- if e.end_row < cursor.row then
- cursor.row = cursor.row + (#e.text - row_count)
- is_cursor_fixed = true
- elseif e.end_row == cursor.row and e.end_col <= cursor.col then
- cursor.row = cursor.row + (#e.text - row_count)
- cursor.col = #e.text[#e.text] + (cursor.col - e.end_col)
- if #e.text == 1 then
- cursor.col = cursor.col + e.start_col
- end
- is_cursor_fixed = true
- end
end
end
local max = api.nvim_buf_line_count(bufnr)
- -- Apply fixed cursor position.
- if is_cursor_fixed then
- local is_valid_cursor = true
- is_valid_cursor = is_valid_cursor and cursor.row < max
- is_valid_cursor = is_valid_cursor and cursor.col <= #(get_line(bufnr, max - 1) or '')
- if is_valid_cursor then
- api.nvim_win_set_cursor(0, { cursor.row + 1, cursor.col })
+ -- no need to restore marks that still exist
+ for _, m in pairs(vim.fn.getmarklist(bufnr or vim.api.nvim_get_current_buf())) do
+ marks[m.mark:sub(2, 2)] = nil
+ end
+ -- restore marks
+ for mark, pos in pairs(marks) do
+ if pos then
+ -- 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], {})
end
end
-- Remove final line if needed
local fix_eol = has_eol_text_edit
- fix_eol = fix_eol
- and (
- api.nvim_buf_get_option(bufnr, 'eol')
- or (api.nvim_buf_get_option(bufnr, 'fixeol') and not api.nvim_buf_get_option(bufnr, 'binary'))
- )
+ fix_eol = fix_eol and (vim.bo[bufnr].eol or (vim.bo[bufnr].fixeol and not vim.bo[bufnr].binary))
fix_eol = fix_eol and get_line(bufnr, max - 1) == ''
if fix_eol then
api.nvim_buf_set_lines(bufnr, -2, -1, false, {})
@@ -551,10 +547,12 @@ end
--- Can be used to extract the completion items from a
--- `textDocument/completion` request, which may return one of
--- `CompletionItem[]`, `CompletionList` or null.
----@param result (table) The result of a `textDocument/completion` request
----@returns (table) List of completion items
+---@deprecated
+---@param result table The result of a `textDocument/completion` request
+---@return lsp.CompletionItem[] List of completion items
---@see https://microsoft.github.io/language-server-protocol/specification#textDocument_completion
function M.extract_completion_items(result)
+ vim.deprecate('vim.lsp.util.extract_completion_items', nil, '0.11')
if type(result) == 'table' and result.items then
-- result is a `CompletionList`
return result.items
@@ -571,7 +569,7 @@ end
--- document.
---
---@param text_document_edit table: a `TextDocumentEdit` object
----@param index number: Optional index of the edit, if from a list of edits (or nil, if not from a list)
+---@param index integer: Optional index of the edit, if from a list of edits (or nil, if not from a list)
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentEdit
function M.apply_text_document_edit(text_document_edit, index, offset_encoding)
local text_document = text_document_edit.textDocument
@@ -610,130 +608,36 @@ end
--- Parses snippets in a completion entry.
---
+---@deprecated
---@param input string unparsed snippet
----@returns string parsed snippet
+---@return string parsed snippet
function M.parse_snippet(input)
+ vim.deprecate('vim.lsp.util.parse_snippet', nil, '0.11')
local ok, parsed = pcall(function()
- return tostring(snippet.parse(input))
+ return snippet.parse(input)
end)
if not ok then
return input
end
- return parsed
-end
-
----@private
---- Sorts by CompletionItem.sortText.
----
---see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
-local function sort_completion_items(items)
- table.sort(items, function(a, b)
- return (a.sortText or a.label) < (b.sortText or b.label)
- end)
-end
-
----@private
---- Returns text that should be inserted when selecting completion item. The
---- precedence is as follows: textEdit.newText > insertText > label
---see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
-local function get_completion_word(item)
- if item.textEdit ~= nil and item.textEdit.newText ~= nil and item.textEdit.newText ~= '' then
- local insert_text_format = protocol.InsertTextFormat[item.insertTextFormat]
- if insert_text_format == 'PlainText' or insert_text_format == nil then
- return item.textEdit.newText
- else
- return M.parse_snippet(item.textEdit.newText)
- end
- elseif item.insertText ~= nil and item.insertText ~= '' then
- local insert_text_format = protocol.InsertTextFormat[item.insertTextFormat]
- if insert_text_format == 'PlainText' or insert_text_format == nil then
- return item.insertText
- else
- return M.parse_snippet(item.insertText)
- end
- end
- return item.label
-end
----@private
---- Some language servers return complementary candidates whose prefixes do not
---- match are also returned. So we exclude completion candidates whose prefix
---- does not match.
-local function remove_unmatch_completion_items(items, prefix)
- return vim.tbl_filter(function(item)
- local word = get_completion_word(item)
- return vim.startswith(word, prefix)
- end, items)
-end
-
---- According to LSP spec, if the client set `completionItemKind.valueSet`,
---- the client must handle it properly even if it receives a value outside the
---- specification.
----
----@param completion_item_kind (`vim.lsp.protocol.completionItemKind`)
----@returns (`vim.lsp.protocol.completionItemKind`)
----@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
-function M._get_completion_item_kind_name(completion_item_kind)
- return protocol.CompletionItemKind[completion_item_kind] or 'Unknown'
+ return tostring(parsed)
end
--- Turns the result of a `textDocument/completion` request into vim-compatible
--- |complete-items|.
---
----@param result The result of a `textDocument/completion` call, e.g. from
----|vim.lsp.buf.completion()|, which may be one of `CompletionItem[]`,
+---@deprecated
+---@param result table The result of a `textDocument/completion` call, e.g.
+--- from |vim.lsp.buf.completion()|, which may be one of `CompletionItem[]`,
--- `CompletionList` or `null`
---@param prefix (string) the prefix to filter the completion items
----@returns { matches = complete-items table, incomplete = bool }
----@see |complete-items|
+---@return table[] items
+---@see complete-items
function M.text_document_completion_list_to_complete_items(result, prefix)
- local items = M.extract_completion_items(result)
- if vim.tbl_isempty(items) then
- return {}
- end
-
- items = remove_unmatch_completion_items(items, prefix)
- sort_completion_items(items)
-
- local matches = {}
-
- for _, completion_item in ipairs(items) do
- local info = ' '
- local documentation = completion_item.documentation
- if documentation then
- if type(documentation) == 'string' and documentation ~= '' then
- info = documentation
- elseif type(documentation) == 'table' and type(documentation.value) == 'string' then
- info = documentation.value
- -- else
- -- TODO(ashkan) Validation handling here?
- end
- end
-
- local word = get_completion_word(completion_item)
- table.insert(matches, {
- word = word,
- abbr = completion_item.label,
- kind = M._get_completion_item_kind_name(completion_item.kind),
- menu = completion_item.detail or '',
- info = info,
- icase = 1,
- dup = 1,
- empty = 1,
- user_data = {
- nvim = {
- lsp = {
- completion_item = completion_item,
- },
- },
- },
- })
- end
-
- return matches
+ vim.deprecate('vim.lsp.util.text_document_completion_list_to_complete_items', nil, '0.11')
+ return require('vim.lsp._completion')._lsp_to_complete_items(result, prefix)
end
----@private
--- Like vim.fn.bufwinid except it works across tabpages.
local function bufwinid(bufnr)
for _, win in ipairs(api.nvim_list_wins()) do
@@ -743,6 +647,19 @@ local function bufwinid(bufnr)
end
end
+--- Get list of buffers for a directory
+local function get_dir_bufs(path)
+ path = path:gsub('([^%w])', '%%%1')
+ local buffers = {}
+ for _, v in ipairs(vim.api.nvim_list_bufs()) do
+ local bufname = vim.api.nvim_buf_get_name(v):gsub('buffer://', '')
+ if bufname:find(path) then
+ table.insert(buffers, v)
+ end
+ end
+ return buffers
+end
+
--- Rename old_fname to new_fname
---
---@param opts (table)
@@ -755,26 +672,41 @@ function M.rename(old_fname, new_fname, opts)
vim.notify('Rename target already exists. Skipping rename.')
return
end
- local oldbuf = vim.fn.bufadd(old_fname)
- vim.fn.bufload(oldbuf)
- -- The there may be pending changes in the buffer
- api.nvim_buf_call(oldbuf, function()
- vim.cmd('w!')
- end)
+ local oldbufs = {}
+ local win = nil
+
+ if vim.fn.isdirectory(old_fname) == 1 then
+ oldbufs = get_dir_bufs(old_fname)
+ else
+ local oldbuf = vim.fn.bufadd(old_fname)
+ table.insert(oldbufs, oldbuf)
+ win = bufwinid(oldbuf)
+ end
+
+ for _, b in ipairs(oldbufs) do
+ vim.fn.bufload(b)
+ -- The there may be pending changes in the buffer
+ api.nvim_buf_call(b, function()
+ vim.cmd('w!')
+ end)
+ end
local ok, err = os.rename(old_fname, new_fname)
assert(ok, err)
- local newbuf = vim.fn.bufadd(new_fname)
- local win = bufwinid(oldbuf)
- if win then
- api.nvim_win_set_buf(win, newbuf)
+ if vim.fn.isdirectory(new_fname) == 0 then
+ local newbuf = vim.fn.bufadd(new_fname)
+ if win then
+ api.nvim_win_set_buf(win, newbuf)
+ end
+ end
+
+ for _, b in ipairs(oldbufs) do
+ api.nvim_buf_delete(b, {})
end
- api.nvim_buf_delete(oldbuf, { force = true })
end
----@private
local function create_file(change)
local opts = change.options or {}
-- from spec: Overwrite wins over `ignoreIfExists`
@@ -789,7 +721,6 @@ local function create_file(change)
vim.fn.bufadd(fname)
end
----@private
local function delete_file(change)
local opts = change.options or {}
local fname = vim.uri_to_fname(change.uri)
@@ -855,9 +786,12 @@ end
--- window for `textDocument/hover`, for parsing the result of
--- `textDocument/signatureHelp`, and potentially others.
---
+--- Note that if the input is of type `MarkupContent` and its kind is `plaintext`,
+--- then the corresponding value is returned without further modifications.
+---
---@param input (`MarkedString` | `MarkedString[]` | `MarkupContent`)
---@param contents (table|nil) List of strings to extend with converted lines. Defaults to {}.
----@returns {contents}, extended with lines of converted markdown.
+---@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)
contents = contents or {}
@@ -865,27 +799,13 @@ function M.convert_input_to_markdown_lines(input, contents)
if type(input) == 'string' then
list_extend(contents, split_lines(input))
else
- assert(type(input) == 'table', 'Expected a table for Hover.contents')
+ assert(type(input) == 'table', 'Expected a table for LSP input')
-- MarkupContent
if input.kind then
- -- The kind can be either plaintext or markdown.
- -- If it's plaintext, then wrap it in a <text></text> block
-
- -- Some servers send input.value as empty, so let's ignore this :(
local value = input.value or ''
-
- if input.kind == 'plaintext' then
- -- wrap this in a <text></text> block so that stylize_markdown
- -- can properly process it as plaintext
- value = string.format('<text>\n%s\n</text>', value)
- end
-
- -- assert(type(value) == 'string')
list_extend(contents, split_lines(value))
-- MarkupString variation 2
elseif input.language then
- -- Some servers send input.value as empty, so let's ignore this :(
- -- assert(type(input.value) == 'string')
table.insert(contents, '```' .. input.language)
list_extend(contents, split_lines(input.value or ''))
table.insert(contents, '```')
@@ -903,12 +823,13 @@ function M.convert_input_to_markdown_lines(input, contents)
return contents
end
---- Converts `textDocument/SignatureHelp` response to markdown lines.
+--- Converts `textDocument/signatureHelp` response to markdown lines.
---
----@param signature_help Response of `textDocument/SignatureHelp`
----@param ft optional filetype that will be use as the `lang` for the label markdown code block
----@param triggers optional list of trigger characters from the lsp server. used to better determine parameter offsets
----@returns list of lines of converted markdown.
+---@param signature_help table 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 table|nil table list of lines of converted markdown.
+---@return table|nil table of active hl
---@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
@@ -932,11 +853,17 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers
end
local label = signature.label
if ft then
- -- wrap inside a code block so stylize_markdown can render it properly
+ -- wrap inside a code block for proper rendering
label = ('```%s\n%s\n```'):format(ft, label)
end
- list_extend(contents, split(label, '\n', true))
+ list_extend(contents, split(label, '\n', { plain = true, trimempty = true }))
if signature.documentation 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 }
+ end
M.convert_input_to_markdown_lines(signature.documentation, contents)
end
if signature.parameters and #signature.parameters > 0 then
@@ -1007,16 +934,22 @@ end
--- Creates a table with sensible default options for a floating window. The
--- table can be passed to |nvim_open_win()|.
---
----@param width (number) window width (in character cells)
----@param height (number) window height (in character cells)
----@param opts (table, optional)
---- - offset_x (number) offset to add to `col`
---- - offset_y (number) offset to add to `row`
+---@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"
----@returns (table) Options
+--- - 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
function M.make_floating_popup_options(width, height, opts)
validate({
opts = { opts, 't', true },
@@ -1034,19 +967,33 @@ function M.make_floating_popup_options(width, height, opts)
or vim.fn.winline() - 1
local lines_below = vim.fn.winheight(0) - lines_above
- if lines_above < lines_below then
+ local anchor_bias = opts.anchor_bias or 'auto'
+
+ local anchor_below
+
+ if anchor_bias == 'below' then
+ anchor_below = (lines_below > lines_above) or (height <= lines_below)
+ elseif anchor_bias == 'above' then
+ local anchor_above = (lines_above > lines_below) or (height <= lines_above)
+ anchor_below = not anchor_above
+ else
+ anchor_below = lines_below > lines_above
+ end
+
+ local border_height = get_border_size(opts).height
+ if anchor_below then
anchor = anchor .. 'N'
- height = math.min(lines_below, height)
+ height = math.max(math.min(lines_below - border_height, height), 0)
row = 1
else
anchor = anchor .. 'S'
- height = math.min(lines_above, height)
+ height = math.max(math.min(lines_above - border_height, height), 0)
row = 0
end
local wincol = opts.relative == 'mouse' and vim.fn.getmousepos().column or vim.fn.wincol()
- if wincol + width + (opts.offset_x or 0) <= api.nvim_get_option('columns') then
+ if wincol + width + (opts.offset_x or 0) <= vim.o.columns then
anchor = anchor .. 'W'
col = 0
else
@@ -1080,7 +1027,7 @@ end
--- Shows document and optionally jumps to the location.
---
---@param location table (`Location`|`LocationLink`)
----@param offset_encoding "utf-8" | "utf-16" | "utf-32"
+---@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.
@@ -1112,7 +1059,7 @@ function M.show_document(location, offset_encoding, opts)
or focus and api.nvim_get_current_win()
or create_window_without_focus()
- api.nvim_buf_set_option(bufnr, 'buflisted', true)
+ vim.bo[bufnr].buflisted = true
api.nvim_win_set_buf(win, bufnr)
if focus then
api.nvim_set_current_win(win)
@@ -1137,7 +1084,7 @@ end
--- Jumps to a location.
---
---@param location table (`Location`|`LocationLink`)
----@param offset_encoding "utf-8" | "utf-16" | "utf-32"
+---@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.
---@return boolean `true` if the jump succeeded
function M.jump_to_location(location, offset_encoding, reuse_win)
@@ -1157,8 +1104,9 @@ end
--- - for Location, range is shown (e.g., function definition)
--- - for LocationLink, targetRange is shown (e.g., body of function definition)
---
----@param location a single `Location` or `LocationLink`
----@returns (bufnr,winnr) buffer and window number of floating window or nil
+---@param location table a single `Location` or `LocationLink`
+---@return integer|nil buffer id of float window
+---@return integer|nil 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
@@ -1171,19 +1119,18 @@ function M.preview_location(location, opts)
end
local range = location.targetRange or location.range
local contents = api.nvim_buf_get_lines(bufnr, range.start.line, range['end'].line + 1, false)
- local syntax = api.nvim_buf_get_option(bufnr, 'syntax')
+ local syntax = vim.bo[bufnr].syntax
if syntax == '' then
-- When no syntax is set, we use filetype as fallback. This might not result
- -- in a valid syntax definition. See also ft detection in stylize_markdown.
+ -- in a valid syntax definition.
-- An empty syntax is more common now with TreeSitter, since TS disables syntax.
- syntax = api.nvim_buf_get_option(bufnr, 'filetype')
+ syntax = vim.bo[bufnr].filetype
end
opts = opts or {}
opts.focus_id = 'location'
return M.open_floating_preview(contents, syntax, opts)
end
----@private
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
@@ -1192,37 +1139,65 @@ local function find_window_by_var(name, value)
end
end
---- Trims empty lines from input and pad top and bottom with empty lines
----
----@param contents table of lines to trim and pad
----@param opts dictionary with optional fields
---- - pad_top number of lines to pad contents at top (default 0)
---- - pad_bottom number of lines to pad contents at bottom (default 0)
----@return contents table of trimmed and padded lines
-function M._trim(contents, opts)
- validate({
- contents = { contents, 't' },
- opts = { opts, 't', true },
- })
- opts = opts or {}
- contents = M.trim_empty_lines(contents)
- if opts.pad_top then
- for _ = 1, opts.pad_top do
- table.insert(contents, 1, '')
+---Returns true if the line is empty or only contains whitespace.
+---@param line string
+---@return boolean
+local function is_blank_line(line)
+ return line and line:match('^%s*$')
+end
+
+---Returns true if the line corresponds to a Markdown thematic break.
+---@param line string
+---@return boolean
+local function is_separator_line(line)
+ return line and line:match('^ ? ? ?%-%-%-+%s*$')
+end
+
+---Replaces separator lines by the given divider and removing surrounding blank lines.
+---@param contents string[]
+---@param divider string
+---@return string[]
+local function replace_separators(contents, divider)
+ local trimmed = {}
+ local l = 1
+ while l <= #contents do
+ local line = contents[l]
+ if is_separator_line(line) then
+ if l > 1 and is_blank_line(contents[l - 1]) then
+ table.remove(trimmed)
+ end
+ table.insert(trimmed, divider)
+ if is_blank_line(contents[l + 1]) then
+ l = l + 1
+ end
+ else
+ table.insert(trimmed, line)
end
+ l = l + 1
end
- if opts.pad_bottom then
- for _ = 1, opts.pad_bottom do
- table.insert(contents, '')
+
+ return trimmed
+end
+
+---Collapses successive blank lines in the input table into a single one.
+---@param contents string[]
+---@return string[]
+local function collapse_blank_lines(contents)
+ local collapsed = {}
+ local l = 1
+ while l <= #contents do
+ local line = contents[l]
+ if is_blank_line(line) then
+ while is_blank_line(contents[l + 1]) do
+ l = l + 1
+ end
end
+ table.insert(collapsed, line)
+ l = l + 1
end
- return contents
+ return collapsed
end
---- Generates a table mapping markdown code block lang to vim syntax,
---- based on g:markdown_fenced_languages
----@return a table of lang -> syntax mappings
----@private
local function get_markdown_fences()
local fences = {}
for _, fence in pairs(vim.g.markdown_fenced_languages or {}) do
@@ -1244,16 +1219,14 @@ end
--- If you want to open a popup with fancy markdown, use `open_floating_preview` instead
---
---@param contents table of lines to show in window
----@param opts dictionary with optional fields
+---@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
---- - pad_top number of lines to pad contents at top
---- - pad_bottom number of lines to pad contents at bottom
--- - separator insert separator after code block
----@returns width,height size of float
+---@return table stripped content
function M.stylize_markdown(bufnr, contents, opts)
validate({
contents = { contents, 't' },
@@ -1264,7 +1237,7 @@ function M.stylize_markdown(bufnr, contents, opts)
-- table of fence types to {ft, begin, end}
-- when ft is nil, we get the ft from the regex match
local matchers = {
- block = { nil, '```+([a-zA-Z0-9_]*)', '```+' },
+ block = { nil, '```+%s*([a-zA-Z0-9_]*)', '```+' },
pre = { nil, '<pre>([a-z0-9]*)', '</pre>' },
code = { '', '<code>', '</code>' },
text = { 'text', '<text>', '</text>' },
@@ -1288,7 +1261,7 @@ function M.stylize_markdown(bufnr, contents, opts)
end
-- Clean up
- contents = M._trim(contents, opts)
+ contents = vim.split(table.concat(contents, '\n'), '\n', { trimempty = true })
local stripped = {}
local highlights = {}
@@ -1348,6 +1321,20 @@ function M.stylize_markdown(bufnr, contents, opts)
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)
+
-- Compute size of float needed to show (wrapped) lines
opts.wrap_at = opts.wrap_at or (vim.wo['wrap'] and api.nvim_win_get_width(0))
local width = M._make_floating_popup_size(stripped, opts)
@@ -1363,7 +1350,6 @@ function M.stylize_markdown(bufnr, contents, opts)
api.nvim_buf_set_lines(bufnr, 0, -1, false, stripped)
local idx = 1
- ---@private
-- keep track of syntaxes we already included.
-- no need to include the same syntax more than once
local langs = {}
@@ -1386,10 +1372,10 @@ function M.stylize_markdown(bufnr, contents, opts)
if not langs[lang] then
-- HACK: reset current_syntax, since some syntax files like markdown won't load if it is already set
pcall(api.nvim_buf_del_var, bufnr, 'current_syntax')
- -- TODO(ashkan): better validation before this.
- if not pcall(vim.cmd, string.format('syntax include %s syntax/%s.vim', lang, ft)) then
+ if #api.nvim_get_runtime_file(('syntax/%s.vim'):format(ft), true) == 0 then
return
end
+ pcall(vim.cmd, string.format('syntax include %s syntax/%s.vim', lang, ft))
langs[lang] = true
end
vim.cmd(
@@ -1424,15 +1410,53 @@ function M.stylize_markdown(bufnr, contents, opts)
return stripped
end
+--- @class lsp.util.NormalizeMarkdownOptions
+--- @field width integer Thematic breaks are expanded to this size. Defaults to 80.
+
+--- Normalizes Markdown input to a canonical form.
+---
+--- The returned Markdown adheres to the GitHub Flavored Markdown (GFM)
+--- specification.
+---
+--- The following transformations are made:
+---
+--- 1. Carriage returns ('\r') and empty lines at the beginning and end are removed
+--- 2. Successive empty lines are collapsed into a single empty line
+--- 3. Thematic breaks are expanded to the given width
+---
---@private
+---@param contents string[]
+---@param opts? lsp.util.NormalizeMarkdownOptions
+---@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 },
+ })
+ opts = opts or {}
+
+ -- 1. Carriage returns are removed
+ contents = vim.split(table.concat(contents, '\n'):gsub('\r', ''), '\n', { trimempty = true })
+
+ -- 2. Successive empty lines are collapsed into a single empty line
+ contents = collapse_blank_lines(contents)
+
+ -- 3. Thematic breaks are expanded to the given width
+ local divider = string.rep('─', opts.width or 80)
+ contents = replace_separators(contents, divider)
+
+ return contents
+end
+
--- Closes the preview window
---
----@param winnr number window id of preview window
+---@param winnr integer window id of preview window
---@param bufnrs table|nil optional list of ignored buffers
local function close_preview_window(winnr, bufnrs)
vim.schedule(function()
-- exit if we are in one of ignored buffers
- if bufnrs and vim.tbl_contains(bufnrs, api.nvim_get_current_buf()) then
+ if bufnrs and vim.list_contains(bufnrs, api.nvim_get_current_buf()) then
return
end
@@ -1442,13 +1466,12 @@ local function close_preview_window(winnr, bufnrs)
end)
end
----@private
--- Creates autocommands to close a preview window when events happen.
---
---@param events table list of events
----@param winnr number window id of preview window
+---@param winnr integer window id of preview window
---@param bufnrs table list of buffers where the preview window will remain visible
----@see |autocmd-events|
+---@see autocmd-events
local function close_preview_autocmd(events, winnr, bufnrs)
local augroup = api.nvim_create_augroup('preview_window_' .. winnr, {
clear = true,
@@ -1478,13 +1501,14 @@ end
--- Computes size of float needed to show contents (with optional wrapping)
---
---@param contents table of lines to show in window
----@param opts dictionary 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
--- - max_width maximal width of floating window
--- - max_height maximal height of floating window
----@returns width,height size of float
+---@return integer width size of float
+---@return integer height size of float
function M._make_floating_popup_size(contents, opts)
validate({
contents = { contents, 't' },
@@ -1503,7 +1527,7 @@ function M._make_floating_popup_size(contents, opts)
width = 0
for i, line in ipairs(contents) do
-- TODO(ashkan) use nvim_strdisplaywidth if/when that is introduced.
- line_widths[i] = vim.fn.strdisplaywidth(line)
+ line_widths[i] = vim.fn.strdisplaywidth(line:gsub('%z', '\n'))
width = math.max(line_widths[i], width)
end
end
@@ -1532,7 +1556,7 @@ function M._make_floating_popup_size(contents, opts)
height = 0
if vim.tbl_isempty(line_widths) then
for _, line in ipairs(contents) do
- local line_width = vim.fn.strdisplaywidth(line)
+ local line_width = vim.fn.strdisplaywidth(line:gsub('%z', '\n'))
height = height + math.ceil(line_width / wrap_at)
end
else
@@ -1553,23 +1577,22 @@ end
---
---@param contents table of lines to show in window
---@param syntax string of syntax to set for opened buffer
----@param opts table with optional fields (additional keys are passed on to |nvim_open_win()|)
---- - height: (number) height of floating window
---- - width: (number) width of floating window
+---@param opts table with optional fields (additional keys are filtered with |vim.lsp.util.make_floating_popup_options()|
+--- before they are passed on to |nvim_open_win()|)
+--- - height: (integer) height of floating window
+--- - width: (integer) width of floating window
--- - wrap: (boolean, default true) wrap long lines
---- - wrap_at: (number) character to wrap at for computing height when wrap is enabled
---- - max_width: (number) maximal width of floating window
---- - max_height: (number) maximal height of floating window
---- - pad_top: (number) number of lines to pad contents at top
---- - pad_bottom: (number) number of lines to pad contents at bottom
+--- - wrap_at: (integer) character to wrap at for computing height when wrap is enabled
+--- - max_width: (integer) maximal width of floating window
+--- - max_height: (integer) maximal height of floating window
--- - focus_id: (string) if a popup with this id is opened, then focus it
--- - close_events: (table) list of events that closes the floating window
--- - focusable: (boolean, default true) Make float focusable
--- - focus: (boolean, default true) If `true`, and if {focusable}
--- is also `true`, focus an existing floating window with the same
--- {focus_id}
----@returns bufnr,winnr buffer and window number of the newly created floating
----preview window
+---@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' },
@@ -1578,7 +1601,6 @@ function M.open_floating_preview(contents, syntax, opts)
})
opts = opts or {}
opts.wrap = opts.wrap ~= false -- wrapping by default
- opts.stylize_markdown = opts.stylize_markdown ~= false and vim.g.syntax_on ~= nil
opts.focus = opts.focus ~= false
opts.close_events = opts.close_events or { 'CursorMoved', 'CursorMovedI', 'InsertCharPre' }
@@ -1610,18 +1632,23 @@ function M.open_floating_preview(contents, syntax, opts)
api.nvim_win_close(existing_float, true)
end
+ -- Create the buffer
local floating_bufnr = api.nvim_create_buf(false, true)
- local do_stylize = syntax == 'markdown' and opts.stylize_markdown
-
- -- Clean up input: trim empty lines from the end, pad
- contents = M._trim(contents, opts)
+ -- Set up the contents, using treesitter for markdown
+ local do_stylize = syntax == 'markdown' and vim.g.syntax_on ~= nil
if do_stylize then
- -- applies the syntax and sets the lines to the buffer
- contents = M.stylize_markdown(floating_bufnr, contents, opts)
+ 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 })
+
if syntax then
- api.nvim_buf_set_option(floating_bufnr, 'syntax', syntax)
+ vim.bo[floating_bufnr].syntax = syntax
end
api.nvim_buf_set_lines(floating_bufnr, 0, -1, true, contents)
end
@@ -1636,17 +1663,18 @@ function M.open_floating_preview(contents, syntax, opts)
local float_option = M.make_floating_popup_options(width, height, opts)
local floating_winnr = api.nvim_open_win(floating_bufnr, false, float_option)
+
if do_stylize then
- api.nvim_win_set_option(floating_winnr, 'conceallevel', 2)
- api.nvim_win_set_option(floating_winnr, 'concealcursor', 'n')
+ vim.wo[floating_winnr].conceallevel = 2
end
-- disable folding
- api.nvim_win_set_option(floating_winnr, 'foldenable', false)
+ vim.wo[floating_winnr].foldenable = false
-- soft wrapping
- api.nvim_win_set_option(floating_winnr, 'wrap', opts.wrap)
+ vim.wo[floating_winnr].wrap = opts.wrap
+
+ vim.bo[floating_bufnr].modifiable = false
+ vim.bo[floating_bufnr].bufhidden = 'wipe'
- api.nvim_buf_set_option(floating_bufnr, 'modifiable', false)
- api.nvim_buf_set_option(floating_bufnr, 'bufhidden', 'wipe')
api.nvim_buf_set_keymap(
floating_bufnr,
'n',
@@ -1670,18 +1698,18 @@ do --[[ References ]]
--- Removes document highlights from a buffer.
---
- ---@param bufnr number Buffer id
+ ---@param bufnr integer|nil Buffer id
function M.buf_clear_references(bufnr)
- validate({ bufnr = { bufnr, 'n', true } })
+ validate({ bufnr = { bufnr, { 'n' }, true } })
api.nvim_buf_clear_namespace(bufnr or 0, reference_ns, 0, -1)
end
--- Shows a list of document highlights for a certain buffer.
---
- ---@param bufnr number Buffer id
+ ---@param bufnr integer Buffer id
---@param references table List of `DocumentHighlight` objects to highlight
---@param offset_encoding string One of "utf-8", "utf-16", "utf-32".
- ---@see https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentContentChangeEvent
+ ---@see https://microsoft.github.io/language-server-protocol/specification/#textDocumentContentChangeEvent
function M.buf_highlight_references(bufnr, references, offset_encoding)
validate({
bufnr = { bufnr, 'n', true },
@@ -1729,18 +1757,23 @@ end)
--- Returns the items with the byte position calculated correctly and in sorted
--- order, for display in quickfix and location lists.
---
+--- The `user_data` field of each resulting item will contain the original
+--- `Location` or `LocationLink` it was computed from.
+---
--- The result can be passed to the {list} argument of |setqflist()| or
--- |setloclist()|.
---
---@param locations table list of `Location`s or `LocationLink`s
---@param offset_encoding string offset_encoding for locations utf-8|utf-16|utf-32
----@returns (table) list of items
+--- default to first client of buffer
+---@return table list of items
function M.locations_to_items(locations, offset_encoding)
if offset_encoding == nil then
vim.notify_once(
'locations_to_items must be called with valid offset encoding',
vim.log.levels.WARN
)
+ offset_encoding = vim.lsp.get_clients({ bufnr = 0 })[1].offset_encoding
end
local items = {}
@@ -1755,7 +1788,7 @@ function M.locations_to_items(locations, offset_encoding)
-- locations may be Location or LocationLink
local uri = d.uri or d.targetUri
local range = d.range or d.targetSelectionRange
- table.insert(grouped[uri], { start = range.start })
+ table.insert(grouped[uri], { start = range.start, location = d })
end
local keys = vim.tbl_keys(grouped)
@@ -1787,6 +1820,7 @@ function M.locations_to_items(locations, offset_encoding)
lnum = row + 1,
col = col + 1,
text = line,
+ user_data = temp.location,
})
end
end
@@ -1802,9 +1836,8 @@ end
--- Converts symbols to quickfix list items.
---
----@param symbols DocumentSymbol[] or SymbolInformation[]
+---@param symbols table DocumentSymbol[] or SymbolInformation[]
function M.symbols_to_items(symbols, bufnr)
- ---@private
local function _symbols_to_items(_symbols, _items, _bufnr)
for _, symbol in ipairs(_symbols) do
if symbol.location then -- SymbolInformation type
@@ -1842,8 +1875,9 @@ function M.symbols_to_items(symbols, bufnr)
end
--- Removes empty lines from the beginning and end.
----@param lines (table) list of lines to trim
----@returns (table) trimmed list of lines
+---@deprecated use `vim.split()` with `trimempty` instead
+---@param lines table list of lines to trim
+---@return table trimmed list of lines
function M.trim_empty_lines(lines)
local start = 1
for i = 1, #lines do
@@ -1867,8 +1901,9 @@ end
---
--- CAUTION: Modifies the input in-place!
---
----@param lines (table) list of lines
----@returns (string) filetype or "markdown" if it was unchanged.
+---@deprecated
+---@param lines table list of lines
+---@return string filetype or "markdown" if it was unchanged.
function M.try_trim_markdown_code_blocks(lines)
local language_id = lines[1]:match('^```(.*)')
if language_id then
@@ -1890,8 +1925,7 @@ function M.try_trim_markdown_code_blocks(lines)
return 'markdown'
end
----@private
----@param window number|nil: window handle or 0 for current, defaults to current
+---@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`
local function make_position_param(window, offset_encoding)
window = window or 0
@@ -1911,9 +1945,9 @@ end
--- Creates a `TextDocumentPositionParams` object for the current buffer and cursor position.
---
----@param window number|nil: window handle or 0 for current, defaults to current
+---@param 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`
----@returns `TextDocumentPositionParams` object
+---@return table `TextDocumentPositionParams` object
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentPositionParams
function M.make_position_params(window, offset_encoding)
window = window or 0
@@ -1926,8 +1960,8 @@ function M.make_position_params(window, offset_encoding)
end
--- Utility function for getting the encoding of the first LSP client on the given buffer.
----@param bufnr (number) buffer handle or 0 for current, defaults to current
----@returns (string) encoding first client if there is one, nil otherwise
+---@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 },
@@ -1935,7 +1969,7 @@ function M._get_offset_encoding(bufnr)
local offset_encoding
- for _, client in pairs(vim.lsp.get_active_clients({ bufnr = bufnr })) do
+ for _, client in pairs(vim.lsp.get_clients({ bufnr = bufnr })) do
if client.offset_encoding == nil then
vim.notify_once(
string.format(
@@ -1949,7 +1983,7 @@ function M._get_offset_encoding(bufnr)
if not offset_encoding then
offset_encoding = this_offset_encoding
elseif offset_encoding ~= this_offset_encoding then
- vim.notify(
+ vim.notify_once(
'warning: multiple different client offset_encodings detected for buffer, this is not supported yet',
vim.log.levels.WARN
)
@@ -1964,9 +1998,9 @@ end
--- `textDocument/codeAction`, `textDocument/colorPresentation`,
--- `textDocument/rangeFormatting`.
---
----@param window number|nil: window handle or 0 for current, defaults to current
+---@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`
----@returns { textDocument = { uri = `current_file_uri` }, range = { start =
+---@return table { textDocument = { uri = `current_file_uri` }, range = { start =
---`current_position`, end = `current_position` } }
function M.make_range_params(window, offset_encoding)
local buf = api.nvim_win_get_buf(window or 0)
@@ -1981,13 +2015,13 @@ 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 number[]|nil {row, col} mark-indexed position.
+---@param start_pos integer[]|nil {row,col} mark-indexed position.
--- Defaults to the start of the last visual selection.
----@param end_pos number[]|nil {row, col} mark-indexed position.
+---@param end_pos integer[]|nil {row,col} mark-indexed position.
--- Defaults to the end of the last visual selection.
----@param bufnr number|nil buffer handle or 0 for current, defaults to current
+---@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`
----@returns { textDocument = { uri = `current_file_uri` }, range = { start =
+---@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({
@@ -2026,24 +2060,25 @@ end
--- Creates a `TextDocumentIdentifier` object for the current buffer.
---
----@param bufnr number|nil: Buffer handle, defaults to current
----@returns `TextDocumentIdentifier`
+---@param bufnr integer|nil: Buffer handle, defaults to current
+---@return table `TextDocumentIdentifier`
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentIdentifier
function M.make_text_document_params(bufnr)
return { uri = vim.uri_from_bufnr(bufnr or 0) }
end
--- Create the workspace params
----@param added
----@param removed
+---@param added table
+---@param removed table
function M.make_workspace_params(added, removed)
return { event = { added = added, removed = removed } }
end
+
--- Returns indentation size.
---
---@see 'shiftwidth'
----@param bufnr (number|nil): Buffer handle, defaults to current
----@returns (number) indentation size
+---@param bufnr (integer|nil): Buffer handle, defaults to current
+---@return (integer) indentation size
function M.get_effective_tabstop(bufnr)
validate({ bufnr = { bufnr, 'n', true } })
local bo = bufnr and vim.bo[bufnr] or vim.bo
@@ -2054,7 +2089,7 @@ end
--- Creates a `DocumentFormattingParams` object for the current buffer and cursor position.
---
---@param options table|nil with valid `FormattingOptions` entries
----@returns `DocumentFormattingParams` object
+---@return `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 } })
@@ -2070,11 +2105,11 @@ end
--- Returns the UTF-32 and UTF-16 offsets for a position in a certain buffer.
---
----@param buf number buffer number (0 for current)
+---@param buf integer buffer number (0 for current)
---@param row 0-indexed line
---@param col 0-indexed byte offset in line
----@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of `buf`
----@returns (number, number) `offset_encoding` index of the character in line {row} column {col} in buffer {buf}
+---@param offset_encoding string 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)
if offset_encoding == nil then
@@ -2082,6 +2117,7 @@ function M.character_offset(buf, row, col, offset_encoding)
'character_offset must be called with valid offset encoding',
vim.log.levels.WARN
)
+ offset_encoding = vim.lsp.get_clients({ bufnr = buf })[1].offset_encoding
end
-- If the col is past the EOL, use the line length.
if col > #line then
@@ -2092,11 +2128,11 @@ end
--- Helper function to return nested values in language server settings
---
----@param settings a table of language server settings
----@param section a string indicating the field of the settings table
----@returns (table or string) The value of settings accessed via section
+---@param settings table language server settings
+---@param section string indicating the field of the settings table
+---@return table|string The value of settings accessed via section
function M.lookup_section(settings, section)
- for part in vim.gsplit(section, '.', true) do
+ for part in vim.gsplit(section, '.', { plain = true }) do
settings = settings[part]
if settings == nil then
return vim.NIL
@@ -2105,9 +2141,93 @@ function M.lookup_section(settings, section)
return settings
end
+--- Converts line range (0-based, end-inclusive) to lsp range,
+--- handles absence of a trailing newline
+---
+---@param bufnr integer
+---@param start_line integer
+---@param end_line integer
+---@param offset_encoding lsp.PositionEncodingKind
+---@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
+
+ ---@type lsp.Position
+ local end_pos
+
+ if end_line == last_line and not vim.api.nvim_get_option_value('endofline', { buf = bufnr }) then
+ end_pos = {
+ line = end_line,
+ character = M.character_offset(bufnr, end_line, #get_line(bufnr, end_line), offset_encoding),
+ }
+ else
+ end_pos = { line = end_line + 1, character = 0 }
+ end
+
+ return {
+ start = { line = start_line, character = 0 },
+ ['end'] = end_pos,
+ }
+end
+
+---@private
+--- Request updated LSP information for a buffer.
+---
+---@class lsp.util.RefreshOptions
+---@field bufnr integer? Buffer to refresh (default: 0)
+---@field only_visible? boolean Whether to only refresh for the visible regions of the buffer (default: false)
+---@field client_id? integer Client ID to refresh (default: all clients)
+--
+---@param method string LSP method to call
+---@param opts? lsp.util.RefreshOptions Options table
+function M._refresh(method, opts)
+ opts = opts or {}
+ local bufnr = opts.bufnr
+ if bufnr == nil or bufnr == 0 then
+ bufnr = api.nvim_get_current_buf()
+ end
+
+ local clients = vim.lsp.get_clients({ bufnr = bufnr, method = method, id = opts.client_id })
+
+ if #clients == 0 then
+ return
+ end
+
+ local textDocument = M.make_text_document_params(bufnr)
+
+ local only_visible = opts.only_visible or false
+
+ if 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)
+ local last = vim.fn.line('w$', window)
+ for _, client in ipairs(clients) do
+ client.request(method, {
+ textDocument = textDocument,
+ range = make_line_range_params(bufnr, first - 1, last - 1, client.offset_encoding),
+ }, nil, bufnr)
+ end
+ end
+ end
+ else
+ for _, client in ipairs(clients) do
+ client.request(method, {
+ textDocument = textDocument,
+ range = make_line_range_params(
+ bufnr,
+ 0,
+ api.nvim_buf_line_count(bufnr) - 1,
+ client.offset_encoding
+ ),
+ }, nil, bufnr)
+ end
+ end
+end
+
M._get_line_byte_from_position = get_line_byte_from_position
+---@nodoc
M.buf_versions = {}
return M
--- vim:sw=2 ts=2 et