feat(lsp_workspace_symbols): added '<C-g>' to toggle live query:

works similar to 'grep_lgrep' action toggling between the fuzzy
matching on the entire workspace symbols and live query which
sends a new LSP query with each keystroke, fuzzy query is saved
between toggles so it can be used as an extra filter for example:
live query 'foo', press ctrl-g and enter '!.lua', to exclude all
lua files.
Addittional changes:
- diagnostics decoupled from LSP, new options under 'diagnostics'
- symbols options has their own category under 'lsp.symbols'
- 'lsp_document_diagnostics' -> 'diagnostics_document'
- 'lsp_workspace_diagnostics' -> 'diagnostics_workspace'
- backward compatibility maintained for the above changes
- all symbols providers use 'async=true' by default
main
bhagwan 2 years ago
parent 6ab7eddfc1
commit 5e4151d495

@ -186,7 +186,7 @@ vim.api.nvim_set_keymap('n', '<c-P>',
| `git_branches` | git branches |
| `git_stash` | git stash |
### LSP
### LSP/Diagnostics
| Command | List |
| ---------------- | ------------------------------------------ |
| `lsp_references` | References |
@ -198,10 +198,12 @@ vim.api.nvim_set_keymap('n', '<c-P>',
| `lsp_workspace_symbols` | Workspace Symbols |
| `lsp_live_workspace_symbols` | Workspace Symbols (live query) |
| `lsp_code_actions` | Code Actions |
| `lsp_document_diagnostics` | Document Diagnostics |
| `lsp_workspace_diagnostics` | Workspace Diagnostics |
| `lsp_incoming_calls` | Incoming Calls |
| `lsp_outgoing_calls` | Outgoing Calls |
| `diagnostics_document` | Document Diagnostics |
| `diagnostics_workspace` | Workspace Diagnostics |
| `lsp_document_diagnostics` | alias to `diagnostics_document` |
| `lsp_workspace_diagnostics` | alias to `diagnostics_workspace` |
### Misc
| Command | List |
@ -755,26 +757,43 @@ require'fzf-lua'.setup {
async_or_timeout = 5000, -- timeout(ms) or 'true' for async calls
file_icons = true,
git_icons = false,
lsp_icons = true,
ui_select = true, -- use 'vim.ui.select' for code actions
symbol_style = 1, -- style for document/workspace symbols
-- false: disable, 1: icon+kind
-- 2: icon only, 3: kind only
-- NOTE: icons are extracted from
-- vim.lsp.protocol.CompletionItemKind
-- colorize using nvim-cmp's CmpItemKindXXX highlights
-- can also be set to 'TS' for treesitter highlights ('TSProperty', etc)
-- or 'false' to disable highlighting
symbol_hl_prefix = "CmpItemKind",
-- additional symbol formatting, works with or without style
symbol_fmt = function(s) return "["..s.."]" end,
severity = "hint",
icons = {
["Error"] = { icon = "", color = "red" }, -- error
["Warning"] = { icon = "", color = "yellow" }, -- warning
["Information"] = { icon = "", color = "blue" }, -- info
["Hint"] = { icon = "", color = "magenta" }, -- hint
-- settings for 'lsp_{document|workspace|lsp_live_workspace}_symbols'
symbols = {
async_or_timeout = true, -- symbols are async by default
symbol_style = 1, -- style for document/workspace symbols
-- false: disable, 1: icon+kind
-- 2: icon only, 3: kind only
-- NOTE: icons are extracted from
-- vim.lsp.protocol.CompletionItemKind
-- colorize using nvim-cmp's CmpItemKindXXX highlights
-- can also be set to 'TS' for treesitter highlights ('TSProperty', etc)
-- or 'false' to disable highlighting
symbol_hl_prefix = "CmpItemKind",
-- additional symbol formatting, works with or without style
symbol_fmt = function(s) return "["..s.."]" end,
},
},
diagnostics ={
prompt = 'Diagnostics ',
cwd_only = false, -- limit to cwd only?
file_icons = true,
git_icons = false,
diag_icons = true,
severity_icons = {
["Error"] = { icon = "", color = "red" },
["Warning"] = { icon = "", color = "yellow" },
["Information"] = { icon = "", color = "blue" },
["Hint"] = { icon = "", color = "magenta" },
},
-- limit to specific severity, use either a string or num:
-- 1 or "hint"
-- 2 or "information"
-- 3 or "warning"
-- 4 or "error"
-- severity_only: keep any matching exact severity
-- severity_limit: keep any equal or more severe (lower)
-- severity_bound: keep any equal or less severe (higher)
},
-- uncomment to use the old help previewer which used a
-- minimized help window to generate the help tag preview

@ -630,5 +630,30 @@ M.grep_lgrep = function(_, opts)
end
end
M.sym_lsym = function(_, opts)
assert(opts.__MODULE__
and type(opts.__MODULE__.workspace_symbols) == 'function'
or type(opts.__MODULE__.live_workspace_symbols) == 'function')
local o = vim.tbl_extend("keep", {
resume = true,
lsp_query = false,
-- ws has both search string and query prompt, when
-- switching from live_ws to ws we want to restore both:
-- * we save the last query prompt when exiting ws
-- * we set query to the last known when entering ws
__prev_query = not opts.fn_reload and opts.__resume_data.last_query,
query = opts.fn_reload and opts.__call_opts.__prev_query,
}, opts.__call_opts or {})
-- 'fn_reload' is set only on 'live_xxx' calls
if opts.fn_reload then
opts.__MODULE__.workspace_symbols(o)
else
opts.__MODULE__.live_workspace_symbols(o)
end
end
return M

@ -299,9 +299,7 @@ M.globals.grep = {
grep_opts = "--binary-files=without-match --line-number --recursive --color=auto --perl-regexp",
rg_opts = "--column --line-number --no-heading --color=always --smart-case --max-columns=512",
_actions = function() return M.globals.actions.files end,
actions = {
["ctrl-g"] = { actions.grep_lgrep }
},
actions = { ["ctrl-g"] = { actions.grep_lgrep } },
-- live_grep_glob options
glob_flag = "--iglob", -- for case sensitive globs use '--glob'
glob_separator = "%s%-%-", -- query separator pattern (lua): ' --'
@ -423,9 +421,7 @@ M.globals.tags = {
git_icons = true,
color_icons = true,
_actions = function() return M.globals.actions.files end,
actions = {
["ctrl-g"] = { actions.grep_lgrep }
},
actions = { ["ctrl-g"] = { actions.grep_lgrep } },
}
M.globals.btags = {
previewer = { _ctor = previewers.builtin.tags },
@ -493,19 +489,37 @@ M.globals.lsp = {
file_icons = true and M._has_devicons,
color_icons = true,
git_icons = false,
lsp_icons = true,
cwd_only = false,
ui_select = true,
async_or_timeout = 5000,
_actions = function() return M.globals.actions.files end,
}
M.globals.lsp.symbols = {
previewer = M._default_previewer_fn,
prompt_postfix = '> ',
file_icons = true and M._has_devicons,
color_icons = true,
git_icons = false,
symbol_style = 1,
symbol_hl_prefix = "CmpItemKind",
symbol_fmt = function(s) return "["..s.."]" end,
async_or_timeout = 5000,
async_or_timeout = true,
_actions = function() return M.globals.actions.files end,
actions = { ["ctrl-g"] = { actions.sym_lsym } },
}
M.globals.diagnostics = {
previewer = M._default_previewer_fn,
prompt = 'Diagnostics> ',
file_icons = true and M._has_devicons,
color_icons = true,
git_icons = false,
diag_icons = true,
_actions = function() return M.globals.actions.files end,
icons = {
["Error"] = { icon = "", color = "red" }, -- error
["Warning"] = { icon = "", color = "yellow" }, -- warning
["Information"] = { icon = "", color = "blue" }, -- info
["Hint"] = { icon = "", color = "magenta" }, -- hint
severity_icons = {
["Error"] = { icon = "", color = "red" },
["Warning"] = { icon = "", color = "yellow" },
["Information"] = { icon = "", color = "blue" },
["Hint"] = { icon = "", color = "magenta" },
},
}
M.globals.builtin = {
@ -804,6 +818,12 @@ function M.normalize_opts(opts, defaults)
['winopts.preview.title'] = 'previewers.builtin.title',
['winopts.preview.scrollbar'] = 'previewers.builtin.scrollbar',
['winopts.preview.scrollchar'] = 'previewers.builtin.scrollchar',
-- Diagnostics & LSP symbols separation options
['symbol_fmt'] = 'lsp.symbol_fmt',
['symbol_style'] = 'lsp.symbol_style',
['symbol_hl_prefix'] = 'lsp.symbol_hl_prefix',
['diag_icons'] = 'lsp.lsp_icons',
['severity_icons'] = 'lsp.icons',
}
-- recursive key loopkup, can also set new value
@ -852,6 +872,7 @@ function M.normalize_opts(opts, defaults)
end
end
-- test for valid git_repo
opts.git_icons = opts.git_icons and path.is_git_repo(opts, true)
@ -968,6 +989,7 @@ M._action_to_helpstr = {
[actions.arg_add] = "arg-list-add",
[actions.arg_del] = "arg-list-delete",
[actions.grep_lgrep] = "grep<->lgrep",
[actions.sym_lsym] = "sym<->lsym",
}
return M

@ -208,12 +208,9 @@ M.fzf = function(opts, contents)
-- some functions such as buffers|tabs
-- need to reacquire current buffer|tab state
if opts._fn_pre_fzf then
opts._fn_pre_fzf(opts)
end
if opts.fn_pre_fzf then
opts.fn_pre_fzf(opts)
end
if opts.__fn_pre_fzf then opts.__fn_pre_fzf(opts) end
if opts._fn_pre_fzf then opts._fn_pre_fzf(opts) end
if opts.fn_pre_fzf then opts.fn_pre_fzf(opts) end
fzf_win:attach_previewer(previewer)
fzf_win:create()
@ -237,12 +234,9 @@ M.fzf = function(opts, contents)
end
table.remove(selected, 1)
end
if opts._fn_post_fzf then
opts._fn_post_fzf(opts, selected)
end
if opts.fn_post_fzf then
opts.fn_post_fzf(opts, selected)
end
if opts.__fn_post_fzf then opts.__fn_post_fzf(opts, selected) end
if opts._fn_post_fzf then opts._fn_post_fzf(opts, selected) end
if opts.fn_post_fzf then opts.fn_post_fzf(opts, selected) end
libuv.process_kill(opts._pid)
fzf_win:check_exit_status(exit_code)
-- retrieve the future action and check:
@ -567,12 +561,12 @@ M.set_header = function(opts, hdr_tbl)
return opts.search and #opts.search>0 and opts.search
end,
},
query = {
hdr_txt_opt = "query_header",
lsp_query = {
hdr_txt_opt = "lsp_query_header",
hdr_txt_str = "Query: ",
hdr_txt_col = "red",
val = function()
return opts.query and #opts.query>0 and opts.query
return opts.lsp_query and #opts.lsp_query>0 and opts.lsp_query
end,
},
regex_filter = {
@ -584,13 +578,19 @@ M.set_header = function(opts, hdr_tbl)
end,
},
actions = {
hdr_txt_opt = "grep_lgrep_header",
hdr_txt_opt = "interactive_header",
hdr_txt_str = "",
val = function()
local is_lsp = opts.__MODULE__.workspace_symbols
local o = {
action = is_lsp and actions.sym_lsym or actions.grep_lgrep,
to_live = is_lsp and "Live Query" or "Regex Search",
to_fuzzy = is_lsp and "Fuzzy Search" or "Fuzzy Search",
}
if opts.no_header_i then return end
for k, v in pairs(opts.actions) do
if type(v) == 'table' and v[1] == actions.grep_lgrep then
local to = opts.fn_reload and 'Grep' or 'Live Grep'
if type(v) == 'table' and v[1] == o.action then
local to = opts.fn_reload and o.to_fuzzy or o.to_live
return (':: <%s> to %s'):format(
utils.ansi_codes.yellow(k),
utils.ansi_codes.red(to))

@ -139,11 +139,15 @@ M.lsp_document_symbols = require'fzf-lua.providers.lsp'.document_symbols
M.lsp_workspace_symbols = require'fzf-lua.providers.lsp'.workspace_symbols
M.lsp_live_workspace_symbols = require'fzf-lua.providers.lsp'.live_workspace_symbols
M.lsp_code_actions = require'fzf-lua.providers.lsp'.code_actions
M.lsp_document_diagnostics = require'fzf-lua.providers.lsp'.diagnostics
M.lsp_workspace_diagnostics = require'fzf-lua.providers.lsp'.workspace_diagnostics
M.lsp_incoming_calls = require'fzf-lua.providers.lsp'.incoming_calls
M.lsp_outgoing_calls = require'fzf-lua.providers.lsp'.outgoing_calls
-- backward compat
M.lsp_document_diagnostics = require'fzf-lua.providers.diagnostic'.diagnostics
M.lsp_workspace_diagnostics = require'fzf-lua.providers.diagnostic'.all
M.diagnostics_document = require'fzf-lua.providers.diagnostic'.diagnostics
M.diagnostics_workspace = require'fzf-lua.providers.diagnostic'.all
M.register_ui_select = require'fzf-lua.providers.ui_select'.register
M.deregister_ui_select = require'fzf-lua.providers.ui_select'.deregister

@ -0,0 +1,193 @@
local core = require "fzf-lua.core"
local utils = require "fzf-lua.utils"
local config = require "fzf-lua.config"
local make_entry = require "fzf-lua.make_entry"
local M = {}
local convert_diagnostic_type = function(severity)
-- convert from string to int
if type(severity) == "string" and not tonumber(severity) then
-- make sure that e.g. error is uppercased to Error
return vim.diagnostic and vim.diagnostic.severity[severity:upper()] or
vim.lsp.protocol.DiagnosticSeverity[severity:gsub("^%l", string.upper)]
else
-- otherwise keep original value, incl. nil
return tonumber(severity)
end
end
local filter_diag_severity = function(opts, severity)
if opts.severity_only ~= nil then
return tonumber(opts.severity_only) == severity
elseif opts.severity_limit ~= nil then
return severity <= tonumber(opts.severity_limit)
elseif opts.severity_bound ~= nil then
return severity >= tonumber(opts.severity_bound)
else
return true
end
end
M.diagnostics = function(opts)
opts = config.normalize_opts(opts, config.globals.diagnostics)
if not opts then return end
-- required for relative paths presentation
if not opts.cwd or #opts.cwd == 0 then
opts.cwd = vim.loop.cwd()
end
if not vim.diagnostic then
local lsp_clients = vim.lsp.buf_get_clients(0)
if utils.tbl_isempty(lsp_clients) then
utils.info("LSP: no client attached")
return
end
end
-- normalize the LSP icons table
opts._severity_icons = {}
for k, v in pairs({
["Error"] = 1,
["Warning"] = 2,
["Information"] = 3,
["Hint"] = 4
}) do
if opts.severity_icons and opts.severity_icons[k] then
opts._severity_icons[v] = opts.severity_icons[k]
end
end
-- hint = 4
-- information = 3
-- warning = 2
-- error = 1
-- severity_only: keep any matching exact severity
-- severity_limit: keep any equal or more severe (lower)
-- severity_bound: keep any equal or less severe (higher)
opts.severity_only = convert_diagnostic_type(opts.severity_only)
opts.severity_limit = convert_diagnostic_type(opts.severity_limit)
opts.severity_bound = convert_diagnostic_type(opts.severity_bound)
local diag_opts = { severity = {}, namespace = opts.namespace }
if opts.severity_only ~= nil then
if opts.severity_limit ~= nil or opts.severity_bound ~= nil then
utils.warn("Invalid severity parameters. Both a specific severity and a limit/bound is not allowed")
return {}
end
diag_opts.severity = opts.severity_only
else
if opts.severity_limit ~= nil then
diag_opts.severity["min"] = opts.severity_limit
end
if opts.severity_bound ~= nil then
diag_opts.severity["max"] = opts.severity_bound
end
end
local curbuf = vim.api.nvim_get_current_buf()
local diag_results = vim.diagnostic and
vim.diagnostic.get(not opts.diag_all and curbuf or nil, diag_opts) or
opts.diag_all and vim.lsp.diagnostic.get_all() or
{[curbuf] = vim.lsp.diagnostic.get(curbuf, opts.client_id)}
local has_diags = false
if vim.diagnostic then
-- format: { <diag array> }
has_diags = not vim.tbl_isempty(diag_results)
else
-- format: { [bufnr] = <diag array>, ... }
for _, diags in pairs(diag_results) do
if #diags > 0 then has_diags = true end
end
end
if not has_diags then
utils.info(string.format('No %s found', 'diagnostics'))
return
end
local preprocess_diag = function(diag, bufnr)
bufnr = bufnr or diag.bufnr
local filename = vim.api.nvim_buf_get_name(bufnr)
-- pre vim.diagnostic (vim.lsp.diagnostic)
-- has 'start|finish' instead of 'end_col|end_lnum'
local start = diag.range and diag.range['start']
-- local finish = diag.range and diag.range['end']
local row = diag.lnum or start.line
local col = diag.col or start.character
local buffer_diag = {
bufnr = bufnr,
filename = filename,
lnum = row + 1,
col = col + 1,
text = vim.trim(diag.message:gsub("[\n]", "")),
type = diag.severity or 1
}
return buffer_diag
end
local contents = function (fzf_cb)
coroutine.wrap(function ()
local co = coroutine.running()
local function process_diagnostics(diags, bufnr)
for _, diag in ipairs(diags) do
-- workspace diagnostics may include
-- empty tables for unused buffers
if not vim.tbl_isempty(diag) and filter_diag_severity(opts, diag.severity) then
-- wrap with 'vim.scheudle' or calls to vim.{fn|api} fail:
-- E5560: vimL function must not be called in a lua loop callback
vim.schedule(function()
local diag_entry = preprocess_diag(diag, bufnr)
local entry = make_entry.lcol(diag_entry, opts)
entry = make_entry.file(entry, opts)
if not entry then
-- entry to be skipped (e.g. 'cwd_only')
coroutine.resume(co)
else
local type = diag_entry.type
if opts.diag_icons and opts._severity_icons[type] then
local severity = opts._severity_icons[type]
local icon = severity.icon
if opts.color_icons then
icon = utils.ansi_codes[severity.color or "dark_grey"](icon)
end
entry = icon .. utils.nbsp .. utils.nbsp .. entry
end
fzf_cb(entry, function() coroutine.resume(co) end)
end
end)
-- wait here for 'vim.schedule' to return
coroutine.yield()
end
end
end
if vim.diagnostic then
process_diagnostics(diag_results)
else
for bufnr, diags in pairs(diag_results) do
process_diagnostics(diags, bufnr)
end
end
-- close the pipe to fzf, this
-- removes the loading indicator
fzf_cb(nil)
end)()
end
opts = core.set_header(opts, opts.headers or {"cwd"})
opts = core.set_fzf_field_index(opts)
return core.fzf_exec(contents, opts)
end
M.all = function(opts)
if not opts then opts = {} end
opts.diag_all = true
return M.diagnostics(opts)
end
return M

@ -13,11 +13,9 @@ end
local jump_to_location = function(opts, result, enc)
local winid = vim.api.nvim_get_current_win()
if opts.winid ~= winid then
-- utils.send_ctrl_c()
vim.api.nvim_win_close(0, false)
end
-- exists the fzf window when use with async
-- safe to call even if the interafce is closed
utils.fzf_exit()
local action = opts.jump_to_single_result_action
if action then
@ -37,15 +35,10 @@ local function location_handler(opts, cb, _, result, ctx, _)
end
local items = vim.lsp.util.locations_to_items(result, encoding)
for _, entry in ipairs(items) do
if not opts.current_buffer_only or
vim.api.nvim_buf_get_name(opts.bufnr) == entry.filename then
if not opts.current_buffer_only or __CTX.bufname == entry.filename then
entry = make_entry.lcol(entry, opts)
entry = make_entry.file(entry, opts)
if entry then
cb(entry, function(err)
if err then return end
end)
end
if entry then cb(entry) end
end
end
end
@ -55,32 +48,24 @@ local function call_hierarchy_handler(opts, cb, _, result, _, _)
--- "from" for incoming calls and "to" for outgoing calls
local call_hierarchy_item = call_hierarchy_call.from or call_hierarchy_call.to
for _, range in pairs(call_hierarchy_call.fromRanges) do
local entry = {
local location = {
filename = assert(vim.uri_to_fname(call_hierarchy_item.uri)),
text = call_hierarchy_item.name,
lnum = range.start.line + 1,
col = range.start.character + 1,
}
entry = make_entry.lcol(entry, opts)
local entry = make_entry.lcol(location, opts)
entry = make_entry.file(entry, opts)
if entry then
cb(entry, function(err)
if err then return end
end)
end
if entry then cb(entry) end
end
end
end
local function symbol_handler(opts, cb, _, result, _, _)
result = vim.tbl_islist(result) and result or {result}
local items = vim.lsp.util.symbols_to_items(result, 0)
local items = vim.lsp.util.symbols_to_items(result, __CTX.bufnr)
for _, entry in ipairs(items) do
if opts.ignore_filename then
entry.filename = opts.filename
end
if (not opts.current_buffer_only or
vim.api.nvim_buf_get_name(opts.bufnr) == entry.filename) and
if (not opts.current_buffer_only or __CTX.bufname == entry.filename) and
(not opts.regex_filter or entry.text:match(opts.regex_filter)) then
if M._sym2style then
local kind = entry.text:match("%[(.-)%]")
@ -90,18 +75,14 @@ local function symbol_handler(opts, cb, _, result, _, _)
end
entry = make_entry.lcol(entry, opts)
entry = make_entry.file(entry, opts)
if entry then
cb(entry, function(err)
if err then return end
end)
end
if entry then cb(entry) end
end
end
end
local function code_action_handler(opts, cb, _, code_actions, context, _)
if not opts.code_actions then opts.code_actions = {} end
local i = utils.tbl_length(opts.code_actions) + 1
local i = vim.tbl_count(opts.code_actions) + 1
for _, action in ipairs(code_actions) do
local text = string.format("%s %s",
utils.ansi_codes.magenta(string.format("%d:", i)),
@ -114,28 +95,11 @@ local function code_action_handler(opts, cb, _, code_actions, context, _)
command = action,
}
opts.code_actions[tostring(i)] = entry
cb(text, function(err)
if err then return end
end)
cb(text)
i = i + 1
end
end
local function diagnostics_handler(opts, cb, co, entry)
local type = entry.type
entry = make_entry.lcol(entry, opts)
entry = make_entry.file(entry, opts)
if not entry then return end
if opts.lsp_icons and opts._severity_icons[type] then
local severity = opts._severity_icons[type]
local icon = severity.icon
if opts.color_icons then
icon = utils.ansi_codes[severity.color or "dark_grey"](icon)
end
entry = icon .. utils.nbsp .. utils.nbsp .. entry
end
cb(entry, function() coroutine.resume(co) end)
end
-- see neovim #15504
-- https://github.com/neovim/neovim/pull/15504#discussion_r698424017
@ -153,58 +117,41 @@ local mk_handler = function(fn)
local client_id = select(4, ...)
local bufnr = select(5, ...)
local lspcfg = select(6, ...)
fn(err, result, { method = method, client_id = client_id, bufnr = bufnr }, lspcfg)
fn(err, result,
{ method = method, client_id = client_id, bufnr = bufnr }, lspcfg)
end
end
end
local function wrap_handler(handler, opts, cb, co)
local function async_lsp_handler(co, handler, opts)
return mk_handler(function(err, result, context, lspcfg)
-- increment callback & result counters
opts.num_callbacks = opts.num_callbacks+1
opts.num_results = opts.num_results or 0 + result and utils.tbl_length(result) or 0
local ret
opts.num_results = (opts.num_results or 0) +
(result and vim.tbl_count(result) or 0)
if err then
ret = err
utils.err(string.format("Error executing '%s': %s", handler.method, err))
utils.send_ctrl_c()
utils.fzf_exit()
coroutine.resume(co, err)
elseif not result or vim.tbl_isempty(result) then
-- Only close the window if all clients sent their results
if opts.num_callbacks == opts.num_clients and opts.num_results == 0 then
ret = utils.info(string.format('No %s found', string.lower(handler.label)))
utils.send_ctrl_c()
-- Do not close the window for 'live_workspace_symbols'
if not opts.fn_reload then
utils.info(string.format('No %s found', string.lower(handler.label)))
utils.fzf_exit()
end
coroutine.resume(co)
end
else
ret = opts.lsp_handler.handler(opts, cb, co, result, context, lspcfg)
if opts.num_callbacks == opts.num_clients then
-- close the pipe to fzf, this
-- removes the loading indicator in fzf
cb(nil)
end
local done = opts.num_callbacks == opts.num_clients
coroutine.resume(co, err, result, context, lspcfg, done)
end
return ret
end)
end
local function set_lsp_fzf_fn(opts)
-- we must make the params here while we're on
-- our current buffer window, anything inside
-- fzf_fn is run while fzf term is open
opts.bufnr = opts.bufnr or vim.api.nvim_get_current_buf()
opts.winid = opts.winid or vim.api.nvim_get_current_win()
opts.filename = vim.api.nvim_buf_get_name(opts.bufnr)
if not opts.lsp_params then
opts.lsp_params = vim.lsp.util.make_position_params(0)
opts.lsp_params.context = { includeDeclaration = true }
end
-- Save no of attached clients so we can determine
-- if all callbacks were completed
opts.num_results = 0
opts.num_callbacks = 0
opts.num_clients = utils.tbl_length(vim.lsp.buf_get_clients(0))
-- consider 'async_or_timeout' only if
-- 'sync|async' wasn't manually set
if opts.sync == nil and opts.async == nil then
@ -215,12 +162,21 @@ local function set_lsp_fzf_fn(opts)
end
end
-- build positional params for the LSP query
-- from the context buffer and cursor position
if not opts.lsp_params then
opts.lsp_params = vim.lsp.util.make_position_params(__CTX.winid)
opts.lsp_params.context = { includeDeclaration = true }
end
if opts.sync or opts.async == false then
-- SYNC
local timeout = 5000
if type(opts.async_or_timeout) == "number" then
timeout = opts.async_or_timeout
end
local lsp_results, err = vim.lsp.buf_request_sync(opts.bufnr,
local lsp_results, err = vim.lsp.buf_request_sync(__CTX.bufnr,
opts.lsp_handler.method, opts.lsp_params, timeout)
if err then
utils.err(string.format("Error executing '%s': %s",
@ -235,66 +191,87 @@ local function set_lsp_fzf_fn(opts)
end
end
if vim.tbl_isempty(results) then
utils.info(string.format('No %s found', string.lower(opts.lsp_handler.label)))
if not opts.fn_reload then
utils.info(string.format('No %s found', string.lower(opts.lsp_handler.label)))
end
elseif not (opts.jump_to_single_result and #results == 1) then
opts.fzf_fn = results
-- LSP request was synchronou but we can
-- still async the fzf feeding
opts.fzf_fn = function (fzf_cb)
coroutine.wrap(function ()
local co = coroutine.running()
for _, e in ipairs(results) do
fzf_cb(e, function() coroutine.resume(co) end)
coroutine.yield()
end
fzf_cb(nil)
end)()
end
end
end
return opts
end
else
opts.fzf_fn = function (cb)
coroutine.wrap(function ()
local co = coroutine.running()
-- cancel all remaining LSP requests once the user
-- made their selection or closed the fzf popup
local fn_cancel_all = function(o)
if o and o._cancel_all then
o._cancel_all()
o._cancel_all = nil
end
end
opts._fn_post_fzf = fn_cancel_all
-- ASYNC
opts.fzf_fn = function (fzf_cb)
coroutine.wrap(function ()
local co = coroutine.running()
-- Save no of attached clients so we can determine
-- if all callbacks were completed
opts.num_results = 0
opts.num_callbacks = 0
opts.num_clients = vim.tbl_count(vim.lsp.buf_get_clients(__CTX.bufnr))
-- when used with 'live_workspace_symbols'
-- cancel all lingering LSP queries
fn_cancel_all(opts)
local _, cancel_all = vim.lsp.buf_request(__CTX.bufnr,
opts.lsp_handler.method, opts.lsp_params,
async_lsp_handler(co, opts.lsp_handler, opts))
-- save this so we can cancel all requests
-- when using `live_ws_symbols`
opts._cancel_all = cancel_all
-- process results from all LSP client
local err, result, context, lspcfg, done
repeat
err, result, context, lspcfg, done = coroutine.yield()
if not err and type(result) == 'table' then
local cb = function(e)
fzf_cb(e, function() coroutine.resume(co) end)
coroutine.yield()
end
opts.lsp_handler.handler(opts, cb,
opts.lsp_handler.method, result, context, lspcfg)
end
until done or err or result == nil
-- reset number of callbacks incase
-- we're being called from 'resume'
opts.num_callbacks = 0
-- no more results
fzf_cb(nil)
-- cancel all currently running requests
-- can happen when using `live_ws_symbols`
if opts._cancel_all then
opts._cancel_all()
-- we only get here once all requests are done
-- so we can clear '_cancel_all'
opts._cancel_all = nil
end
-- local cancel_all = vim.lsp.buf_request_all(opts.bufnr,
-- opts.lsp_handler.method, opts.lsp_params,
-- wrap_request_all(opts.lsp_handler))
local _, cancel_all = vim.lsp.buf_request(opts.bufnr,
opts.lsp_handler.method, opts.lsp_params,
wrap_handler(opts.lsp_handler, opts, cb, co))
-- save this so we can cancel all requests
-- when using `live_ws_symbols`
opts._cancel_all = cancel_all
-- cancel all remaining LSP requests
-- once the user made their selection
-- or closed the fzf popup
opts._fn_post_fzf = function()
if opts._cancel_all then
opts._cancel_all()
opts._cancel_all = nil
end
end
end)()
end)()
end
end
return opts
end
local set_async_default = function(opts, bool)
if not opts then opts = {} end
if opts.sync == nil and
opts.async == nil then
opts.async = bool
end
return opts
end
local normalize_lsp_opts = function(opts, cfg)
opts = config.normalize_opts(opts, cfg)
if not opts then return end
@ -303,13 +280,22 @@ local normalize_lsp_opts = function(opts, cfg)
opts.prompt = opts.lsp_handler.label .. (opts.prompt_postfix or '')
end
opts.bufnr = nil
opts.winid = nil
opts.filename = nil
-- required for relative paths presentation
if not opts.cwd or #opts.cwd == 0 then
opts.cwd = vim.loop.cwd()
end
-- save current win/buf context
-- ignore when fzf window is already open
if not __CTX or not utils.fzf_winobj() then
__CTX = {
winid = vim.api.nvim_get_current_win(),
bufnr = vim.api.nvim_get_current_buf(),
bufname = vim.api.nvim_buf_get_name(0)
}
end
opts.code_actions = nil
opts.num_results = nil
opts.num_callbacks = nil
opts.num_clients = nil
return opts
end
@ -317,9 +303,8 @@ end
local function fzf_lsp_locations(opts)
opts = normalize_lsp_opts(opts, config.globals.lsp)
if not opts then return end
opts = core.set_header(opts, opts.headers or {"cwd"})
opts = core.set_fzf_field_index(opts)
if opts.force_uri == nil then opts.force_uri = true end
opts = core.set_fzf_field_index(opts)
opts = set_lsp_fzf_fn(opts)
if not opts.fzf_fn then return end
return core.fzf_exec(opts.fzf_fn, opts)
@ -348,7 +333,7 @@ end
-- see $VIMRUNTIME/lua/vim/buf.lua:pick_call_hierarchy_item()
M.call_hierarchy = function(opts)
opts.lsp_params = vim.lsp.util.make_position_params(0)
opts.lsp_params = vim.lsp.util.make_position_params(__CTX.winid)
local method = "textDocument/prepareCallHierarchy"
local res, err = vim.lsp.buf_request_sync(
0, method, opts.lsp_params, 2000)
@ -418,10 +403,10 @@ local function gen_sym2style_map(opts)
end
M.document_symbols = function(opts)
opts = set_async_default(opts, true)
opts = normalize_lsp_opts(opts, config.globals.lsp)
opts = normalize_lsp_opts(opts, config.globals.lsp.symbols)
if not opts then return end
opts = core.set_header(opts, opts.headers or {"cwd","regex_filter"})
opts.__MODULE__ = opts.__MODULE__ or M
opts = core.set_header(opts, opts.headers or {"regex_filter"})
opts = core.set_fzf_field_index(opts)
if opts.force_uri == nil then opts.force_uri = true end
if not opts.fzf_opts or opts.fzf_opts['--with-nth'] == nil then
@ -429,7 +414,6 @@ M.document_symbols = function(opts)
opts.fzf_opts["--with-nth"] = '2..'
opts.fzf_opts["--tiebreak"] = 'index'
end
opts.ignore_filename = true
opts = set_lsp_fzf_fn(opts)
if not opts.fzf_fn then return end
if opts.symbol_style or opts.symbol_fmt then
@ -439,29 +423,109 @@ M.document_symbols = function(opts)
return core.fzf_exec(opts.fzf_fn, opts)
end
local function get_last_lspquery(opts)
if opts.__MODULE__ and opts.__MODULE__.get_last_search then
return opts.__MODULE__.get_last_lspquery(opts)
end
return M.__last_ws_lsp_query
end
local function set_last_lspquery(opts, query)
if opts.__MODULE__ and opts.__MODULE__.set_last_search then
opts.__MODULE__.set_last_lspquery(opts, query)
return
end
M.__last_ws_lsp_query = query
if config.__resume_data then
config.__resume_data.last_query = query
end
end
M.workspace_symbols = function(opts)
opts = set_async_default(opts, true)
opts = normalize_lsp_opts(opts, config.globals.lsp)
opts = normalize_lsp_opts(opts, config.globals.lsp.symbols)
if not opts then return end
opts.__MODULE__ = opts.__MODULE__ or M
if not opts.lsp_query and opts.resume then
opts.lsp_query = get_last_lspquery(opts)
end
set_last_lspquery(opts, opts.lsp_query)
opts.lsp_params = {query = opts.lsp_query or ''}
opts = core.set_header(opts, opts.headers or {"cwd","query","regex_filter"})
opts = core.set_header(opts, opts.headers or
{ "actions", "cwd", "lsp_query", "regex_filter" })
opts = core.set_fzf_field_index(opts)
if opts.force_uri == nil then opts.force_uri = true end
opts = set_lsp_fzf_fn(opts)
if not opts.fzf_fn then return end
if opts.symbol_style or opts.symbol_fmt then
opts.fn_pre_fzf = function() gen_sym2style_map(opts) end
opts.fn_post_fzf = function() M._sym2style = nil end
-- when using an empty string grep (as in 'grep_project') or
-- when switching from grep to live_grep using 'ctrl-g' users
-- may find it confusing why is the last typed query not
-- considered the last search so we find out if that's the
-- case and use the last typed prompt as the grep string
opts.fn_post_fzf = function(o, _)
M._sym2style = nil
local last_lspquery = get_last_lspquery(o)
local last_query = config.__resume_data and config.__resume_data.last_query
if not last_lspquery or #last_lspquery==0
and (last_query and #last_query>0) then
set_last_lspquery(opts, last_query)
end
end
end
return core.fzf_exec(opts.fzf_fn, opts)
end
M.live_workspace_symbols = function(opts)
opts = normalize_lsp_opts(opts, config.globals.lsp.symbols)
if not opts then return end
-- needed by 'actions.sym_lsym'
-- prepend the prompt with asterisk
opts.__MODULE__ = opts.__MODULE__ or M
opts.prompt = opts.prompt and opts.prompt:match("^%*") or '*'..opts.prompt
-- exec empty query is the default here
if opts.exec_empty_query == nil then
opts.exec_empty_query = true
end
if not opts.lsp_query and opts.resume then
opts.lsp_query = get_last_lspquery(opts)
end
-- sent to the LSP server
opts.lsp_params = {query = opts.lsp_query or opts.query or ''}
opts.query = opts.lsp_query or opts.query
-- don't use the automatic coroutine since we
-- use our own
opts.func_async_callback = false
opts.fn_reload = function(query)
if query and not (opts.save_last_search == false) then
set_last_lspquery(opts, query)
end
opts.lsp_params = {query = query or ''}
opts = set_lsp_fzf_fn(opts)
return opts.fzf_fn
end
opts = core.set_header(opts, opts.headers or {"actions", "cwd", "regex_filter"})
opts = core.set_fzf_field_index(opts)
if opts.force_uri == nil then opts.force_uri = true end
if opts.symbol_style or opts.symbol_fmt then
opts.fn_pre_fzf = function() gen_sym2style_map(opts) end
opts.fn_post_fzf = function() M._sym2style = nil end
end
core.fzf_exec(nil, opts)
end
-- Converts 'vim.diagnostic.get' to legacy style 'get_line_diagnostics()'
local function get_line_diagnostics(opts)
local function get_line_diagnostics(_)
if not vim.diagnostic then
return vim.lsp.diagnostic.get_line_diagnostics()
end
local diag = vim.diagnostic.get(opts.bufnr, {lnum = vim.api.nvim_win_get_cursor(0)[1]-1})
local diag = vim.diagnostic.get(__CTX.bufnr, {lnum = vim.api.nvim_win_get_cursor(0)[1]-1})
return diag and diag[1] and {{
source = diag[1].source,
message = diag[1].message,
@ -626,219 +690,9 @@ M.code_actions = function(opts)
end
local convert_diagnostic_type = function(severity)
-- convert from string to int
if type(severity) == "string" and not tonumber(severity) then
-- make sure that e.g. error is uppercased to Error
return vim.diagnostic and vim.diagnostic.severity[severity:upper()] or
vim.lsp.protocol.DiagnosticSeverity[severity:gsub("^%l", string.upper)]
else
-- otherwise keep original value, incl. nil
return tonumber(severity)
end
end
local filter_diag_severity = function(opts, severity)
if opts.severity_only ~= nil then
return tonumber(opts.severity_only) == severity
elseif opts.severity_limit ~= nil then
return severity <= tonumber(opts.severity_limit)
elseif opts.severity_bound ~= nil then
return severity >= tonumber(opts.severity_bound)
else
return true
end
end
M.diagnostics = function(opts)
opts = normalize_lsp_opts(opts, config.globals.lsp)
if not opts then return end
if not vim.diagnostic then
local lsp_clients = vim.lsp.buf_get_clients(0)
if utils.tbl_isempty(lsp_clients) then
utils.info("LSP: no client attached")
return
end
end
opts.winid = vim.api.nvim_get_current_win()
local current_buf = vim.api.nvim_get_current_buf()
-- normalize the LSP icons table
opts._severity_icons = {}
for k, v in pairs({
["Error"] = 1,
["Warning"] = 2,
["Information"] = 3,
["Hint"] = 4
}) do
if opts.icons and opts.icons[k] then
opts._severity_icons[v] = opts.icons[k]
end
end
-- hint = 4
-- information = 3
-- warning = 2
-- error = 1
-- severity_only: keep any matching exact severity
-- severity_limit: keep any equal or more severe (lower)
-- severity_bound: keep any equal or less severe (higher)
opts.severity_only = convert_diagnostic_type(opts.severity_only)
opts.severity_limit = convert_diagnostic_type(opts.severity_limit)
opts.severity_bound = convert_diagnostic_type(opts.severity_bound)
local diag_opts = { severity = {}, namespace = opts.namespace }
if opts.severity_only ~= nil then
if opts.severity_limit ~= nil or opts.severity_bound ~= nil then
utils.warn("Invalid severity parameters. Both a specific severity and a limit/bound is not allowed")
return {}
end
diag_opts.severity = opts.severity_only
else
if opts.severity_limit ~= nil then
diag_opts.severity["min"] = opts.severity_limit
end
if opts.severity_bound ~= nil then
diag_opts.severity["max"] = opts.severity_bound
end
end
local diag_results = vim.diagnostic and
vim.diagnostic.get(not opts.diag_all and current_buf or nil, diag_opts) or
opts.diag_all and vim.lsp.diagnostic.get_all() or
{[current_buf] = vim.lsp.diagnostic.get(current_buf, opts.client_id)}
local has_diags = false
if vim.diagnostic then
-- format: { <diag array> }
has_diags = not vim.tbl_isempty(diag_results)
else
-- format: { [bufnr] = <diag array>, ... }
for _, diags in pairs(diag_results) do
if #diags > 0 then has_diags = true end
end
end
if not has_diags then
utils.info(string.format('No %s found', string.lower(opts.lsp_handler.label)))
return
end
local preprocess_diag = function(diag, bufnr)
bufnr = bufnr or diag.bufnr
local filename = vim.api.nvim_buf_get_name(bufnr)
-- pre vim.diagnostic (vim.lsp.diagnostic)
-- has 'start|finish' instead of 'end_col|end_lnum'
local start = diag.range and diag.range['start']
-- local finish = diag.range and diag.range['end']
local row = diag.lnum or start.line
local col = diag.col or start.character
local buffer_diag = {
bufnr = bufnr,
filename = filename,
lnum = row + 1,
col = col + 1,
text = vim.trim(diag.message:gsub("[\n]", "")),
type = diag.severity or 1
}
return buffer_diag
end
opts.fzf_fn = function (fzf_cb)
coroutine.wrap(function ()
local co = coroutine.running()
local function process_diagnostics(diags, bufnr)
for _, diag in ipairs(diags) do
-- workspace diagnostics may include
-- empty tables for unused buffers
if not vim.tbl_isempty(diag) then
if filter_diag_severity(opts, diag.severity) then
-- wrap with 'vim.scheudle' or calls to vim.{fn|api} fail:
-- E5560: vimL function must not be called in a lua loop callback
vim.schedule(function()
diagnostics_handler(opts, fzf_cb, co, preprocess_diag(diag, bufnr))
end)
-- wait here for 'diagnostics_handler' to return
coroutine.yield()
end
end
end
end
if vim.diagnostic then
process_diagnostics(diag_results)
else
for bufnr, diags in pairs(diag_results) do
process_diagnostics(diags, bufnr)
end
end
-- close the pipe to fzf, this
-- removes the loading indicator
fzf_cb(nil)
end)()
end
opts = core.set_header(opts, opts.headers or {"cwd"})
opts = core.set_fzf_field_index(opts)
if opts.force_uri == nil then opts.force_uri = true end
return core.fzf_exec(opts.fzf_fn, opts)
end
M.workspace_diagnostics = function(opts)
if not opts then opts = {} end
opts.diag_all = true
return M.diagnostics(opts)
end
local last_search = {}
M.live_workspace_symbols = function(opts)
opts = normalize_lsp_opts(opts, config.globals.lsp)
if not opts then return end
-- exec empty query is the default here
if opts.exec_empty_query == nil then
opts.exec_empty_query = true
end
if not opts.query and opts.resume then
opts.query = last_search.query
end
-- sent to the LSP server
opts.lsp_params = {query = opts.query or ''}
-- must get those here, otherwise we get the
-- fzf terminal buffer and window IDs
opts.bufnr = vim.api.nvim_get_current_buf()
opts.winid = vim.api.nvim_get_current_win()
opts.fn_reload = function(query)
if query and not (opts.save_last_query == false) then
last_search = { query = query }
config.__resume_data.last_query = query
end
opts.sync = true
opts.async = false
opts.lsp_params = {query = query or ''}
opts = set_lsp_fzf_fn(opts)
return opts.fzf_fn
end
opts = core.set_fzf_field_index(opts)
if opts.force_uri == nil then opts.force_uri = true end
if opts.symbol_style or opts.symbol_fmt then
opts.fn_pre_fzf = function() gen_sym2style_map(opts) end
opts.fn_post_fzf = function() M._sym2style = nil end
end
core.fzf_exec(nil, opts)
end
local function check_capabilities(feature)
local clients = vim.lsp.buf_get_clients(0)
local clients = vim.lsp.buf_get_clients(__CTX and __CTX.bufnr or 0)
for _, client in pairs(clients) do
if vim.fn.has("nvim-0.8") == 1 then
@ -915,18 +769,6 @@ local handlers = {
server_capability = "workspaceSymbolProvider",
method = "workspace/symbol",
handler = symbol_handler },
["diagnostics"] = {
label = "Diagnostics",
resolved_capability = nil,
server_capability = nil,
method = nil,
handler = diagnostics_handler },
["workspace_diagnostics"] = {
label = "Workspace Diagnostics",
resolved_capability = nil,
server_capability = nil,
method = nil,
handler = diagnostics_handler },
["incoming_calls"] = {
label = "Incoming Calls",
resolved_capability = "call_hierarchy",

@ -403,6 +403,15 @@ function M.get_visual_selection()
return table.concat(lines, "\n")
end
function M.fzf_exit()
vim.cmd([[lua require('fzf-lua.win').win_leave()]])
end
function M.fzf_winobj()
-- use 'loadstring' to prevent circular require
return loadstring("return require'fzf-lua'.win.__SELF()")()
end
function M.send_ctrl_c()
vim.api.nvim_feedkeys(
vim.api.nvim_replace_termcodes("<C-c>", true, false, true), 'n', true)

Loading…
Cancel
Save