Repository: ibhagwan/fzf-lua
Pull request #1955: fix(rpc): use `nvim -l` to get argv easilier (also less side effects)
Author: login: phanen
Created: 2025-04-06T10:10:45Z | Merged: 2025-04-12T02:14:11Z

1. `nvim -l` still quit immediately even with `coroutine.yield`...
```
local co = coroutine.create(function()
  coroutine.yield()
  os.exit(0)
end)

vim.defer_fn(function()
  print('defer done')
  coroutine.resume(co)
end, 1000)

coroutine.resume(co)
```

2. `vim.wait` don't work on windows?


Diff:
diff --git a/.github/workflows/vimdoc.yaml b/.github/workflows/vimdoc.yaml
index bd27392c9..eeb535c2c 100644
--- a/.github/workflows/vimdoc.yaml
+++ b/.github/workflows/vimdoc.yaml
@@ -14,7 +14,7 @@ jobs:
       - uses: rhysd/action-setup-vim@v1
         with:
           neovim: true
-          version: v0.10.3
+          version: v0.10.4
       - name: Setup treesitter
         run: |
           mkdir -p ~/.local/share/nvim/site/pack/vendor/start
@@ -28,7 +28,7 @@ jobs:
         run: |
           export PATH="${PWD}/build/:${PATH}"
           export PACKPATH=$HOME/.local/share/nvim/site
-          nvim --headless -u ~/.local/share/nvim/site/pack/vendor/start/ts-vimdoc.nvim/scripts/init.lua  -c "lua require('ts-vimdoc').docgen({input_file='README.md', output_file='doc/fzf-lua.txt', project_name='fzf-lua', version='For Neovim >= 0.7.0'})" -c "qa"
+          nvim --headless -u ~/.local/share/nvim/site/pack/vendor/start/ts-vimdoc.nvim/scripts/init.lua  -c "lua require('ts-vimdoc').docgen({input_file='README.md', output_file='doc/fzf-lua.txt', project_name='fzf-lua', version='For Neovim >= 0.9.0'})" -c "qa"
       - name: Commit changes
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/README.md b/README.md
index 66d35055c..1cec8b333 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
 
 # fzf :heart: lua
 
