-
Notifications
You must be signed in to change notification settings - Fork 132
/
fzf.lua
262 lines (240 loc) · 9.83 KB
/
fzf.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
-- 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 select 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
-- Note: for some unknown reason, even though 'termopen' cmd is wrapped with
-- `sh -c`, on rare occasions (or unique systems?) when using `fish` shell,
-- commands that use the input redirection will hang indefintely (#633)
-- Using `cat` instead to read from the FIFO named pipe seems to solve it,
-- this is also better as it lets fzf handle spawning and terminating the
-- command which is consistent with the behavior above (with string cmds)
FZF_DEFAULT_COMMAND = string.format("cat %s", vim.fn.shellescape(fifotmpname))
-- Previously used command, left commented for documentation reasons
-- 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 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 that 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
-- I'm not sure why this happens (probably a neovim bug) but when pressing
-- <C-c> in quick successsion immediately after opening the window neovim
-- hangs the CPU at 100% at the last `coroutine.yield` before returning from
-- this function. At this point it seems that the fzf subprocess was started
-- and killed but `on_exit` is never called. In order to avoid calling `yield`
-- I tried checking the job/coroutine status in different ways:
-- * coroutine.status(co): always returns 'running'
-- * vim.fn.job_pid: always returns the corrent pid (even if it doesn't
-- exist anymore)
-- * vim.fn.jobwait({job_pid}, 0): always returns '-1' (even when looping
-- with 'vim.defer_fn(fn, 100)')
-- * uv.os_priority(job_pid): always returns '0'
-- `sudo strace -s 99 -ffp <pid> when neovim is stuck:
-- [pid 27433] <... epoll_wait resumed>[{events=EPOLLIN, data={u32=18, u64=18}}], 1024, -1) = 1
-- [pid 27432] <... write resumed>) = 8
-- [pid 27433] read(18, "\1\0\0\0\0\0\0\0", 1024) = 8
-- [pid 27432] epoll_wait(9, <unfinished ...>
-- [pid 27433] epoll_wait(15, <unfinished ...>
-- [pid 27432] <... epoll_wait resumed>[], 1024, 0) = 0
-- [pid 27432] epoll_wait(9, [], 1024, 0) = 0
-- [pid 27432] epoll_wait(9, [], 1024, 0) = 0
-- [pid 27432] epoll_wait(9, [], 1024, 0) = 0
-- [pid 27432] write(32, "\3", 1) = 1
-- [pid 27432] write(18, "\1\0\0\0\0\0\0\0", 8 <unfinished ...>
-- [pid 27433] <... epoll_wait resumed>[{events=EPOLLIN, data={u32=18, u64=18}}], 1024, -1) = 1
-- [pid 27432] <... write resumed>) = 8
-- [pid 27433] read(18, "\1\0\0\0\0\0\0\0", 1024) = 8
-- [pid 27432] epoll_wait(9, <unfinished ...>
-- [pid 27433] epoll_wait(15, <unfinished ...>
-- [pid 27432] <... epoll_wait resumed>[], 1024, 0) = 0
-- [pid 27432] epoll_wait(9, [], 1024, 0) = 0
-- [pid 27432] epoll_wait(9, [], 1024, 0) = 0
-- [pid 27432] epoll_wait(9, [], 1024, 0) = 0
-- [pid 27432] write(32, "\3", 1) = 1
-- [pid 27432] write(18, "\1\0\0\0\0\0\0\0", 8 <unfinished ...>
--
-- As a workaround we map buffer <C-c> to <Esc> for the fzf buffer
-- `vim.keymap.set` to avoid breaking compatibility with older neovim versions
vim.api.nvim_buf_set_keymap(0, "", "<C-c>", "<Esc>", { noremap = false })
vim.api.nvim_buf_set_keymap(0, "t", "<C-c>", "<Esc>", { noremap = false })
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 seems 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