* fix(notify): don't report request on new line * ref(notify): update message format * ref(msgs): always quote values + decrease duplication * fix(ci): undefined variables * ref(actions): temporary silent actions.__index errors * cleanup * revert: panic effort, we continue to use error for those Co-authored-by: Simon Hauser <Simon-Hauser@outlook.de>
492 lines
15 KiB
Lua
492 lines
15 KiB
Lua
local channel = require("plenary.async.control").channel
|
|
|
|
local action_state = require "telescope.actions.state"
|
|
local actions = require "telescope.actions"
|
|
local conf = require("telescope.config").values
|
|
local entry_display = require "telescope.pickers.entry_display"
|
|
local finders = require "telescope.finders"
|
|
local make_entry = require "telescope.make_entry"
|
|
local pickers = require "telescope.pickers"
|
|
local strings = require "plenary.strings"
|
|
local utils = require "telescope.utils"
|
|
|
|
local lsp = {}
|
|
|
|
lsp.references = function(opts)
|
|
local filepath = vim.api.nvim_buf_get_name(opts.bufnr)
|
|
local lnum = vim.api.nvim_win_get_cursor(opts.winnr)[1]
|
|
local params = vim.lsp.util.make_position_params(opts.winnr)
|
|
params.context = { includeDeclaration = vim.F.if_nil(opts.include_declaration, true) }
|
|
|
|
vim.lsp.buf_request(opts.bufnr, "textDocument/references", params, function(err, result, ctx, _config)
|
|
if err then
|
|
vim.api.nvim_err_writeln("Error when finding references: " .. err.message)
|
|
return
|
|
end
|
|
|
|
local locations = {}
|
|
if result then
|
|
local results = vim.lsp.util.locations_to_items(result, vim.lsp.get_client_by_id(ctx.client_id).offset_encoding)
|
|
locations = vim.tbl_filter(function(v)
|
|
-- Remove current line from result
|
|
return not (v.filename == filepath and v.lnum == lnum)
|
|
end, results or {})
|
|
end
|
|
|
|
if vim.tbl_isempty(locations) then
|
|
return
|
|
end
|
|
|
|
pickers.new(opts, {
|
|
prompt_title = "LSP References",
|
|
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),
|
|
}):find()
|
|
end)
|
|
end
|
|
|
|
local function list_or_jump(action, title, opts)
|
|
opts = opts or {}
|
|
|
|
local params = vim.lsp.util.make_position_params(opts.winnr)
|
|
vim.lsp.buf_request(opts.bufnr, action, params, function(err, result, ctx, _config)
|
|
if err then
|
|
vim.api.nvim_err_writeln("Error when executing " .. action .. " : " .. err.message)
|
|
return
|
|
end
|
|
local flattened_results = {}
|
|
if result then
|
|
-- textDocument/definition can return Location or Location[]
|
|
if not vim.tbl_islist(result) then
|
|
flattened_results = { result }
|
|
end
|
|
|
|
vim.list_extend(flattened_results, result)
|
|
end
|
|
|
|
local offset_encoding = vim.lsp.get_client_by_id(ctx.client_id).offset_encoding
|
|
|
|
if #flattened_results == 0 then
|
|
return
|
|
elseif #flattened_results == 1 and opts.jump_type ~= "never" then
|
|
if opts.jump_type == "tab" then
|
|
vim.cmd "tabedit"
|
|
elseif opts.jump_type == "split" then
|
|
vim.cmd "new"
|
|
elseif opts.jump_type == "vsplit" then
|
|
vim.cmd "vnew"
|
|
end
|
|
vim.lsp.util.jump_to_location(flattened_results[1], offset_encoding)
|
|
else
|
|
local locations = vim.lsp.util.locations_to_items(flattened_results, offset_encoding)
|
|
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),
|
|
}):find()
|
|
end
|
|
end)
|
|
end
|
|
|
|
lsp.definitions = function(opts)
|
|
return list_or_jump("textDocument/definition", "LSP Definitions", opts)
|
|
end
|
|
|
|
lsp.type_definitions = function(opts)
|
|
return list_or_jump("textDocument/typeDefinition", "LSP Type Definitions", opts)
|
|
end
|
|
|
|
lsp.implementations = function(opts)
|
|
return list_or_jump("textDocument/implementation", "LSP Implementations", opts)
|
|
end
|
|
|
|
lsp.document_symbols = function(opts)
|
|
local params = vim.lsp.util.make_position_params(opts.winnr)
|
|
vim.lsp.buf_request(opts.bufnr, "textDocument/documentSymbol", params, function(err, result, _, _)
|
|
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 = vim.lsp.util.symbols_to_items(result or {}, opts.bufnr) or {}
|
|
locations = utils.filter_symbols(locations, opts)
|
|
if locations == nil 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.ignore_filename = opts.ignore_filename or true
|
|
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),
|
|
},
|
|
}):find()
|
|
end)
|
|
end
|
|
|
|
lsp.code_actions = function(opts)
|
|
local params = vim.F.if_nil(opts.params, vim.lsp.util.make_range_params(opts.winnr))
|
|
local lnum = vim.api.nvim_win_get_cursor(opts.winnr)[1]
|
|
|
|
params.context = {
|
|
diagnostics = vim.lsp.diagnostic.get_line_diagnostics(opts.bufnr, lnum - 1),
|
|
}
|
|
|
|
local results_lsp, err = vim.lsp.buf_request_sync(
|
|
opts.bufnr,
|
|
"textDocument/codeAction",
|
|
params,
|
|
vim.F.if_nil(opts.timeout, 10000)
|
|
)
|
|
|
|
if err then
|
|
utils.notify("builin.lsp_code_actions", {
|
|
msg = err,
|
|
level = "ERROR",
|
|
})
|
|
return
|
|
end
|
|
|
|
if not results_lsp or vim.tbl_isempty(results_lsp) then
|
|
utils.notify("builtin.lsp_document_symbols", {
|
|
msg = "No results from textDocument/codeAction",
|
|
level = "INFO",
|
|
})
|
|
return
|
|
end
|
|
|
|
local idx = 1
|
|
local results = {}
|
|
local widths = {
|
|
idx = 0,
|
|
command_title = 0,
|
|
client_name = 0,
|
|
}
|
|
|
|
for client_id, response in pairs(results_lsp) do
|
|
if response.result then
|
|
local client = vim.lsp.get_client_by_id(client_id)
|
|
|
|
for _, result in pairs(response.result) do
|
|
local entry = {
|
|
idx = idx,
|
|
command_title = result.title:gsub("\r\n", "\\r\\n"):gsub("\n", "\\n"),
|
|
client = client,
|
|
client_name = client and client.name or "",
|
|
command = result,
|
|
}
|
|
|
|
for key, value in pairs(widths) do
|
|
widths[key] = math.max(value, strings.strdisplaywidth(entry[key]))
|
|
end
|
|
|
|
table.insert(results, entry)
|
|
idx = idx + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
if #results == 0 then
|
|
utils.notify("builtin.lsp_document_symbols", {
|
|
msg = "No code actions available",
|
|
level = "INFO",
|
|
})
|
|
return
|
|
end
|
|
|
|
local displayer = entry_display.create {
|
|
separator = " ",
|
|
items = {
|
|
{ width = widths.idx + 1 }, -- +1 for ":" suffix
|
|
{ width = widths.command_title },
|
|
{ width = widths.client_name },
|
|
},
|
|
}
|
|
|
|
local function make_display(entry)
|
|
return displayer {
|
|
{ entry.value.idx .. ":", "TelescopePromptPrefix" },
|
|
{ entry.value.command_title },
|
|
{ entry.value.client_name, "TelescopeResultsComment" },
|
|
}
|
|
end
|
|
|
|
-- If the text document version is 0, set it to nil instead so that Neovim
|
|
-- won't refuse to update a buffer that it believes is newer than edits.
|
|
-- See: https://github.com/eclipse/eclipse.jdt.ls/issues/1695
|
|
-- Source:
|
|
-- https://github.com/neovim/nvim-lspconfig/blob/486f72a25ea2ee7f81648fdfd8999a155049e466/lua/lspconfig/jdtls.lua#L62
|
|
local function fix_zero_version(workspace_edit)
|
|
if workspace_edit and workspace_edit.documentChanges then
|
|
for _, change in pairs(workspace_edit.documentChanges) do
|
|
local text_document = change.textDocument
|
|
if text_document and text_document.version and text_document.version == 0 then
|
|
text_document.version = nil
|
|
end
|
|
end
|
|
end
|
|
return workspace_edit
|
|
end
|
|
|
|
--[[
|
|
-- actions is (Command | CodeAction)[] | null
|
|
-- CodeAction
|
|
-- title: String
|
|
-- kind?: CodeActionKind
|
|
-- diagnostics?: Diagnostic[]
|
|
-- isPreferred?: boolean
|
|
-- edit?: WorkspaceEdit
|
|
-- command?: Command
|
|
--
|
|
-- Command
|
|
-- title: String
|
|
-- command: String
|
|
-- arguments?: any[]
|
|
--]]
|
|
local transform_action = opts.transform_action
|
|
or function(action)
|
|
-- Remove 0 -version from LSP codeaction request payload.
|
|
-- Is only run on the "java.apply.workspaceEdit" codeaction.
|
|
-- Fixed Java/jdtls compatibility with Telescope
|
|
-- See fix_zero_version commentary for more information
|
|
local command = (action.command and action.command.command) or action.command
|
|
if not (command == "java.apply.workspaceEdit") then
|
|
return action
|
|
end
|
|
local arguments = (action.command and action.command.arguments) or action.arguments
|
|
action.edit = fix_zero_version(arguments[1])
|
|
return action
|
|
end
|
|
|
|
local execute_action = opts.execute_action
|
|
or function(action, offset_encoding)
|
|
if action.edit or type(action.command) == "table" then
|
|
if action.edit then
|
|
vim.lsp.util.apply_workspace_edit(action.edit, offset_encoding)
|
|
end
|
|
if type(action.command) == "table" then
|
|
vim.lsp.buf.execute_command(action.command)
|
|
end
|
|
else
|
|
vim.lsp.buf.execute_command(action)
|
|
end
|
|
end
|
|
|
|
pickers.new(opts, {
|
|
prompt_title = "LSP Code Actions",
|
|
finder = finders.new_table {
|
|
results = results,
|
|
entry_maker = function(action)
|
|
return {
|
|
value = action,
|
|
ordinal = action.idx .. action.command_title,
|
|
display = make_display,
|
|
}
|
|
end,
|
|
},
|
|
attach_mappings = function(prompt_bufnr)
|
|
actions.select_default:replace(function()
|
|
local selection = action_state.get_selected_entry()
|
|
actions.close(prompt_bufnr)
|
|
local action = selection.value.command
|
|
local client = selection.value.client
|
|
local eff_execute = function(transformed)
|
|
execute_action(transformed, client.offset_encoding)
|
|
end
|
|
if
|
|
not action.edit
|
|
and client
|
|
and type(client.resolved_capabilities.code_action) == "table"
|
|
and client.resolved_capabilities.code_action.resolveProvider
|
|
then
|
|
client.request("codeAction/resolve", action, function(resolved_err, resolved_action)
|
|
if resolved_err then
|
|
utils.notify("builtin.lsp_code_actions", {
|
|
msg = string.format("codeAction/resolve failed: %s : %s", resolved_err.code, resolved_err.message),
|
|
level = "ERROR",
|
|
})
|
|
return
|
|
end
|
|
if resolved_action then
|
|
eff_execute(transform_action(resolved_action))
|
|
else
|
|
eff_execute(transform_action(action))
|
|
end
|
|
end)
|
|
else
|
|
eff_execute(transform_action(action))
|
|
end
|
|
end)
|
|
|
|
return true
|
|
end,
|
|
sorter = conf.generic_sorter(opts),
|
|
}):find()
|
|
end
|
|
|
|
lsp.range_code_actions = function(opts)
|
|
opts.params = vim.lsp.util.make_given_range_params({ opts.start_line, 1 }, { opts.end_line, 1 }, opts.bufnr)
|
|
lsp.code_actions(opts)
|
|
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, _, _)
|
|
if err then
|
|
vim.api.nvim_err_writeln("Error when finding workspace symbols: " .. err.message)
|
|
return
|
|
end
|
|
|
|
local locations = vim.lsp.util.symbols_to_items(server_result or {}, opts.bufnr) or {}
|
|
locations = utils.filter_symbols(locations, opts)
|
|
if locations == nil 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 = utils.get_default(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(bufnr, "workspace/symbol", { query = prompt }, tx)
|
|
|
|
-- Handle 0.5 / 0.5.1 handler situation
|
|
local err, res = rx()
|
|
assert(not err, err)
|
|
|
|
local locations = vim.lsp.util.symbols_to_items(res or {}, bufnr) or {}
|
|
if not vim.tbl_isempty(locations) then
|
|
locations = utils.filter_symbols(locations, opts) 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 = conf.generic_sorter(opts),
|
|
}):find()
|
|
end
|
|
|
|
local function check_capabilities(feature, bufnr)
|
|
local clients = vim.lsp.buf_get_clients(bufnr)
|
|
|
|
local supported_client = false
|
|
for _, client in pairs(clients) do
|
|
supported_client = client.resolved_capabilities[feature]
|
|
if supported_client then
|
|
break
|
|
end
|
|
end
|
|
|
|
if supported_client then
|
|
return true
|
|
else
|
|
if #clients == 0 then
|
|
utils.notify("builtin.lsp_*", {
|
|
msg = "no client attached",
|
|
level = "INFO",
|
|
})
|
|
else
|
|
utils.notify("builtin.lsp_*", {
|
|
msg = "server does not support " .. feature,
|
|
level = "INFO",
|
|
})
|
|
end
|
|
return false
|
|
end
|
|
end
|
|
|
|
local feature_map = {
|
|
["code_actions"] = "code_action",
|
|
["document_symbols"] = "document_symbol",
|
|
["references"] = "find_references",
|
|
["definitions"] = "goto_definition",
|
|
["type_definitions"] = "type_definition",
|
|
["implementations"] = "implementation",
|
|
["workspace_symbols"] = "workspace_symbol",
|
|
}
|
|
|
|
local function apply_checks(mod)
|
|
for k, v in pairs(mod) do
|
|
mod[k] = function(opts)
|
|
opts = opts or {}
|
|
|
|
local feature_name = feature_map[k]
|
|
if feature_name and not check_capabilities(feature_name, opts.bufnr) then
|
|
return
|
|
end
|
|
v(opts)
|
|
end
|
|
end
|
|
|
|
return mod
|
|
end
|
|
|
|
return apply_checks(lsp)
|