Files
chupson b4da76be54
Some checks failed
Tests / unit tests (brew update && brew install ripgrep, macos-latest, nightly) (push) Has been cancelled
Tests / unit tests (brew update && brew install ripgrep, macos-latest, v0.10.0) (push) Has been cancelled
Tests / unit tests (brew update && brew install ripgrep, macos-latest, v0.9.5) (push) Has been cancelled
Tests / unit tests (choco install ripgrep, windows-2022, nightly) (push) Has been cancelled
Tests / unit tests (choco install ripgrep, windows-2022, v0.10.0) (push) Has been cancelled
Tests / unit tests (choco install ripgrep, windows-2022, v0.9.5) (push) Has been cancelled
Tests / unit tests (sudo apt-get update && sudo apt-get install -y ripgrep, ubuntu-22.04, nightly) (push) Has been cancelled
Tests / unit tests (sudo apt-get update && sudo apt-get install -y ripgrep, ubuntu-22.04, v0.10.0) (push) Has been cancelled
Tests / unit tests (sudo apt-get update && sudo apt-get install -y ripgrep, ubuntu-22.04, v0.9.5) (push) Has been cancelled
Linting and style checking / Luacheck (push) Has been cancelled
Linting and style checking / stylua (push) Has been cancelled
fix(lsp): stop using deprecated client.supports_method function (#3468)
2025-05-11 21:21:28 -04:00

562 lines
17 KiB
Lua

local channel = require("plenary.async.control").channel
local actions = require "telescope.actions"
local sorters = require "telescope.sorters"
local conf = require("telescope.config").values
local finders = require "telescope.finders"
local make_entry = require "telescope.make_entry"
local pickers = require "telescope.pickers"
local utils = require "telescope.utils"
local lsp = {}
local function call_hierarchy(opts, method, title, direction, item)
vim.lsp.buf_request(opts.bufnr, method, { item = item }, function(err, result)
if err then
vim.api.nvim_err_writeln("Error handling " .. title .. ": " .. err.message)
return
end
if not result or vim.tbl_isempty(result) then
return
end
local locations = {}
for _, ch_call in pairs(result) do
local ch_item = ch_call[direction]
for _, rng in pairs(ch_call.fromRanges) do
table.insert(locations, {
filename = vim.uri_to_fname(ch_item.uri),
text = ch_item.name,
lnum = rng.start.line + 1,
col = rng.start.character + 1,
})
end
end
pickers
.new(opts, {
prompt_title = title,
finder = finders.new_table {
results = locations,
entry_maker = opts.entry_maker or make_entry.gen_from_quickfix(opts),
},
previewer = conf.qflist_previewer(opts),
sorter = conf.generic_sorter(opts),
push_cursor_on_edit = true,
push_tagstack_on_edit = true,
})
:find()
end)
end
local function pick_call_hierarchy_item(call_hierarchy_items)
if not call_hierarchy_items or vim.tbl_isempty(call_hierarchy_items) then
return
end
if #call_hierarchy_items == 1 then
return call_hierarchy_items[1]
end
local items = {}
for i, item in pairs(call_hierarchy_items) do
local entry = item.detail or item.name
table.insert(items, string.format("%d. %s", i, entry))
end
local choice = vim.fn.inputlist(items)
if choice < 1 or choice > #items then
return
end
return call_hierarchy_items[choice]
end
---@param win number? Window handler
---@param extra table? Extra fields in params
---@return table|(fun(client: vim.lsp.Client): table) parmas to send to the server
local function client_position_params(win, extra)
win = win or vim.api.nvim_get_current_win()
if vim.fn.has "nvim-0.11" == 0 then
local params = vim.lsp.util.make_position_params(win)
if extra then
params = vim.tbl_extend("force", params, extra)
end
return params
end
return function(client)
local params = vim.lsp.util.make_position_params(win, client.offset_encoding)
if extra then
params = vim.tbl_extend("force", params, extra)
end
return params
end
end
local function calls(opts, direction)
local params = client_position_params()
vim.lsp.buf_request(opts.bufnr, "textDocument/prepareCallHierarchy", params, function(err, result)
if err then
vim.api.nvim_err_writeln("Error when preparing call hierarchy: " .. err)
return
end
local call_hierarchy_item = pick_call_hierarchy_item(result)
if not call_hierarchy_item then
return
end
if direction == "from" then
call_hierarchy(opts, "callHierarchy/incomingCalls", "LSP Incoming Calls", direction, call_hierarchy_item)
else
call_hierarchy(opts, "callHierarchy/outgoingCalls", "LSP Outgoing Calls", direction, call_hierarchy_item)
end
end)
end
lsp.incoming_calls = function(opts)
calls(opts, "from")
end
lsp.outgoing_calls = function(opts)
calls(opts, "to")
end
--- convert `item` type back to something we can pass to `vim.lsp.util.jump_to_location`
--- stopgap for pre-nvim 0.10 - after which we can simply use the `user_data`
--- field on the items in `vim.lsp.util.locations_to_items`
---@param item vim.quickfix.entry
---@param offset_encoding string|nil utf-8|utf-16|utf-32
---@return lsp.Location
local function item_to_location(item, offset_encoding)
local line = math.max(0, item.lnum - 1)
local character = math.max(0, utils.str_byteindex(item.text, item.col, offset_encoding or "utf-16") - 1)
local uri
if utils.is_uri(item.filename) then
uri = item.filename
else
uri = vim.uri_from_fname(item.filename)
end
return {
uri = uri,
range = {
start = {
line = line,
character = character,
},
["end"] = {
line = line,
character = character,
},
},
}
end
---@alias telescope.lsp.list_or_jump_action
---| "textDocument/references"
---| "textDocument/definition"
---| "textDocument/typeDefinition"
---| "textDocument/implementation"
---@param action telescope.lsp.list_or_jump_action
---@param items vim.quickfix.entry[]
---@param opts table
---@return vim.quickfix.entry[]
local apply_action_handler = function(action, items, opts)
if action == "textDocument/references" and not opts.include_current_line then
local lnum = vim.api.nvim_win_get_cursor(opts.winnr)[1]
items = vim.tbl_filter(function(v)
return not (v.filename == opts.curr_filepath and v.lnum == lnum)
end, items)
end
return items
end
---@param items vim.quickfix.entry[]
---@param opts table
---@return vim.quickfix.entry[]
local function filter_file_ignore_patters(items, opts)
local file_ignore_patterns = vim.F.if_nil(opts.file_ignore_patterns, conf.file_ignore_patterns)
file_ignore_patterns = file_ignore_patterns or {}
if vim.tbl_isempty(file_ignore_patterns) then
return items
end
return vim.tbl_filter(function(item)
for _, patt in ipairs(file_ignore_patterns) do
if string.match(item.filename, patt) then
return false
end
end
return true
end, items)
end
---@param action telescope.lsp.list_or_jump_action
---@param title string prompt title
---@param funname string: name of the calling function
---@param params lsp.TextDocumentPositionParams|(fun(client: vim.lsp.Client, bufnr: integer): table?)
---@param opts table
local function list_or_jump(action, title, funname, params, opts)
opts.reuse_win = vim.F.if_nil(opts.reuse_win, false)
opts.curr_filepath = vim.api.nvim_buf_get_name(opts.bufnr)
vim.lsp.buf_request_all(opts.bufnr, action, params, function(results_per_client)
local items = {}
local first_encoding
local errors = {}
for client_id, result_or_error in pairs(results_per_client) do
local error, result = result_or_error.error, result_or_error.result
if error then
errors[client_id] = error
else
if result ~= nil then
local locations = {}
if not utils.islist(result) then
vim.list_extend(locations, { result })
else
vim.list_extend(locations, result)
end
local offset_encoding = vim.lsp.get_client_by_id(client_id).offset_encoding
if not vim.tbl_isempty(result) then
first_encoding = offset_encoding
end
vim.list_extend(items, vim.lsp.util.locations_to_items(locations, offset_encoding))
end
end
end
for _, error in pairs(errors) do
vim.api.nvim_err_writeln("Error when executing " .. action .. " : " .. error.message)
end
items = apply_action_handler(action, items, opts)
items = filter_file_ignore_patters(items, opts)
if vim.tbl_isempty(items) then
utils.notify(funname, {
msg = string.format("No %s found", title),
level = "INFO",
})
return
end
if #items == 1 and opts.jump_type ~= "never" then
local item = items[1]
if opts.curr_filepath ~= item.filename then
local cmd
if opts.jump_type == "tab" then
cmd = "tabedit"
elseif opts.jump_type == "split" then
cmd = "new"
elseif opts.jump_type == "vsplit" then
cmd = "vnew"
elseif opts.jump_type == "tab drop" then
cmd = "tab drop"
end
if cmd then
vim.cmd(string.format("%s %s", cmd, item.filename))
end
end
local location = item_to_location(item, first_encoding)
vim.lsp.util.show_document(location, first_encoding, { reuse_win = opts.reuse_win })
else
pickers
.new(opts, {
prompt_title = title,
finder = finders.new_table {
results = items,
entry_maker = opts.entry_maker or make_entry.gen_from_quickfix(opts),
},
previewer = conf.qflist_previewer(opts),
sorter = conf.generic_sorter(opts),
push_cursor_on_edit = true,
push_tagstack_on_edit = true,
})
:find()
end
end)
end
lsp.references = function(opts)
opts.include_current_line = vim.F.if_nil(opts.include_current_line, false)
local params = client_position_params(opts.winnr, {
context = { includeDeclaration = vim.F.if_nil(opts.include_declaration, true) },
})
return list_or_jump("textDocument/references", "LSP References", "builtin.lsp_references", params, opts)
end
lsp.definitions = function(opts)
local params = client_position_params(opts.winnr)
return list_or_jump("textDocument/definition", "LSP Definitions", "builtin.lsp_definitions", params, opts)
end
lsp.type_definitions = function(opts)
local params = client_position_params(opts.winnr)
return list_or_jump(
"textDocument/typeDefinition",
"LSP Type Definitions",
"builtin.lsp_type_definitions",
params,
opts
)
end
lsp.implementations = function(opts)
local params = client_position_params(opts.winnr)
return list_or_jump("textDocument/implementation", "LSP Implementations", "builtin.lsp_implementations", params, opts)
end
local symbols_sorter = function(symbols)
if vim.tbl_isempty(symbols) then
return symbols
end
local current_buf = vim.api.nvim_get_current_buf()
-- sort adequately for workspace symbols
local filename_to_bufnr = {}
for _, symbol in ipairs(symbols) do
if filename_to_bufnr[symbol.filename] == nil then
filename_to_bufnr[symbol.filename] = vim.uri_to_bufnr(vim.uri_from_fname(symbol.filename))
end
symbol.bufnr = filename_to_bufnr[symbol.filename]
end
table.sort(symbols, function(a, b)
if a.bufnr == b.bufnr then
return a.lnum < b.lnum
end
if a.bufnr == current_buf then
return true
end
if b.bufnr == current_buf then
return false
end
return a.bufnr < b.bufnr
end)
return symbols
end
lsp.document_symbols = function(opts)
local params = client_position_params(opts.winnr)
vim.lsp.buf_request(opts.bufnr, "textDocument/documentSymbol", params, function(err, result, ctx, _)
if err then
vim.api.nvim_err_writeln("Error when finding document symbols: " .. err.message)
return
end
if not result or vim.tbl_isempty(result) then
utils.notify("builtin.lsp_document_symbols", {
msg = "No results from textDocument/documentSymbol",
level = "INFO",
})
return
end
local locations
if vim.fn.has "nvim-0.11" == 1 then
local client = assert(vim.lsp.get_client_by_id(ctx.client_id))
locations = vim.lsp.util.symbols_to_items(result or {}, opts.bufnr, client.offset_encoding) or {}
else
locations = vim.lsp.util.symbols_to_items(result or {}, opts.bufnr) or {}
end
locations = utils.filter_symbols(locations, opts, symbols_sorter)
if vim.tbl_isempty(locations) then
-- error message already printed in `utils.filter_symbols`
return
end
if vim.tbl_isempty(locations) then
utils.notify("builtin.lsp_document_symbols", {
msg = "No document_symbol locations found",
level = "INFO",
})
return
end
opts.path_display = { "hidden" }
pickers
.new(opts, {
prompt_title = "LSP Document Symbols",
finder = finders.new_table {
results = locations,
entry_maker = opts.entry_maker or make_entry.gen_from_lsp_symbols(opts),
},
previewer = conf.qflist_previewer(opts),
sorter = conf.prefilter_sorter {
tag = "symbol_type",
sorter = conf.generic_sorter(opts),
},
push_cursor_on_edit = true,
push_tagstack_on_edit = true,
})
:find()
end)
end
lsp.workspace_symbols = function(opts)
local params = { query = opts.query or "" }
vim.lsp.buf_request(opts.bufnr, "workspace/symbol", params, function(err, server_result, ctx, _)
if err then
vim.api.nvim_err_writeln("Error when finding workspace symbols: " .. err.message)
return
end
local locations
if vim.fn.has "nvim-0.11" == 1 then
local client = assert(vim.lsp.get_client_by_id(ctx.client_id))
locations = vim.lsp.util.symbols_to_items(server_result or {}, opts.bufnr, client.offset_encoding) or {}
else
locations = vim.lsp.util.symbols_to_items(server_result or {}, opts.bufnr) or {}
end
locations = utils.filter_symbols(locations, opts, symbols_sorter)
if vim.tbl_isempty(locations) then
-- error message already printed in `utils.filter_symbols`
return
end
if vim.tbl_isempty(locations) then
utils.notify("builtin.lsp_workspace_symbols", {
msg = "No results from workspace/symbol. Maybe try a different query: "
.. "'Telescope lsp_workspace_symbols query=example'",
level = "INFO",
})
return
end
opts.ignore_filename = vim.F.if_nil(opts.ignore_filename, false)
pickers
.new(opts, {
prompt_title = "LSP Workspace Symbols",
finder = finders.new_table {
results = locations,
entry_maker = opts.entry_maker or make_entry.gen_from_lsp_symbols(opts),
},
previewer = conf.qflist_previewer(opts),
sorter = conf.prefilter_sorter {
tag = "symbol_type",
sorter = conf.generic_sorter(opts),
},
})
:find()
end)
end
local function get_workspace_symbols_requester(bufnr, opts)
local cancel = function() end
return function(prompt)
local tx, rx = channel.oneshot()
cancel()
cancel = vim.lsp.buf_request_all(bufnr, "workspace/symbol", { query = prompt }, tx)
local results = rx() ---@type table<integer, {error: lsp.ResponseError?, result: lsp.WorkspaceSymbol?}>
local locations = {} ---@type vim.quickfix.entry[]
for client_id, client_res in pairs(results) do
if client_res.error then
vim.api.nvim_err_writeln("Error when executing workspace/symbol : " .. client_res.error.message)
elseif client_res.result ~= nil then
if vim.fn.has "nvim-0.11" == 1 then
local client = assert(vim.lsp.get_client_by_id(client_id))
vim.list_extend(locations, vim.lsp.util.symbols_to_items(client_res.result, bufnr, client.offset_encoding))
else
vim.list_extend(locations, vim.lsp.util.symbols_to_items(client_res.result, bufnr))
end
end
end
if not vim.tbl_isempty(locations) then
locations = utils.filter_symbols(locations, opts, symbols_sorter) or {}
end
return locations
end
end
lsp.dynamic_workspace_symbols = function(opts)
pickers
.new(opts, {
prompt_title = "LSP Dynamic Workspace Symbols",
finder = finders.new_dynamic {
entry_maker = opts.entry_maker or make_entry.gen_from_lsp_symbols(opts),
fn = get_workspace_symbols_requester(opts.bufnr, opts),
},
previewer = conf.qflist_previewer(opts),
sorter = sorters.highlighter_only(opts),
attach_mappings = function(_, map)
map("i", "<c-space>", actions.to_fuzzy_refine)
return true
end,
})
:find()
end
local function check_capabilities(method, bufnr)
--TODO(clason): remove when dropping support for Nvim 0.9
local get_clients = vim.fn.has "nvim-0.10" == 1 and vim.lsp.get_clients or vim.lsp.get_active_clients
local clients = get_clients { bufnr = bufnr }
for _, client in pairs(clients) do
if vim.fn.has "nvim-0.11" == 1 then
if client:supports_method(method, bufnr) then
return true
end
else
if client.supports_method(method, { bufnr = bufnr }) then
return true
end
end
end
if #clients == 0 then
utils.notify("builtin.lsp_*", {
msg = "no client attached",
level = "INFO",
})
else
utils.notify("builtin.lsp_*", {
msg = "server does not support " .. method,
level = "INFO",
})
end
return false
end
local feature_map = {
["document_symbols"] = "textDocument/documentSymbol",
["references"] = "textDocument/references",
["definitions"] = "textDocument/definition",
["type_definitions"] = "textDocument/typeDefinition",
["implementations"] = "textDocument/implementation",
["workspace_symbols"] = "workspace/symbol",
["incoming_calls"] = "callHierarchy/incomingCalls",
["outgoing_calls"] = "callHierarchy/outgoingCalls",
}
local function apply_checks(mod)
for k, v in pairs(mod) do
mod[k] = function(opts)
opts = opts or {}
local method = feature_map[k]
if method and not check_capabilities(method, opts.bufnr) then
return
end
v(opts)
end
end
return mod
end
return apply_checks(lsp)