diff options
Diffstat (limited to 'runtime/lua/vim')
-rw-r--r-- | runtime/lua/vim/diagnostic.lua | 21 | ||||
-rw-r--r-- | runtime/lua/vim/filetype.lua | 101 | ||||
-rw-r--r-- | runtime/lua/vim/lsp.lua | 196 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/buf.lua | 21 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/handlers.lua | 76 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/util.lua | 133 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/query.lua | 27 |
7 files changed, 372 insertions, 203 deletions
diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua index 417b661155..b4537c2882 100644 --- a/runtime/lua/vim/diagnostic.lua +++ b/runtime/lua/vim/diagnostic.lua @@ -30,7 +30,7 @@ M.handlers = setmetatable({}, { __newindex = function(t, name, handler) vim.validate { handler = {handler, "t" } } rawset(t, name, handler) - if not global_diagnostic_options[name] then + if global_diagnostic_options[name] == nil then global_diagnostic_options[name] = true end end, @@ -552,7 +552,8 @@ end --- - `table`: Enable this feature with overrides. Use an empty table to use default values. --- - `function`: Function with signature (namespace, bufnr) that returns any of the above. --- ----@param opts table Configuration table with the following keys: +---@param opts table|nil When omitted or "nil", retrieve the current configuration. Otherwise, a +--- configuration table with the following keys: --- - underline: (default true) Use underline for diagnostics. Options: --- * severity: Only underline diagnostics matching the given severity --- |diagnostic-severity| @@ -599,7 +600,7 @@ end --- global diagnostic options. function M.config(opts, namespace) vim.validate { - opts = { opts, 't' }, + opts = { opts, 't', true }, namespace = { namespace, 'n', true }, } @@ -611,10 +612,13 @@ function M.config(opts, namespace) t = global_diagnostic_options end - for opt in pairs(global_diagnostic_options) do - if opts[opt] ~= nil then - t[opt] = opts[opt] - end + if not opts then + -- Return current config + return vim.deepcopy(t) + end + + for k, v in pairs(opts) do + t[k] = v end if namespace then @@ -823,6 +827,7 @@ M.handlers.signs = { } bufnr = get_bufnr(bufnr) + opts = opts or {} if opts.signs and opts.signs.severity then diagnostics = filter_by_severity(opts.signs.severity, diagnostics) @@ -890,6 +895,7 @@ M.handlers.underline = { } bufnr = get_bufnr(bufnr) + opts = opts or {} if opts.underline and opts.underline.severity then diagnostics = filter_by_severity(opts.underline.severity, diagnostics) @@ -942,6 +948,7 @@ M.handlers.virtual_text = { } bufnr = get_bufnr(bufnr) + opts = opts or {} local severity if opts.virtual_text then diff --git a/runtime/lua/vim/filetype.lua b/runtime/lua/vim/filetype.lua index 196e782f4e..819587bb3e 100644 --- a/runtime/lua/vim/filetype.lua +++ b/runtime/lua/vim/filetype.lua @@ -15,7 +15,7 @@ local function starsetf(ft) end end, { -- Starset matches should always have lowest priority - priority = -1, + priority = -math.huge, }} end @@ -119,7 +119,13 @@ local extension = { tcc = "cpp", hxx = "cpp", hpp = "cpp", - cpp = function() + cpp = function(path, bufnr) + if vim.g.cynlib_syntax_for_cc then + return "cynlib" + end + return "cpp" + end, + cc = function(path, bufnr) if vim.g.cynlib_syntax_for_cc then return "cynlib" end @@ -770,6 +776,14 @@ local extension = { mm = function() vim.fn["dist#ft#FTmm"]() end, mms = function() vim.fn["dist#ft#FTmms"]() end, p = function() vim.fn["dist#ft#FTprogress_pascal"]() end, + patch = function(path, bufnr) + local firstline = getline(bufnr, 1) + if string.find(firstline, "^From " .. string.rep("%x", 40) .. "+ Mon Sep 17 00:00:00 2001$") then + return "gitsendemail" + else + return "diff" + end + end, pl = function() vim.fn["dist#ft#FTpl"]() end, pp = function() vim.fn["dist#ft#FTpp"]() end, pro = function() vim.fn["dist#ft#ProtoCheck"]('idlang') end, @@ -860,6 +874,10 @@ local filename = { ["exim.conf"] = "exim", exports = "exports", [".fetchmailrc"] = "fetchmail", + fvSchemes = function() vim.fn["dist#ft#FTfoam"]() end, + fvSolution = function() vim.fn["dist#ft#FTfoam"]() end, + fvConstraints = function() vim.fn["dist#ft#FTfoam"]() end, + fvModels = function() vim.fn["dist#ft#FTfoam"]() end, fstab = "fstab", mtab = "fstab", [".gdbinit"] = "gdb", @@ -1034,6 +1052,7 @@ local filename = { ["tidy.conf"] = "tidy", tidyrc = "tidy", [".tidyrc"] = "tidy", + [".tmux.conf"] = "tmux", ["/.cargo/config"] = "toml", Pipfile = "toml", ["Gopkg.lock"] = "toml", @@ -1071,6 +1090,7 @@ local filename = { [".alias"] = function() vim.fn["dist#ft#CSH"]() end, [".bashrc"] = function() vim.fn["dist#ft#SetFileTypeSH"]("bash") end, [".cshrc"] = function() vim.fn["dist#ft#CSH"]() end, + [".env"] = function() vim.fn["dist#ft#SetFileTypeSH"](vim.fn.getline(1)) end, [".kshrc"] = function() vim.fn["dist#ft#SetFileTypeSH"]("ksh") end, [".login"] = function() vim.fn["dist#ft#CSH"]() end, [".profile"] = function() vim.fn["dist#ft#SetFileTypeSH"](vim.fn.getline(1)) end, @@ -1228,6 +1248,8 @@ local pattern = { [".*/%.config/systemd/user/.*%.d/.*%.conf"] = "systemd", [".*/etc/systemd/system/.*%.d/.*%.conf"] = "systemd", [".*%.t%.html"] = "tilde", + ["%.?tmux.*%.conf"] = "tmux", + ["%.?tmux.*%.conf.*"] = { "tmux", { priority = -1 } }, [".*/%.cargo/config"] = "toml", [".*/%.cargo/credentials"] = "toml", [".*/etc/udev/udev%.conf"] = "udevconf", @@ -1334,6 +1356,21 @@ local pattern = { ["zlog.*"] = starsetf('zsh'), ["zsh.*"] = starsetf('zsh'), ["ae%d+%.txt"] = 'mail', + ["[a-zA-Z0-9].*Dict"] = function() vim.fn["dist#ft#FTfoam"]() end, + ["[a-zA-Z0-9].*Dict%..*"] = function() vim.fn["dist#ft#FTfoam"]() end, + ["[a-zA-Z].*Properties"] = function() vim.fn["dist#ft#FTfoam"]() end, + ["[a-zA-Z].*Properties%..*"] = function() vim.fn["dist#ft#FTfoam"]() end, + [".*Transport%..*"] = function() vim.fn["dist#ft#FTfoam"]() end, + [".*/constant/g"] = function() vim.fn["dist#ft#FTfoam"]() end, + [".*/0/.*"] = function() vim.fn["dist#ft#FTfoam"]() end, + [".*/0%.orig/.*"] = function() vim.fn["dist#ft#FTfoam"]() end, + [".*/etc/sensors%.d/[^.].*"] = starsetf('sensors'), + [".*%.git/.*"] = function(path, bufnr) + local firstline = getline(bufnr, 1) + if firstline:find("^" .. string.rep("%x", 40) .. "+ ") or firstline:sub(1, 5) == "ref: " then + return "git" + end + end, -- END PATTERN } -- luacheck: pop @@ -1361,7 +1398,7 @@ local pattern_sorted = sort_by_priority(pattern) ---@private local function normalize_path(path) - return (path:gsub("\\", "/")) + return (path:gsub("\\", "/"):gsub("^~", vim.env.HOME)) end --- Add new filetype mappings. @@ -1456,6 +1493,25 @@ local function dispatch(ft, path, bufnr, ...) end ---@private +local function match_pattern(name, path, tail, pat) + -- If the pattern contains a / match against the full path, otherwise just the tail + local fullpat = "^" .. pat .. "$" + local matches + if pat:find("/") then + -- Similar to |autocmd-pattern|, if the pattern contains a '/' then check for a match against + -- both the short file name (as typed) and the full file name (after expanding to full path + -- and resolving symlinks) + matches = name:match(fullpat) or path:match(fullpat) + else + matches = tail:match(fullpat) + end + return matches +end + +--- Set the filetype for the given buffer from a file name. +--- +---@param name string File name (can be an absolute or relative path) +---@param bufnr number|nil The buffer to set the filetype for. Defaults to the current buffer. function M.match(name, bufnr) -- When fired from the main filetypedetect autocommand the {bufnr} argument is omitted, so we use -- the current buffer. The {bufnr} argument is provided to allow extensibility in case callers @@ -1476,21 +1532,18 @@ function M.match(name, bufnr) return end - -- Next, check the file path against available patterns - for _, v in ipairs(pattern_sorted) do + -- Next, check the file path against available patterns with non-negative priority + local j = 1 + for i, v in ipairs(pattern_sorted) do local k = next(v) - local ft = v[k][1] - -- If the pattern contains a / match against the full path, otherwise just the tail - local pat = "^" .. k .. "$" - local matches - if k:find("/") then - -- Similar to |autocmd-pattern|, if the pattern contains a '/' then check for a match against - -- both the short file name (as typed) and the full file name (after expanding to full path - -- and resolving symlinks) - matches = name:match(pat) or path:match(pat) - else - matches = tail:match(pat) + local opts = v[k][2] + if opts.priority < 0 then + j = i + break end + + local ft = v[k][1] + local matches = match_pattern(name, path, tail, k) if matches then if dispatch(ft, path, bufnr, matches) then return @@ -1498,11 +1551,25 @@ function M.match(name, bufnr) end end - -- Finally, check file extension + -- Next, check file extension local ext = vim.fn.fnamemodify(name, ":e") if dispatch(extension[ext], path, bufnr) then return end + + -- Finally, check patterns with negative priority + for i = j, #pattern_sorted do + local v = pattern_sorted[i] + local k = next(v) + + local ft = v[k][1] + local matches = match_pattern(name, path, tail, k) + if matches then + if dispatch(ft, path, bufnr, matches) then + return + end + end + end end return M diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index fb08e54dfc..8b7eb4ac90 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -312,56 +312,72 @@ do --- client_id → state --- --- state + --- use_incremental_sync: bool + --- buffers: bufnr -> buffer_state + --- + --- 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 - --- use_incremental_sync: bool - --- buffers?: table (bufnr → lines); for incremental sync only + --- Only set if incremental_sync is used + --- --- timer?: uv_timer + --- lines: table local state_by_client = {} ---@private function changetracking.init(client, bufnr) + local use_incremental_sync = ( + if_nil(client.config.flags.allow_incremental_sync, true) + and client.resolved_capabilities.text_document_did_change == protocol.TextDocumentSyncKind.Incremental + ) local state = state_by_client[client.id] if not state then state = { - pending_changes = {}; - last_flush = {}; - use_incremental_sync = ( - if_nil(client.config.flags.allow_incremental_sync, true) - and client.resolved_capabilities.text_document_did_change == protocol.TextDocumentSyncKind.Incremental - ); + buffers = {}; + debounce = client.config.flags.debounce_text_changes or 150, + use_incremental_sync = use_incremental_sync; } state_by_client[client.id] = state end - if not state.use_incremental_sync then - return - end - if not state.buffers then - state.buffers = {} + if not state.buffers[bufnr] then + local buf_state = {} + state.buffers[bufnr] = buf_state + if use_incremental_sync then + buf_state.lines = nvim_buf_get_lines(bufnr, 0, -1, true) + buf_state.pending_changes = {} + end end - state.buffers[bufnr] = nvim_buf_get_lines(bufnr, 0, -1, true) end ---@private function changetracking.reset_buf(client, bufnr) - changetracking.flush(client) + changetracking.flush(client, bufnr) local state = state_by_client[client.id] - if state then - if state.buffers then - state.buffers[bufnr] = nil + if state and state.buffers then + local buf_state = state.buffers[bufnr] + 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 - state.last_flush = {} end end ---@private function changetracking.reset(client_id) local state = state_by_client[client_id] - if state then - state_by_client[client_id] = nil - changetracking._reset_timer(state) + 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 + end + end + state.buffers = {} end ---@private @@ -371,35 +387,27 @@ do -- debounce can be skipped and otherwise maybe reduced. -- -- This turns the debounce into a kind of client rate limiting - local function next_debounce(debounce, state, bufnr) + local function next_debounce(debounce, buf_state) if debounce == 0 then return 0 end local ns_to_ms = 0.000001 - local last_flush = state.last_flush[bufnr] - if not last_flush then + if not buf_state.last_flush then return debounce end local now = uv.hrtime() - local ms_since_last_flush = (now - last_flush) * ns_to_ms - local remaining_debounce = debounce - ms_since_last_flush - if remaining_debounce > 0 then - return remaining_debounce - else - state.last_flush[bufnr] = now - return 0 - end + local ms_since_last_flush = (now - buf_state.last_flush) * ns_to_ms + return math.max(debounce - ms_since_last_flush, 0) end ---@private function changetracking.prepare(bufnr, firstline, lastline, new_lastline) - local incremental_changes = function(client) - local cached_buffers = state_by_client[client.id].buffers + local incremental_changes = function(client, buf_state) local curr_lines = nvim_buf_get_lines(bufnr, 0, -1, true) local line_ending = buf_get_line_ending(bufnr) local incremental_change = sync.compute_diff( - cached_buffers[bufnr], curr_lines, firstline, lastline, new_lastline, client.offset_encoding or 'utf-16', line_ending) - cached_buffers[bufnr] = curr_lines + buf_state.lines, curr_lines, firstline, lastline, new_lastline, client.offset_encoding or 'utf-16', line_ending) + buf_state.lines = curr_lines return incremental_change end local full_changes = once(function() @@ -413,76 +421,68 @@ do return end local state = state_by_client[client.id] - local debounce = next_debounce(client.config.flags.debounce_text_changes or 150, state, bufnr) - if debounce == 0 then - local changes = state.use_incremental_sync and incremental_changes(client) or full_changes() - client.notify("textDocument/didChange", { - textDocument = { - uri = uri; - version = util.buf_versions[bufnr]; - }; - contentChanges = { changes, } - }) - return - end - changetracking._reset_timer(state) + 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 - if not state.pending_changes[uri] then - state.pending_changes[uri] = {} - end - table.insert(state.pending_changes[uri], incremental_changes(client)) + table.insert(buf_state.pending_changes, incremental_changes(client, buf_state)) end - state.pending_change = function() - state.pending_change = nil - state.last_flush[bufnr] = uv.hrtime() + buf_state.pending_change = function() + buf_state.pending_change = nil + buf_state.last_flush = uv.hrtime() if client.is_stopped() or not vim.api.nvim_buf_is_valid(bufnr) then return end - if state.use_incremental_sync then - for change_uri, content_changes in pairs(state.pending_changes) do - client.notify("textDocument/didChange", { - textDocument = { - uri = change_uri; - version = util.buf_versions[vim.uri_to_bufnr(change_uri)]; - }; - contentChanges = content_changes, - }) - end - state.pending_changes = {} - else - client.notify("textDocument/didChange", { - textDocument = { - uri = uri; - version = util.buf_versions[bufnr]; - }; - contentChanges = { full_changes() }, - }) - end + local changes = state.use_incremental_sync and buf_state.pending_changes or { full_changes() } + client.notify("textDocument/didChange", { + textDocument = { + uri = uri, + version = util.buf_versions[bufnr], + }, + contentChanges = changes, + }) + buf_state.pending_changes = {} + end + if debounce == 0 then + buf_state.pending_change() + else + local timer = vim.loop.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)) end - state.timer = vim.loop.new_timer() - -- Must use schedule_wrap because `full_changes()` calls nvim_buf_get_lines - state.timer:start(debounce, 0, vim.schedule_wrap(state.pending_change)) end end - function changetracking._reset_timer(state) - if state.timer then - state.timer:stop() - state.timer:close() - state.timer = nil + function changetracking._reset_timer(buf_state) + if buf_state.timer then + buf_state.timer:stop() + buf_state.timer:close() + buf_state.timer = nil end end --- Flushes any outstanding change notification. ---@private - function changetracking.flush(client) + function changetracking.flush(client, bufnr) local state = state_by_client[client.id] - if state then - changetracking._reset_timer(state) - if state.pending_change then - state.pending_change() + 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 + else + for _, buf_state in pairs(state.buffers) do + changetracking._reset_timer(buf_state) + if buf_state.pending_change then + buf_state.pending_change() + end end end end @@ -988,7 +988,7 @@ function lsp.start_client(config) or error(string.format("not found: %q request handler for client %q.", method, client.name)) end -- Ensure pending didChange notifications are sent so that the server doesn't operate on a stale state - changetracking.flush(client) + changetracking.flush(client, bufnr) bufnr = resolve_bufnr(bufnr) local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, handler, bufnr) local success, request_id = rpc.request(method, params, function(err, result) @@ -1045,14 +1045,16 @@ function lsp.start_client(config) ---@private --- Sends a notification to an LSP server. --- - ---@param method (string) LSP method name. - ---@param params (optional, table) LSP request params. - ---@param bufnr (number) Buffer handle, or 0 for current. + ---@param method string LSP method name. + ---@param params table|nil LSP request params. ---@returns {status} (bool) true if the notification was successful. ---If it is false, then it will always be false ---(the client has shutdown). - function client.notify(...) - return rpc.notify(...) + function client.notify(method, params) + if method ~= 'textDocument/didChange' then + changetracking.flush(client) + end + return rpc.notify(method, params) end ---@private diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 543fdb0237..c9b73e4b70 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -184,7 +184,7 @@ function M.formatting_sync(options, timeout_ms) local result, err = client.request_sync('textDocument/formatting', params, timeout_ms, bufnr) if result and result.result then - util.apply_text_edits(result.result, bufnr) + util.apply_text_edits(result.result, bufnr, client.offset_encoding) elseif err then vim.notify('vim.lsp.buf.formatting_sync: ' .. err, vim.log.levels.WARN) end @@ -228,7 +228,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, bufnr) + util.apply_text_edits(result.result, bufnr, client.offset_encoding) elseif err then vim.notify(string.format("vim.lsp.buf.formatting_seq_sync: (%s) %s", client.name, err), vim.log.levels.WARN) end @@ -503,7 +503,7 @@ local function on_code_action_results(results, ctx) ---@private local function apply_action(action, client) if action.edit then - util.apply_workspace_edit(action.edit) + util.apply_workspace_edit(action.edit, client.offset_encoding) end if action.command then local command = type(action.command) == 'table' and action.command or action @@ -627,14 +627,19 @@ end --- Executes an LSP server command. --- ----@param command A valid `ExecuteCommandParams` object +---@param command_params table A valid `ExecuteCommandParams` object ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand -function M.execute_command(command) +function M.execute_command(command_params) validate { - command = { command.command, 's' }, - arguments = { command.arguments, 't', true } + command = { command_params.command, 's' }, + arguments = { command_params.arguments, 't', true } } - request('workspace/executeCommand', command) + command_params = { + command=command_params.command, + arguments=command_params.arguments, + workDoneToken=command_params.workDoneToken, + } + request('workspace/executeCommand', command_params ) end return M diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua index a48302cc4b..c0843e1577 100644 --- a/runtime/lua/vim/lsp/handlers.lua +++ b/runtime/lua/vim/lsp/handlers.lua @@ -111,13 +111,15 @@ M['client/registerCapability'] = function(_, _, ctx) end --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit -M['workspace/applyEdit'] = function(_, workspace_edit) +M['workspace/applyEdit'] = function(_, workspace_edit, ctx) if not workspace_edit then return end -- TODO(ashkan) Do something more with label? + local client_id = ctx.client_id + local client = vim.lsp.get_client_by_id(client_id) if workspace_edit.label then print("Workspace edit", workspace_edit.label) end - local status, result = pcall(util.apply_workspace_edit, workspace_edit.edit) + local status, result = pcall(util.apply_workspace_edit, workspace_edit.edit, client.offset_encoding) return { applied = status; failureReason = result; @@ -159,6 +161,30 @@ M['textDocument/codeLens'] = function(...) end +--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references +M['textDocument/references'] = function(_, result, ctx, config) + if not result or vim.tbl_isempty(result) then + vim.notify('No references found') + else + config = config or {} + if config.loclist then + vim.fn.setloclist(0, {}, ' ', { + title = 'References'; + items = util.locations_to_items(result, ctx.offset_encoding); + context = ctx; + }) + api.nvim_command("lopen") + else + vim.fn.setqflist({}, ' ', { + title = 'References'; + items = util.locations_to_items(result, ctx.offset_encoding); + context = ctx; + }) + api.nvim_command("botright copen") + end + end +end + ---@private --- Return a function that converts LSP responses to list items and opens the list @@ -169,23 +195,26 @@ end --- loclist: (boolean) use the location list (default is to use the quickfix list) --- ---@param map_result function `((resp, bufnr) -> list)` to convert the response ----@param entity name of the resource used in a `not found` error message -local function response_to_list(map_result, entity) - return function(_,result, ctx, config) +---@param entity string name of the resource used in a `not found` error message +---@param title_fn function Function to call to generate list title +local function response_to_list(map_result, entity, title_fn) + return function(_, result, ctx, config) if not result or vim.tbl_isempty(result) then vim.notify('No ' .. entity .. ' found') else config = config or {} if config.loclist then vim.fn.setloclist(0, {}, ' ', { - title = 'Language Server'; + title = title_fn(ctx); items = map_result(result, ctx.bufnr); + context = ctx; }) api.nvim_command("lopen") else vim.fn.setqflist({}, ' ', { - title = 'Language Server'; + title = title_fn(ctx); items = map_result(result, ctx.bufnr); + context = ctx; }) api.nvim_command("botright copen") end @@ -194,31 +223,36 @@ local function response_to_list(map_result, entity) end ---see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references -M['textDocument/references'] = response_to_list(util.locations_to_items, 'references') - --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol -M['textDocument/documentSymbol'] = response_to_list(util.symbols_to_items, 'document symbols') +M['textDocument/documentSymbol'] = response_to_list(util.symbols_to_items, 'document symbols', function(ctx) + local fname = vim.fn.fnamemodify(vim.uri_to_fname(ctx.params.textDocument.uri), ":.") + return string.format('Symbols in %s', fname) +end) --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_symbol -M['workspace/symbol'] = response_to_list(util.symbols_to_items, 'symbols') +M['workspace/symbol'] = response_to_list(util.symbols_to_items, 'symbols', function(ctx) + return string.format("Symbols matching '%s'", ctx.params.query) +end) --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rename -M['textDocument/rename'] = function(_, result, _) +M['textDocument/rename'] = function(_, result, ctx, _) if not result then return end - util.apply_workspace_edit(result) + local client = vim.lsp.get_client_by_id(ctx.client_id) + util.apply_workspace_edit(result, client.offset_encoding) end --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rangeFormatting M['textDocument/rangeFormatting'] = function(_, result, ctx, _) if not result then return end - util.apply_text_edits(result, ctx.bufnr) + local client = vim.lsp.get_client_by_id(ctx.client_id) + util.apply_text_edits(result, ctx.bufnr, client.offset_encoding) end --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting M['textDocument/formatting'] = function(_, result, ctx, _) if not result then return end - util.apply_text_edits(result, ctx.bufnr) + local client = vim.lsp.get_client_by_id(ctx.client_id) + util.apply_text_edits(result, ctx.bufnr, client.offset_encoding) end --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion @@ -277,19 +311,23 @@ local function location_handler(_, result, ctx, _) local _ = log.info() and log.info(ctx.method, 'No location found') return nil end + local client = vim.lsp.get_client_by_id(ctx.client_id) -- textDocument/definition can return Location or Location[] -- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition if vim.tbl_islist(result) then - util.jump_to_location(result[1]) + util.jump_to_location(result[1], client.offset_encoding) if #result > 1 then - vim.fn.setqflist({}, ' ', {title = 'LSP locations', items = util.locations_to_items(result)}) + vim.fn.setqflist({}, ' ', { + title = 'LSP locations', + items = util.locations_to_items(result, client.offset_encoding) + }) api.nvim_command("copen") end else - util.jump_to_location(result) + util.jump_to_location(result, client.offset_encoding) end end diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 0af4fcb036..8be1683d86 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -193,6 +193,11 @@ end local function get_lines(bufnr, rows) rows = type(rows) == "table" and rows or { rows } + -- This is needed for bufload and bufloaded + if bufnr == 0 then + bufnr = vim.api.nvim_get_current_buf() + end + ---@private local function buf_lines() local lines = {} @@ -272,7 +277,7 @@ 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|nil defaults to utf-16 +---@param offset_encoding string utf-8|utf-16|utf-32 --- 1-indexed local function get_line_byte_from_position(bufnr, position, offset_encoding) -- LSP's line and characters are 0-indexed @@ -281,7 +286,7 @@ 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 - local line = get_line(bufnr, position.line) + local line = get_line(bufnr, position.line) or '' local ok, result ok, result = pcall(_str_byteindex_enc, line, col, offset_encoding) if ok then @@ -355,15 +360,14 @@ end --- Applies a list of text edits to a buffer. ---@param text_edits table list of `TextEdit` objects ---@param bufnr number Buffer id ----@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to encoding of first client of `bufnr` +---@param offset_encoding string utf-8|utf-16|utf-32 defaults to encoding of first client of `bufnr` ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textEdit function M.apply_text_edits(text_edits, bufnr, offset_encoding) validate { text_edits = { text_edits, 't', false }; bufnr = { bufnr, 'number', false }; - offset_encoding = { offset_encoding, 'string', true }; + offset_encoding = { offset_encoding, 'string', false }; } - offset_encoding = offset_encoding or M._get_offset_encoding(bufnr) if not next(text_edits) then return end if not api.nvim_buf_is_loaded(bufnr) then vim.fn.bufload(bufnr) @@ -397,25 +401,6 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) end end) - -- Some LSP servers may return +1 range of the buffer content but nvim_buf_set_text can't accept it so we should fix it here. - local has_eol_text_edit = false - local max = vim.api.nvim_buf_line_count(bufnr) - local len = _str_utfindex_enc(vim.api.nvim_buf_get_lines(bufnr, -2, -1, false)[1] or '', nil, offset_encoding) - text_edits = vim.tbl_map(function(text_edit) - if max <= text_edit.range.start.line then - text_edit.range.start.line = max - 1 - text_edit.range.start.character = len - text_edit.newText = '\n' .. text_edit.newText - has_eol_text_edit = true - end - if max <= text_edit.range['end'].line then - text_edit.range['end'].line = max - 1 - text_edit.range['end'].character = len - has_eol_text_edit = true - end - return text_edit - end, text_edits) - -- 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 = vim.api.nvim_get_current_buf() == bufnr @@ -435,16 +420,38 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) -- 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 + text_edit.newText, _ = string.gsub(text_edit.newText, '\r\n?', '\n') + + -- Convert from LSP style ranges to Neovim style ranges. local e = { start_row = text_edit.range.start.line, - start_col = get_line_byte_from_position(bufnr, text_edit.range.start), + 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']), + end_col = get_line_byte_from_position(bufnr, text_edit.range['end'], offset_encoding), text = vim.split(text_edit.newText, '\n', true), } + + -- Some LSP servers may return +1 range of the buffer content but nvim_buf_set_text can't accept it so we should fix it here. + local max = vim.api.nvim_buf_line_count(bufnr) + if max <= e.start_row or max <= e.end_row then + local len = #(get_line(bufnr, max - 1) or '') + if max <= e.start_row then + e.start_row = max - 1 + e.start_col = len + table.insert(e.text, 1, '') + end + if max <= e.end_row then + e.end_row = max - 1 + e.end_col = len + end + has_eol_text_edit = true + end vim.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) @@ -459,10 +466,13 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) end end + local max = vim.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 < vim.api.nvim_buf_line_count(bufnr) - is_valid_cursor = is_valid_cursor and cursor.col <= #(vim.api.nvim_buf_get_lines(bufnr, cursor.row, cursor.row + 1, false)[1] or '') + 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 vim.api.nvim_win_set_cursor(0, { cursor.row + 1, cursor.col }) end @@ -471,7 +481,7 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) -- Remove final line if needed local fix_eol = has_eol_text_edit fix_eol = fix_eol and api.nvim_buf_get_option(bufnr, 'fixeol') - fix_eol = fix_eol and (vim.api.nvim_buf_get_lines(bufnr, -2, -1, false)[1] or '') == '' + fix_eol = fix_eol and get_line(bufnr, max - 1) == '' if fix_eol then vim.api.nvim_buf_set_lines(bufnr, -2, -1, false, {}) end @@ -509,9 +519,12 @@ end ---@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) ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentEdit -function M.apply_text_document_edit(text_document_edit, index) +function M.apply_text_document_edit(text_document_edit, index, offset_encoding) local text_document = text_document_edit.textDocument local bufnr = vim.uri_to_bufnr(text_document.uri) + if offset_encoding == nil then + vim.notify_once("apply_text_document_edit must be called with valid offset encoding", vim.log.levels.WARN) + end -- For lists of text document edits, -- do not check the version after the first edit. @@ -530,7 +543,7 @@ function M.apply_text_document_edit(text_document_edit, index) return end - M.apply_text_edits(text_document_edit.edits, bufnr) + M.apply_text_edits(text_document_edit.edits, bufnr, offset_encoding) end --- Parses snippets in a completion entry. @@ -727,9 +740,13 @@ end --- Applies a `WorkspaceEdit`. --- ----@param workspace_edit (table) `WorkspaceEdit` +---@param workspace_edit table `WorkspaceEdit` +---@param offset_encoding string utf-8|utf-16|utf-32 (required) --see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit -function M.apply_workspace_edit(workspace_edit) +function M.apply_workspace_edit(workspace_edit, offset_encoding) + if offset_encoding == nil then + vim.notify_once("apply_workspace_edit must be called with valid offset encoding", vim.log.levels.WARN) + end if workspace_edit.documentChanges then for idx, change in ipairs(workspace_edit.documentChanges) do if change.kind == "rename" then @@ -745,7 +762,7 @@ function M.apply_workspace_edit(workspace_edit) elseif change.kind then error(string.format("Unsupported change: %q", vim.inspect(change))) else - M.apply_text_document_edit(change, idx) + M.apply_text_document_edit(change, idx, offset_encoding) end end return @@ -758,7 +775,7 @@ function M.apply_workspace_edit(workspace_edit) for uri, changes in pairs(all_changes) do local bufnr = vim.uri_to_bufnr(uri) - M.apply_text_edits(changes, bufnr) + M.apply_text_edits(changes, bufnr, offset_encoding) end end @@ -834,7 +851,8 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers local active_hl local active_signature = signature_help.activeSignature or 0 -- If the activeSignature is not inside the valid range, then clip it. - if active_signature >= #signature_help.signatures then + -- In 3.15 of the protocol, activeSignature was allowed to be negative + if active_signature >= #signature_help.signatures or active_signature < 0 then active_signature = 0 end local signature = signature_help.signatures[active_signature + 1] @@ -975,12 +993,16 @@ end --- Jumps to a location. --- ----@param location (`Location`|`LocationLink`) +---@param location table (`Location`|`LocationLink`) +---@param offset_encoding string utf-8|utf-16|utf-32 (required) ---@returns `true` if the jump succeeded -function M.jump_to_location(location) +function M.jump_to_location(location, offset_encoding) -- location may be Location or LocationLink local uri = location.uri or location.targetUri if uri == nil then return end + if offset_encoding == nil then + vim.notify_once("jump_to_location must be called with valid offset encoding", vim.log.levels.WARN) + end local bufnr = vim.uri_to_bufnr(uri) -- Save position in jumplist vim.cmd "normal! m'" @@ -995,7 +1017,7 @@ function M.jump_to_location(location) api.nvim_buf_set_option(bufnr, 'buflisted', true) local range = location.range or location.targetSelectionRange local row = range.start.line - local col = get_line_byte_from_position(bufnr, range.start) + local col = get_line_byte_from_position(bufnr, range.start, offset_encoding) api.nvim_win_set_cursor(0, {row + 1, col}) -- Open folds under the cursor vim.cmd("normal! zv") @@ -1504,11 +1526,13 @@ do --[[ References ]] --- ---@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 `offset_encoding` of first client of `bufnr` + ---@param offset_encoding string One of "utf-8", "utf-16", "utf-32". ---@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} } - offset_encoding = offset_encoding or M._get_offset_encoding(bufnr) + validate { + bufnr = {bufnr, 'n', true}, + offset_encoding = { offset_encoding, 'string', false }; + } for _, reference in ipairs(references) do local start_line, start_char = reference["range"]["start"]["line"], reference["range"]["start"]["character"] local end_line, end_char = reference["range"]["end"]["line"], reference["range"]["end"]["character"] @@ -1541,9 +1565,14 @@ end) --- The result can be passed to the {list} argument of |setqflist()| or --- |setloclist()|. --- ----@param locations (table) list of `Location`s or `LocationLink`s +---@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 -function M.locations_to_items(locations) +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) + end + local items = {} local grouped = setmetatable({}, { __index = function(t, k) @@ -1583,7 +1612,7 @@ function M.locations_to_items(locations) local pos = temp.start local row = pos.line local line = lines[row] or "" - local col = pos.character + local col = M._str_byteindex_enc(line, pos.character, offset_encoding) table.insert(items, { filename = filename, lnum = row + 1, @@ -1767,7 +1796,13 @@ function M._get_offset_encoding(bufnr) local offset_encoding for _, client in pairs(vim.lsp.buf_get_clients(bufnr)) do - local this_offset_encoding = client.offset_encoding or "utf-16" + if client.offset_encoding == nil then + vim.notify_once( + string.format("Client (id: %s) offset_encoding is nil. Do not unset offset_encoding.", client.id), + vim.log.levels.ERROR + ) + end + local this_offset_encoding = client.offset_encoding if not offset_encoding then offset_encoding = this_offset_encoding elseif offset_encoding ~= this_offset_encoding then @@ -1814,7 +1849,7 @@ function M.make_given_range_params(start_pos, end_pos, bufnr, offset_encoding) end_pos = {end_pos, 't', true}; offset_encoding = {offset_encoding, 's', true}; } - bufnr = bufnr or 0 + bufnr = bufnr or vim.api.nvim_get_current_buf() offset_encoding = offset_encoding or M._get_offset_encoding(bufnr) local A = list_extend({}, start_pos or api.nvim_buf_get_mark(bufnr, '<')) local B = list_extend({}, end_pos or api.nvim_buf_get_mark(bufnr, '>')) @@ -1896,7 +1931,9 @@ end ---@returns (number, number) `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) - offset_encoding = offset_encoding or M._get_offset_encoding(buf) + if offset_encoding == nil then + vim.notify_once("character_offset must be called with valid offset encoding", vim.log.levels.WARN) + end -- If the col is past the EOL, use the line length. if col > #line then return _str_utfindex_enc(line, nil, offset_encoding) diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index ebed502c92..b3036ea679 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -138,6 +138,13 @@ function M.get_query(lang, query_name) end end +local query_cache = setmetatable({}, { + __index = function(tbl, key) + rawset(tbl, key, {}) + return rawget(tbl, key) + end +}) + --- Parse {query} as a string. (If the query is in a file, the caller --- should read the contents into a string before calling). --- @@ -151,17 +158,23 @@ end --- -` info.captures` also points to `captures`. --- - `info.patterns` contains information about predicates. --- ----@param lang The language ----@param query A string containing the query (s-expr syntax) +---@param lang string The language +---@param query string A string containing the query (s-expr syntax) --- ---@returns The query function M.parse_query(lang, query) language.require_language(lang) - local self = setmetatable({}, Query) - self.query = vim._ts_parse_query(lang, query) - self.info = self.query:inspect() - self.captures = self.info.captures - return self + local cached = query_cache[lang][query] + if cached then + return cached + else + local self = setmetatable({}, Query) + self.query = vim._ts_parse_query(lang, query) + self.info = self.query:inspect() + self.captures = self.info.captures + query_cache[lang][query] = self + return self + end end --- Gets the text corresponding to a given node |