aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/lsp
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua/vim/lsp')
-rw-r--r--runtime/lua/vim/lsp/buf.lua31
-rw-r--r--runtime/lua/vim/lsp/diagnostic.lua27
-rw-r--r--runtime/lua/vim/lsp/handlers.lua3
-rw-r--r--runtime/lua/vim/lsp/log.lua1
-rw-r--r--runtime/lua/vim/lsp/rpc.lua13
-rw-r--r--runtime/lua/vim/lsp/sync.lua79
-rw-r--r--runtime/lua/vim/lsp/tagfunc.lua76
-rw-r--r--runtime/lua/vim/lsp/util.lua258
8 files changed, 280 insertions, 208 deletions
diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua
index 747d761730..f02ebfb9dc 100644
--- a/runtime/lua/vim/lsp/buf.lua
+++ b/runtime/lua/vim/lsp/buf.lua
@@ -165,7 +165,7 @@ end
--- saved. {timeout_ms} is passed on to |vim.lsp.buf_request_sync()|. Example:
---
--- <pre>
---- vim.api.nvim_command[[autocmd BufWritePre <buffer> lua vim.lsp.buf.formatting_sync()]]
+--- autocmd BufWritePre <buffer> lua vim.lsp.buf.formatting_sync()
--- </pre>
---
---@param options Table with valid `FormattingOptions` entries
@@ -176,9 +176,10 @@ function M.formatting_sync(options, timeout_ms)
if client == nil then return end
local params = util.make_formatting_params(options)
- local result, err = client.request_sync("textDocument/formatting", params, timeout_ms, vim.api.nvim_get_current_buf())
+ local bufnr = vim.api.nvim_get_current_buf()
+ local result, err = client.request_sync("textDocument/formatting", params, timeout_ms, bufnr)
if result and result.result then
- util.apply_text_edits(result.result)
+ util.apply_text_edits(result.result, bufnr)
elseif err then
vim.notify("vim.lsp.buf.formatting_sync: " .. err, vim.log.levels.WARN)
end
@@ -202,6 +203,7 @@ end
---the remaining clients in the order as they occur in the `order` list.
function M.formatting_seq_sync(options, timeout_ms, order)
local clients = vim.tbl_values(vim.lsp.buf_get_clients());
+ local bufnr = vim.api.nvim_get_current_buf()
-- sort the clients according to `order`
for _, client_name in pairs(order or {}) do
@@ -220,7 +222,7 @@ function M.formatting_seq_sync(options, timeout_ms, order)
local params = util.make_formatting_params(options)
local result, err = client.request_sync("textDocument/formatting", params, timeout_ms, vim.api.nvim_get_current_buf())
if result and result.result then
- util.apply_text_edits(result.result)
+ util.apply_text_edits(result.result, bufnr)
elseif err then
vim.notify(string.format("vim.lsp.buf.formatting_seq_sync: (%s) %s", client.name, err), vim.log.levels.WARN)
end
@@ -261,6 +263,7 @@ function M.rename(new_name)
request('textDocument/rename', params)
end
+ ---@private
local function prepare_rename(err, result)
if err == nil and result == nil then
vim.notify('nothing to rename', vim.log.levels.INFO)
@@ -369,7 +372,7 @@ end
function M.list_workspace_folders()
local workspace_folders = {}
for _, client in pairs(vim.lsp.buf_get_clients()) do
- for _, folder in pairs(client.workspaceFolders or {}) do
+ for _, folder in pairs(client.workspace_folders or {}) do
table.insert(workspace_folders, folder.name)
end
end
@@ -389,7 +392,7 @@ function M.add_workspace_folder(workspace_folder)
local params = util.make_workspace_params({{uri = vim.uri_from_fname(workspace_folder); name = workspace_folder}}, {{}})
for _, client in pairs(vim.lsp.buf_get_clients()) do
local found = false
- for _, folder in pairs(client.workspaceFolders or {}) do
+ for _, folder in pairs(client.workspace_folders or {}) do
if folder.name == workspace_folder then
found = true
print(workspace_folder, "is already part of this workspace")
@@ -398,10 +401,10 @@ function M.add_workspace_folder(workspace_folder)
end
if not found then
vim.lsp.buf_notify(0, 'workspace/didChangeWorkspaceFolders', params)
- if not client.workspaceFolders then
- client.workspaceFolders = {}
+ if not client.workspace_folders then
+ client.workspace_folders = {}
end
- table.insert(client.workspaceFolders, params.event.added[1])
+ table.insert(client.workspace_folders, params.event.added[1])
end
end
end
@@ -415,10 +418,10 @@ function M.remove_workspace_folder(workspace_folder)
if not (workspace_folder and #workspace_folder > 0) then return end
local params = util.make_workspace_params({{}}, {{uri = vim.uri_from_fname(workspace_folder); name = workspace_folder}})
for _, client in pairs(vim.lsp.buf_get_clients()) do
- for idx, folder in pairs(client.workspaceFolders) do
+ for idx, folder in pairs(client.workspace_folders) do
if folder.name == workspace_folder then
vim.lsp.buf_notify(0, 'workspace/didChangeWorkspaceFolders', params)
- client.workspaceFolders[idx] = nil
+ client.workspace_folders[idx] = nil
return
end
end
@@ -444,9 +447,9 @@ end
--- by events such as `CursorHold`, eg:
---
--- <pre>
---- vim.api.nvim_command [[autocmd CursorHold <buffer> lua vim.lsp.buf.document_highlight()]]
---- vim.api.nvim_command [[autocmd CursorHoldI <buffer> lua vim.lsp.buf.document_highlight()]]
---- vim.api.nvim_command [[autocmd CursorMoved <buffer> lua vim.lsp.buf.clear_references()]]
+--- autocmd CursorHold <buffer> lua vim.lsp.buf.document_highlight()
+--- autocmd CursorHoldI <buffer> lua vim.lsp.buf.document_highlight()
+--- autocmd CursorMoved <buffer> lua vim.lsp.buf.clear_references()
--- </pre>
---
--- Note: Usage of |vim.lsp.buf.document_highlight()| requires the following highlight groups
diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua
index 1e6f83c1ba..075da41b23 100644
--- a/runtime/lua/vim/lsp/diagnostic.lua
+++ b/runtime/lua/vim/lsp/diagnostic.lua
@@ -220,12 +220,9 @@ function M.on_publish_diagnostics(_, result, ctx, config)
end
vim.diagnostic.set(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id))
-
- -- Keep old autocmd for back compat. This should eventually be removed.
- vim.api.nvim_command("doautocmd <nomodeline> User LspDiagnosticsChanged")
end
---- Clear diagnotics and diagnostic cache.
+--- Clear diagnostics and diagnostic cache.
---
--- Diagnostic producers should prefer |vim.diagnostic.reset()|. However,
--- this method signature is still used internally in some parts of the LSP
@@ -434,24 +431,6 @@ end
--- Move to the next diagnostic
---
---@deprecated Prefer |vim.diagnostic.goto_next()|
----
----@param opts table|nil Configuration table. Keys:
---- - {client_id}: (number)
---- - If nil, will consider all clients attached to buffer.
---- - {cursor_position}: (Position, default current position)
---- - See |nvim_win_get_cursor()|
---- - {wrap}: (boolean, default true)
---- - Whether to loop around file or not. Similar to 'wrapscan'
---- - {severity}: (DiagnosticSeverity)
---- - Exclusive severity to consider. Overrides {severity_limit}
---- - {severity_limit}: (DiagnosticSeverity)
---- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
---- - {enable_popup}: (boolean, default true)
---- - Call |vim.lsp.diagnostic.show_line_diagnostics()| on jump
---- - {popup_opts}: (table)
---- - Table to pass as {opts} parameter to |vim.lsp.diagnostic.show_line_diagnostics()|
---- - {win_id}: (number, default 0)
---- - Window ID
function M.goto_next(opts)
if opts then
if opts.severity then
@@ -565,7 +544,7 @@ end
--- Open a floating window with the diagnostics from {line_nr}
---
----@deprecated Prefer |vim.diagnostic.show_line_diagnostics()|
+---@deprecated Prefer |vim.diagnostic.open_float()|
---
---@param opts table Configuration table
--- - all opts for |vim.lsp.diagnostic.get_line_diagnostics()| and
@@ -717,5 +696,3 @@ end
-- }}}
return M
-
--- vim: fdm=marker
diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua
index fa4f2b22a5..22089aaaa6 100644
--- a/runtime/lua/vim/lsp/handlers.lua
+++ b/runtime/lua/vim/lsp/handlers.lua
@@ -28,7 +28,7 @@ local function progress_handler(_, result, ctx, _)
local client_name = client and client.name or string.format("id=%d", client_id)
if not client then
err_message("LSP[", client_name, "] client has shut down after sending the message")
- return
+ return vim.NIL
end
local val = result.value -- unspecified yet
local token = result.token -- string or number
@@ -70,6 +70,7 @@ M['window/workDoneProgress/create'] = function(_, result, ctx)
local client_name = client and client.name or string.format("id=%d", client_id)
if not client then
err_message("LSP[", client_name, "] client has shut down after sending the message")
+ return vim.NIL
end
client.messages.progress[token] = {}
return vim.NIL
diff --git a/runtime/lua/vim/lsp/log.lua b/runtime/lua/vim/lsp/log.lua
index 4597f1919a..dbc473b52c 100644
--- a/runtime/lua/vim/lsp/log.lua
+++ b/runtime/lua/vim/lsp/log.lua
@@ -101,6 +101,7 @@ function log.set_level(level)
end
--- Gets the current log level.
+---@return string current log level
function log.get_level()
return current_log_level
end
diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua
index bce1e9f35d..1fb75ddeb7 100644
--- a/runtime/lua/vim/lsp/rpc.lua
+++ b/runtime/lua/vim/lsp/rpc.lua
@@ -230,7 +230,7 @@ function default_dispatchers.on_error(code, err)
end
--- Starts an LSP server process and create an LSP RPC client object to
---- interact with it.
+--- interact with it. Communication with the server is currently limited to stdio.
---
---@param cmd (string) Command to start the LSP server.
---@param cmd_args (table) List of additional string arguments to pass to {cmd}.
@@ -264,8 +264,6 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
if extra_spawn_params and extra_spawn_params.cwd then
assert(is_dir(extra_spawn_params.cwd), "cwd must be a directory")
- elseif not (vim.fn.executable(cmd) == 1) then
- error(string.format("The given command %q is not executable.", cmd))
end
if dispatchers then
local user_dispatchers = dispatchers
@@ -325,7 +323,14 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
end
handle, pid = uv.spawn(cmd, spawn_params, onexit)
if handle == nil then
- error(string.format("start `%s` failed: %s", cmd, pid))
+ local msg = string.format("Spawning language server with cmd: `%s` failed", cmd)
+ if string.match(pid, "ENOENT") then
+ msg = msg .. ". The language server is either not installed, missing from PATH, or not executable."
+ else
+ msg = msg .. string.format(" with error message: %s", pid)
+ end
+ vim.notify(msg, vim.log.levels.WARN)
+ return
end
end
diff --git a/runtime/lua/vim/lsp/sync.lua b/runtime/lua/vim/lsp/sync.lua
index 585c14a00f..5df2a4d144 100644
--- a/runtime/lua/vim/lsp/sync.lua
+++ b/runtime/lua/vim/lsp/sync.lua
@@ -75,42 +75,45 @@ local function byte_to_utf(line, byte, offset_encoding)
end
---@private
+local function compute_line_length(line, offset_encoding)
+ local length
+ local _
+ if offset_encoding == 'utf-16' then
+ _, length = str_utfindex(line)
+ elseif offset_encoding == 'utf-32' then
+ length, _ = str_utfindex(line)
+ else
+ length = #line
+ end
+ return length
+end
+
+---@private
-- Given a line, byte idx, alignment, and offset_encoding convert to the aligned
-- utf-8 index and either the utf-16, or utf-32 index.
---@param line string the line to index into
---@param byte integer the byte idx
----@param align string when dealing with multibyte characters,
--- to choose the start of the current character or the beginning of the next.
--- Used for incremental sync for start/end range respectively
---@param offset_encoding string utf-8|utf-16|utf-32|nil (default: utf-8)
---@returns table<string, int> byte_idx and char_idx of first change position
-local function align_position(line, byte, align, offset_encoding)
+local function align_end_position(line, byte, offset_encoding)
local char
-- If on the first byte, or an empty string: the trivial case
if byte == 1 or #line == 0 then
char = byte
-- Called in the case of extending an empty line "" -> "a"
elseif byte == #line + 1 then
- byte = byte + str_utf_end(line, #line)
- char = byte_to_utf(line, byte, offset_encoding)
+ char = compute_line_length(line, offset_encoding) + 1
else
-- Modifying line, find the nearest utf codepoint
- if align == 'start' then
- byte = byte + str_utf_start(line, byte)
- char = byte_to_utf(line, byte, offset_encoding)
- elseif align == 'end' then
- local offset = str_utf_end(line, byte)
- -- If the byte does not fall on the start of the character, then
- -- align to the start of the next character.
- if offset > 0 then
- char = byte_to_utf(line, byte, offset_encoding) + 1
- byte = byte + offset
- else
- char = byte_to_utf(line, byte, offset_encoding)
- byte = byte + offset
- end
+ local offset = str_utf_end(line, byte)
+ -- If the byte does not fall on the start of the character, then
+ -- align to the start of the next character.
+ if offset > 0 then
+ char = byte_to_utf(line, byte, offset_encoding) + 1
+ byte = byte + offset
else
- error('`align` must be start or end.')
+ char = byte_to_utf(line, byte, offset_encoding)
+ byte = byte + offset
end
-- Extending line, find the nearest utf codepoint for the last valid character
end
@@ -131,7 +134,7 @@ local function compute_start_range(prev_lines, curr_lines, firstline, lastline,
-- occur on a new line pointed to by lastline. This occurs during insertion of
-- new lines(O), the new newline is inserted at the line indicated by
-- new_lastline.
- -- If firstline == new_lastline, the first change occured on a line that was deleted.
+ -- If firstline == new_lastline, the first change occurred on a line that was deleted.
-- In this case, the first byte change is also at the first byte of firstline
if firstline == new_lastline or firstline == lastline then
return { line_idx = firstline, byte_idx = 1, char_idx = 1 }
@@ -154,7 +157,18 @@ local function compute_start_range(prev_lines, curr_lines, firstline, lastline,
end
-- Convert byte to codepoint if applicable
- local byte_idx, char_idx = align_position(prev_line, start_byte_idx, 'start', offset_encoding)
+ local char_idx
+ local byte_idx
+ if start_byte_idx == 1 or (#prev_line == 0 and start_byte_idx == 1)then
+ byte_idx = start_byte_idx
+ char_idx = 1
+ elseif start_byte_idx == #prev_line + 1 then
+ byte_idx = start_byte_idx
+ char_idx = compute_line_length(prev_line, offset_encoding) + 1
+ else
+ byte_idx = start_byte_idx + str_utf_start(prev_line, start_byte_idx)
+ char_idx = byte_to_utf(prev_line, start_byte_idx, offset_encoding)
+ end
-- Return the start difference (shared for new and prev lines)
return { line_idx = firstline, byte_idx = byte_idx, char_idx = char_idx }
@@ -174,7 +188,7 @@ end
---@param offset_encoding string
---@returns (int, int) end_line_idx and end_col_idx of range
local function compute_end_range(prev_lines, curr_lines, start_range, firstline, lastline, new_lastline, offset_encoding)
- -- If firstline == new_lastline, the first change occured on a line that was deleted.
+ -- If firstline == new_lastline, the first change occurred on a line that was deleted.
-- In this case, the last_byte...
if firstline == new_lastline then
return { line_idx = (lastline - new_lastline + firstline), byte_idx = 1, char_idx = 1 }, { line_idx = firstline, byte_idx = 1, char_idx = 1 }
@@ -219,11 +233,12 @@ local function compute_end_range(prev_lines, curr_lines, start_range, firstline,
-- Iterate from end to beginning of shortest line
local prev_end_byte_idx = prev_line_length - byte_offset + 1
+
-- Handle case where lines match
if prev_end_byte_idx == 0 then
prev_end_byte_idx = 1
end
- local prev_byte_idx, prev_char_idx = align_position(prev_line, prev_end_byte_idx, 'start', offset_encoding)
+ local prev_byte_idx, prev_char_idx = align_end_position(prev_line, prev_end_byte_idx, offset_encoding)
local prev_end_range = { line_idx = prev_line_idx, byte_idx = prev_byte_idx, char_idx = prev_char_idx }
local curr_end_range
@@ -236,7 +251,7 @@ local function compute_end_range(prev_lines, curr_lines, start_range, firstline,
if curr_end_byte_idx == 0 then
curr_end_byte_idx = 1
end
- local curr_byte_idx, curr_char_idx = align_position(curr_line, curr_end_byte_idx, 'start', offset_encoding)
+ local curr_byte_idx, curr_char_idx = align_end_position(curr_line, curr_end_byte_idx, offset_encoding)
curr_end_range = { line_idx = curr_line_idx, byte_idx = curr_byte_idx, char_idx = curr_char_idx }
end
@@ -280,18 +295,6 @@ local function extract_text(lines, start_range, end_range, line_ending)
end
end
-local function compute_line_length(line, offset_encoding)
- local length
- local _
- if offset_encoding == 'utf-16' then
- _, length = str_utfindex(line)
- elseif offset_encoding == 'utf-32' then
- length, _ = str_utfindex(line)
- else
- length = #line
- end
- return length
-end
---@private
-- rangelength depends on the offset encoding
-- bytes for utf-8 (clangd with extenion)
diff --git a/runtime/lua/vim/lsp/tagfunc.lua b/runtime/lua/vim/lsp/tagfunc.lua
new file mode 100644
index 0000000000..5c55e8559f
--- /dev/null
+++ b/runtime/lua/vim/lsp/tagfunc.lua
@@ -0,0 +1,76 @@
+local lsp = vim.lsp
+local util = vim.lsp.util
+
+---@private
+local function mk_tag_item(name, range, uri, offset_encoding)
+ local bufnr = vim.uri_to_bufnr(uri)
+ -- This is get_line_byte_from_position is 0-indexed, call cursor expects a 1-indexed position
+ local byte = util._get_line_byte_from_position(bufnr, range.start, offset_encoding) + 1
+ return {
+ name = name,
+ filename = vim.uri_to_fname(uri),
+ cmd = string.format('call cursor(%d, %d)|', range.start.line + 1, byte),
+ }
+end
+
+---@private
+local function query_definition(pattern)
+ local params = lsp.util.make_position_params()
+ local results_by_client, err = lsp.buf_request_sync(0, 'textDocument/definition', params, 1000)
+ if err then
+ return {}
+ end
+ local results = {}
+ 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(results_by_client) do
+ local client = lsp.get_client_by_id(client_id)
+ local result = lsp_results.result or {}
+ if result.range then -- Location
+ add(result.range, result.uri)
+ else -- Location[] or LocationLink[]
+ for _, item in pairs(result) do
+ if item.range then -- Location
+ add(item.range, item.uri, client.offset_encoding)
+ else -- LocationLink
+ add(item.targetSelectionRange, item.targetUri, client.offset_encoding)
+ end
+ end
+ end
+ end
+ return results
+end
+
+---@private
+local function query_workspace_symbols(pattern)
+ local results_by_client, err = lsp.buf_request_sync(0, 'workspace/symbol', { query = pattern }, 1000)
+ if err then
+ return {}
+ end
+ local results = {}
+ for client_id, symbols in pairs(results_by_client) do
+ local client = lsp.get_client_by_id(client_id)
+ for _, symbol in pairs(symbols.result or {}) do
+ local loc = symbol.location
+ local item = mk_tag_item(symbol.name, loc.range, loc.uri, client.offset_encoding)
+ item.kind = lsp.protocol.SymbolKind[symbol.kind] or 'Unknown'
+ table.insert(results, item)
+ end
+ end
+ return results
+end
+
+---@private
+local function tagfunc(pattern, flags)
+ local matches
+ if string.match(flags, 'c') then
+ matches = query_definition(pattern)
+ else
+ matches = query_workspace_symbols(pattern)
+ end
+ -- fall back to tags if no matches
+ return #matches > 0 and matches or vim.NIL
+end
+
+return tagfunc
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index ba5c20ef9f..fad2b962b5 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -148,6 +148,93 @@ local function sort_by_key(fn)
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
+local function get_lines(bufnr, rows)
+ rows = type(rows) == "table" and rows or { rows }
+
+ ---@private
+ local function buf_lines()
+ local lines = {}
+ for _, row in pairs(rows) do
+ lines[row] = (vim.api.nvim_buf_get_lines(bufnr, row, row + 1, false) or { "" })[1]
+ end
+ return lines
+ end
+
+ local uri = vim.uri_from_bufnr(bufnr)
+
+ -- load the buffer if this is not a file uri
+ -- Custom language server protocol extensions can result in servers sending URIs with custom schemes. Plugins are able to load these via `BufReadCmd` autocmds.
+ if uri:sub(1, 4) ~= "file" then
+ vim.fn.bufload(bufnr)
+ 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
+ local fd = uv.fs_open(filename, "r", 438)
+ if not fd then return "" end
+ local stat = uv.fs_fstat(fd)
+ local data = uv.fs_read(fd, stat.size, 0)
+ uv.fs_close(fd)
+
+ local lines = {} -- rows we need to retrieve
+ local need = 0 -- keep track of how many unique rows we need
+ for _, row in pairs(rows) do
+ if not lines[row] then
+ need = need + 1
+ end
+ lines[row] = true
+ end
+
+ local found = 0
+ local lnum = 0
+
+ for line in string.gmatch(data, "([^\n]*)\n?") do
+ if lines[lnum] == true then
+ lines[lnum] = line
+ found = found + 1
+ if found == need then break end
+ end
+ lnum = lnum + 1
+ end
+
+ -- change any lines we didn't find to the empty string
+ for i, line in pairs(lines) do
+ if line == true then
+ lines[i] = ""
+ end
+ end
+ return lines
+end
+
+
+---@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
+---@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
--- 1-indexed
@@ -158,31 +245,25 @@ local function get_line_byte_from_position(bufnr, position, offset_encoding)
-- When on the first character, we can ignore the difference between byte and
-- character
if col > 0 then
- if not api.nvim_buf_is_loaded(bufnr) then
- vim.fn.bufload(bufnr)
- end
+ local line = get_line(bufnr, position.line)
+ local ok, result
- local line = position.line
- local lines = api.nvim_buf_get_lines(bufnr, line, line + 1, false)
- if #lines > 0 then
- local ok, result
-
- if offset_encoding == "utf-16" or not offset_encoding then
- ok, result = pcall(vim.str_byteindex, lines[1], col, true)
- elseif offset_encoding == "utf-32" then
- ok, result = pcall(vim.str_byteindex, lines[1], col, false)
- end
+ if offset_encoding == "utf-16" or not offset_encoding then
+ ok, result = pcall(vim.str_byteindex, line, col, true)
+ elseif offset_encoding == "utf-32" then
+ ok, result = pcall(vim.str_byteindex, line, col, false)
+ end
- if ok then
- return result
- end
- return math.min(#lines[1], col)
+ if ok then
+ return result
end
+ return math.min(#line, col)
end
return col
end
--- Process and return progress reports from lsp server
+---@private
function M.get_progress_messages()
local new_messages = {}
@@ -246,6 +327,10 @@ end
---@param bufnr number Buffer id
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textEdit
function M.apply_text_edits(text_edits, bufnr)
+ validate {
+ text_edits = { text_edits, 't', false };
+ bufnr = { bufnr, 'number', false };
+ }
if not next(text_edits) then return end
if not api.nvim_buf_is_loaded(bufnr) then
vim.fn.bufload(bufnr)
@@ -474,7 +559,7 @@ local function remove_unmatch_completion_items(items, prefix)
end, items)
end
---- Acording to LSP spec, if the client set `completionItemKind.valueSet`,
+--- 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.
---
@@ -574,7 +659,7 @@ function M.rename(old_fname, new_fname, opts)
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`
@@ -586,7 +671,7 @@ 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)
@@ -1054,7 +1139,7 @@ function M.stylize_markdown(bufnr, contents, opts)
markdown_lines[#stripped] = true
end
else
- -- strip any emty lines or separators prior to this separator in actual markdown
+ -- 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
@@ -1087,7 +1172,7 @@ function M.stylize_markdown(bufnr, contents, opts)
local idx = 1
---@private
- -- keep track of syntaxes we already inlcuded.
+ -- keep track of syntaxes we already included.
-- no need to include the same syntax more than once
local langs = {}
local fences = get_markdown_fences()
@@ -1223,18 +1308,21 @@ end
---
---@param contents table of lines to show in window
---@param syntax string of syntax to set for opened buffer
----@param opts dictionary with optional fields
---- - height of floating window
---- - width of floating window
---- - wrap boolean enable wrapping of long lines (defaults to true)
---- - wrap_at character to wrap at for computing height when wrap is enabled
---- - max_width maximal width of floating window
---- - max_height maximal height of floating window
---- - pad_top number of lines to pad contents at top
---- - pad_bottom number of lines to pad contents at bottom
---- - focus_id if a popup with this id is opened, then focus it
---- - close_events list of events that closes the floating window
---- - focusable (boolean, default true): Make float focusable
+---@param opts table with optional fields (additional keys are passed on to |vim.api.nvim_open_win()|)
+--- - height: (number) height of floating window
+--- - width: (number) width of floating window
+--- - wrap: (boolean, default true) wrap long lines
+--- - wrap_at: (string) 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
+--- - 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
function M.open_floating_preview(contents, syntax, opts)
@@ -1246,12 +1334,13 @@ 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
+ opts.focus = opts.focus ~= false
opts.close_events = opts.close_events or {"CursorMoved", "CursorMovedI", "BufHidden", "InsertCharPre"}
local bufnr = api.nvim_get_current_buf()
-- check if this popup is focusable and we need to focus
- if opts.focus_id and opts.focusable ~= false then
+ 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
@@ -1314,7 +1403,7 @@ function M.open_floating_preview(contents, syntax, opts)
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", "q", "<cmd>bdelete<cr>", {silent = true, noremap = true})
+ api.nvim_buf_set_keymap(floating_bufnr, "n", "q", "<cmd>bdelete<cr>", {silent = true, noremap = true, nowait = true})
M.close_preview_autocmd(opts.close_events, floating_winnr)
-- save focus_id
@@ -1331,7 +1420,7 @@ do --[[ References ]]
--- Removes document highlights from a buffer.
---
- ---@param bufnr buffer id
+ ---@param bufnr number Buffer id
function M.buf_clear_references(bufnr)
validate { bufnr = {bufnr, 'n', true} }
api.nvim_buf_clear_namespace(bufnr, reference_ns, 0, -1)
@@ -1339,9 +1428,9 @@ do --[[ References ]]
--- Shows a list of document highlights for a certain buffer.
---
- ---@param bufnr buffer id
- ---@param references List of `DocumentHighlight` objects to highlight
- ---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to utf-16
+ ---@param bufnr number Buffer id
+ ---@param references table List of `DocumentHighlight` objects to highlight
+ ---@param offset_encoding string One of "utf-8", "utf-16", "utf-32", or nil. Defaults to utf-16
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#documentHighlight
function M.buf_highlight_references(bufnr, references, offset_encoding)
validate { bufnr = {bufnr, 'n', true} }
@@ -1372,88 +1461,6 @@ local position_sort = sort_by_key(function(v)
return {v.start.line, v.start.character}
end)
---- Gets the zero-indexed line from the given uri.
----@param uri string uri of the resource to get the line from
----@param row number zero-indexed line number
----@return string the line at row in filename
--- For non-file uris, we load the buffer and get the line.
--- If a loaded buffer exists, then that is used.
--- Otherwise we get the line using libuv which is a lot faster than loading the buffer.
-function M.get_line(uri, row)
- return M.get_lines(uri, { row })[row]
-end
-
---- Gets the zero-indexed lines from the given uri.
----@param uri string uri of the resource to get the lines from
----@param rows number[] zero-indexed line numbers
----@return table<number string> a table mapping rows to lines
--- For non-file uris, we load the buffer and get the lines.
--- If a loaded buffer exists, then that is used.
--- Otherwise we get the lines using libuv which is a lot faster than loading the buffer.
-function M.get_lines(uri, rows)
- rows = type(rows) == "table" and rows or { rows }
-
- local function buf_lines(bufnr)
- local lines = {}
- for _, row in pairs(rows) do
- lines[row] = (vim.api.nvim_buf_get_lines(bufnr, row, row + 1, false) or { "" })[1]
- end
- return lines
- end
-
- -- load the buffer if this is not a file uri
- -- Custom language server protocol extensions can result in servers sending URIs with custom schemes. Plugins are able to load these via `BufReadCmd` autocmds.
- if uri:sub(1, 4) ~= "file" then
- local bufnr = vim.uri_to_bufnr(uri)
- vim.fn.bufload(bufnr)
- return buf_lines(bufnr)
- end
-
- local filename = vim.uri_to_fname(uri)
-
- -- use loaded buffers if available
- if vim.fn.bufloaded(filename) == 1 then
- local bufnr = vim.fn.bufnr(filename, false)
- return buf_lines(bufnr)
- end
-
- -- get the data from the file
- local fd = uv.fs_open(filename, "r", 438)
- if not fd then return "" end
- local stat = uv.fs_fstat(fd)
- local data = uv.fs_read(fd, stat.size, 0)
- uv.fs_close(fd)
-
- local lines = {} -- rows we need to retrieve
- local need = 0 -- keep track of how many unique rows we need
- for _, row in pairs(rows) do
- if not lines[row] then
- need = need + 1
- end
- lines[row] = true
- end
-
- local found = 0
- local lnum = 0
-
- for line in string.gmatch(data, "([^\n]*)\n?") do
- if lines[lnum] == true then
- lines[lnum] = line
- found = found + 1
- if found == need then break end
- end
- lnum = lnum + 1
- end
-
- -- change any lines we didn't find to the empty string
- for i, line in pairs(lines) do
- if line == true then
- lines[i] = ""
- end
- end
- return lines
-end
-
--- Returns the items with the byte position calculated correctly and in sorted
--- order, for display in quickfix and location lists.
---
@@ -1496,7 +1503,7 @@ function M.locations_to_items(locations)
end
-- get all the lines for this uri
- local lines = M.get_lines(uri, uri_rows)
+ local lines = get_lines(vim.uri_to_bufnr(uri), uri_rows)
for _, temp in ipairs(rows) do
local pos = temp.start
@@ -1541,7 +1548,7 @@ function M.set_qflist(items)
})
end
--- Acording to LSP spec, if the client set "symbolKind.valueSet",
+-- 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)
@@ -1773,8 +1780,7 @@ end
---@param col 0-indexed byte offset in line
---@returns (number, number) UTF-32 and UTF-16 index of the character in line {row} column {col} in buffer {buf}
function M.character_offset(bufnr, row, col)
- local uri = vim.uri_from_bufnr(bufnr)
- local line = M.get_line(uri, row)
+ local line = get_line(bufnr, row)
-- If the col is past the EOL, use the line length.
if col > #line then
return str_utfindex(line)