ibhagwan / fzf-lua

Improved fzf.vim written in lua
GNU Affero General Public License v3.0
2.17k stars 142 forks source link

How to use fzf-lua in L3MON4D3/LuaSnip? - LuaSnip's functionNode requires function return type string #489

Closed mloskamp closed 2 years ago

mloskamp commented 2 years ago

Question

Is there a way for LuaSnip to apply fzf-lua functions directly without io.popen() in functionNodes? The fzf-lua function has to return a string. Apparently, most fzf-lua functions live on their side effect, but I can't find their return value (and its type).

Description

fzf-lua functions don't seem to return strings. I still struggle to understand how to properly wrap them into coroutines such that I can extract their output and return a plain lua string.

(long) Description

Now that fzf-lua has totally changed my life with nvim, I got hooked to using it in combination with LuaSnip.

I am now stuck at understanding what the exact return type of the different fzf-lua commands is.

My specific first goal is to use fzf in LuaSnip for ledger files, autocompleting the payee and the accounts inside a LuaSnip snippet with fzf-lua (or plain fzf) functionality. While I did not get anything meaningful set up with fzf-lua, I finally got the closest I could get with the code below.

The functionNode, shorthand f(, ...) requires a function which returns a string. I haven't found a way to have any fzf-lua function return a simple lua string. Most of them seem to exist for their side effects of opening the file(s) chosen by the user.

typical shorthands from LuaSnip shorthands

-- LuaSnip shorthands (link above)
local payees = function()
        p=io.popen("ledger payees | fzf","r")
        pl=p:read()
        p:close()
        return pl
end
ls.add_snippets("ledger", {
    s("fn", {
                f(function()return os.date("%F").." "end,{1}, {user_args={}}),
                f(payees,                                  {2}, {user_args={}}),
                i(1),i(2),t({"",}),i(0)
    }),
  })

While the functionality itself works like a charm (without any further key strokes, I am immediately in fzf mode and can fuzzy-type the payee), this messes up my screen, here after choosing "BadShop" from the list of payees. It still shows stuff around this which had been on the screen before. I have to change into normal mode, CTRL-l to refresh screen display, and re-enter insert mode. However, this stops the active LuaSnip snippet (jumping with TAB etc). Apparently, fzf has some side effects within nvim which change the text visible on-screen or leave the screen text from the shell session or from some earlier nvim session. I can't figure out what happens and can't put my question more focused.

Screenshot from 2022-07-27 08-59-11

