You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

679 lines
20 KiB
Lua

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

local utils = require "fzf-lua.utils"
local path = require "fzf-lua.path"
local M = {}
-- default action map key
local _default_action = "default"
-- return fzf '--expect=' string from actions keyval tbl
M.expect = function(actions)
if not actions then return nil end
local keys = {}
for k, v in pairs(actions) do
if k ~= _default_action and v ~= false then
table.insert(keys, k)
end
end
if #keys > 0 then
return string.format("--expect=%s", table.concat(keys, ','))
end
return nil
end
M.normalize_selected = function(actions, selected)
-- 1. If there are no additional actions but the default
-- the selected table will contain the selected item(s)
-- 2. If at least one non-default action was defined, our 'expect'
-- function above sent fzf the '--expect` flag, from `man fzf`:
-- When this option is set, fzf will print the name of
-- the key pressed as the first line of its output (or
-- as the second line if --print-query is also used).
--
-- The below makes separates the keybind from the item(s)
-- and makes sure 'selected' contains only items or {}
-- so it can always be enumerated safely
if not actions or not selected then return end
local action = _default_action
if utils.tbl_length(actions)>1 or not actions[_default_action] then
-- keybind should be in item #1
-- default keybind is an empty string
-- so we leave that as "default"
if #selected[1] > 0 then
action = selected[1]
end
-- entries are items #2+
local entries = {}
for i = 2, #selected do
table.insert(entries, selected[i])
end
return action, entries
else
return action, selected
end
end
M.act = function(actions, selected, opts)
if not actions or not selected then return end
local keybind, entries = M.normalize_selected(actions, selected)
local action = actions[keybind]
if type(action) == 'table' then
for _, f in ipairs(action) do
f(entries, opts)
end
elseif type(action) == 'function' then
action(entries, opts)
elseif type(action) == 'string' then
vim.cmd(action)
elseif keybind ~= _default_action then
utils.warn(("unsupported action: '%s', type:%s")
:format(keybind, type(action)))
end
end
M.resume = function(_, _)
-- must call via vim.cmd or we create
-- circular 'require'
-- TODO: is this really a big deal?
vim.cmd("lua require'fzf-lua'.resume()")
end
M.vimcmd = function(vimcmd, selected, noesc)
for i = 1, #selected do
vim.cmd(("%s %s"):format(vimcmd,
noesc and selected[i] or vim.fn.fnameescape(selected[i])))
end
end
M.vimcmd_file = function(vimcmd, selected, opts)
local curbuf = vim.api.nvim_buf_get_name(0)
local is_term = utils.is_term_buffer(0)
for i = 1, #selected do
local entry = path.entry_to_file(selected[i], opts, opts.force_uri)
entry.ctag = opts._ctag and path.entry_to_ctag(selected[i])
local fullpath = entry.path or entry.uri and entry.uri:match("^%a+://(.*)")
if not path.starts_with_separator(fullpath) then
fullpath = path.join({opts.cwd or vim.loop.cwd(), fullpath})
end
if vimcmd == 'e'
and curbuf ~= fullpath
and not vim.o.hidden and
utils.buffer_is_dirty(nil, true) then
-- warn the user when trying to switch from a dirty buffer
-- when `:set nohidden`
return
end
-- add current location to jumplist
if not is_term then vim.cmd("normal! m`") end
-- only change buffer if we need to (issue #122)
if vimcmd ~= "e" or curbuf ~= fullpath then
if entry.path then
-- do not run ':<cmd> <file>' for uri entries (#341)
vim.cmd(vimcmd .. " " .. vim.fn.fnameescape(entry.path))
elseif vimcmd ~= 'e' then
-- uri entries only execute new buffers (new|vnew|tabnew)
vim.cmd(vimcmd)
end
end
-- Java LSP entries, 'jdt://...' or LSP locations
if entry.uri then
vim.lsp.util.jump_to_location(entry, "utf-16")
elseif entry.ctag then
vim.api.nvim_win_set_cursor(0, {1, 0})
vim.fn.search(entry.ctag, "W")
elseif entry.line>1 or entry.col>1 then
-- make sure we have valid column
-- 'nvim-dap' for example sets columns to 0
entry.col = entry.col and entry.col>0 and entry.col or 1
vim.api.nvim_win_set_cursor(0, {tonumber(entry.line), tonumber(entry.col)-1})
end
if not is_term and not opts.no_action_zz then vim.cmd("norm! zvzz") end
end
end
-- file actions
M.file_edit = function(selected, opts)
local vimcmd = "e"
M.vimcmd_file(vimcmd, selected, opts)
end
M.file_split = function(selected, opts)
local vimcmd = "new"
M.vimcmd_file(vimcmd, selected, opts)
end
M.file_vsplit = function(selected, opts)
local vimcmd = "vnew"
M.vimcmd_file(vimcmd, selected, opts)
end
M.file_tabedit = function(selected, opts)
local vimcmd = "tabnew"
M.vimcmd_file(vimcmd, selected, opts)
end
M.file_open_in_background = function(selected, opts)
local vimcmd = "badd"
M.vimcmd_file(vimcmd, selected, opts)
end
local sel_to_qf = function(selected, opts, is_loclist)
local qf_list = {}
for i = 1, #selected do
local file = path.entry_to_file(selected[i], opts)
local text = selected[i]:match(":%d+:%d?%d?%d?%d?:?(.*)$")
table.insert(qf_list, {
filename = file.bufname or file.path,
lnum = file.line,
col = file.col,
text = text,
})
end
if is_loclist then
vim.fn.setloclist(0, qf_list)
vim.cmd 'lopen'
else
vim.fn.setqflist(qf_list)
vim.cmd 'copen'
end
end
M.file_sel_to_qf = function(selected, opts)
sel_to_qf(selected, opts)
end
M.file_sel_to_ll = function(selected, opts)
sel_to_qf(selected, opts, true)
end
M.file_edit_or_qf = function(selected, opts)
if #selected>1 then
return M.file_sel_to_qf(selected, opts)
else
return M.file_edit(selected, opts)
end
end
M.file_switch = function(selected, opts)
local bufnr = nil
local entry = path.entry_to_file(selected[1])
local fullpath = entry.path
if not path.starts_with_separator(fullpath) then
fullpath = path.join({opts.cwd or vim.loop.cwd(), fullpath})
end
for _, b in ipairs(vim.api.nvim_list_bufs()) do
local bname = vim.api.nvim_buf_get_name(b)
if bname and bname == fullpath then
bufnr = b
break
end
end
if not bufnr then return false end
local is_term = utils.is_term_buffer(0)
if not is_term then vim.cmd("normal! m`") end
local winid = utils.winid_from_tab_buf(0, bufnr)
if winid then vim.api.nvim_set_current_win(winid) end
if entry.line>1 or entry.col>1 then
vim.api.nvim_win_set_cursor(0, {tonumber(entry.line), tonumber(entry.col)-1})
end
if not is_term and not opts.no_action_zz then vim.cmd("norm! zvzz") end
return true
end
M.file_switch_or_edit = function(...)
M.file_switch(...)
M.file_edit(...)
end
-- buffer actions
M.vimcmd_buf = function(vimcmd, selected, opts)
local curbuf = vim.api.nvim_get_current_buf()
local lnum = vim.api.nvim_win_get_cursor(0)[1]
local is_term = utils.is_term_buffer(0)
for i = 1, #selected do
local entry = path.entry_to_file(selected[i], opts)
if not entry.bufnr then return end
assert(type(entry.bufnr) == 'number')
if vimcmd == 'b'
and curbuf ~= entry.bufnr
and not vim.o.hidden and
utils.buffer_is_dirty(nil, true) then
-- warn the user when trying to switch from a dirty buffer
-- when `:set nohidden`
return
end
-- add current location to jumplist
if not is_term then vim.cmd("normal! m`") end
if vimcmd ~= "b" or curbuf ~= entry.bufnr then
local cmd = vimcmd .. " " .. entry.bufnr
local ok, res = pcall(vim.cmd, cmd)
if not ok then
utils.warn(("':%s' failed: %s"):format(cmd, res))
end
end
if vimcmd ~= "bd" then
if curbuf ~= entry.bufnr or lnum ~= entry.line then
-- make sure we have valid column
entry.col = entry.col and entry.col>0 and entry.col or 1
vim.api.nvim_win_set_cursor(0, {tonumber(entry.line), tonumber(entry.col)-1})
end
if not is_term and not opts.no_action_zz then vim.cmd("norm! zvzz") end
end
end
end
M.buf_edit = function(selected, opts)
local vimcmd = "b"
M.vimcmd_buf(vimcmd, selected, opts)
end
M.buf_split = function(selected, opts)
local vimcmd = "split | b"
M.vimcmd_buf(vimcmd, selected, opts)
end
M.buf_vsplit = function(selected, opts)
local vimcmd = "vertical split | b"
M.vimcmd_buf(vimcmd, selected, opts)
end
M.buf_tabedit = function(selected, opts)
local vimcmd = "tab split | b"
M.vimcmd_buf(vimcmd, selected, opts)
end
M.buf_del = function(selected, opts)
local vimcmd = "bd"
local bufnrs = vim.tbl_filter(function(line)
local b = tonumber(line:match("%[(%d+)"))
return not utils.buffer_is_dirty(b, true)
end, selected)
M.vimcmd_buf(vimcmd, bufnrs, opts)
end
M.buf_switch = function(selected, _)
local tabnr = tonumber(selected[1]:match("(%d+)%)"))
if tabnr then
vim.cmd("tabn " .. tabnr)
else
tabnr = vim.api.nvim_win_get_tabpage(0)
end
local bufnr = tonumber(string.match(selected[1], "%[(%d+)"))
if bufnr then
local winid = utils.winid_from_tab_buf(tabnr, bufnr)
if winid then vim.api.nvim_set_current_win(winid) end
end
end
M.buf_switch_or_edit = function(...)
M.buf_switch(...)
M.buf_edit(...)
end
M.buf_sel_to_qf = function(selected, opts)
return sel_to_qf(selected, opts)
end
M.buf_sel_to_ll = function(selected, opts)
return sel_to_qf(selected, opts, true)
end
M.buf_edit_or_qf = function(selected, opts)
if #selected>1 then
return M.buf_sel_to_qf(selected, opts)
else
return M.buf_edit(selected, opts)
end
end
M.colorscheme = function(selected)
local colorscheme = selected[1]
vim.cmd("colorscheme " .. colorscheme)
end
M.ensure_insert_mode = function()
-- not sure what is causing this, tested with
-- 'NVIM v0.6.0-dev+575-g2ef9d2a66'
-- vim.cmd("startinsert") doesn't start INSERT mode
-- 'mode' returns { blocking = false, mode = "t" }
-- manually input 'i' seems to workaround this issue
-- **only if fzf term window was succefully opened (#235)
-- this is only required after the 'nt' (normal-terminal)
-- mode was introduced along with the 'ModeChanged' event
-- https://github.com/neovim/neovim/pull/15878
-- https://github.com/neovim/neovim/pull/15840
-- local has_mode_nt = not vim.tbl_isempty(
-- vim.fn.getcompletion('ModeChanged', 'event'))
-- or vim.fn.has('nvim-0.6') == 1
-- if has_mode_nt then
-- local mode = vim.api.nvim_get_mode()
-- local wininfo = vim.fn.getwininfo(vim.api.nvim_get_current_win())[1]
-- if vim.bo.ft == 'fzf'
-- and wininfo.terminal == 1
-- and mode and mode.mode == 't' then
-- vim.cmd[[noautocmd lua vim.api.nvim_feedkeys('i', 'n', true)]]
-- end
-- end
utils.warn("calling 'ensure_insert_mode' is no longer required and can be safely omitted.")
end
M.run_builtin = function(selected)
local method = selected[1]
vim.cmd(string.format("lua require'fzf-lua'.%s()", method))
end
M.ex_run = function(selected)
local cmd = selected[1]
vim.cmd("stopinsert")
vim.fn.feedkeys(string.format(":%s", cmd), "n")
return cmd
end
M.ex_run_cr = function(selected)
local cmd = M.ex_run(selected)
utils.feed_keys_termcodes("<CR>")
vim.fn.histadd("cmd", cmd)
end
M.exec_menu = function(selected)
local cmd = selected[1]
vim.cmd("emenu " .. cmd)
end
M.search = function(selected)
local query = selected[1]
vim.cmd("stopinsert")
vim.fn.feedkeys(string.format("/%s", query), "n")
return query
end
M.search_cr = function(selected)
local query = M.search(selected)
utils.feed_keys_termcodes("<CR>")
vim.fn.histadd("search", query)
end
M.goto_mark = function(selected)
local mark = selected[1]
mark = mark:match("[^ ]+")
vim.cmd("stopinsert")
vim.cmd("normal! '" .. mark)
-- vim.fn.feedkeys(string.format("'%s", mark))
end
M.goto_jump = function(selected, opts)
if opts.jump_using_norm then
local jump, _, _, _ = selected[1]:match("(%d+)%s+(%d+)%s+(%d+)%s+(.*)")
if tonumber(jump) then
vim.cmd(("normal! %d"):format(jump))
end
else
local _, lnum, col, filepath = selected[1]:match("(%d+)%s+(%d+)%s+(%d+)%s+(.*)")
local ok, res = pcall(vim.fn.expand, filepath)
if not ok then filepath = ''
else filepath = res end
if not filepath or not vim.loop.fs_stat(filepath) then
-- no accessible file
-- jump is in current
filepath = vim.api.nvim_buf_get_name(0)
end
local entry = ("%s:%d:%d:"):format(filepath, tonumber(lnum), tonumber(col)+1)
M.file_edit({ entry }, opts)
end
end
M.spell_apply = function(selected)
local word = selected[1]
vim.cmd("normal! ciw" .. word)
vim.cmd("stopinsert")
end
M.set_filetype = function(selected)
vim.api.nvim_buf_set_option(0, 'filetype', selected[1])
end
M.packadd = function(selected)
for i = 1, #selected do
vim.cmd("packadd " .. selected[i])
end
end
local function helptags(s)
return vim.tbl_map(function(x)
return x:match("[^%s]+")
end, s)
end
M.help = function(selected)
local vimcmd = "help"
M.vimcmd(vimcmd, helptags(selected), true)
end
M.help_vert = function(selected)
local vimcmd = "vert help"
M.vimcmd(vimcmd, helptags(selected), true)
end
M.help_tab = function(selected)
local vimcmd = "tab help"
M.vimcmd(vimcmd, helptags(selected), true)
end
local function mantags(s)
return vim.tbl_map(function(x)
return x:match("[^[,( ]+")
end, s)
end
M.man = function(selected)
local vimcmd = "Man"
M.vimcmd(vimcmd, mantags(selected))
end
M.man_vert = function(selected)
local vimcmd = "vert Man"
M.vimcmd(vimcmd, mantags(selected))
end
M.man_tab = function(selected)
local vimcmd = "tab Man"
M.vimcmd(vimcmd, mantags(selected))
end
M.git_switch = function(selected, opts)
local cmd = path.git_cwd({"git", "checkout"}, opts)
local git_ver = utils.git_version()
-- git switch was added with git version 2.23
if git_ver and git_ver >= 2.23 then
cmd = path.git_cwd({"git", "switch"}, opts)
end
-- remove anything past space
local branch = selected[1]:match("[^ ]+")
-- do nothing for active branch
if branch:find("%*") ~= nil then return end
if branch:find("^remotes/") then
table.insert(cmd, "--detach")
end
table.insert(cmd, branch)
local output = utils.io_systemlist(cmd)
if utils.shell_error() then
utils.err(unpack(output))
else
utils.info(unpack(output))
vim.cmd("edit!")
end
end
M.git_checkout = function(selected, opts)
local cmd_checkout = path.git_cwd({"git", "checkout"}, opts)
local cmd_cur_commit = path.git_cwd({"git", "rev-parse", "--short HEAD"}, opts)
local commit_hash = selected[1]:match("[^ ]+")
if utils.input("Checkout commit " .. commit_hash .. "? [y/n] ") == "y" then
local current_commit = utils.io_systemlist(cmd_cur_commit)
if(commit_hash == current_commit) then return end
table.insert(cmd_checkout, commit_hash)
local output = utils.io_systemlist(cmd_checkout)
if utils.shell_error() then
utils.err(unpack(output))
else
utils.info(unpack(output))
vim.cmd("edit!")
end
end
end
local git_exec = function(selected, opts, cmd)
for _, e in ipairs(selected) do
local file = path.relative(path.entry_to_file(e, opts).path, opts.cwd)
local _cmd = vim.deepcopy(cmd)
table.insert(_cmd, file)
local output = utils.io_systemlist(_cmd)
if utils.shell_error() then
utils.err(unpack(output))
-- elseif not vim.tbl_isempty(output) then
-- utils.info(unpack(output))
end
end
end
M.git_stage = function(selected, opts)
local cmd = path.git_cwd({"git", "add", "--"}, opts)
git_exec(selected, opts, cmd)
end
M.git_unstage = function(selected, opts)
local cmd = path.git_cwd({"git", "reset", "--"}, opts)
git_exec(selected, opts, cmd)
end
M.git_reset = function(selected, opts)
local cmd = path.git_cwd({"git", "checkout", "HEAD", "--"}, opts)
git_exec(selected, opts, cmd)
end
M.git_stash_drop = function(selected, opts)
local cmd = path.git_cwd({"git", "stash", "drop"}, opts)
git_exec(selected, opts, cmd)
end
M.git_stash_pop = function(selected, opts)
if utils.input("Pop " .. #selected .. " stash(es)? [y/n] ") == "y" then
local cmd = path.git_cwd({"git", "stash", "pop"}, opts)
git_exec(selected, opts, cmd)
vim.cmd("e!")
end
end
M.git_stash_apply = function(selected, opts)
if utils.input("Apply " .. #selected .. " stash(es)? [y/n] ") == "y" then
local cmd = path.git_cwd({"git", "stash", "apply"}, opts)
git_exec(selected, opts, cmd)
vim.cmd("e!")
end
end
M.git_buf_edit = function(selected, opts)
local cmd = path.git_cwd({"git", "show"}, opts)
local git_root = path.git_root(opts, true)
local win = vim.api.nvim_get_current_win()
local buffer_filetype = vim.bo.filetype
local file = path.relative(vim.fn.expand("%:p"), git_root)
local commit_hash = selected[1]:match("[^ ]+")
table.insert(cmd, commit_hash .. ":" .. file)
local git_file_contents = utils.io_systemlist(cmd)
local buf = vim.api.nvim_create_buf(true, true)
local file_name = string.gsub(file,"$","[" .. commit_hash .. "]")
vim.api.nvim_buf_set_lines(buf,0,0,true,git_file_contents)
vim.api.nvim_buf_set_name(buf,file_name)
vim.api.nvim_buf_set_option(buf, 'buftype', 'nofile')
vim.api.nvim_buf_set_option(buf, 'bufhidden', 'wipe')
vim.api.nvim_buf_set_option(buf, 'filetype', buffer_filetype)
vim.api.nvim_buf_set_option(buf, 'modifiable', false)
vim.api.nvim_win_set_buf(win, buf)
end
M.git_buf_tabedit = function(selected, opts)
vim.cmd('tab split')
M.git_buf_edit(selected, opts)
end
M.git_buf_split = function(selected, opts)
vim.cmd('split')
M.git_buf_edit(selected, opts)
end
M.git_buf_vsplit = function(selected, opts)
vim.cmd('vsplit')
M.git_buf_edit(selected, opts)
end
M.arg_add = function(selected, opts)
local vimcmd = "argadd"
M.vimcmd_file(vimcmd, selected, opts)
end
M.arg_del = function(selected, opts)
local vimcmd = "argdel"
M.vimcmd_file(vimcmd, selected, opts)
end
M.grep_lgrep = function(_, opts)
-- 'MODULE' is set on 'grep' and 'live_grep' calls
assert(opts.__MODULE__
and type(opts.__MODULE__.grep) == 'function'
or type(opts.__MODULE__.live_grep) == 'function')
local o = vim.tbl_extend("keep", {
search = false,
resume = true,
resume_search_default = '',
rg_glob = opts.rg_glob or opts.__call_opts.rg_glob,
-- globs always require command processing with 'multiprocess'
requires_processing = opts.rg_glob or opts.__call_opts.rg_glob,
-- grep has both search string and query prompt, when switching
-- from live_grep to grep we want to restore both:
-- * we save the last query prompt when exiting grep
-- * we set query to the last known when entering grep
__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_grep' calls
if opts.fn_reload then
opts.__MODULE__.grep(o)
else
opts.__MODULE__.live_grep(o)
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