aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua')
-rw-r--r--runtime/lua/vim/filetype.lua27
-rw-r--r--runtime/lua/vim/filetype/detect.lua15
-rw-r--r--runtime/lua/vim/keymap.lua19
-rw-r--r--runtime/lua/vim/lsp.lua460
-rw-r--r--runtime/lua/vim/lsp/buf.lua14
-rw-r--r--runtime/lua/vim/lsp/rpc.lua25
-rw-r--r--runtime/lua/vim/lsp/util.lua2
-rw-r--r--runtime/lua/vim/shared.lua2
8 files changed, 368 insertions, 196 deletions
diff --git a/runtime/lua/vim/filetype.lua b/runtime/lua/vim/filetype.lua
index 1b209e6a9d..99c98764dd 100644
--- a/runtime/lua/vim/filetype.lua
+++ b/runtime/lua/vim/filetype.lua
@@ -142,6 +142,7 @@ local extension = {
asp = function(path, bufnr)
return require('vim.filetype.detect').asp(bufnr)
end,
+ astro = 'astro',
atl = 'atlas',
as = 'atlas',
ahk = 'autohotkey',
@@ -668,6 +669,21 @@ local extension = {
moo = 'moo',
moon = 'moonscript',
mp = 'mp',
+ mpiv = function(path, bufnr)
+ return 'mp', function(b)
+ vim.b[b].mp_metafun = 1
+ end
+ end,
+ mpvi = function(path, bufnr)
+ return 'mp', function(b)
+ vim.b[b].mp_metafun = 1
+ end
+ end,
+ mpxl = function(path, bufnr)
+ return 'mp', function(b)
+ vim.b[b].mp_metafun = 1
+ end
+ end,
mof = 'msidl',
odl = 'msidl',
msql = 'msql',
@@ -776,6 +792,7 @@ local extension = {
ptl = 'python',
ql = 'ql',
qll = 'ql',
+ qmd = 'quarto',
R = function(path, bufnr)
return require('vim.filetype.detect').r(bufnr)
end,
@@ -2472,7 +2489,15 @@ function M.match(args)
-- Finally, check file contents
if contents or bufnr then
- contents = contents or M.getlines(bufnr)
+ if contents == nil then
+ if api.nvim_buf_line_count(bufnr) > 101 then
+ -- only need first 100 and last line for current checks
+ contents = M.getlines(bufnr, 1, 100)
+ contents[#contents + 1] = M.getlines(bufnr, -1)
+ else
+ contents = M.getlines(bufnr)
+ end
+ end
-- If name is nil, catch any errors from the contents filetype detection function.
-- If the function tries to use the filename that is nil then it will fail,
-- but this enables checks which do not need a filename to still work.
diff --git a/runtime/lua/vim/filetype/detect.lua b/runtime/lua/vim/filetype/detect.lua
index 14f076717f..2be9dcff88 100644
--- a/runtime/lua/vim/filetype/detect.lua
+++ b/runtime/lua/vim/filetype/detect.lua
@@ -930,7 +930,7 @@ function M.progress_pascal(bufnr)
return 'progress'
end
--- Distinguish between "default" and Cproto prototype file.
+-- Distinguish between "default", Prolog and Cproto prototype file.
function M.proto(bufnr, default)
-- Cproto files have a comment in the first line and a function prototype in
-- the second line, it always ends in ";". Indent files may also have
@@ -940,7 +940,18 @@ function M.proto(bufnr, default)
if getlines(bufnr, 2):find('.;$') then
return 'cpp'
else
- return default
+ -- Recognize Prolog by specific text in the first non-empty line;
+ -- require a blank after the '%' because Perl uses "%list" and "%translate"
+ local line = nextnonblank(bufnr, 1)
+ if
+ line and line:find(':%-')
+ or matchregex(line, [[\c\<prolog\>]])
+ or findany(line, { '^%s*%%+%s', '^%s*%%+$', '^%s*/%*' })
+ then
+ return 'prolog'
+ else
+ return default
+ end
end
end
diff --git a/runtime/lua/vim/keymap.lua b/runtime/lua/vim/keymap.lua
index 7265beb56b..219de16b5c 100644
--- a/runtime/lua/vim/keymap.lua
+++ b/runtime/lua/vim/keymap.lua
@@ -36,14 +36,17 @@ local keymap = {}
---@param lhs string Left-hand side |{lhs}| of the mapping.
---@param rhs string|function Right-hand side |{rhs}| of the mapping. Can also be a Lua function.
--
----@param opts table A table of |:map-arguments| such as "silent". In addition to the options
---- listed in |nvim_set_keymap()|, this table also accepts the following keys:
---- - buffer: (number or boolean) Add a mapping to the given buffer. When "true"
---- or 0, use the current buffer.
---- - remap: (boolean) Make the mapping recursive. This is the
---- inverse of the "noremap" option from |nvim_set_keymap()|.
---- Default `false`.
---- - replace_keycodes: (boolean) defaults to true if "expr" is true.
+---@param opts table A table of |:map-arguments|.
+--- + Accepts options accepted by the {opts} parameter in |nvim_set_keymap()|,
+--- with the following notable differences:
+--- - replace_keycodes: Defaults to `true` if "expr" is `true`.
+--- - noremap: Always overridden with the inverse of "remap" (see below).
+--- + In addition to those options, the table accepts the following keys:
+--- - buffer: (number or boolean) Add a mapping to the given buffer.
+--- When `0` or `true`, use the current buffer.
+--- - remap: (boolean) Make the mapping recursive.
+--- This is the inverse of the "noremap" option from |nvim_set_keymap()|.
+--- Defaults to `false`.
---@see |nvim_set_keymap()|
function keymap.set(mode, lhs, rhs, opts)
vim.validate({
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua
index bf2201d9c8..fd64c1a495 100644
--- a/runtime/lua/vim/lsp.lua
+++ b/runtime/lua/vim/lsp.lua
@@ -338,54 +338,166 @@ end
local changetracking = {}
do
- --@private
- --- client_id → state
+ ---@private
+ ---
+ --- LSP has 3 different sync modes:
+ --- - None (Servers will read the files themselves when needed)
+ --- - Full (Client sends the full buffer content on updates)
+ --- - Incremental (Client sends only the changed parts)
+ ---
+ --- Changes are tracked per buffer.
+ --- A buffer can have multiple clients attached and each client needs to send the changes
+ --- To minimize the amount of changesets to compute, computation is grouped:
+ ---
+ --- None: One group for all clients
+ --- Full: One group for all clients
+ --- Incremental: One group per `offset_encoding`
---
- --- state
- --- use_incremental_sync: bool
- --- buffers: bufnr -> buffer_state
+ --- Sending changes can be debounced per buffer. To simplify the implementation the
+ --- smallest debounce interval is used and we don't group clients by different intervals.
---
- --- buffer_state
- --- pending_change?: function that the timer starts to trigger didChange
- --- pending_changes: table (uri -> list of pending changeset tables));
- --- Only set if incremental_sync is used
+ --- @class CTGroup
+ --- @field sync_kind number TextDocumentSyncKind, considers config.flags.allow_incremental_sync
+ --- @field offset_encoding "utf-8"|"utf-16"|"utf-32"
---
- --- timer?: uv_timer
- --- lines: table
- local state_by_client = {}
+ --- @class CTBufferState
+ --- @field name string name of the buffer
+ --- @field lines string[] snapshot of buffer lines from last didChange
+ --- @field lines_tmp string[]
+ --- @field pending_changes table[] List of debounced changes in incremental sync mode
+ --- @field timer nil|userdata uv_timer
+ --- @field last_flush nil|number uv.hrtime of the last flush/didChange-notification
+ --- @field needs_flush boolean true if buffer updates haven't been sent to clients/servers yet
+ --- @field refs number how many clients are using this group
+ ---
+ --- @class CTGroupState
+ --- @field buffers table<number, CTBufferState>
+ --- @field debounce number debounce duration in ms
+ --- @field clients table<number, table> clients using this state. {client_id, client}
---@private
- function changetracking.init(client, bufnr)
- local use_incremental_sync = (
- if_nil(client.config.flags.allow_incremental_sync, true)
- and vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'change')
- == protocol.TextDocumentSyncKind.Incremental
+ ---@param group CTGroup
+ ---@return string
+ local function group_key(group)
+ if group.sync_kind == protocol.TextDocumentSyncKind.Incremental then
+ return tostring(group.sync_kind) .. '\0' .. group.offset_encoding
+ end
+ return tostring(group.sync_kind)
+ end
+
+ ---@private
+ ---@type table<CTGroup, CTGroupState>
+ local state_by_group = setmetatable({}, {
+ __index = function(tbl, k)
+ return rawget(tbl, group_key(k))
+ end,
+ __newindex = function(tbl, k, v)
+ rawset(tbl, group_key(k), v)
+ end,
+ })
+
+ ---@private
+ ---@return CTGroup
+ local function get_group(client)
+ local allow_inc_sync = if_nil(client.config.flags.allow_incremental_sync, true)
+ local change_capability =
+ vim.tbl_get(client.server_capabilities or {}, 'textDocumentSync', 'change')
+ local sync_kind = change_capability or protocol.TextDocumentSyncKind.None
+ if not allow_inc_sync and change_capability == protocol.TextDocumentSyncKind.Incremental then
+ sync_kind = protocol.TextDocumentSyncKind.Full
+ end
+ return {
+ sync_kind = sync_kind,
+ offset_encoding = client.offset_encoding,
+ }
+ end
+
+ ---@private
+ ---@param state CTBufferState
+ local function incremental_changes(state, encoding, bufnr, firstline, lastline, new_lastline)
+ local prev_lines = state.lines
+ local curr_lines = state.lines_tmp
+
+ local changed_lines = nvim_buf_get_lines(bufnr, firstline, new_lastline, true)
+ for i = 1, firstline do
+ curr_lines[i] = prev_lines[i]
+ end
+ for i = firstline + 1, new_lastline do
+ curr_lines[i] = changed_lines[i - firstline]
+ end
+ for i = lastline + 1, #prev_lines do
+ curr_lines[i - lastline + new_lastline] = prev_lines[i]
+ end
+ if tbl_isempty(curr_lines) then
+ -- Can happen when deleting the entire contents of a buffer, see https://github.com/neovim/neovim/issues/16259.
+ curr_lines[1] = ''
+ end
+
+ local line_ending = buf_get_line_ending(bufnr)
+ local incremental_change = sync.compute_diff(
+ state.lines,
+ curr_lines,
+ firstline,
+ lastline,
+ new_lastline,
+ encoding,
+ line_ending
)
- local state = state_by_client[client.id]
- if not state then
+
+ -- Double-buffering of lines tables is used to reduce the load on the garbage collector.
+ -- At this point the prev_lines table is useless, but its internal storage has already been allocated,
+ -- so let's keep it around for the next didChange event, in which it will become the next
+ -- curr_lines table. Note that setting elements to nil doesn't actually deallocate slots in the
+ -- internal storage - it merely marks them as free, for the GC to deallocate them.
+ for i in ipairs(prev_lines) do
+ prev_lines[i] = nil
+ end
+ state.lines = curr_lines
+ state.lines_tmp = prev_lines
+
+ return incremental_change
+ end
+
+ ---@private
+ function changetracking.init(client, bufnr)
+ assert(client.offset_encoding, 'lsp client must have an offset_encoding')
+ local group = get_group(client)
+ local state = state_by_group[group]
+ if state then
+ state.debounce = math.min(state.debounce, client.config.flags.debounce_text_changes or 150)
+ state.clients[client.id] = client
+ else
state = {
buffers = {},
debounce = client.config.flags.debounce_text_changes or 150,
- use_incremental_sync = use_incremental_sync,
+ clients = {
+ [client.id] = client,
+ },
}
- state_by_client[client.id] = state
+ state_by_group[group] = state
end
- if not state.buffers[bufnr] then
- local buf_state = {
+ local buf_state = state.buffers[bufnr]
+ if buf_state then
+ buf_state.refs = buf_state.refs + 1
+ else
+ buf_state = {
name = api.nvim_buf_get_name(bufnr),
+ lines = {},
+ lines_tmp = {},
+ pending_changes = {},
+ needs_flush = false,
+ refs = 1,
}
state.buffers[bufnr] = buf_state
- if use_incremental_sync then
+ if group.sync_kind == protocol.TextDocumentSyncKind.Incremental then
buf_state.lines = nvim_buf_get_lines(bufnr, 0, -1, true)
- buf_state.lines_tmp = {}
- buf_state.pending_changes = {}
end
end
end
---@private
function changetracking._get_and_set_name(client, bufnr, name)
- local state = state_by_client[client.id] or {}
+ local state = state_by_group[get_group(client)] or {}
local buf_state = (state.buffers or {})[bufnr]
local old_name = buf_state.name
buf_state.name = name
@@ -395,32 +507,33 @@ do
---@private
function changetracking.reset_buf(client, bufnr)
changetracking.flush(client, bufnr)
- local state = state_by_client[client.id]
- if state and state.buffers then
- local buf_state = state.buffers[bufnr]
+ local state = state_by_group[get_group(client)]
+ if not state then
+ return
+ end
+ assert(state.buffers, 'CTGroupState must have buffers')
+ local buf_state = state.buffers[bufnr]
+ buf_state.refs = buf_state.refs - 1
+ assert(buf_state.refs >= 0, 'refcount on buffer state must not get negative')
+ if buf_state.refs == 0 then
state.buffers[bufnr] = nil
- if buf_state and buf_state.timer then
- buf_state.timer:stop()
- buf_state.timer:close()
- buf_state.timer = nil
- end
+ changetracking._reset_timer(buf_state)
end
end
---@private
- function changetracking.reset(client_id)
- local state = state_by_client[client_id]
+ function changetracking.reset(client)
+ local state = state_by_group[get_group(client)]
if not state then
return
end
- for _, buf_state in pairs(state.buffers) do
- if buf_state.timer then
- buf_state.timer:stop()
- buf_state.timer:close()
- buf_state.timer = nil
+ state.clients[client.id] = nil
+ if vim.tbl_count(state.clients) == 0 then
+ for _, buf_state in pairs(state.buffers) do
+ changetracking._reset_timer(buf_state)
end
+ state.buffers = {}
end
- state.buffers = {}
end
---@private
@@ -430,6 +543,10 @@ do
-- debounce can be skipped and otherwise maybe reduced.
--
-- This turns the debounce into a kind of client rate limiting
+ --
+ ---@param debounce number
+ ---@param buf_state CTBufferState
+ ---@return number
local function next_debounce(debounce, buf_state)
if debounce == 0 then
return 0
@@ -444,83 +561,36 @@ do
end
---@private
- function changetracking.prepare(bufnr, firstline, lastline, new_lastline)
- local incremental_changes = function(client, buf_state)
- local prev_lines = buf_state.lines
- local curr_lines = buf_state.lines_tmp
-
- local changed_lines = nvim_buf_get_lines(bufnr, firstline, new_lastline, true)
- for i = 1, firstline do
- curr_lines[i] = prev_lines[i]
- end
- for i = firstline + 1, new_lastline do
- curr_lines[i] = changed_lines[i - firstline]
- end
- for i = lastline + 1, #prev_lines do
- curr_lines[i - lastline + new_lastline] = prev_lines[i]
- end
- if tbl_isempty(curr_lines) then
- -- Can happen when deleting the entire contents of a buffer, see https://github.com/neovim/neovim/issues/16259.
- curr_lines[1] = ''
- end
-
- local line_ending = buf_get_line_ending(bufnr)
- local incremental_change = sync.compute_diff(
- buf_state.lines,
- curr_lines,
- firstline,
- lastline,
- new_lastline,
- client.offset_encoding or 'utf-16',
- line_ending
- )
-
- -- Double-buffering of lines tables is used to reduce the load on the garbage collector.
- -- At this point the prev_lines table is useless, but its internal storage has already been allocated,
- -- so let's keep it around for the next didChange event, in which it will become the next
- -- curr_lines table. Note that setting elements to nil doesn't actually deallocate slots in the
- -- internal storage - it merely marks them as free, for the GC to deallocate them.
- for i in ipairs(prev_lines) do
- prev_lines[i] = nil
- end
- buf_state.lines = curr_lines
- buf_state.lines_tmp = prev_lines
+ ---@param bufnr number
+ ---@param sync_kind number protocol.TextDocumentSyncKind
+ ---@param state CTGroupState
+ ---@param buf_state CTBufferState
+ local function send_changes(bufnr, sync_kind, state, buf_state)
+ if not buf_state.needs_flush then
+ return
+ end
+ buf_state.last_flush = uv.hrtime()
+ buf_state.needs_flush = false
- return incremental_change
+ if not api.nvim_buf_is_valid(bufnr) then
+ buf_state.pending_changes = {}
+ return
end
- local full_changes = once(function()
- return {
- text = buf_get_full_text(bufnr),
+
+ local changes
+ if sync_kind == protocol.TextDocumentSyncKind.None then
+ return
+ elseif sync_kind == protocol.TextDocumentSyncKind.Incremental then
+ changes = buf_state.pending_changes
+ buf_state.pending_changes = {}
+ else
+ changes = {
+ { text = buf_get_full_text(bufnr) },
}
- end)
+ end
local uri = vim.uri_from_bufnr(bufnr)
- return function(client)
- if
- vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'change')
- == protocol.TextDocumentSyncKind.None
- then
- return
- end
- local state = state_by_client[client.id]
- local buf_state = state.buffers[bufnr]
- changetracking._reset_timer(buf_state)
- local debounce = next_debounce(state.debounce, buf_state)
- if state.use_incremental_sync then
- -- This must be done immediately and cannot be delayed
- -- The contents would further change and startline/endline may no longer fit
- table.insert(buf_state.pending_changes, incremental_changes(client, buf_state))
- end
- buf_state.pending_change = function()
- if buf_state.pending_change == nil then
- return
- end
- buf_state.pending_change = nil
- buf_state.last_flush = uv.hrtime()
- if client.is_stopped() or not api.nvim_buf_is_valid(bufnr) then
- return
- end
- local changes = state.use_incremental_sync and buf_state.pending_changes
- or { full_changes() }
+ for _, client in pairs(state.clients) do
+ if not client.is_stopped() and lsp.buf_is_attached(bufnr, client.id) then
client.notify('textDocument/didChange', {
textDocument = {
uri = uri,
@@ -528,46 +598,90 @@ do
},
contentChanges = changes,
})
- buf_state.pending_changes = {}
+ end
+ end
+ end
+
+ ---@private
+ function changetracking.send_changes(bufnr, firstline, lastline, new_lastline)
+ local groups = {}
+ for _, client in pairs(lsp.get_active_clients({ bufnr = bufnr })) do
+ local group = get_group(client)
+ groups[group_key(group)] = group
+ end
+ for _, group in pairs(groups) do
+ local state = state_by_group[group]
+ if not state then
+ error(
+ string.format(
+ 'changetracking.init must have been called for all LSP clients. group=%s states=%s',
+ vim.inspect(group),
+ vim.inspect(vim.tbl_keys(state_by_group))
+ )
+ )
+ end
+ local buf_state = state.buffers[bufnr]
+ buf_state.needs_flush = true
+ changetracking._reset_timer(buf_state)
+ local debounce = next_debounce(state.debounce, buf_state)
+ if group.sync_kind == protocol.TextDocumentSyncKind.Incremental then
+ -- This must be done immediately and cannot be delayed
+ -- The contents would further change and startline/endline may no longer fit
+ local changes = incremental_changes(
+ buf_state,
+ group.offset_encoding,
+ bufnr,
+ firstline,
+ lastline,
+ new_lastline
+ )
+ table.insert(buf_state.pending_changes, changes)
end
if debounce == 0 then
- buf_state.pending_change()
+ send_changes(bufnr, group.sync_kind, state, buf_state)
else
local timer = uv.new_timer()
buf_state.timer = timer
- -- Must use schedule_wrap because `full_changes()` calls nvim_buf_get_lines
- timer:start(debounce, 0, vim.schedule_wrap(buf_state.pending_change))
+ timer:start(
+ debounce,
+ 0,
+ vim.schedule_wrap(function()
+ changetracking._reset_timer(buf_state)
+ send_changes(bufnr, group.sync_kind, state, buf_state)
+ end)
+ )
end
end
end
+ ---@private
function changetracking._reset_timer(buf_state)
- if buf_state.timer then
- buf_state.timer:stop()
- buf_state.timer:close()
+ local timer = buf_state.timer
+ if timer then
buf_state.timer = nil
+ if not timer:is_closing() then
+ timer:stop()
+ timer:close()
+ end
end
end
--- Flushes any outstanding change notification.
---@private
function changetracking.flush(client, bufnr)
- local state = state_by_client[client.id]
+ local group = get_group(client)
+ local state = state_by_group[group]
if not state then
return
end
if bufnr then
local buf_state = state.buffers[bufnr] or {}
changetracking._reset_timer(buf_state)
- if buf_state.pending_change then
- buf_state.pending_change()
- end
+ send_changes(bufnr, group.sync_kind, state, buf_state)
else
- for _, buf_state in pairs(state.buffers) do
+ for buf, buf_state in pairs(state.buffers) do
changetracking._reset_timer(buf_state)
- if buf_state.pending_change then
- buf_state.pending_change()
- end
+ send_changes(buf, group.sync_kind, state, buf_state)
end
end
end
@@ -577,7 +691,7 @@ end
--- Default handler for the 'textDocument/didOpen' LSP notification.
---
---@param bufnr number Number of the buffer, or 0 for current
----@param client Client object
+---@param client table Client object
local function text_document_did_open_handler(bufnr, client)
changetracking.init(client, bufnr)
if not vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then
@@ -871,7 +985,7 @@ end
--- - debounce_text_changes (number, default 150): Debounce didChange
--- notifications to the server by the given number in milliseconds. No debounce
--- occurs if nil
---- - exit_timeout (number, default 500): Milliseconds to wait for server to
+--- - exit_timeout (number|boolean, default false): Milliseconds to wait for server to
--- exit cleanly after sending the 'shutdown' request before sending kill -15.
--- If set to false, nvim exits immediately after sending the 'shutdown' request to the server.
---
@@ -968,12 +1082,20 @@ function lsp.start_client(config)
---@private
local function set_defaults(client, bufnr)
- if client.server_capabilities.definitionProvider and vim.bo[bufnr].tagfunc == '' then
+ local capabilities = client.server_capabilities
+ if capabilities.definitionProvider and vim.bo[bufnr].tagfunc == '' then
vim.bo[bufnr].tagfunc = 'v:lua.vim.lsp.tagfunc'
end
- if client.server_capabilities.completionProvider and vim.bo[bufnr].omnifunc == '' then
+ if capabilities.completionProvider and vim.bo[bufnr].omnifunc == '' then
vim.bo[bufnr].omnifunc = 'v:lua.vim.lsp.omnifunc'
end
+ if
+ capabilities.documentRangeFormattingProvider
+ and vim.bo[bufnr].formatprg == ''
+ and vim.bo[bufnr].formatexpr == ''
+ then
+ vim.bo[bufnr].formatexpr = 'v:lua.vim.lsp.formatexpr()'
+ end
end
---@private
@@ -986,6 +1108,9 @@ function lsp.start_client(config)
if vim.bo[bufnr].omnifunc == 'v:lua.vim.lsp.omnifunc' then
vim.bo[bufnr].omnifunc = nil
end
+ if vim.bo[bufnr].formatexpr == 'v:lua.vim.lsp.formatexpr()' then
+ vim.bo[bufnr].formatexpr = nil
+ end
end
---@private
@@ -1019,11 +1144,16 @@ function lsp.start_client(config)
end)
end
end
-
+ local client = active_clients[client_id] and active_clients[client_id]
+ or uninitialized_clients[client_id]
active_clients[client_id] = nil
uninitialized_clients[client_id] = nil
- changetracking.reset(client_id)
+ -- Client can be absent if executable starts, but initialize fails
+ -- init/attach won't have happened
+ if client then
+ changetracking.reset(client)
+ end
if code ~= 0 or (signal ~= 0 and signal ~= 15) then
local msg =
string.format('Client %s quit with exit code %s and signal %s', client_id, code, signal)
@@ -1403,9 +1533,7 @@ do
return true
end
util.buf_versions[bufnr] = changedtick
- local compute_change_and_notify =
- changetracking.prepare(bufnr, firstline, lastline, new_lastline)
- for_each_buffer_client(bufnr, compute_change_and_notify)
+ changetracking.send_changes(bufnr, firstline, lastline, new_lastline)
end
end
@@ -1681,7 +1809,7 @@ api.nvim_create_autocmd('VimLeavePre', {
local send_kill = false
for client_id, client in pairs(active_clients) do
- local timeout = if_nil(client.config.flags.exit_timeout, 500)
+ local timeout = if_nil(client.config.flags.exit_timeout, false)
if timeout then
send_kill = true
timeouts[client_id] = timeout
@@ -1717,13 +1845,14 @@ api.nvim_create_autocmd('VimLeavePre', {
end,
})
+---@private
--- Sends an async request for all active clients attached to the
--- buffer.
---
---@param bufnr (number) Buffer handle, or 0 for current.
---@param method (string) LSP method name
----@param params (optional, table) Parameters to send to the server
----@param handler (optional, function) See |lsp-handler|
+---@param params table|nil Parameters to send to the server
+---@param handler function|nil See |lsp-handler|
--- If nil, follows resolution strategy defined in |lsp-handler-configuration|
---
---@returns 2-tuple:
@@ -1982,29 +2111,32 @@ function lsp.formatexpr(opts)
return 1
end
- local start_line = vim.v.lnum
- local end_line = start_line + vim.v.count - 1
+ local start_lnum = vim.v.lnum
+ local end_lnum = start_lnum + vim.v.count - 1
- if start_line > 0 and end_line > 0 then
- local params = {
- textDocument = util.make_text_document_params(),
- range = {
- start = { line = start_line - 1, character = 0 },
- ['end'] = { line = end_line - 1, character = 0 },
- },
- }
- params.options = util.make_formatting_params().options
- local client_results =
- vim.lsp.buf_request_sync(0, 'textDocument/rangeFormatting', params, timeout_ms)
-
- -- Apply the text edits from one and only one of the clients.
- for client_id, response in pairs(client_results) do
+ if start_lnum <= 0 or end_lnum <= 0 then
+ return 0
+ end
+ local bufnr = api.nvim_get_current_buf()
+ for _, client in pairs(lsp.get_active_clients({ bufnr = bufnr })) do
+ if client.supports_method('textDocument/rangeFormatting') then
+ local params = util.make_formatting_params()
+ local end_line = vim.fn.getline(end_lnum)
+ local end_col = util._str_utfindex_enc(end_line, nil, client.offset_encoding)
+ params.range = {
+ start = {
+ line = start_lnum - 1,
+ character = 0,
+ },
+ ['end'] = {
+ line = end_lnum - 1,
+ character = end_col,
+ },
+ }
+ local response =
+ client.request_sync('textDocument/rangeFormatting', params, timeout_ms, bufnr)
if response.result then
- vim.lsp.util.apply_text_edits(
- response.result,
- 0,
- vim.lsp.get_client_by_id(client_id).offset_encoding
- )
+ vim.lsp.util.apply_text_edits(response.result, 0, client.offset_encoding)
return 0
end
end
diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua
index 63f4688d94..6a070928d9 100644
--- a/runtime/lua/vim/lsp/buf.lua
+++ b/runtime/lua/vim/lsp/buf.lua
@@ -61,7 +61,7 @@ end
---
---@param options table|nil additional options
--- - reuse_win: (boolean) Jump to existing window if buffer is already open.
---- - on_list: (function) handler for list results. See |on-list-handler|
+--- - on_list: (function) handler for list results. See |lsp-on-list-handler|
function M.declaration(options)
local params = util.make_position_params()
request_with_options('textDocument/declaration', params, options)
@@ -71,7 +71,7 @@ end
---
---@param options table|nil additional options
--- - reuse_win: (boolean) Jump to existing window if buffer is already open.
---- - on_list: (function) handler for list results. See |on-list-handler|
+--- - on_list: (function) handler for list results. See |lsp-on-list-handler|
function M.definition(options)
local params = util.make_position_params()
request_with_options('textDocument/definition', params, options)
@@ -81,7 +81,7 @@ end
---
---@param options table|nil additional options
--- - reuse_win: (boolean) Jump to existing window if buffer is already open.
---- - on_list: (function) handler for list results. See |on-list-handler|
+--- - on_list: (function) handler for list results. See |lsp-on-list-handler|
function M.type_definition(options)
local params = util.make_position_params()
request_with_options('textDocument/typeDefinition', params, options)
@@ -91,7 +91,7 @@ end
--- quickfix window.
---
---@param options table|nil additional options
---- - on_list: (function) handler for list results. See |on-list-handler|
+--- - on_list: (function) handler for list results. See |lsp-on-list-handler|
function M.implementation(options)
local params = util.make_position_params()
request_with_options('textDocument/implementation', params, options)
@@ -503,7 +503,7 @@ end
---@param context (table) Context for the request
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references
---@param options table|nil additional options
---- - on_list: (function) handler for list results. See |on-list-handler|
+--- - on_list: (function) handler for list results. See |lsp-on-list-handler|
function M.references(context, options)
validate({ context = { context, 't', true } })
local params = util.make_position_params()
@@ -516,7 +516,7 @@ end
--- Lists all symbols in the current buffer in the quickfix window.
---
---@param options table|nil additional options
---- - on_list: (function) handler for list results. See |on-list-handler|
+--- - on_list: (function) handler for list results. See |lsp-on-list-handler|
function M.document_symbol(options)
local params = { textDocument = util.make_text_document_params() }
request_with_options('textDocument/documentSymbol', params, options)
@@ -659,7 +659,7 @@ end
---
---@param query (string, optional)
---@param options table|nil additional options
---- - on_list: (function) handler for list results. See |on-list-handler|
+--- - on_list: (function) handler for list results. See |lsp-on-list-handler|
function M.workspace_symbol(query, options)
query = query or npcall(vim.fn.input, 'Query: ')
if query == nil then
diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua
index 913eee19a2..0926912066 100644
--- a/runtime/lua/vim/lsp/rpc.lua
+++ b/runtime/lua/vim/lsp/rpc.lua
@@ -58,12 +58,10 @@ end
---@private
--- Parses an LSP Message's header
---
----@param header: The header to parse.
----@returns Parsed headers
+---@param header string: The header to parse.
+---@return table parsed headers
local function parse_headers(header)
- if type(header) ~= 'string' then
- return nil
- end
+ assert(type(header) == 'string', 'header must be a string')
local headers = {}
for line in vim.gsplit(header, '\r\n', true) do
if line == '' then
@@ -189,9 +187,9 @@ end
--- Creates an RPC response object/table.
---
----@param code RPC error code defined in `vim.lsp.protocol.ErrorCodes`
----@param message (optional) arbitrary message to send to server
----@param data (optional) arbitrary data to send to server
+---@param code number RPC error code defined in `vim.lsp.protocol.ErrorCodes`
+---@param message string|nil arbitrary message to send to server
+---@param data any|nil arbitrary data to send to server
local function rpc_response_error(code, message, data)
-- TODO should this error or just pick a sane error (like InternalError)?
local code_name = assert(protocol.ErrorCodes[code], 'Invalid RPC error code')
@@ -248,13 +246,13 @@ end
---
---@param cmd (string) Command to start the LSP server.
---@param cmd_args (table) List of additional string arguments to pass to {cmd}.
----@param dispatchers (table, optional) Dispatchers for LSP message types. Valid
+---@param dispatchers table|nil Dispatchers for LSP message types. Valid
---dispatcher names are:
--- - `"notification"`
--- - `"server_request"`
--- - `"on_error"`
--- - `"on_exit"`
----@param extra_spawn_params (table, optional) Additional context for the LSP
+---@param extra_spawn_params table|nil Additional context for the LSP
--- server process. May contain:
--- - {cwd} (string) Working directory for the LSP server process
--- - {env} (table) Additional environment variables for LSP server process
@@ -342,6 +340,9 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
end
handle, pid = uv.spawn(cmd, spawn_params, onexit)
if handle == nil then
+ stdin:close()
+ stdout:close()
+ stderr:close()
local msg = string.format('Spawning language server with cmd: `%s` failed', cmd)
if string.match(pid, 'ENOENT') then
msg = msg
@@ -434,7 +435,7 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
end
end
- stderr:read_start(function(_err, chunk)
+ stderr:read_start(function(_, chunk)
if chunk then
local _ = log.error() and log.error('rpc', cmd, 'stderr', chunk)
end
@@ -520,7 +521,7 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
-- This works because we are expecting vim.NIL here
elseif decoded.id and (decoded.result ~= vim.NIL or decoded.error ~= vim.NIL) then
-- We sent a number, so we expect a number.
- local result_id = tonumber(decoded.id)
+ local result_id = assert(tonumber(decoded.id), 'response id must be a number')
-- Notify the user that a response was received for the request
local notify_reply_callback = notify_reply_callbacks and notify_reply_callbacks[result_id]
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index eac21db386..283099bbcf 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -113,7 +113,7 @@ end
--- Convert byte index to `encoding` index.
--- Convenience wrapper around vim.str_utfindex
---@param line string line to be indexed
----@param index number byte index (utf-8), or `nil` for length
+---@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`
function M._str_utfindex_enc(line, index, encoding)
diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua
index d6c3e25b3b..e1b4ed4ea9 100644
--- a/runtime/lua/vim/shared.lua
+++ b/runtime/lua/vim/shared.lua
@@ -526,7 +526,7 @@ function vim.trim(s)
return s:match('^%s*(.*%S)') or ''
end
---- Escapes magic chars in a Lua pattern.
+--- Escapes magic chars in |lua-patterns|.
---
---@see https://github.com/rxi/lume
---@param s string String to escape