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.

214 lines
6.8 KiB
Lua

-- slimmed down version of nvim-fzf's 'raw_fzf', changes include:
-- DOES NOT SUPPORT WINDOWS
-- does not close the pipe before all writes are complete
-- option to not add '\n' on content function callbacks
-- https://github.com/vijaymarupudi/nvim-fzf/blob/master/lua/fzf.lua
local uv = vim.loop
local M = {}
-- workaround to a potential 'tempname' bug? (#222)
-- neovim doesn't guarantee the existence of the
-- parent temp dir potentially failing `mkfifo`
-- https://github.com/neovim/neovim/issues/1432
-- https://github.com/neovim/neovim/pull/11284
local function tempname()
local tmpname = vim.fn.tempname()
local parent = vim.fn.fnamemodify(tmpname, ':h')
-- parent must exist for `mkfifo` to succeed
-- if the neovim temp dir was deleted or the
-- tempname already exists we use 'os.tmpname'
if not uv.fs_stat(parent) or uv.fs_stat(tmpname) then
tmpname = os.tmpname()
-- 'os.tmpname' touches the file which
-- will also fail `mkfifo`, delete it
vim.fn.delete(tmpname)
end
return tmpname
end
-- contents can be either a table with tostring()able items, or a function that
-- can be called repeatedly for values. the latter can use coroutines for async
-- behavior.
function M.raw_fzf(contents, fzf_cli_args, opts)
if not coroutine.running() then
error("[Fzf-lua] function must be called inside a coroutine.")
end
if not opts then opts = {} end
local cwd = opts.fzf_cwd or opts.cwd
local cmd = opts.fzf_bin or 'fzf'
local fifotmpname = tempname()
local outputtmpname = tempname()
-- we use a temporary env $FZF_DEFAULT_COMMAND instead of piping
-- the command to fzf, this way fzf kills the command when it exits
-- this is especially important with our shell helper as io.write fails
-- to delect when the pipe is broken (EPIPE) so the neovim headless
-- instance never terminates which hangs fzf on exit
local FZF_DEFAULT_COMMAND = nil
if fzf_cli_args then cmd = cmd .. " " .. fzf_cli_args end
if opts.fzf_cli_args then cmd = cmd .. " " .. opts.fzf_cli_args end
if contents then
if type(contents) == "string" and #contents>0 then
if opts.silent_fail ~= false then
contents = ("%s || true"):format(contents)
end
FZF_DEFAULT_COMMAND = contents
else
cmd = ("%s < %s"):format(cmd, vim.fn.shellescape(fifotmpname))
end
end
cmd = ("%s > %s"):format(cmd, vim.fn.shellescape(outputtmpname))
local fd, output_pipe = nil, nil
local finish_called = false
local write_cb_count = 0
-- Create the output pipe
-- We use tbl for perf reasons, from ':help system':
-- If {cmd} is a List it runs directly (no 'shell')
-- If {cmd} is a String it runs in the 'shell'
vim.fn.system({"mkfifo", fifotmpname})
local function finish(_)
-- mark finish if once called
finish_called = true
-- close pipe if there are no outstanding writes
if output_pipe and write_cb_count == 0 then
output_pipe:close()
output_pipe = nil
end
end
local function write_cb(data, cb)
if not output_pipe then return end
write_cb_count = write_cb_count + 1
output_pipe:write(data, function(err)
-- decrement write call count
write_cb_count = write_cb_count - 1
-- this will call the user's cb
if cb then cb(err) end
if err then
-- can fail with premature process kill
finish(2)
elseif finish_called and write_cb_count == 0 then
-- 'termopen.on_exit' already called and did not close the
-- pipe due to write_cb_count>0, since this is the last call
-- we can close the fzf pipe
finish(3)
end
end)
end
-- nvim-fzf compatibility, builds the user callback functions
-- 1st argument: callback function that adds newline to each write
-- 2nd argument: callback function thhat writes the data as is
-- 3rd argument: direct access to the pipe object
local function usr_write_cb(nl)
local function end_of_data(usrdata, cb)
if usrdata == nil then
if cb then cb(nil) end
finish(5)
return true
end
return false
end
if nl then
return function(usrdata, cb)
if not end_of_data(usrdata, cb) then
write_cb(tostring(usrdata).."\n", cb)
end
end
else
return function(usrdata, cb)
if not end_of_data(usrdata, cb) then
write_cb(usrdata, cb)
end
end
end
end
local co = coroutine.running()
vim.fn.termopen({"sh", "-c", cmd}, {
cwd = cwd,
env = {
['SHELL'] = 'sh',
['FZF_DEFAULT_COMMAND'] = FZF_DEFAULT_COMMAND,
['SKIM_DEFAULT_COMMAND'] = FZF_DEFAULT_COMMAND,
},
on_exit = function(_, rc, _)
local output = {}
local f = io.open(outputtmpname)
if f then
for v in f:lines() do
table.insert(output, v)
end
f:close()
end
finish(1)
vim.fn.delete(fifotmpname)
vim.fn.delete(outputtmpname)
if #output == 0 then output = nil end
coroutine.resume(co, output, rc)
end
})
vim.cmd[[set ft=fzf]]
-- terminal behavior seem to have changed after the introduction
-- of 'nt' mode (terminal-normal mode) which is included in 0.6
-- https://github.com/neovim/neovim/pull/15878
-- preferably I'd like to check if the vim patch is included using
-- vim.fn.has('patch-8.2.3461')
-- but this doesn't work for vim patches > 8.1 as explained in:
-- https://github.com/neovim/neovim/issues/9635
-- however, since this patch was included in 0.6 we can test
-- for neovim version 0.6
-- beats me why 'nvim_get_mode().mode' still returns 'nt' even
-- after we're clearly in insert mode or why `:startinsert`
-- won't change the mode from 'nt' to 't' so we use feedkeys()
-- instead.
-- this "retires" 'actions.ensure_insert_mode' and solves the
-- issue of calling an fzf-lua mapping from insert mode (#429)
if vim.fn.has('nvim-0.6') == 1 then
vim.cmd([[noautocmd lua vim.api.nvim_feedkeys(]]
.. [[vim.api.nvim_replace_termcodes("<Esc>i", true, false, true)]]
.. [[, 'n', true)]])
else
vim.cmd[[startinsert]]
end
if not contents or type(contents) == "string" then
goto wait_for_fzf
end
-- have to open this after there is a reader (termopen)
-- otherwise this will block
fd = uv.fs_open(fifotmpname, "w", -1)
output_pipe = uv.new_pipe(false)
output_pipe:open(fd)
-- print(output_pipe:getpeername())
-- this part runs in the background, when the user has selected, it will
-- error out, but that doesn't matter so we just break out of the loop.
if contents then
if type(contents) == "table" then
if not vim.tbl_isempty(contents) then
write_cb(vim.tbl_map(function(x) return x.."\n" end, contents))
end
finish(4)
else
contents(usr_write_cb(true), usr_write_cb(false), output_pipe)
end
end
::wait_for_fzf::
return coroutine.yield()
end
return M