-![Neovim version](https://img.shields.io/badge/Neovim-0.7-57A143?style=flat-square&logo=neovim)
+![Neovim version](https://img.shields.io/badge/Neovim-0.9-57A143?style=flat-square&logo=neovim)
 
 [Quickstart](#quickstart) • [Installation](#installation) • [Usage](#usage) • [Commands](#commands) • [Customization](#customization) • [Wiki](https://github.com/ibhagwan/fzf-lua/wiki)
 
@@ -55,7 +55,7 @@ Using [lazy.nvim](https://github.com/folke/lazy.nvim)
 
 ### Dependencies
 
-- [`neovim`](https://github.com/neovim/neovim/releases) version > `0.7.0`
+- [`neovim`](https://github.com/neovim/neovim/releases) version >= `0.9`
 - [`fzf`](https://github.com/junegunn/fzf) version > `0.25`
   or [`skim`](https://github.com/skim-rs/skim) binary installed
 - [nvim-web-devicons](https://github.com/nvim-tree/nvim-web-devicons)
diff --git a/lua/fzf-lua/devicons.lua b/lua/fzf-lua/devicons.lua
index e8fabd0a9..b1bc16729 100644
--- a/lua/fzf-lua/devicons.lua
+++ b/lua/fzf-lua/devicons.lua
@@ -45,8 +45,6 @@ function NvimWebDevicons:new()
 end
 
 function NvimWebDevicons:load(do_not_lazy_load)
-  -- limit devicons support to nvim >=0.8, although official support is >=0.7
-  -- running setup on 0.7 errs with "W18: Invalid character in group name"
   if not self._package_loaded
       -- do not trigger lazy loading
       and (not do_not_lazy_load or package.loaded[self._package_name])
@@ -180,7 +178,7 @@ function MiniIcons:new()
 end
 
 function MiniIcons:load(do_not_lazy_load)
-  if not self._package_loaded and utils.__HAS_NVIM_08
+  if not self._package_loaded
       -- do not trigger lazy loading
       and (not do_not_lazy_load or package.loaded[self._package_name])
   then
diff --git a/lua/fzf-lua/fzf.lua b/lua/fzf-lua/fzf.lua
index 1b5edca81..e54f73993 100644
--- a/lua/fzf-lua/fzf.lua
+++ b/lua/fzf-lua/fzf.lua
@@ -279,6 +279,8 @@ function M.raw_fzf(contents, fzf_cli_args, opts)
       ["SHELL"] = shell_cmd[1],
       ["FZF_DEFAULT_COMMAND"] = FZF_DEFAULT_COMMAND,
       ["SKIM_DEFAULT_COMMAND"] = FZF_DEFAULT_COMMAND,
+      ["FZF_LUA_SERVER"] = vim.g.fzf_lua_server,
+      ["VIMRUNTIME"] = vim.env.VIMRUNTIME,
       ["FZF_DEFAULT_OPTS"] = (function()
         -- Newer style `--preview-window` options in FZF_DEFAULT_OPTS such as:
         --   --preview-window "right,50%,hidden,<60(up,70%,hidden)"
diff --git a/lua/fzf-lua/init.lua b/lua/fzf-lua/init.lua
index 0c9bb7e43..43ab17b2a 100644
--- a/lua/fzf-lua/init.lua
+++ b/lua/fzf-lua/init.lua
@@ -84,12 +84,7 @@ function M.setup_highlights(override)
     { "FzfLuaScrollFloatFull",   "scrollfloat_f",  { default = default, link = "PmenuThumb" } },
     { "FzfLuaDirIcon",           "dir_icon",       { default = default, link = "Directory" } },
     { "FzfLuaDirPart",           "dir_part",       { default = default, link = "Comment" } },
-    { "FzfLuaFilePart", "file_part",
-      {
-        default = default,
-        link = utils.__HAS_NVIM_08 and "@none" or "Normal",
-      }
-    },
+    { "FzfLuaFilePart",          "file_part",      { default = default, link = "@none" } },
     -- Fzf terminal hls, colors from `vim.api.nvim_get_color_map()`
     { "FzfLuaHeaderBind", "header_bind",
       { default = default, fg = is_light and "MediumSpringGreen" or "BlanchedAlmond" } },
@@ -146,20 +141,6 @@ function M.setup_highlights(override)
     vim.api.nvim_set_hl(0, hl_name, hl_def)
   end
 
-  -- linking to a cleared hl is bugged in neovim 0.8.x
-  -- resulting in a pink background for hls linked to `Normal`
-  if not utils.__HAS_NVIM_09 and utils.__HAS_NVIM_08 then
-    for _, a in ipairs(highlights) do
-      local hl_name, opt_name = a[1], a[2]
-      if utils.is_hl_cleared(hl_name) then
-        -- reset any invalid hl, this will cause our 'winhighlight'
-        -- string to look something akin to `Normal:,FloatBorder:`
-        -- which uses terminal fg|bg colors instead
-        utils.map_set(config.setup_opts, "__HLS." .. opt_name, "")
-      end
-    end
-  end
-
   -- Init the colormap singleton
   utils.COLORMAP()
 end
diff --git a/lua/fzf-lua/libuv.lua b/lua/fzf-lua/libuv.lua
index d0fc7a115..580ff3936 100644
--- a/lua/fzf-lua/libuv.lua
+++ b/lua/fzf-lua/libuv.lua
@@ -51,6 +51,8 @@ end
 ---@param opts {cwd: string, cmd: string|table, env: table?, cb_finish: function, cb_write: function, cb_err: function, cb_pid: function, fn_transform: function?, EOL: string?, process1: boolean?, profile: boolean?}
 ---@param fn_transform function?
 ---@param fn_done function?
+---@return uv.uv_process_t proc
+---@return integer         pid
 M.spawn = function(opts, fn_transform, fn_done)
   local EOL = opts.EOL or "\n"
   local output_pipe = uv.new_pipe(false)
@@ -89,11 +91,26 @@ M.spawn = function(opts, fn_transform, fn_done)
   else
     table.insert(args, tostring(opts.cmd))
   end
+
+  ---@diagnostic disable-next-line: missing-fields
   local handle, pid = uv.spawn(shell, {
     args = args,
     stdio = { nil, output_pipe, error_pipe },
     cwd = opts.cwd,
-    env = opts.env,
+    ---@diagnostic disable-next-line: assign-type-mismatch
+    env = (function()
+      -- uv.spawn will override all env when table provided?
+      -- steal from $VIMRUNTIME/lua/vim/_system.lua
+      local env = vim.fn.environ() --- @type table<string,string>
+      env["NVIM"] = vim.v.servername
+      env["NVIM_LISTEN_ADDRESS"] = nil
+      env = vim.tbl_extend("keep", opts.env or {}, env or {})
+      local renv = {} --- @type string[]
+      for k, v in pairs(env) do
+        renv[#renv + 1] = string.format("%s=%s", k, tostring(v))
+      end
+      return renv
+    end)(),
     verbatim = _is_win,
   }, function(code, signal)
     on_exit_called = true
@@ -218,6 +235,7 @@ M.spawn = function(opts, fn_transform, fn_done)
     output_pipe:read_start(read_cb)
     error_pipe:read_start(err_cb)
   end
+  return handle, pid
 end
 
 M.async_spawn = coroutinify(M.spawn)
@@ -675,20 +693,10 @@ M.wrap_spawn_stdio = function(opts, fn_transform, fn_preprocess, fn_postprocess)
   assert(opts and type(opts) == "string")
   assert(not fn_transform or type(fn_transform) == "string")
   local nvim_bin = os.getenv("FZF_LUA_NVIM_BIN") or vim.v.progpath
-  local nvim_runtime = os.getenv("FZF_LUA_NVIM_BIN") and ""
-      or string.format(
-        _is_win and [[set VIMRUNTIME=%s& ]] or "VIMRUNTIME=%s ",
-        _is_win and vim.fs.normalize(vim.env.VIMRUNTIME) or M.shellescape(vim.env.VIMRUNTIME)
-      )
-  local lua_cmd = ("lua %sloadfile([[%s]])().spawn_stdio(%s,%s,%s,%s)"):format(
-    _has_nvim_010 and "vim.g.did_load_filetypes=1; " or "",
-    vim.fn.fnamemodify(_is_win and vim.fs.normalize(__FILE__) or __FILE__, ":h") .. "/spawn.lua",
-    opts, fn_transform, fn_preprocess, fn_postprocess
-  )
-  local cmd_str = ("%s%s -n --headless -u NONE -i NONE --cmd %s"):format(
-    nvim_runtime,
+  local cmd_str = ("%s -u NONE -l %s %s"):format(
     M.shellescape(_is_win and vim.fs.normalize(nvim_bin) or nvim_bin),
-    M.shellescape(lua_cmd)
+    vim.fn.fnamemodify(_is_win and vim.fs.normalize(__FILE__) or __FILE__, ":h") .. "/spawn.lua",
+    M.shellescape(("return %s,%s,%s,%s"):format(opts, fn_transform, fn_preprocess, fn_postprocess))
   )
   return cmd_str
 end
diff --git a/lua/fzf-lua/previewer/builtin.lua b/lua/fzf-lua/previewer/builtin.lua
index dc8a8f29f..044f7343a 100644
--- a/lua/fzf-lua/previewer/builtin.lua
+++ b/lua/fzf-lua/previewer/builtin.lua
@@ -957,40 +957,37 @@ function Previewer.buffer_or_file:do_syntax(entry)
         ))
       end
       if syntax_limit_reached == 0 then
-        local fallback = not utils.__HAS_NVIM_09
-        if utils.__HAS_NVIM_09 then
-          fallback = (function()
-            local ft = entry.filetype
-                or self.ext_ft_override and self.ext_ft_override[path.extension(entry.path)]
-                or vim.filetype.match({ buf = bufnr, filename = entry.path })
-            if type(ft) ~= "string" then
-              return true
-            end
-            local ts_enabled = (function()
-              if not self.treesitter or
-                  self.treesitter.enabled == false or
-                  self.treesitter.disabled == true or
-                  (type(self.treesitter.enabled) == "table" and
-                    not utils.tbl_contains(self.treesitter.enabled, ft)) or
-                  (type(self.treesitter.disabled) == "table" and
-                    utils.tbl_contains(self.treesitter.disabled, ft)) then
-                return false
-              end
-              return true
-            end)()
-            local ts_success = ts_enabled and ts_attach(bufnr, ft)
-            if not ts_success then
-              pcall(function() vim.bo[bufnr].syntax = ft end)
-            else
-              -- Use buf local var as setting ft might have unintended consequences
-              -- currently only being used in `update_render_markdown` but might be
-              -- of use in the future?
-              vim.b[bufnr]._ft = ft
-              self:update_render_markdown()
-              self:update_ts_context()
+        local fallback = (function()
+          local ft = entry.filetype
+              or self.ext_ft_override and self.ext_ft_override[path.extension(entry.path)]
+              or vim.filetype.match({ buf = bufnr, filename = entry.path })
+          if type(ft) ~= "string" then
+            return true
+          end
+          local ts_enabled = (function()
+            if not self.treesitter or
+                self.treesitter.enabled == false or
+                self.treesitter.disabled == true or
+                (type(self.treesitter.enabled) == "table" and
+                  not utils.tbl_contains(self.treesitter.enabled, ft)) or
+                (type(self.treesitter.disabled) == "table" and
+                  utils.tbl_contains(self.treesitter.disabled, ft)) then
+              return false
             end
+            return true
           end)()
-        end
+          local ts_success = ts_enabled and ts_attach(bufnr, ft)
+          if not ts_success then
+            pcall(function() vim.bo[bufnr].syntax = ft end)
+          else
+            -- Use buf local var as setting ft might have unintended consequences
+            -- currently only being used in `update_render_markdown` but might be
+            -- of use in the future?
+            vim.b[bufnr]._ft = ft
+            self:update_render_markdown()
+            self:update_ts_context()
+          end
+        end)()
         if fallback then
           if entry.filetype == "help" then
             -- if entry.filetype and #entry.filetype>0 then
diff --git a/lua/fzf-lua/profiles/default.lua b/lua/fzf-lua/profiles/default.lua
index 7f76abd61..f5f386c49 100644
--- a/lua/fzf-lua/profiles/default.lua
+++ b/lua/fzf-lua/profiles/default.lua
@@ -1,5 +1,4 @@
 return {
   desc = "fzf-lua default options",
-  -- Defaults to picker info in win title if neovim version >= 0.9, prompt otherwise
-  require("fzf-lua").utils.__HAS_NVIM_09 and "default-title" or "default-prompt"
+  "default-title",
 }
diff --git a/lua/fzf-lua/providers/lsp.lua b/lua/fzf-lua/providers/lsp.lua
index 2aa3e4c56..68d3c8dd9 100644
--- a/lua/fzf-lua/providers/lsp.lua
+++ b/lua/fzf-lua/providers/lsp.lua
@@ -8,14 +8,6 @@ local make_entry = require "fzf-lua.make_entry"
 
 local M = {}
 
-local function handler_capability(handler)
-  if utils.__HAS_NVIM_08 then
-    return handler.server_capability
-  else
-    return handler.resolved_capability
-  end
-end
-
 local function check_capabilities(handler, silent)
   local clients = utils.lsp_get_clients({ bufnr = core.CTX().bufnr })
 
@@ -24,25 +16,14 @@ local function check_capabilities(handler, silent)
   local num_clients = 0
 
   for _, client in pairs(clients) do
-    if utils.__HAS_NVIM_09 then
-      -- https://github.com/neovim/neovim/blob/65738202f8be3ca63b75197d48f2c7a9324c035b/runtime/doc/news.txt#L118-L122
-      -- Dynamic registration of LSP capabilities. An implication of this change is
-      -- that checking a client's `server_capabilities` is no longer a sufficient
-      -- indicator to see if a server supports a feature. Instead use
-      -- `client.supports_method(<method>)`. It considers both the dynamic
-      -- capabilities and static `server_capabilities`.
-      if client:supports_method(handler.prep or handler.method) then
-        num_clients = num_clients + 1
-      end
-    elseif utils.__HAS_NVIM_08 then
-      if client.server_capabilities[handler.server_capability] then
-        num_clients = num_clients + 1
-      end
-    else
-      ---@diagnostic disable-next-line: undefined-field
-      if client.resolved_capabilities[handler.resolved_capability] then
-        num_clients = num_clients + 1
-      end
+    -- https://github.com/neovim/neovim/blob/65738202f8be3ca63b75197d48f2c7a9324c035b/runtime/doc/news.txt#L118-L122
+    -- Dynamic registration of LSP capabilities. An implication of this change is
+    -- that checking a client's `server_capabilities` is no longer a sufficient
+    -- indicator to see if a server supports a feature. Instead use
+    -- `client.supports_method(<method>)`. It considers both the dynamic
+    -- capabilities and static `server_capabilities`.
+    if client:supports_method(handler.prep or handler.method) then
+      num_clients = num_clients + 1
     end
   end
 
@@ -318,70 +299,60 @@ end
 local handlers = {
   ["code_actions"] = {
     label = "Code Actions",
-    resolved_capability = "code_action",
     server_capability = "codeActionProvider",
     method = "textDocument/codeAction",
     handler = code_action_handler
   },
   ["references"] = {
     label = "References",
-    resolved_capability = "find_references",
     server_capability = "referencesProvider",
     method = "textDocument/references",
     handler = location_handler
   },
   ["definitions"] = {
     label = "Definitions",
-    resolved_capability = "goto_definition",
     server_capability = "definitionProvider",
     method = "textDocument/definition",
     handler = location_handler
   },
   ["declarations"] = {
     label = "Declarations",
-    resolved_capability = "goto_declaration",
     server_capability = "declarationProvider",
     method = "textDocument/declaration",
     handler = location_handler
   },
   ["typedefs"] = {
     label = "Type Definitions",
-    resolved_capability = "type_definition",
     server_capability = "typeDefinitionProvider",
     method = "textDocument/typeDefinition",
     handler = location_handler
   },
   ["implementations"] = {
     label = "Implementations",
-    resolved_capability = "implementation",
     server_capability = "implementationProvider",
     method = "textDocument/implementation",
     handler = location_handler
   },
   ["document_symbols"] = {
     label = "Document Symbols",
-    resolved_capability = "document_symbol",
     server_capability = "documentSymbolProvider",
     method = "textDocument/documentSymbol",
     handler = symbol_handler
   },
   ["workspace_symbols"] = {
     label = "Workspace Symbols",
-    resolved_capability = "workspace_symbol",
     server_capability = "workspaceSymbolProvider",
     method = "workspace/symbol",
     handler = symbol_handler
   },
   ["live_workspace_symbols"] = {
     label = "Workspace Symbols",
-    resolved_capability = "workspace_symbol",
     server_capability = "workspaceSymbolProvider",
     method = "workspace/symbol",
     handler = symbol_handler
   },
   ["incoming_calls"] = {
     label = "Incoming Calls",
-    resolved_capability = "call_hierarchy",
     server_capability = "callHierarchyProvider",
     method = "callHierarchy/incomingCalls",
     prep = "textDocument/prepareCallHierarchy",
@@ -389,7 +360,6 @@ local handlers = {
   },
   ["outgoing_calls"] = {
     label = "Outgoing Calls",
-    resolved_capability = "call_hierarchy",
     server_capability = "callHierarchyProvider",
     method = "callHierarchy/outgoingCalls",
     prep = "textDocument/prepareCallHierarchy",
@@ -709,7 +679,7 @@ M.finder = function(opts)
       opts.silent = opts.silent == nil and true or opts.silent
       opts.no_autoclose = true
       opts.lsp_handler = handlers[method]
-      opts.lsp_handler.capability = handler_capability(opts.lsp_handler)
+      opts.lsp_handler.capability = opts.lsp_handler.server_capability
       opts.lsp_params = lsp_params -- reset previous calls params if existed
 
       -- returns nil for no client attached, false for unsupported capability
@@ -1006,7 +976,7 @@ local function wrap_fn(key, fn)
   return function(opts)
     opts = opts or {}
     opts.lsp_handler = handlers[key]
-    opts.lsp_handler.capability = handler_capability(opts.lsp_handler)
+    opts.lsp_handler.capability = opts.lsp_handler.server_capability
 
     -- check_capabilities will print the appropriate warning
     if not check_capabilities(opts.lsp_handler) then
diff --git a/lua/fzf-lua/shell.lua b/lua/fzf-lua/shell.lua
index e6c848f99..4f90220f6 100644
--- a/lua/fzf-lua/shell.lua
+++ b/lua/fzf-lua/shell.lua
@@ -85,27 +85,13 @@ function M.raw_async_action(fn, fzf_field_expression, debug)
   -- this is for windows WSL and AppImage users, their nvim path isn't just
   -- 'nvim', it can be something else
   local nvim_bin = os.getenv("FZF_LUA_NVIM_BIN") or vim.v.progpath
-  local nvim_runtime = os.getenv("FZF_LUA_NVIM_BIN") and "" or string.format(
-    utils._if_win([[set VIMRUNTIME=%s& ]], "VIMRUNTIME=%s "),
-    utils._if_win(path.normalize(vim.env.VIMRUNTIME),
-      libuv.shellescape(vim.env.VIMRUNTIME)))
-
-  local call_args = ("fzf_lua_server=[[%s]], fnc_id=%d %s"):format(
-    vim.g.fzf_lua_server, id, debug and ", debug=true" or "")
-
-  -- we need to add '--' to mark the end of command options otherwise
-  -- our preview command will fail when the selected items contain
-  -- special shell chars ('+', '-', etc), examples where this can
-  -- happen are the `git status` command and git branches from diff
-  -- worktrees (#600)
-  local action_cmd = ("%s%s -n --headless -u NONE -i NONE --cmd %s -- %s"):format(
-    nvim_runtime,
+
+  -- all args after `-l` will be in `_G.args`
+  local action_cmd = ("%s -u NONE -l %s %s %s %s"):format(
     libuv.shellescape(path.normalize(nvim_bin)),
-    libuv.shellescape(("lua %sloadfile([[%s]])().rpc_nvim_exec_lua({%s})"):format(
-      utils.__HAS_NVIM_010 and "vim.g.did_load_filetypes=1; " or "",
-      path.join { vim.g.fzf_lua_directory, "shell_helper.lua" },
-      call_args
-    )),
+    libuv.shellescape(path.normalize(path.join { vim.g.fzf_lua_directory, "shell_helper.lua" })),
+    id,
+    debug,
     fzf_field_expression)
 
   return action_cmd, id
diff --git a/lua/fzf-lua/shell_helper.lua b/lua/fzf-lua/shell_helper.lua
index 6ab45c0a5..b8ee9b695 100644
--- a/lua/fzf-lua/shell_helper.lua
+++ b/lua/fzf-lua/shell_helper.lua
@@ -4,6 +4,10 @@ local uv = vim.uv or vim.loop
 
 local _is_win = vim.fn.has("win32") == 1 or vim.fn.has("win64") == 1
 
+if vim.v.servername and #vim.v.servername > 0 then
+  pcall(vim.fn.serverstop, vim.v.servername)
+end
+
 ---@return string
 local function windows_pipename()
   local tmpname = vim.fn.tempname()
@@ -20,40 +24,40 @@ end
 
 local preview_socket, preview_socket_path = get_preview_socket()
 
-uv.listen(preview_socket, 100, function(_)
-  local preview_receive_socket = uv.new_pipe(false)
-  -- start listening
+local preview_receive_socket
+uv.listen(preview_socket, 1, function(_)
+  preview_receive_socket = uv.new_pipe(false)
   uv.accept(preview_socket, preview_receive_socket)
+
+  -- Avoid dangling temp dir on premature process kills (live grep)
+  -- see more complete note in spawn.lua
+  if not _is_win then
+    uv.fs_unlink(preview_socket_path)
+    local tmpdir = vim.fn.fnamemodify(preview_socket_path, ":h")
+    if tmpdir and #tmpdir > 0 then uv.fs_rmdir(tmpdir) end
+  end
+
   preview_receive_socket:read_start(function(err, data)
     assert(not err)
     if not data then
       uv.close(preview_receive_socket)
       uv.close(preview_socket)
-      vim.schedule(function()
-        vim.cmd([[qall]])
-      end)
+      -- on windows: ci fail when use uv.stop()
+      -- on linux: zero event can freeze https://github.com/ibhagwan/fzf-lua/pull/1955#issuecomment-2785474217
+      -- uv.stop()
+      os.exit(0)
       return
     end
     io.write(data)
   end)
 end)
 
-local function rpc_nvim_exec_lua(opts)
+local rpc_nvim_exec_lua = function(opts)
   local success, errmsg = pcall(function()
-    -- fzf selection is unpacked as the argument list
-    local fzf_selection = {}
-    local nargs = vim.fn.argc()
-    for i = 0, nargs - 1 do
-      -- On Windows, vim.fn.argv() normalizes the path (replaces bslash with fslash)
-      -- while vim.v.argv provides access to the raw argument, however, vim.v.argv
-      -- contains the headless wrapper arguments so we need to index backwards
-      table.insert(fzf_selection,
-        _is_win and vim.v.argv[#vim.v.argv - nargs + 1 + i] or vim.fn.argv(i))
-    end
     -- for skim compatibility
     local preview_lines = vim.env.FZF_PREVIEW_LINES or vim.env.LINES
     local preview_cols = vim.env.FZF_PREVIEW_COLUMNS or vim.env.COLUMNS
-    local chan_id = vim.fn.sockconnect("pipe", opts.fzf_lua_server, { rpc = true })
+    local chan_id = vim.fn.sockconnect("pipe", vim.env.FZF_LUA_SERVER or vim.env.NVIM, { rpc = true })
     vim.rpcrequest(chan_id, "nvim_exec_lua", [[
       local luaargs = {...}
       local function_id = luaargs[1]
@@ -66,37 +70,19 @@ local function rpc_nvim_exec_lua(opts)
     ]], {
       opts.fnc_id,
       preview_socket_path,
-      fzf_selection,
+      opts.fzf_selection,
       tonumber(preview_lines),
       tonumber(preview_cols),
     })
     vim.fn.chanclose(chan_id)
   end)
 
-  -- Avoid dangling temp dir on premature process kills (live grep)
-  -- see more complete note in spawn.lua
-  local tmpdir = vim.fn.fnamemodify(vim.fn.tempname(), ":h")
-  if tmpdir and #tmpdir > 0 then
-    vim.fn.delete(tmpdir, "rf")
-  end
-  if vim.v.servername and #vim.v.servername > 0 then
-    pcall(vim.fn.serverstop, vim.v.servername)
-  end
-
   if not success or opts.debug then
     io.stderr:write(("[DEBUG] debug = %s\n"):format(opts.debug))
     io.stderr:write(("[DEBUG] function ID = %d\n"):format(opts.fnc_id))
     io.stderr:write(("[DEBUG] fzf_lua_server = %s\n"):format(opts.fzf_lua_server))
-    for i = 1, #vim.v.argv do
-      io.stderr:write(("[DEBUG] argv[%d] = %s\n"):format(i, vim.v.argv[i]))
-    end
-    local nargs = vim.fn.argc()
-    for i = 0, nargs - 1 do
-      io.stderr:write(("[DEBUG] argv[%d] = %s\n"):format(i, vim.fn.argv(i)))
-    end
-    for i = 0, nargs - 1 do
-      local argv_idx = #vim.v.argv - nargs + 1 + i
-      io.stderr:write(("[DEBUG] v:arg[%d:%d] = %s\n"):format(i, argv_idx, vim.v.argv[argv_idx]))
+    for i, v in pairs(_G.arg) do
+      io.stderr:write(("[DEBUG] argv[%d] = %s\n"):format(i, v))
     end
     for _, var in ipairs({ "LINES", "COLUMNS" }) do
       io.stderr:write(("[DEBUG] $%s = %s\n"):format(var, os.getenv(var) or "<null>"))
@@ -105,10 +91,18 @@ local function rpc_nvim_exec_lua(opts)
 
   if not success then
     io.stderr:write(("FzfLua Error: %s\n"):format(errmsg or "<null>"))
-    vim.cmd([[qall]])
+    os.exit(1)
   end
+
+  uv.run("once")
+  uv.run() -- noreturn, quit by os.exit
 end
 
-return {
-  rpc_nvim_exec_lua = rpc_nvim_exec_lua,
+local args = vim.deepcopy(_G.arg)
+args[0] = nil -- remove filename
+local opts = {
+  fnc_id = tonumber(table.remove(args, 1)),
+  debug = table.remove(args, 1) == "true",
+  fzf_selection = args
 }
+rpc_nvim_exec_lua(opts)
diff --git a/lua/fzf-lua/spawn.lua b/lua/fzf-lua/spawn.lua
index 6569ca337..6848c2091 100644
--- a/lua/fzf-lua/spawn.lua
+++ b/lua/fzf-lua/spawn.lua
@@ -1,17 +1,7 @@
 -- This file should only be loaded from the headless instance
+local uv = vim.uv or vim.loop
 assert(#vim.api.nvim_list_uis() == 0)
 
--- When running CI tests there's a bug on Windows which adds a space after the runtime
--- path, this will subsequently cause `vim.filetype.match` to fail  when it attempts to
---`require("vim.filetype.detect")` which will fail our custom `path.ft_match`. This will
--- fail mini.icons UI tests as any icon that requires the use of `vim.filetype.match`
--- will return a default icon
-local VIMRUNTIME = os.getenv("VIMRUNTIME")
-if VIMRUNTIME and VIMRUNTIME:match("%s$") then
-  vim.opt.runtimepath:prepend(vim.trim(VIMRUNTIME))
-  -- package.path = ("%s/lua/?.lua;"):format(vim.trim(VIMRUNTIME)) .. package.path
-end
-
 -- path to this file
 local __FILE__ = debug.getinfo(1, "S").source:gsub("^@", "")
 
@@ -42,5 +32,8 @@ end
 
 -- global var indicating a headless instance
 _G._fzf_lua_is_headless = true
-
-return { spawn_stdio = require("fzf-lua.libuv").spawn_stdio }
+local _, pid = require("fzf-lua.libuv").spawn_stdio(loadstring(_G.arg[1])())
+-- while vim.uv.run() do end -- os.exit in spawn_stdio
+while uv.os_getpriority(pid) do
+  vim.wait(100, function() return uv.os_getpriority(pid) == nil end)
+end
diff --git a/lua/fzf-lua/test/helpers.lua b/lua/fzf-lua/test/helpers.lua
index ed1ae902b..33ddf3ade 100644
--- a/lua/fzf-lua/test/helpers.lua
+++ b/lua/fzf-lua/test/helpers.lua
@@ -136,6 +136,7 @@ M.new_child_neovim = function()
           on_close = function()
             _G._fzf_lua_on_create = nil
             _G._fzf_postprocess_called = nil
+            _G._fzf_load_called = nil
           end,
         },
         keymap = { fzf = {
@@ -265,13 +266,14 @@ M.new_child_neovim = function()
     MiniTest.expect.reference_screenshot(child.get_buf_lines(buf, screenshot_opts), path, opts)
   end
 
+  local wait_timeout = (M.IS_WIN() and 5000 or 2000)
   --- waits until condition fn evals to true, checking every interval ms
   --- times out at timeout ms
   ---@param condition fun(): boolean
   ---@param timeout? integer, defaults to 2000
   ---@param interval? integer, defaults to 100
   child.wait_until = function(condition, timeout, interval)
-    local max = timeout or 2000
+    local max = timeout or wait_timeout
     local inc = interval or 100
     for _ = 0, max, inc do
       if condition() then
diff --git a/lua/fzf-lua/utils.lua b/lua/fzf-lua/utils.lua
index 2e1ad10e2..6546b50f0 100644
--- a/lua/fzf-lua/utils.lua
+++ b/lua/fzf-lua/utils.lua
@@ -10,8 +10,6 @@ local uv = vim.uv or vim.loop
 
 local M = {}
 
-M.__HAS_NVIM_08 = vim.fn.has("nvim-0.8") == 1
-M.__HAS_NVIM_09 = vim.fn.has("nvim-0.9") == 1
 M.__HAS_NVIM_010 = vim.fn.has("nvim-0.10") == 1
 M.__HAS_NVIM_0102 = vim.fn.has("nvim-0.10.2") == 1
 M.__HAS_NVIM_011 = vim.fn.has("nvim-0.11") == 1
@@ -587,21 +585,9 @@ end
 
 -- Helper func to test for invalid (cleared) highlights
 function M.is_hl_cleared(hl)
-  -- `vim.api.nvim_get_hl_by_name` is deprecated since v0.9.0
-  if vim.api.nvim_get_hl then
-    local ok, hl_def = pcall(vim.api.nvim_get_hl, 0, { name = hl, link = false })
-    if not ok or M.tbl_isempty(hl_def) then
-      return true
-    end
-  else
-    ---@diagnostic disable-next-line: deprecated
-    local ok, hl_def = pcall(vim.api.nvim_get_hl_by_name, hl, true)
-    -- Not sure if this is the right way but it seems that cleared
-    -- highlights return 'hl_def[true] == 6' (?) and 'hl_def[true]'
-    -- does not exist at all otherwise
-    if not ok or hl_def[true] then
-      return true
-    end
+  local ok, hl_def = pcall(vim.api.nvim_get_hl, 0, { name = hl, link = false })
+  if not ok or M.tbl_isempty(hl_def) then
+    return true
   end
 end
 
@@ -1011,14 +997,6 @@ function M.nvim_buf_get_name(bufnr, bufinfo)
   return bufname
 end
 
-function M.wo(win, k, v)
-  if M.__HAS_NVIM_08 then
-    vim.api.nvim_set_option_value(k, v, { scope = "local", win = win })
-  else
-    vim.wo[win][k] = v
-  end
-end
-
 function M.zz()
   -- skip for terminal buffers
   if M.is_term_buffer() then return end
diff --git a/lua/fzf-lua/win.lua b/lua/fzf-lua/win.lua
index a930d08dc..69cce02ec 100644
--- a/lua/fzf-lua/win.lua
+++ b/lua/fzf-lua/win.lua
@@ -592,8 +592,8 @@ function FzfWin:set_backdrop()
     zindex = self.winopts.zindex - 2,
     border = "none",
   })
-  utils.wo(self.backdrop_win, "winhighlight", "Normal:" .. self.hls.backdrop)
-  utils.wo(self.backdrop_win, "winblend", self.winopts.backdrop)
+  vim.wo[self.backdrop_win].winhighlight = "Normal:" .. self.hls.backdrop
+  vim.wo[self.backdrop_win].winblend = self.winopts.backdrop
   vim.bo[self.backdrop_buf].buftype = "nofile"
   vim.bo[self.backdrop_buf].filetype = "fzflua_backdrop"
 end
@@ -701,13 +701,7 @@ function FzfWin:set_winopts(win, opts, ignore_events)
   end
   for opt, value in pairs(opts) do
     if utils.nvim_has_option(opt) then
-      -- PROBABLY DOESN'T MATTER (WHO USES 0.5?) BUT WHY NOT LOL
-      -- minor backward compatibility fix, with neovim version < 0.7
-      -- nvim_win_get_option("scroloff") which should return -1
-      -- returns an invalid (really big number instead which panics
-      -- when called with nvim_win_set_option, wrapping in a pcall
-      -- ensures this plugin still works for neovim version as low as 0.5!
-      pcall(function() vim.wo[win][opt] = value end)
+      vim.wo[win][opt] = value
     end
   end
   if save_ei then
@@ -793,9 +787,7 @@ function FzfWin:redraw_main()
   self:generate_layout()
 
   local winopts = vim.tbl_extend("keep", (function()
-    if not utils.__HAS_NVIM_09 or
-        (type(self.winopts.title) ~= "string" and type(self.winopts.title) ~= "table")
-    then
+    if type(self.winopts.title) ~= "string" and type(self.winopts.title) ~= "table" then
       return {}
     end
     return {
@@ -857,7 +849,6 @@ function FzfWin:treesitter_detach(buf)
 end
 
 function FzfWin:treesitter_attach()
-  if not utils.__HAS_NVIM_09 then return end
   if not self._o.winopts.treesitter then return end
   -- local utf8 = require("fzf-lua.lib.utf8")
   local function trim(s) return (string.gsub(s, "^%s*(.-)%s*$", "%1")) end
@@ -1218,7 +1209,7 @@ function FzfWin.unhide()
   -- Send SIGWINCH to to trigger resize in the fzf process
   -- We will use the trigger to reload necessary buffer lists
   local pid = fn.jobpid(vim.bo[self._hidden_fzf_bufnr].channel)
-  vim.tbl_map(function(_pid) libuv.process_kill(_pid, 28) end, vim._os_proc_children(pid))
+  vim.tbl_map(function(_pid) libuv.process_kill(_pid, 28) end, api.nvim_get_proc_children(pid))
   vim.bo[self._hidden_fzf_bufnr].bufhidden = "wipe"
   self.fzf_bufnr = self._hidden_fzf_bufnr
   self._hidden_fzf_bufnr = nil
@@ -1331,8 +1322,7 @@ function FzfWin:update_preview_scrollbar()
 end
 
 function FzfWin.update_win_title(winid, winopts, o)
-  -- neovim >= 0.9 added window title
-  if not utils.__HAS_NVIM_09 or (type(o.title) ~= "string" and type(o.title) ~= "table") then
+  if type(o.title) ~= "string" and type(o.title) ~= "table" then
     return
   end
   vim.api.nvim_win_set_config(winid,
@@ -1359,8 +1349,7 @@ function FzfWin:update_main_title(title)
 end
 
 function FzfWin:update_preview_title(title)
-  -- neovim >= 0.9 added window title
-  if not utils.__HAS_NVIM_09 or (type(title) ~= "string" and type(title) ~= "table") then
+  if type(title) ~= "string" and type(title) ~= "table" then
     return
   end
   -- since `nvim_win_set_config` removes all styling, save backup
diff --git a/plugin/fzf-lua.lua b/plugin/fzf-lua.lua
index a9a51c9e0..fc9488631 100644
--- a/plugin/fzf-lua.lua
+++ b/plugin/fzf-lua.lua
@@ -1,10 +1,8 @@
 if vim.g.loaded_fzf_lua == 1 then return end
 vim.g.loaded_fzf_lua = 1
 
--- Should never be called, below nvim 0.7 "plugin/fzf-lua.vim"
--- sets `vim.g.loaded_fzf_lua=1`
-if vim.fn.has("nvim-0.7") ~= 1 then
-  vim.api.nvim_err_writeln("Fzf-lua minimum requirement is Neovim versions 0.7")
+if vim.fn.has("nvim-0.9") ~= 1 then
+  vim.notify("Fzf-lua requires neovim >= v0.9", vim.log.levels.ERROR, { title = "fzf-lua" })
   return
 end
 
diff --git a/scripts/headless_fd.sh b/scripts/headless_fd.sh
index 259a4555a..f683bdde6 100755
--- a/scripts/headless_fd.sh
+++ b/scripts/headless_fd.sh
@@ -130,8 +130,7 @@ if [ $# -gt 0 ]; then
 fi
 
 VIMRUNTIME=/usr/share/nvim/runtime \
-/usr/bin/nvim -n --headless -u NONE -i NONE --cmd "lua vim.g.did_load_filetypes=1; loadfile(
-  [[${BASEDIR}/../lua/fzf-lua/spawn.lua]])().spawn_stdio(
+/usr/bin/nvim -u NONE -l ${BASEDIR}/../lua/fzf-lua/spawn.lua "return
   -- opts
   {
     g = {
@@ -155,4 +154,4 @@ VIMRUNTIME=/usr/share/nvim/runtime \
   [==[
     return require(\"fzf-lua.make_entry\").preprocess
   ]==]
-)"
+"
diff --git a/tests/win_spec.lua b/tests/win_spec.lua
index 6bed00f3a..c8813d2a7 100644
--- a/tests/win_spec.lua
+++ b/tests/win_spec.lua
@@ -38,21 +38,18 @@ T["win"]["hide"]["ensure gc called after win hidden (#1782)"] = function()
       return setmetatable(t, mt)
     end
   ]])
-  eq(child.lua_get([[_G._fzf_load_called]]), vim.NIL)
   child.wait_until(function()
-    child.lua(
-      [[FzfLua.files{ previewer = 'builtin', winopts = { preview = { hidden = false  } } }]])
+    child.lua([[FzfLua.files{ previewer = false }]]) -- make it faster
     child.wait_until(function()
       return child.lua_get([[_G._fzf_load_called]]) == true
-    end, 5000)
-    child.lua([[_G._fzf_load_called = nil]])
-    child.lua([[FzfLua.win.hide()]])
+    end)
+    child.lua([[FzfLua.hide()]])
     child.wait_until(function()
-      return child.lua_get([[_G._fzf_lua_on_create]]) == vim.NIL
-    end, 5000)
+      return child.lua_get([[_G._fzf_load_called]]) == vim.NIL
+    end)
     child.lua([[collectgarbage('collect')]])
     return child.lua_get([[_G._gc_called]]) == true
-  end, 100000)
+  end)
 end
 
 T["win"]["hide"]["buffer deleted after win hidden (#1783)"] = function()
@@ -61,7 +58,7 @@ T["win"]["hide"]["buffer deleted after win hidden (#1783)"] = function()
   child.wait_until(function()
     return child.lua_get([[_G._fzf_lua_on_create]]) == true
   end)
-  child.lua([[FzfLua.win.hide()]])
+  child.lua([[FzfLua.hide()]])
   child.wait_until(function()
     return child.lua_get([[_G._fzf_lua_on_create]]) == vim.NIL
   end)
@@ -75,35 +72,27 @@ T["win"]["hide"]["buffer deleted after win hidden (#1783)"] = function()
 end
 
 T["win"]["hide"]["can resume after close CTX win (#1936)"] = function()
-  -- reload({ "hide" })
   eq(child.lua_get([[_G._fzf_lua_on_create]]), vim.NIL)
   child.cmd([[new]])
   child.lua([[FzfLua.files()]])
   child.wait_until(function()
     return child.lua_get([[_G._fzf_lua_on_create]]) == true
   end)
-  vim.uv.sleep(100)
-  child.type_keys([[<esc>]])
+  child.lua([[FzfLua.hide()]])
   child.wait_until(function()
     return child.lua_get([[_G._fzf_lua_on_create]]) == vim.NIL
   end)
-  vim.uv.sleep(100)
   child.cmd([[close]])
-  vim.uv.sleep(100)
-  child.lua([[FzfLua.resume()]])
-  -- child.lua([[FzfLua.unhide()]])
+  child.lua([[FzfLua.unhide()]])
   child.wait_until(function()
     return child.lua_get([[_G._fzf_lua_on_create]]) == true
   end)
-  vim.uv.sleep(100)
   child.type_keys("<c-j>")
-  vim.uv.sleep(100)
   child.type_keys("<c-j>")
 end
 
 T["win"]["hide"]["actions on multi-select but zero-match #1961"] = function()
   reload({ "hide" })
-  eq(child.lua_get([[_G._fzf_lua_on_create]]), vim.NIL)
   child.lua([[FzfLua.files{
     -- profile = "hide",
     query = "README.md",


Reviews:
user: login: ibhagwan
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-08T03:45:27Z
user: login: phanen
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T03:53:23Z
user: login: phanen
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T04:12:12Z
user: login: ibhagwan
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-08T04:22:56Z
user: login: ibhagwan
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-08T04:33:57Z
user: login: phanen
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T04:35:53Z
user: login: ibhagwan
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-08T04:45:12Z
user: login: phanen
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T04:52:57Z
user: login: ibhagwan
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-08T04:53:15Z
user: login: ibhagwan
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-08T05:02:12Z
user: login: ibhagwan
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-08T08:50:14Z
user: login: ibhagwan
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-08T08:55:09Z
user: login: phanen
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T09:32:38Z
user: login: ibhagwan
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-08T09:36:05Z
user: login: phanen
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T09:37:33Z
user: login: ibhagwan
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-08T09:41:38Z
user: login: phanen
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T09:44:47Z
user: login: ibhagwan
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-08T09:59:54Z
user: login: phanen
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T10:13:18Z

Review comments:
diff_hunk: @@ -89,11 +89,25 @@ M.spawn = function(opts, fn_transform, fn_done)
   else
     table.insert(args, tostring(opts.cmd))
   end
+
+  ---@diagnostic disable-next-line: missing-fields
   local handle, pid = uv.spawn(shell, {
     args = args,
     stdio = { nil, output_pipe, error_pipe },
     cwd = opts.cwd,
-    env = opts.env,
+    ---@diagnostic disable-next-line: assign-type-mismatch
+    env = (function()
+      -- uv.spawn will override all env when table provided?
+      -- steal from $VIMRUNTIME/lua/vim/_system.lua
+      local env = vim.fn.environ() --- @type table<string,string>
+      env["NVIM"] = vim.v.servername
+      env["NVIM_LISTEN_ADDRESS"] = nil
+      env = vim.tbl_extend("keep", opts.env or {}, env or {})
+      local renv = {} --- @type string[]
+      for k, v in pairs(env) do
+        renv[#renv + 1] = string.format("%s=%s", k, tostring(v))
+      end
path: lua/fzf-lua/libuv.lua
user: login: ibhagwan
body: How is this working without `return renv`?
created_at: 2025-04-08T03:45:27Z
author_association: OWNER
diff_hunk: @@ -89,11 +89,25 @@ M.spawn = function(opts, fn_transform, fn_done)
   else
     table.insert(args, tostring(opts.cmd))
   end
+
+  ---@diagnostic disable-next-line: missing-fields
   local handle, pid = uv.spawn(shell, {
     args = args,
     stdio = { nil, output_pipe, error_pipe },
     cwd = opts.cwd,
-    env = opts.env,
+    ---@diagnostic disable-next-line: assign-type-mismatch
+    env = (function()
+      -- uv.spawn will override all env when table provided?
+      -- steal from $VIMRUNTIME/lua/vim/_system.lua
+      local env = vim.fn.environ() --- @type table<string,string>
+      env["NVIM"] = vim.v.servername
+      env["NVIM_LISTEN_ADDRESS"] = nil
+      env = vim.tbl_extend("keep", opts.env or {}, env or {})
+      local renv = {} --- @type string[]
+      for k, v in pairs(env) do
+        renv[#renv + 1] = string.format("%s=%s", k, tostring(v))
+      end
path: lua/fzf-lua/libuv.lua
user: login: phanen
body: I forgot it. But it still work. BTW, why we need to pass a $VIMRUNTIME in cli args before?
created_at: 2025-04-08T03:53:23Z
author_association: COLLABORATOR
diff_hunk: @@ -89,11 +89,25 @@ M.spawn = function(opts, fn_transform, fn_done)
   else
     table.insert(args, tostring(opts.cmd))
   end
+
+  ---@diagnostic disable-next-line: missing-fields
   local handle, pid = uv.spawn(shell, {
     args = args,
     stdio = { nil, output_pipe, error_pipe },
     cwd = opts.cwd,
-    env = opts.env,
+    ---@diagnostic disable-next-line: assign-type-mismatch
+    env = (function()
+      -- uv.spawn will override all env when table provided?
+      -- steal from $VIMRUNTIME/lua/vim/_system.lua
+      local env = vim.fn.environ() --- @type table<string,string>
+      env["NVIM"] = vim.v.servername
+      env["NVIM_LISTEN_ADDRESS"] = nil
+      env = vim.tbl_extend("keep", opts.env or {}, env or {})
+      local renv = {} --- @type string[]
+      for k, v in pairs(env) do
+        renv[#renv + 1] = string.format("%s=%s", k, tostring(v))
+      end
path: lua/fzf-lua/libuv.lua
user: login: phanen
body: I see: https://github.com/ibhagwan/fzf-lua/issues/950.
created_at: 2025-04-08T04:12:12Z
author_association: COLLABORATOR
diff_hunk: @@ -89,11 +89,25 @@ M.spawn = function(opts, fn_transform, fn_done)
   else
     table.insert(args, tostring(opts.cmd))
   end
+
+  ---@diagnostic disable-next-line: missing-fields
   local handle, pid = uv.spawn(shell, {
     args = args,
     stdio = { nil, output_pipe, error_pipe },
     cwd = opts.cwd,
-    env = opts.env,
+    ---@diagnostic disable-next-line: assign-type-mismatch
+    env = (function()
+      -- uv.spawn will override all env when table provided?
+      -- steal from $VIMRUNTIME/lua/vim/_system.lua
+      local env = vim.fn.environ() --- @type table<string,string>
+      env["NVIM"] = vim.v.servername
+      env["NVIM_LISTEN_ADDRESS"] = nil
+      env = vim.tbl_extend("keep", opts.env or {}, env or {})
+      local renv = {} --- @type string[]
+      for k, v in pairs(env) do
+        renv[#renv + 1] = string.format("%s=%s", k, tostring(v))
+      end
path: lua/fzf-lua/libuv.lua
user: login: ibhagwan
body: > I see: #950.

I’m wondering is VIMRUNTIME still required with the `-l` change.
created_at: 2025-04-08T04:22:56Z
author_association: OWNER
diff_hunk: @@ -279,6 +279,8 @@ function M.raw_fzf(contents, fzf_cli_args, opts)
       ["SHELL"] = shell_cmd[1],
       ["FZF_DEFAULT_COMMAND"] = FZF_DEFAULT_COMMAND,
       ["SKIM_DEFAULT_COMMAND"] = FZF_DEFAULT_COMMAND,
+      ["FZF_LUA_SERVER"] = vim.g.fzf_lua_server or vim.v.servername,
path: lua/fzf-lua/fzf.lua
user: login: ibhagwan
body: Not sure I understand why `vim.v.servername` is used here? Why is `vim.g.fzf_lua_server` ever `nil`?
created_at: 2025-04-08T04:33:57Z
author_association: OWNER
diff_hunk: @@ -279,6 +279,8 @@ function M.raw_fzf(contents, fzf_cli_args, opts)
       ["SHELL"] = shell_cmd[1],
       ["FZF_DEFAULT_COMMAND"] = FZF_DEFAULT_COMMAND,
       ["SKIM_DEFAULT_COMMAND"] = FZF_DEFAULT_COMMAND,
+      ["FZF_LUA_SERVER"] = vim.g.fzf_lua_server or vim.v.servername,
path: lua/fzf-lua/fzf.lua
user: login: phanen
body: I also use `vim.env.FZF_LUA_SERVER or vim.env.NVIM` in `shell_helper`
Just defensive programming, should I remove it? It's harmless though.

created_at: 2025-04-08T04:35:53Z
author_association: COLLABORATOR
diff_hunk: @@ -20,40 +24,41 @@ end
 
 local preview_socket, preview_socket_path = get_preview_socket()
 
-uv.listen(preview_socket, 100, function(_)
-  local preview_receive_socket = uv.new_pipe(false)
-  -- start listening
+local chan_id
+local preview_receive_socket
+uv.listen(preview_socket, 1, function(_)
+  preview_receive_socket = uv.new_pipe(false)
   uv.accept(preview_socket, preview_receive_socket)
+
+  -- Avoid dangling temp dir on premature process kills (live grep)
+  -- see more complete note in spawn.lua
+  if not _is_win then
+    uv.fs_unlink(preview_socket_path)
+    local tmpdir = vim.fn.fnamemodify(preview_socket_path, ":h")
+    if tmpdir and #tmpdir > 0 then uv.fs_rmdir(tmpdir) end
+  end
+
   preview_receive_socket:read_start(function(err, data)
     assert(not err)
     if not data then
       uv.close(preview_receive_socket)
       uv.close(preview_socket)
-      vim.schedule(function()
-        vim.cmd([[qall]])
-      end)
+      if _is_win then -- uv.stop() quit slow on windows
+        os.exit(0)
+      end
+      uv.stop() -- exit
       return
     end
     io.write(data)
   end)
 end)
 
-local function rpc_nvim_exec_lua(opts)
+local rpc_nvim_exec_lua = function(opts)
   local success, errmsg = pcall(function()
-    -- fzf selection is unpacked as the argument list
-    local fzf_selection = {}
-    local nargs = vim.fn.argc()
-    for i = 0, nargs - 1 do
-      -- On Windows, vim.fn.argv() normalizes the path (replaces bslash with fslash)
-      -- while vim.v.argv provides access to the raw argument, however, vim.v.argv
-      -- contains the headless wrapper arguments so we need to index backwards
-      table.insert(fzf_selection,
-        _is_win and vim.v.argv[#vim.v.argv - nargs + 1 + i] or vim.fn.argv(i))
-    end
     -- for skim compatibility
     local preview_lines = vim.env.FZF_PREVIEW_LINES or vim.env.LINES
     local preview_cols = vim.env.FZF_PREVIEW_COLUMNS or vim.env.COLUMNS
-    local chan_id = vim.fn.sockconnect("pipe", opts.fzf_lua_server, { rpc = true })
+    chan_id = vim.fn.sockconnect("pipe", vim.env.FZF_LUA_SERVER or vim.env.NVIM, { rpc = true })
path: lua/fzf-lua/shell_helper.lua
user: login: ibhagwan
body: When do we call shell helper outside of the fzf environment so  `FZF_LUA_SERVER` is empty?
If there is such a case and we connect to self (`NVIM`), wouldn’t it be better to skip the RPC call and call the function from the _registry directly (using the fnc_id)?
created_at: 2025-04-08T04:45:12Z
author_association: OWNER
diff_hunk: @@ -20,40 +24,41 @@ end
 
 local preview_socket, preview_socket_path = get_preview_socket()
 
-uv.listen(preview_socket, 100, function(_)
-  local preview_receive_socket = uv.new_pipe(false)
-  -- start listening
+local chan_id
+local preview_receive_socket
+uv.listen(preview_socket, 1, function(_)
+  preview_receive_socket = uv.new_pipe(false)
   uv.accept(preview_socket, preview_receive_socket)
+
+  -- Avoid dangling temp dir on premature process kills (live grep)
+  -- see more complete note in spawn.lua
+  if not _is_win then
+    uv.fs_unlink(preview_socket_path)
+    local tmpdir = vim.fn.fnamemodify(preview_socket_path, ":h")
+    if tmpdir and #tmpdir > 0 then uv.fs_rmdir(tmpdir) end
+  end
+
   preview_receive_socket:read_start(function(err, data)
     assert(not err)
     if not data then
       uv.close(preview_receive_socket)
       uv.close(preview_socket)
-      vim.schedule(function()
-        vim.cmd([[qall]])
-      end)
+      if _is_win then -- uv.stop() quit slow on windows
+        os.exit(0)
+      end
+      uv.stop() -- exit
       return
     end
     io.write(data)
   end)
 end)
 
-local function rpc_nvim_exec_lua(opts)
+local rpc_nvim_exec_lua = function(opts)
   local success, errmsg = pcall(function()
-    -- fzf selection is unpacked as the argument list
-    local fzf_selection = {}
-    local nargs = vim.fn.argc()
-    for i = 0, nargs - 1 do
-      -- On Windows, vim.fn.argv() normalizes the path (replaces bslash with fslash)
-      -- while vim.v.argv provides access to the raw argument, however, vim.v.argv
-      -- contains the headless wrapper arguments so we need to index backwards
-      table.insert(fzf_selection,
-        _is_win and vim.v.argv[#vim.v.argv - nargs + 1 + i] or vim.fn.argv(i))
-    end
     -- for skim compatibility
     local preview_lines = vim.env.FZF_PREVIEW_LINES or vim.env.LINES
     local preview_cols = vim.env.FZF_PREVIEW_COLUMNS or vim.env.COLUMNS
-    local chan_id = vim.fn.sockconnect("pipe", opts.fzf_lua_server, { rpc = true })
+    chan_id = vim.fn.sockconnect("pipe", vim.env.FZF_LUA_SERVER or vim.env.NVIM, { rpc = true })
path: lua/fzf-lua/shell_helper.lua
user: login: phanen
body: I try to make `shell_helper` usable if we copy debug info out then used it in cmdline. 
(before this commit it's always useable since we pass fzf-lua-server socket address in cli args). 
But just now I found this only can be used in shell from nvim's `:term`...

> If there is such a case and we connect to self (`NVIM`), wouldn’t it be better to skip the RPC call and call the function from the _registry directly (using the fnc_id)?

`$NVIM` always points to parent instance (not chlid self (`vim.v.servername`), which is never used and stoped already since we have dangling socket)
created_at: 2025-04-08T04:52:56Z
author_association: COLLABORATOR
diff_hunk: @@ -279,6 +279,8 @@ function M.raw_fzf(contents, fzf_cli_args, opts)
       ["SHELL"] = shell_cmd[1],
       ["FZF_DEFAULT_COMMAND"] = FZF_DEFAULT_COMMAND,
       ["SKIM_DEFAULT_COMMAND"] = FZF_DEFAULT_COMMAND,
+      ["FZF_LUA_SERVER"] = vim.g.fzf_lua_server or vim.v.servername,
path: lua/fzf-lua/fzf.lua
user: login: ibhagwan
body: > I also use `vim.env.FZF_LUA_SERVER or vim.env.NVIM` in `shell_helper` Just defensive programming, should I remove it? It's harmless though.

No problem with being defensive but in some cases if we know with 100% certainty a certain var must exist IMHO it’s better to let this fail and we learn from the error what went wrong, otherwise we may get into a situation where things work but not quite right and we’ll have a hard time figuring out the “voodoo” issue.

Not sure about this one though, maybe this can workaround the `serverstart` issue with a bad XDG_RUNTIME_DIR (#716), or it might be better to let or fail as not having g:_fzf_lua_server` is critical enough to purposefully fail this?
created_at: 2025-04-08T04:53:15Z
author_association: OWNER
diff_hunk: @@ -20,40 +24,41 @@ end
 
 local preview_socket, preview_socket_path = get_preview_socket()
 
-uv.listen(preview_socket, 100, function(_)
-  local preview_receive_socket = uv.new_pipe(false)
-  -- start listening
+local chan_id
+local preview_receive_socket
+uv.listen(preview_socket, 1, function(_)
+  preview_receive_socket = uv.new_pipe(false)
   uv.accept(preview_socket, preview_receive_socket)
+
+  -- Avoid dangling temp dir on premature process kills (live grep)
+  -- see more complete note in spawn.lua
+  if not _is_win then
+    uv.fs_unlink(preview_socket_path)
+    local tmpdir = vim.fn.fnamemodify(preview_socket_path, ":h")
+    if tmpdir and #tmpdir > 0 then uv.fs_rmdir(tmpdir) end
+  end
+
   preview_receive_socket:read_start(function(err, data)
     assert(not err)
     if not data then
       uv.close(preview_receive_socket)
       uv.close(preview_socket)
-      vim.schedule(function()
-        vim.cmd([[qall]])
-      end)
+      if _is_win then -- uv.stop() quit slow on windows
+        os.exit(0)
+      end
+      uv.stop() -- exit
       return
     end
     io.write(data)
   end)
 end)
 
-local function rpc_nvim_exec_lua(opts)
+local rpc_nvim_exec_lua = function(opts)
   local success, errmsg = pcall(function()
-    -- fzf selection is unpacked as the argument list
-    local fzf_selection = {}
-    local nargs = vim.fn.argc()
-    for i = 0, nargs - 1 do
-      -- On Windows, vim.fn.argv() normalizes the path (replaces bslash with fslash)
-      -- while vim.v.argv provides access to the raw argument, however, vim.v.argv
-      -- contains the headless wrapper arguments so we need to index backwards
-      table.insert(fzf_selection,
-        _is_win and vim.v.argv[#vim.v.argv - nargs + 1 + i] or vim.fn.argv(i))
-    end
     -- for skim compatibility
     local preview_lines = vim.env.FZF_PREVIEW_LINES or vim.env.LINES
     local preview_cols = vim.env.FZF_PREVIEW_COLUMNS or vim.env.COLUMNS
-    local chan_id = vim.fn.sockconnect("pipe", opts.fzf_lua_server, { rpc = true })
+    chan_id = vim.fn.sockconnect("pipe", vim.env.FZF_LUA_SERVER or vim.env.NVIM, { rpc = true })
path: lua/fzf-lua/shell_helper.lua
user: login: ibhagwan
body: > I try to make shell_helper usable if we copy debug info out then used it in cmdline.
(before this commit it's always useable since we pass fzf-lua-server socket address in cli args).

I see, so we can run an action without knowing/specifying fzf lua’a server address.

> But just now I found this only can be used in shell from nvim's :term...

Bummer
created_at: 2025-04-08T05:02:12Z
author_association: OWNER
diff_hunk: @@ -20,40 +24,42 @@ end
 
 local preview_socket, preview_socket_path = get_preview_socket()
 
-uv.listen(preview_socket, 100, function(_)
-  local preview_receive_socket = uv.new_pipe(false)
-  -- start listening
+local chan_id
+local preview_receive_socket
+uv.listen(preview_socket, 1, function(_)
+  preview_receive_socket = uv.new_pipe(false)
   uv.accept(preview_socket, preview_receive_socket)
+
+  -- Avoid dangling temp dir on premature process kills (live grep)
+  -- see more complete note in spawn.lua
+  if not _is_win then
+    uv.fs_unlink(preview_socket_path)
+    local tmpdir = vim.fn.fnamemodify(preview_socket_path, ":h")
+    if tmpdir and #tmpdir > 0 then uv.fs_rmdir(tmpdir) end
+  end
+
   preview_receive_socket:read_start(function(err, data)
     assert(not err)
     if not data then
       uv.close(preview_receive_socket)
       uv.close(preview_socket)
-      vim.schedule(function()
-        vim.cmd([[qall]])
-      end)
+      if _is_win then -- uv.stop() quit slow on windows
path: lua/fzf-lua/shell_helper.lua
user: login: ibhagwan
body: Can probably remove this condition since we now always os.exit
created_at: 2025-04-08T08:50:14Z
author_association: OWNER
diff_hunk: @@ -105,10 +91,18 @@ local function rpc_nvim_exec_lua(opts)
 
   if not success then
     io.stderr:write(("FzfLua Error: %s\n"):format(errmsg or "<null>"))
-    vim.cmd([[qall]])
+    os.exit(1)
   end
+
+  uv.run() -- return when uv.stop()
path: lua/fzf-lua/shell_helper.lua
user: login: ibhagwan
body: Since we no longer call uv.stop and use os.exit should we move the channel close as well?
created_at: 2025-04-08T08:55:09Z
author_association: OWNER
diff_hunk: @@ -105,10 +91,18 @@ local function rpc_nvim_exec_lua(opts)
 
   if not success then
     io.stderr:write(("FzfLua Error: %s\n"):format(errmsg or "<null>"))
-    vim.cmd([[qall]])
+    os.exit(1)
   end
+
+  uv.run() -- return when uv.stop()
path: lua/fzf-lua/shell_helper.lua
user: login: phanen
body: We cannot close since `fn.chanclose` is not allowed when `uv.run`
created_at: 2025-04-08T09:32:38Z
author_association: COLLABORATOR
diff_hunk: @@ -105,10 +91,18 @@ local function rpc_nvim_exec_lua(opts)
 
   if not success then
     io.stderr:write(("FzfLua Error: %s\n"):format(errmsg or "<null>"))
-    vim.cmd([[qall]])
+    os.exit(1)
   end
+
+  uv.run() -- return when uv.stop()
path: lua/fzf-lua/shell_helper.lua
user: login: ibhagwan
body: What I’m saying is since we don’t uv.stop do we even need the uv.run?
created_at: 2025-04-08T09:36:05Z
author_association: OWNER
diff_hunk: @@ -105,10 +91,18 @@ local function rpc_nvim_exec_lua(opts)
 
   if not success then
     io.stderr:write(("FzfLua Error: %s\n"):format(errmsg or "<null>"))
-    vim.cmd([[qall]])
+    os.exit(1)
   end
+
+  uv.run() -- return when uv.stop()
path: lua/fzf-lua/shell_helper.lua
user: login: phanen
body: Using `vim.wait`?

I find this https://github.com/ibhagwan/fzf-lua/pull/1955/commits/7e2873e8ba481ffbfda2b517cc23b9e4a2543b0f  can fix ci

created_at: 2025-04-08T09:37:33Z
author_association: COLLABORATOR
diff_hunk: @@ -105,10 +91,18 @@ local function rpc_nvim_exec_lua(opts)
 
   if not success then
     io.stderr:write(("FzfLua Error: %s\n"):format(errmsg or "<null>"))
-    vim.cmd([[qall]])
+    os.exit(1)
   end
+
+  uv.run() -- return when uv.stop()
path: lua/fzf-lua/shell_helper.lua
user: login: ibhagwan
body: > Using `vim.wait`?
> 
> I find this [7e2873e](https://github.com/ibhagwan/fzf-lua/commit/7e2873e8ba481ffbfda2b517cc23b9e4a2543b0f) can fix ci

Nice find!

do you think it’s worth doing the same in spawn.lua for consistency?
created_at: 2025-04-08T09:41:38Z
author_association: OWNER
diff_hunk: @@ -105,10 +91,18 @@ local function rpc_nvim_exec_lua(opts)
 
   if not success then
     io.stderr:write(("FzfLua Error: %s\n"):format(errmsg or "<null>"))
-    vim.cmd([[qall]])
+    os.exit(1)
   end
+
+  uv.run() -- return when uv.stop()
path: lua/fzf-lua/shell_helper.lua
user: login: phanen
body: No. This hack don't apply to spawn.lua I don't know why.

Here the first `uv.run("whatever")` will always return quickly (due to `chanclose`?). The second `uv.run` will be expected `noreturn` and finally quit by `os.exit`.
created_at: 2025-04-08T09:44:46Z
author_association: COLLABORATOR
diff_hunk: @@ -218,6 +233,7 @@ M.spawn = function(opts, fn_transform, fn_done)
     output_pipe:read_start(read_cb)
     error_pipe:read_start(err_cb)
   end
+  return pid
path: lua/fzf-lua/libuv.lua
user: login: ibhagwan
body: Shall we change this to `return handle, pid` so the return values match the `uv.spawn` return values?
created_at: 2025-04-08T09:59:53Z
author_association: OWNER
diff_hunk: @@ -218,6 +233,7 @@ M.spawn = function(opts, fn_transform, fn_done)
     output_pipe:read_start(read_cb)
     error_pipe:read_start(err_cb)
   end
+  return pid
path: lua/fzf-lua/libuv.lua
user: login: phanen
body: make sense
created_at: 2025-04-08T10:13:18Z
author_association: COLLABORATOR

PR comments:
user: login: ibhagwan
created_at: 2025-04-06T11:09:53Z
body: > nvim -l still quit immediately even with coroutine.yield...

because we have to yield to the thread outside of the coroutine.create function, not sure if this is possible, maybe there is a libuv function that waits for a thread, I’ll look.
author_association: OWNER
user: login: ibhagwan
created_at: 2025-04-06T11:10:59Z
body: ```
uv.thread_join({thread})                                      *uv.thread_join()*

                > method form `thread:join()`

                Parameters:
                - `thread`: `luv_thread_t userdata`

                Waits for the `thread` to finish executing its entry function.

                Returns: `boolean` or `fail`
```
author_association: OWNER
user: login: ibhagwan
created_at: 2025-04-06T13:54:57Z
body: I wonder why this fails on Windows, I no longer have access to a Windows machine so I can’t play around with it.
author_association: OWNER
user: login: phanen
created_at: 2025-04-06T14:24:31Z
body: Even on linux, I'm confusing about the behavior of `uv.run()`, sometimes it wait for all callback then infinitely hang? Sometimes it work like `uv.run("once")` (when put it at the last line of file).

The linux ci passed now only because `uv.run` wait all callback and hang but then fzf kill it. (leaving dangling tmpfiles)
author_association: COLLABORATOR
user: login: ibhagwan
created_at: 2025-04-06T17:04:48Z
body: > The linux ci passed now only because uv.run wait all callback and hang but then fzf kill it. (leaving dangling tmpfiles)

Not optimal for sure
author_association: OWNER
user: login: phanen
created_at: 2025-04-07T03:51:01Z
body: > The linux ci passed now only because `uv.run` wait all callback and hang but then fzf kill it. (leaving dangling tmpfiles)

This description is not correct. fzf didn't kill it. It just cannot return from `uv.run`, so we need to delete tmpfile in callback.

> Even on linux, I'm confusing about the behavior of `uv.run()`, sometimes it wait for all callback then infinitely hang? Sometimes it work like `uv.run("once")` (when put it at the last line of file).

Remove or defer `fn.chanclose`, then `uv.run` will be no return.

Now the only problem is windows: `uv.run` seems quit immediately.
author_association: COLLABORATOR
user: login: ibhagwan
created_at: 2025-04-07T04:08:04Z
body: > Now the only problem is windows: uv.run seems quit immediately.

I’m asking myself is all this hassle worth it for the only edge case when action argument is the pwd? Even if we make this work, what other issues might dome up from this change…
author_association: OWNER
user: login: phanen
created_at: 2025-04-07T04:23:23Z
body: Nvim will load all argv as buffer... so I guess this way maybe a slightly faster (but I didn't feel the change)
~(And if child has load file as buffer, why need one more rpc... rather than let child cat it to stdout. But nvim cannot be used as pager OOTB... there's https://github.com/brianhuster/nvcat but still not very fast.. in the case a better approache is run a chlid as daemon... off topic)~

Some other edges (this related to `spawn.lua`
```
FzfLua live_grep query=/dev/null
```

> Remove or defer `fn.chanclose`, then `uv.run` will be no return.

`uv.stop` can get out it.

Anyway `-l` should be better... I recall there's a PR to use `-l` as a test runner in neovim core.

*EDIT*: https://github.com/neovim/neovim/pull/30483 change from `-ll` to `-l`.

Neovim seems use `-l`/`-ll` as parent then spawn and test new child (to run api, command, or screen data by `ext_linegrid`).

Although a bit different: we need to run directly as a `-l` child here (do rpc with parent), which looks not tested a lot in neovim core.

> I’m asking myself is all this hassle worth it for the only edge case when action argument is the pwd? Even if we make this work, what other issues might dome up from this change…

`vim.v.argv` can be a good enough workaround then..
Another way is to pack args into a table then pass as +`lua rpc_nvim_exec_lua(..., {+}, {q},...)`.
author_association: COLLABORATOR
user: login: phanen
created_at: 2025-04-07T13:59:39Z
body: ~* `opts.fzf_lua_server` seems unusable on windows (reuse vim.env.NVIM here... but on linux there's still some flatpak problem related to naming?~ Irrelavant

* I did something wrong about shellescape on windows, so I just avoid serialized a table...
* `uv.stop` take much long time on windows, so just `os.exit` immediately
author_association: COLLABORATOR
user: login: ibhagwan
created_at: 2025-04-08T03:37:45Z
body: Looks much better now without the string serialized args :)
author_association: OWNER
user: login: phanen
created_at: 2025-04-08T04:17:06Z
body: Fail to to make `nvim -l spawn.lua` pass the ci so I revert it finally (although it seems working well if run fzf-lua interactively). We're still able to use `shell_helper` to forward processed stream from fzf-lua server. (Didn't try but if I understand correctly, I guess ci can be passed if `multiprocess=false`).

I need to figure out the behavior of `vim.{api,fn,rpcrequst,...}` (required by `fn_{transform,pre/postprocess}`) in luv callback (when `nvim -l`). Neovim upstream will spawn `nvim -l/-ll` for testsuite, but only native lua is used (no 
 `vim.api`, and `vim.rpcrequest` is also never used (it just use a own lua client built from scratch via `vim.mpack` https://github.com/neovim/neovim/tree/700a25e6218e016b5adb0ddee740be4618d717a2/test/client).
author_association: COLLABORATOR
user: login: ibhagwan
created_at: 2025-04-08T04:29:34Z
body: > We're still able to use shell_helper to forward processed stream from fzf-lua server. (Didn't try but if I understand correctly, I guess ci can be passed if multiprocess=false).

I might be wrong but if I recall the code correctly multiprocess has no effect on shell_helper? Multiprocess only wraps string commands using spawn.lua?

> Neovim upstream will spawn nvim -l/-ll for testsuite

I’m pretty sure we cannot use `-ll` due to the missing lua runtime.
author_association: OWNER
user: login: ibhagwan
created_at: 2025-04-08T04:31:00Z
body: > Fail to to make nvim -l spawn.lua pass the ci so I revert it finally (although it seems working well if run fzf-lua interactively).

Does the CI fail on all OS’s or just Windows?
author_association: OWNER
user: login: phanen
created_at: 2025-04-08T04:38:33Z
body: > Does the CI fail on all OS’s or just Windows?

All os on several same test: https://github.com/ibhagwan/fzf-lua/actions/runs/14314121577?pr=1955.

> I might be wrong but if I recall the code correctly multiprocess has no effect on shell_helper? Multiprocess only wraps string commands using spawn.lua?

~I means if we use `multiprocess=false`, then `spawn.lua` will not be used. And `shell_helper` will be used instead? (since it can also print to stdout?~
No, I'm wrong, `opts.multiprocess=false` cannot avoid ci failure. 
author_association: COLLABORATOR
user: login: ibhagwan
created_at: 2025-04-08T05:03:37Z
body: > No, I'm wrong, opts.multiprocess=false cannot avoid ci failure.

Might be a goose chase, do we really need to CI multiprocess=false given it’s only used for debugging / troubleshooting issues?
author_association: OWNER
user: login: ibhagwan
created_at: 2025-04-08T05:16:38Z
body: If I can see the branch of the failed tests maybe I’ll have an idea how to fix it, I can see the load even failed, if we run the branch locally with a “load” event added in `setup` similar to the test helper, does it get executed?
https://github.com/ibhagwan/fzf-lua/blob/13ec251a963ed5645dc8ad49e0fde13dac42ec0f/lua/fzf-lua/test/helpers.lua#L141-L144
author_association: OWNER
user: login: ibhagwan
created_at: 2025-04-08T05:25:32Z
body: Looking at this https://github.com/ibhagwan/fzf-lua/commit/d99d21d238b6ada46e55c61cacb4275c2789b36a previous commit I think I know why the CI fails with spawn.

~~If the spawn command is run using FZF_DEFAULT_COMMAND it will inherit FZF_LUA_SERVER and there will be no issue but that’s not always the case, for example, the Windows tests use `opts.pipe_cmd` in this case the server env var won’t be inherited, to fix this the command must have the server env var prefixed (like the runtime was), so this line:~~
https://github.com/ibhagwan/fzf-lua/blob/13ec251a963ed5645dc8ad49e0fde13dac42ec0f/lua/fzf-lua/libuv.lua#L688-L689

~~Should resolve into `FZF_LUA_SERVER=… nvim -l …`~~

I’m not sure about this… the fzf_lua_server is set via the call args in spawn_stdio
https://github.com/ibhagwan/fzf-lua/blob/13ec251a963ed5645dc8ad49e0fde13dac42ec0f/lua/fzf-lua/libuv.lua#L325-L331

author_association: OWNER
user: login: phanen
created_at: 2025-04-08T05:31:28Z
body: > Should resolve into FZF_LUA_SERVER=… nvim -l …

No, I `make test` locally, it don't fix...
author_association: COLLABORATOR
user: login: ibhagwan
created_at: 2025-04-08T05:33:05Z
body: > > Should resolve into FZF_LUA_SERVER=… nvim -l …
> 
> No, I `make test` locally, it don't fix...

Yes I realized after I wrote this was most likely not going to work.
author_association: OWNER
user: login: phanen
created_at: 2025-04-08T05:35:26Z
body: Ensure there's no call to `vim.schedule/vim.fn/vim.api/vim.rpcrequest` in `fn_transform/fn_xx` then it will work. (just a guessing)

I think this is related:
```
                                                        *api-fast*
Most API functions are "deferred": they are queued on the main loop and
processed sequentially with normal input.  So if the editor is waiting for
user input in a "modal" fashion (e.g. the |hit-enter-prompt|), the request
will block.  Non-deferred (fast) functions such as |nvim_get_mode()| and
|nvim_input()| are served immediately (i.e. without waiting in the input
queue).  Lua code can use |vim.in_fast_event()| to detect a fast context.
```

The spinner...
![image](https://github.com/user-attachments/assets/0a3fe2da-1b41-474e-aa45-e80bef7b8473)

If we just want to get the query, we can use this (in `fzf --man`), then we can also avoid pass args and being loaded by `nvim --headless`
```
       FZF_QUERY           Current query string
```
author_association: COLLABORATOR
user: login: ibhagwan
created_at: 2025-04-08T05:44:06Z
body: ```lua
require("fzf-lua").setup({
  keymap = {
    fzf = {
      true,
      load = function()
        print("called")
        _G._fzf_load_called = true
      end,
    }
  }
})
```
~~Strangely, the callbacks works with `FzfLua zoxide` but not with `FzfLua files`, still troubleshooting.~~ my bad it works.
author_association: OWNER
user: login: phanen
created_at: 2025-04-08T05:55:58Z
body: `nvim -l a.lua`: `uv.run()` prevent `vim.schedule/fn` in this case:
```
vim.schedule(function() print(vim.fn.eval('&rtp')) end)
vim.uv.run()
```

I find I can make it quit by resize the screen...

So when I use `while uv.run() do end`, it just prevent all stuffs other than `vim.uv`
author_association: COLLABORATOR
user: login: ibhagwan
created_at: 2025-04-08T06:01:21Z
body: Unsure yet why but the issue is with the workaround for #1914, removing the workaround makes it work:
https://github.com/ibhagwan/fzf-lua/blob/13ec251a963ed5645dc8ad49e0fde13dac42ec0f/tests/file/ui_spec.lua#L34-L38
author_association: OWNER
user: login: ibhagwan
created_at: 2025-04-08T06:12:21Z
body: > Ensure there's no call to vim.schedule/vim.fn/vim.api/vim.rpcrequest in fn_transform/fn_xx then it will work. (just a guessing)

You’re right, when I remove the post process schedule / vim.rpc it works, but why does this block only in the testing?
author_association: OWNER
user: login: ibhagwan
created_at: 2025-04-08T06:24:40Z
body: Maybe instead of using `uv.run` in spawn we can wait for the process to finish (using the process handle returned from uv.spawn)?

If we can get a list of threads from the process handle we can use `uv.thread_join`.
author_association: OWNER
user: login: ibhagwan
created_at: 2025-04-08T07:12:21Z
body: I changed the wait condition at the end of spawn.lua to the below, it passes the load/postprocess condition but now fails randomly on clearing the create event:
```lua
-- while vim.uv.run() do end -- os.exit in spawn_stdio
while tonumber(vim.uv.os_getpriority(pid)) do
  vim.wait(100, function() return tonumber(vim.uv.os_getpriority(pid)) == nil end)
end
```
author_association: OWNER
user: login: ibhagwan
created_at: 2025-04-08T07:17:50Z
body: Getting some other weird issues where the cursor hangs on backspace after triggering the zero event (requires ctrl-c to unlock) - will also need to be looked at.
author_association: OWNER
user: login: phanen
created_at: 2025-04-08T07:43:29Z
body: ~Why we want to avoid `vim.wait(..)`... what's wrong with `while true do vim.wait(100) end`
`os.exit` can quit as expected~
> Getting some other weird issues where the cursor hangs on backspace after triggering the zero event (requires ctrl-c to unlock) - will also need to be looked at.

Ok, maybe that's the problem of `vim.wait`
author_association: COLLABORATOR
user: login: ibhagwan
created_at: 2025-04-08T07:53:27Z
body: > Why we want to avoid `vim.wait(..)`... what's wrong with `while true do vim.wait(100) end` `os.exit` can quit as expected

Idk, waiting just seems like unsound practice, maybe I’m wrong.

> > Getting some other weird issues where the cursor hangs on backspace after triggering the zero event (requires ctrl-c to unlock) - will also need to be looked at.


I’m not sure, if this comes from the actions, the issue would be in shell_helper,  my guess would be another uv.run block.
author_association: OWNER
user: login: phanen
created_at: 2025-04-08T08:01:11Z
body: ~No I reproduce this without making change to shell,shell_helper.~
EDIT: yes that's related to `nvim -l shell_helper`
author_association: COLLABORATOR
user: login: ibhagwan
created_at: 2025-04-08T08:06:18Z
body: > No I reproduce this without making change to shell,shell_helper.

I can’t reproduce this on main, most likely the issue comes from the changes in this PR.
author_association: OWNER
user: login: ibhagwan
created_at: 2025-04-08T16:12:27Z
body: Are you still working on this one? I feel we’re close to this being ready.

One thing we’ll have to add is up the dependency to neovim 0.8/0.9,I think `-l` was added with 0.8 I’ll have to check, we can also get rid of more backward compact code once we do that.
author_association: OWNER
user: login: phanen
created_at: 2025-04-08T16:19:15Z
body: `fs.normalize` is `v0.8.0`  things. But already exist in codebases..
It seems `v0.9.0+`... https://github.com/neovim/neovim/commit/7c94bcd2d77e2e54b8836ab8325460a367b79eae

Latest `v0.8.3` don't have it, but here a same name option existing `v0.1.0 ~ v0.8.3`:
```
							*-l*
-l		Lisp mode.  Sets the 'lisp' and 'showmatch' options on.
```

According to https://github.com/tweekmonster/helpful.vim/, `-ll` is also `v0.9.0+`
author_association: COLLABORATOR
user: login: ibhagwan
created_at: 2025-04-08T16:27:34Z
body: > `fs.normalize` is `v0.8.0` things. But already exist in codebases.. It seems `v0.9.0+`... [neovim/neovim@7c94bcd](https://github.com/neovim/neovim/commit/7c94bcd2d77e2e54b8836ab8325460a367b79eae)
> 
> Latest `v0.8.3` don't have it, but here a same name option existing `v0.1.0 ~ v0.8.3`:
> 
> ```
> 							*-l*
> -l		Lisp mode.  Sets the 'lisp' and 'showmatch' options on.
> ```

I think we can require 0.9, it’s been long enough, this should simplify things and we can remove quite a few `if utils.__HAS…`, especially the annoying one for the highlight bug in 0.8 lol
https://github.com/ibhagwan/fzf-lua/blob/47b85a25c0c0b2c20b4e75199ed01bb71e7814f5/lua/fzf-lua/init.lua#L149-L161

author_association: OWNER
user: login: phanen
created_at: 2025-04-08T16:46:41Z
body: * https://repology.org/project/neovim/versions ubuntu 22.04 is v0.6.1 (EOL is 2027)
* Current min support version is v0.7.0? https://github.com/ibhagwan/fzf-lua/commit/aaf7604bca38890cb487b29738f8cb02586bc818.

If merge this that's would be `v0.7.0`(I install a appimage, it work well with builtin previewer) -> `v0.9.0`... although I start using neovim since `v0.10.0`... I'm not sure how many user will complain...
author_association: COLLABORATOR
user: login: ibhagwan
created_at: 2025-04-08T16:50:08Z
body: > * https://repology.org/project/neovim/versions ubuntu 22.04 is v0.6.1 (EOL is 2027)
> * Current min support version is v0.7.0? [aaf7604](https://github.com/ibhagwan/fzf-lua/commit/aaf7604bca38890cb487b29738f8cb02586bc818).
> 
> If merge this that's would be `v0.7.0`(which work well with builtin previewer) -> `v0.9.0`... although I start using neovim since `v0.10.0`... I'm not sure how many user will complain...

I think we can tag latest commit to work with 0.7 and require 0.9, anyone using neovim at this point is most likely using 0.10+, almost no plugins support 0.7/0.8, lspconfig/treesitter all require 0.9, if someone has an old config and wants to update fzf-lua they can use the `tag = …`.
author_association: OWNER
user: login: phanen
created_at: 2025-04-08T17:05:58Z
body: > I think we can tag latest commit to work with 0.7 and require 0.9, anyone using neovim at this point is most likely using 0.10+,

Make sense. I never see a ancient user for neovim, someone use old version but just keep compatibale config. And since neovim never backport thing to unsupported release. So I think ideally nothing will break if stick to a tag.

I found fzf-lua have always been a `0.0.xx` release package: https://github.com/folke/lazy.nvim/blob/b6eba0d02600b678959f90876bc678178b4dd4f6/manifest#L23236-L24903. We should be listed in https://0ver.org/ (:

author_association: COLLABORATOR
user: login: ibhagwan
created_at: 2025-04-08T17:10:12Z
body: > I found fzf-lua have always been a 0.0.xx release package:

luarocks package is automatically versioned based on the number of commits:
https://github.com/ibhagwan/fzf-lua/blob/main/.github/workflows/luarocks-release.yaml
author_association: OWNER
user: login: ibhagwan
created_at: 2025-04-08T17:11:44Z
body: > We should be listed in https://0ver.org/ (:

We can consider a versioning system, I never bothered with it as I feel the project doesn’t need it, will just create an overhead to “release a version”.
author_association: OWNER
user: login: ibhagwan
created_at: 2025-04-09T01:04:52Z
body: https://github.com/ibhagwan/fzf-lua/commit/d82ead472fc4fd72bafd1d506e1a9f6d17dcbbfc, let me know if you think I missed anything, `fzf-lua.txt` should be updated by the CI.

We should also update the testing to use 0.11.0 as "stable" but I didn't want to do this now as it would introduce too much noise updating the screenshots so it's better to do in another PR.
author_association: OWNER
user: login: phanen
created_at: 2025-04-09T03:56:26Z
body: > We can consider a versioning system, I never bothered with it as I feel the project doesn’t need it, will just create an overhead to “release a version”.

fzf-lua usually fix issues quickly on HEAD. Tag with minimal support neovim version should be enough.
author_association: COLLABORATOR
user: login: ibhagwan
created_at: 2025-04-11T19:07:24Z
body: I’ve been using this PR for two days now, seems stable at least on my Linux, still unsure when we should merge this, do you want to fix the Windows CI first or remove the fixup commit?

These type of changes usually have some after effect we never think about so we’ll have to be more alert in case something breaks.
author_association: OWNER
user: login: phanen
created_at: 2025-04-12T01:10:02Z
body: We can merge this first since gc fixes are unrelated here. gc actually work in my win10 vm when use interactively but fail in test.

I cherry pick those fixes to another PR. (that's a blocker before changing the default profile to "hide")
author_association: COLLABORATOR
user: login: ibhagwan
created_at: 2025-04-12T02:14:07Z
body: As always, tysm @phanen for your amazing work on this :)
author_association: OWNER