@ -2,103 +2,284 @@ if not pcall(require, "fzf") then
return
end
-- local fzf = require "fzf "
local fzf = require " fzf "
-- local fzf_helpers = require("fzf.helpers")
-- local path = require "fzf-lua.path"
local core = require " fzf-lua.core "
local utils = require " fzf-lua.utils "
local config = require " fzf-lua.config "
local actions = require " fzf-lua.actions "
local M = { }
local getopts_lsp = function ( opts , cfg )
opts = config.getopts ( opts , cfg , {
" cwd " , " prompt " , " actions " , " winopts " ,
" file_icons " , " color_icons " , " git_icons " ,
" separator "
} )
local function location_handler ( opts , cb , _ , result )
result = vim.tbl_islist ( result ) and result or { result }
-- Jump immediately if there is only one location
-- TODO: we can't preform the jump due to fzf already
-- being open resulting in:
-- Error executing vim.schedule lua callback:
-- /usr/share/nvim/runtime/lua/vim/lsp/util.lua:976:
-- Vim(normal):Can't re-enter normal mode from terminal mode
--[[ if #result == 1 then
utils.send_ctrl_c ( )
return vim.lsp . util.jump_to_location ( result [ 1 ] )
end ] ]
local items = vim.lsp . util.locations_to_items ( result )
for _ , entry in ipairs ( items ) do
entry = core.make_entry_lcol ( opts , entry )
entry = core.make_entry_file ( opts , entry )
cb ( entry , function ( err )
if err then return end
end )
end
end
if not opts.timeout then
opts.timeout = config.lsp . timeout or 10000
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 )
for _ , entry in ipairs ( items ) do
entry.filename = opts.filename
entry = core.make_entry_lcol ( opts , entry )
entry = core.make_entry_file ( opts , entry )
cb ( entry , function ( err )
if err then return end
end )
end
return opts
end
local set_fzf_args = function ( opts )
local line_placeholder = 2
if opts.file_icons == true or opts.git_icons == true then
line_placeholder = 3
local function code_action_handler ( opts , cb , _ , code_actions )
opts.code_actions = { }
for i , action in ipairs ( code_actions ) do
local text = string.format ( " %s %s " ,
utils.ansi_codes . magenta ( string.format ( " %d: " , i ) ) ,
action.title )
opts.code_actions [ tostring ( i ) ] = action
cb ( text , function ( err )
if err then return end
end )
end
end
opts.cli_args = " --nth=3 --delimiter='[: \\ t]' "
opts.preview_args = string.format ( " --highlight-line={%d} " , line_placeholder )
opts.preview_offset = string.format ( " +{%d}-/2 " , line_placeholder )
return opts
local function diagnostics_handler ( opts , cb , _ , entry )
local type = entry.type
entry = core.make_entry_lcol ( opts , entry )
entry = core.make_entry_file ( opts , entry )
if opts.lsp_icons and opts.cfg . icons [ type ] then
local severity = opts.cfg . icons [ type ]
local icon = severity.icon
if opts.color_icons then
icon = utils.ansi_codes [ severity.color or " dark_grey " ] ( icon )
end
if opts.file_icons or opts.git_icons then
-- entry = icon .. utils.nbsp .. utils.nbsp .. entry
entry = icon .. utils.nbsp .. utils.nbsp .. entry
else
-- entry = icon .. utils.nbsp .. " " .. entry
entry = icon .. utils.nbsp .. " " .. entry
end
end
cb ( entry , function ( err )
if err then return end
end )
end
M.refs_async = function ( opts )
local function wrap_handler ( handler )
return function ( err , method , result , client_id , bufnr , lspcfg )
local ret
if err then
ret = utils.err ( err.message )
utils.send_ctrl_c ( )
elseif not result or vim.tbl_isempty ( result ) then
ret = utils.info ( string.format ( ' No %s found ' , string.lower ( handler.label ) ) )
utils.send_ctrl_c ( )
else
ret = handler.target ( err , method , result , client_id , bufnr , lspcfg )
end
return ret
end
end
opts = getopts_lsp ( opts , config.lsp )
local function set_lsp_fzf_fn ( opts )
-- hangs/returns empty if inside the coroutine
local params = vim.lsp . util.make_position_params ( )
params.context = { includeDeclaration = true }
local results_lsp , lsp_err = vim.lsp . buf_request_sync ( 0 , " textDocument/references " , params , opts.timeout )
if lsp_err then
utils.err ( " Error finding LSP references: " .. lsp_err )
return
-- 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 = vim.api . nvim_get_current_buf ( )
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 ( )
opts.lsp_params . context = { includeDeclaration = true }
end
opts.fzf_fn = function ( cb )
coroutine.wrap ( function ( )
local co = coroutine.running ( )
local locations = { }
for _ , server_results in pairs ( results_lsp ) do
if server_results.result then
vim.list_extend ( locations , vim.lsp . util.locations_to_items ( server_results.result ) or { } )
end
end
if vim.tbl_isempty ( locations ) then
utils.info ( " LSP references is empty. " )
vim.api . nvim_feedkeys (
vim.api . nvim_replace_termcodes ( " <C-c> " , true , false , true ) ,
' n ' , true )
return
-- callback when a location is found
-- we use this passthrough so we can send the
-- coroutine variable (not used rn but who knows?)
opts.lsp_handler . target = function ( _ , _ , result )
return opts.lsp_handler . handler ( opts , cb , co , result )
end
for _ , entry in ipairs ( locations ) do
entry = core.make_entry_lcol ( opts , entry )
entry = core.make_entry_file ( opts , entry )
cb( entry , function ( err )
if err then return end
coroutine.resume ( co )
-- cb(nil) -- to close the pipe to fzf, this removes the loading
-- indicator in fzf
end )
coroutine.yield ( )
local _ , cancel_all = vim.lsp . buf_request ( opts.bufnr ,
opts.lsp_handler. method , opts.lsp_params ,
wrap_handler( opts.lsp_handler ) )
-- cancel all remaining LSP requests
-- once the user made their selection
-- or closed the fzf popup
opts.post_select_cb = function ( )
if cancel_all then cancel_all ( ) end
end
-- coroutine.yield()
end ) ( )
end
opts = set_fzf_args ( opts )
return opts
end
local set_fzf_files_args = function ( opts )
local line_placeholder = 2
if opts.file_icons or opts.git_icons or opts.lsp_icons then
line_placeholder = line_placeholder + 1
end
opts.cli_args = opts.cli_args or " --delimiter='[: \\ t]' "
opts.filespec = string.format ( " {%d} " , line_placeholder - 1 )
opts.preview_args = string.format ( " --highlight-line={%d} " , line_placeholder )
opts.preview_offset = string.format ( " +{%d}-/2 " , line_placeholder )
return opts
end
local normalize_lsp_opts = function ( opts , cfg )
opts = config.getopts ( opts , cfg , {
" cwd " , " actions " , " winopts " ,
" file_icons " , " color_icons " ,
" git_icons " , " lsp_icons " , " severity " ,
" severity_exact " , " severity_bound " ,
} )
if not opts.cwd then opts.cwd = vim.loop . cwd ( ) end
if not opts.prompt then
opts.prompt = opts.lsp_handler . label .. cfg.prompt
end
opts.cfg = nil
opts.bufnr = nil
opts.filename = nil
opts.lsp_params = nil
opts.code_actions = nil
return opts
end
local function fzf_lsp_locations ( opts )
opts = normalize_lsp_opts ( opts , config.lsp )
opts = set_fzf_files_args ( opts )
opts = set_lsp_fzf_fn ( opts )
return core.fzf_files ( opts )
end
-- define the functions for wrap_module_fncs
M.references = function ( opts )
return fzf_lsp_locations ( opts )
end
M.definitions = function ( opts )
return fzf_lsp_locations ( opts )
end
M.declarations = function ( opts )
return fzf_lsp_locations ( opts )
end
M.typedefs = function ( opts )
return fzf_lsp_locations ( opts )
end
M.implementations = function ( opts )
return fzf_lsp_locations ( opts )
end
M.document_symbols = function ( opts )
if not opts then opts = { } end
-- TODO: filename hiding
-- since single document
opts.ignore_filename = true
return fzf_lsp_locations ( opts )
end
M.workspace_symbols = function ( opts )
return fzf_lsp_locations ( opts )
end
M.code_actions = function ( opts )
opts = normalize_lsp_opts ( opts , config.lsp )
opts.lsp_params = vim.lsp . util.make_range_params ( )
opts.lsp_params . context = {
diagnostics = vim.lsp . diagnostic.get_line_diagnostics ( )
}
-- "apply action" as default function
if not opts.actions then opts.actions = { } end
opts.actions . default = ( function ( selected )
local idx = selected [ 2 ] : match ( " (%d+) " )
local action = opts.code_actions [ idx ]
if not action then return end
if action.edit or type ( action.command ) == ' table ' then
if action.edit then
vim.lsp . util.apply_workspace_edit ( action.edit )
end
if type ( action.command ) == ' table ' then
vim.lsp . buf.execute_command ( action.command )
end
else
vim.lsp . buf.execute_command ( action )
end
end )
opts.nomulti = true
opts.preview_window = ' right:0 '
opts.cli_args = " --delimiter=':' "
opts = set_lsp_fzf_fn ( opts )
coroutine.wrap ( function ( )
local selected = fzf.fzf ( opts.fzf_fn ,
core.build_fzf_cli ( opts ) ,
config.winopts ( opts.winopts ) )
if opts.post_select_cb then
opts.post_select_cb ( )
end
if not selected then return end
actions.act ( opts.actions , selected [ 1 ] , selected )
end ) ( )
end
local convert_diagnostic_type = function ( severity )
-- convert from string to int
if type ( severity ) == ' string ' then
if type ( severity ) == " string " then
-- make sure that e.g. error is uppercased to Error
return vim.lsp . protocol.DiagnosticSeverity [ severity : gsub ( " ^%l " , string.upper ) ]
end
-- otherwise keep original value, incl. nil
if type ( severity ) ~= " number " then return nil end
return severity
end
local filter_diag_severity = function ( opts , severity )
if opts.severity ~= nil then
return opts.severity == severity
elseif opts.severity_limit ~= nil then
return severity <= opts.severity_limit
if opts.severity _exact ~= nil then
return opts.severity _exact == severity
elseif opts.severity ~= nil then
return severity <= opts.severity
elseif opts.severity_bound ~= nil then
return severity >= opts.severity_bound
else
@ -106,24 +287,34 @@ local filter_diag_severity = function(opts, severity)
end
end
local diagnostics_to_tbl = function ( opts )
opts = opts or { }
local items = { }
M. diagnostics = function ( opts )
opts = normalize_lsp_opts( opts , config.lsp )
local lsp_type_diagnostic = vim.lsp . protocol.DiagnosticSeverity
local current_buf = vim.api . nvim_get_current_buf ( )
-- save this so handler can get the lsp icon
opts.cfg = config.lsp
-- hint = 4
-- information = 3
-- warning = 2
-- error = 1
-- severity: keep any equal or more severe (lower)
-- severity_exact: keep any matching exact severity
-- severity_bound: keep any equal or less severe (higher)
opts.severity = convert_diagnostic_type ( opts.severity )
opts.severity_limit = convert_diagnostic_type ( opts.severity_limit )
opts.severity_ exact = convert_diagnostic_type ( opts.severity_exac t)
opts.severity_bound = convert_diagnostic_type ( opts.severity_bound )
local validate_severity = 0
for _ , v in ipairs ( { opts.severity , opts.severity_limit , opts.severity_bound } ) do
for _ , v in ipairs ( { opts.severity _exact , opts.severity , opts.severity_bound } ) do
if v ~= nil then
validate_severity = validate_severity + 1
end
if validate_severity > 1 then
utils. info( ' LSP: invalid severity parameters ' )
return { }
utils. warn( " Invalid severity params, ignoring severity filters " )
opts.severity , opts.severity_exact , opts.severity_bound = nil , nil , nil
end
end
@ -148,52 +339,135 @@ local diagnostics_to_tbl = function(opts)
return buffer_diag
end
local buffer_diags = opts.get_all and vim.lsp . diagnostic.get_all ( ) or
{ [ current_buf ] = vim.lsp . diagnostic.get ( current_buf , opts.client_id ) }
for bufnr , diags in pairs ( buffer_diags ) do
for _ , diag in ipairs ( diags ) do
-- workspace diagnostics may include empty tables for unused bufnr
if not vim.tbl_isempty ( diag ) then
if filter_diag_severity ( opts , diag.severity ) then
table.insert ( items , preprocess_diag ( diag , bufnr ) )
opts.fzf_fn = function ( cb )
coroutine.wrap ( function ( )
local co = coroutine.running ( )
local buffer_diags = opts.diag_all and vim.lsp . diagnostic.get_all ( ) or
{ [ current_buf ] = vim.lsp . diagnostic.get ( current_buf , opts.client_id ) }
if # buffer_diags == 0 then
utils.info ( string.format ( ' No %s found ' , string.lower ( opts.lsp_handler . label ) ) )
utils.send_ctrl_c ( )
end
for bufnr , diags in pairs ( buffer_diags ) do
for _ , diag in ipairs ( diags ) do
-- workspace diagnostics may include empty tables for unused bufnr
if not vim.tbl_isempty ( diag ) then
if filter_diag_severity ( opts , diag.severity ) then
diagnostics_handler ( opts , cb , co ,
preprocess_diag ( diag , bufnr ) )
end
end
end
end
end
-- coroutine.yield()
end ) ( )
end
-- sort results by bufnr (prioritize cur buf), severity, lnum
table.sort ( items , function ( a , b )
if a.bufnr == b.bufnr then
if a.type == b.type then
return a.lnum < b.lnum
else
return a.type < b.type
end
opts = set_fzf_files_args ( opts )
return core.fzf_files ( opts )
end
M.workspace_diagnostics = function ( opts )
if not opts then opts = { } end
opts.diag_all = true
return M.diagnostics ( opts )
end
local function check_capabilities ( feature )
local clients = vim.lsp . buf_get_clients ( 0 )
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.info ( " LSP: no client attached " )
else
-- prioritize for current bufnr
if a.bufnr == current_buf then
return true
end
if b.bufnr == current_buf then
return false
end
return a.bufnr < b.bufnr
utils.info ( " LSP: server does not support " .. feature )
end
end )
return items
return false
end
end
local handlers = {
[ " code_actions " ] = {
label = " Code Actions " ,
capability = " code_action " ,
method = " textDocument/codeAction " ,
handler = code_action_handler } ,
[ " references " ] = {
label = " References " ,
capability = " find_references " ,
method = " textDocument/references " ,
handler = location_handler } ,
[ " definitions " ] = {
label = " Definitions " ,
capability = " goto_definition " ,
method = " textDocument/definition " ,
handler = location_handler } ,
[ " declarations " ] = {
label = " Declarations " ,
capability = " goto_declaration " ,
method = " textDocument/declaration " ,
handler = location_handler } ,
[ " typedefs " ] = {
label = " Type Definitions " ,
capability = " type_definition " ,
method = " textDocument/typeDefinition " ,
handler = location_handler } ,
[ " implementations " ] = {
label = " Implementations " ,
capability = " implementation " ,
method = " textDocument/implementation " ,
handler = location_handler } ,
[ " document_symbols " ] = {
label = " Document Symbols " ,
capability = " document_symbol " ,
method = " textDocument/documentSymbol " ,
handler = symbol_handler } ,
[ " workspace_symbols " ] = {
label = " Workspace Symbols " ,
capability = " workspace_symbol " ,
method = " workspace/symbol " ,
handler = symbol_handler } ,
[ " diagnostics " ] = {
label = " Diagnostics " ,
capability = nil ,
method = nil ,
handler = diagnostics_handler } ,
[ " workspace_diagnostics " ] = {
label = " Workspace Diagnostics " ,
capability = nil ,
method = nil ,
handler = diagnostics_handler } ,
}
M.lsp_diag = function ( opts )
local locations = diagnostics_to_tbl ( opts )
local function wrap_module_fncs ( mod )
for k , v in pairs ( mod ) do
mod [ k ] = function ( opts )
opts = opts or { }
if vim.tbl_isempty ( locations ) then
utils.info ( " LSP diagnostics is empty. " )
return
if not opts.lsp_handler then opts.lsp_handler = handlers [ k ] end
if not opts.lsp_handler then
utils.err ( string.format ( " No LSP handler defined for %s " , k ) )
return
end
if opts.lsp_handler and opts.lsp_handler . capability
and not check_capabilities ( opts.lsp_handler . capability ) then
return
end
v ( opts )
end
end
return lsp_run ( opts , config.lsp , locations )
return mod
end
return M
return wrap_module_fncs( M)