To reproduce: save the above lua file to /tmp/ledger.lua, open a fresh ledger file with nvim new.ledger and type :luafile /tmp/ledger.lua<cr> (nvim's command line). If you don't have a ledger file yet, replace the above io.popen command to "ledger -f payees.ledger | fzf", where payees.ledger is e.g. the following file:

2022-07-27 SomeShop
  food  $ 1,23
  cash

2022-07-27 OtherShop
  food  $ 1,23
  cash

2022-07-27 BadShop
  food  $ 1,23
  cash

2022-07-27 BestShop
  food  $ 1,23
  cash

2022-07-27 WorstShop
  food  $ 1,23
  cash

Info

NVIM v0.8.0-dev Build type: RelWithDebInfo LuaJIT 2.1.0-beta3 Compilation: /usr/bin/cc -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=1 -DNVIM_TS_HAS_SET_MATCH_LIMIT -DNVIM_TS_HAS_SET_ALLOCATOR -O2 -g -Og -g -Wall -Wextra -pedantic -Wno-unused-parameter -Wstrict-prototypes -std=gnu99 -Wshadow -Wconversion -Wdouble-promotion -Wmissing-noreturn -Wmissing-format-attribute -Wmissing-prototypes -Wimplicit-fallthrough -Wvla -fstack-protector-strong -fno-common -fdiagnostics-color=always -DINCLUDE_GENERATED_DECLARATIONS -D_GNU_SOURCE -DNVIM_MSGPACK_HAS_FLOAT32 -DNVIM_UNIBI_HAS_VAR_FROM -DMIN_LOG_LEVEL=3 -I/home/myself/Downloads/neovim/build/cmake.config -I/home/myself/Downloads/neovim/src -I/home/myself/Downloads/neovim/.deps/usr/include -I/usr/include -I/home/myself/Downloads/neovim/build/src/nvim/auto -I/home/myself/Downloads/neovim/build/include Compiled by myself@mycomputer

Features: +acl +iconv +tui See ":help feature-compile"

system vimrc file: "$VIM/sysinit.vim" fall-back for $VIM: "/usr/local/share/nvim"

Run :checkhealth for more info

ibhagwan commented 2 years ago

The reason fzf-lua does not return any values is due to it being run inside a lua coroutine, there are many reasons for that but mainly for performance/UI responsiveness.

Both the commands and the API simplify that for you so you never have to think about coroutines, if you wish to access the return values directly without having to specify actions keybinds you need to run the couroutine yourself:

This requires updating the plugin to https://github.com/ibhagwan/fzf-lua/commit/0bfbe039390451aecc38ac521fff3ec1212f99ce as I changed the order of arguments to the core.fzf function to match the fzf_exec API.

-- simple selection from table
coroutine.wrap(function()
  local retval = require'fzf-lua'.fzf({ "1", "2", "3", "4" })
  print("return value", retval[1])
end)()

The call arguments (and thus contents) is equal to the ones used by the fzf_exec API, that means that in your case you'd need to supply ledger payees as a string argument so fzf-lua knows to pipe it into fzf:

coroutine.wrap(function()
  local retval = require'fzf-lua'.fzf("ledger payees")
  print("return value", retval[1])
end)()

However I'm not sure this will work without further modifiations in the luasnip case as you still can't simply call return inside the couroutine.wrap as it is non-blocking, once coroutine.wrap() is called it opens fzf and releases control back to the UI and the function returns.

What you need for this to work is to have luasnip call the f function inside the same coroutine fzf-lua is using, that means you someone need to coroutine.wrap both the call to f (or higher up the callstack) and the call to fzf-lua.fzf.

mloskamp commented 2 years ago

Wow, thanks for the quick and detailed reply! My Lua experience started only with my neovim journey (not too many days ago). When first reading the coroutine sections 5.2 and 2.11 from the Lua 5.1 documentation, I was hoping to save the best for last.

Your reply definitely sparked me off to dive deeper into understanding the return behaviour of coroutines. The implicit "main" function of a Lua coroutine as explained in section 2.11 returns a value which, in turn, is returned as the second value of a call coroutine.resume(ch). (The first value is a boolean, true if the implicit main function finished successfully, false otherwise.)

local ch=coroutine.create(
        function()
                return "this is a plain string returned as the **second** value by the \"main\" function of a coroutine"
        end
)
print('calling "local val1,val2=coroutine.resume(ch)"')
local val1,val2=coroutine.resume(ch)
print("coroutine.status(ch) = ", coroutine.status(ch))
print("val1: ", val)
print("val2: ", val2)
print("type(val1): ", type(val1))
print("type(val2): ", type(val2))

Here's the output:

calling "local val1,val2=coroutine.resume(ch)"
coroutine.status(ch) =  dead
val1:   nil
val2:   this is a plain string returned as the **second** value by the "main" function of a coroutine`
type(val1):     boolean
type(val2):     string

This seems to hint into a first necessary step to a possible solution of my original problem when using fzf-lua. However, you're right that this a little more fiddly as the coroutine stuff is already handled by lua-fzf itself.

I will look deeper into this later today hopefully. I think it will be necessary to understand whether creating a coroutine which somehow wraps a call to fzf-lua functions is the path to try, or to register fzf-lua functions as the implicit "main" function of a coroutine.create(some_fzf-lua_function) call. Or maybe even the fzf-lua functions already return two values, a first (boolean) and a second (string).

fzf itself simply writes its output to STDOUT (according to Usage section of junegunn/fzf at github). This might bring me back to io.popen(), which in the end might end in the same terminal mess that I was trying to avoid in the first place...

... to be further investigated ...

ibhagwan commented 2 years ago

Wow, thanks for the quick and detailed reply! My Lua experience started only with my neovim journey (not too many days ago). When first reading the coroutine sections 5.2 and 2.11 from the Lua 5.1 documentation, I was hoping to save the best for last.

Your lua knowledge for a few days of investment is quite impressive :-)

This seems to hint into a first necessary step to a possible solution of my original problem when using fzf-lua. However, you're right that this a little more fiddly as the coroutine stuff is already handled by lua-fzf itself.

Technically you can wrap the original API with your own coroutine and have full control, not sure if this would help you but another alternative to the code snipped I posted could be:

coroutine.wrap(function()
  local co = coroutine.running()
  require'fzf-lua'.fzf_exec({ "1", "2", "3", "4"}, {
    actions = {
      ['default'] = function(selected)
        print("selected[1]:", selected[1])
        coroutine.resume(co, selected[1])
      end
    }
  })
  print("waiting for fzf")
  local retval = coroutine.yield()
  print("retval:", retval)
end)()

fzf itself simply writes its output to STDOUT (according to Usage section of junegunn/fzf at github). This might bring me back to io.popen(), which in the end might end in the same terminal mess that I was trying to avoid in the first place...

You could also use a neovim RPC named pipe as output.

mloskamp commented 2 years ago

Finally, it came to me that your code samples already are the key to the solution. I had previously "ignored" those open/close parentheses at the very end of coroutine.wrap(...)() and the ones of the anonymous function() right at the start of coroutine.wrap (i.e. the coroutine's "main" function). I put them aside as some kind of syntactic sugar.

After paying their respect, I filled them with meaning - the positions of the text that shall be replaced by the fzf's return value, i.e. the user's choice of payees. Your hint with the RPC call was the next key: the positions can be provided inside the coroutine's separate thread which communicates through Lua's API functions with neovim's main process: here is a correctly working standalone example with mock numbers and without the LuaSnip embeddings:

coroutine.wrap(function(start_row,start_col,end_row,end_col)
  local retval = require'fzf-lua'.fzf("ledger payees")
  print("return value", retval[1])
  vim.api.nvim_buf_set_text(0,start_row,start_col,end_row,end_col,{"-- "..retval[1],""})
end)(0,0,0,0)  -- to keep it simple, just enter the text at the very beginning of the buffer.

disclaimer for the interested reader: the above code still throws some error message in LuaSnip

For the purpose of my original problem, though, I consider my question answered within fzf-lua's realm, and will close this thread now. THANKS A TON AGAIN!!! And cudos to the entire neovim community.

*My further steps are, in my view, no longer connected to the intricacies of fzf-lua and their coroutine magic, but just where to find the relevant start_{row,col} and end{row,col} positions in the LuaSnip realm.

I hope I will be able to include the final error-free full code of my LuaSnips even after closing this thread. ... once I have it. :sweat_smile: :zzz:*

ibhagwan commented 2 years ago

Brilliant @mloskamp, I didn’t know you could pass arguments to the coroutine start like that!

Good luck and don’t hesitate to reopen if you need more help.