current commit
261023.3 · fix(history): map picker positions to original history indices
---@diagnostic disable: unused-local, unused-function, unused
local MiniTest = require("mini.test")
local helpers = require("fzf-lua.test.helpers")
local child = helpers.new_child_neovim()
local eq = helpers.expect.equality
local new_set = MiniTest.new_set
local exec_lua = child.lua

-- Helpers with child processes
--stylua: ignore start
---@format disable-next
local reload = function(config) child.unload(); child.setup(config) end
local set_cursor = function(...) return child.set_cursor(...) end
local get_cursor = function(...) return child.get_cursor(...) end
local set_lines = function(...) return child.set_lines(...) end
local get_lines = function(...) return child.get_lines(...) end
local type_keys = function(...) return child.type_keys(...) end
local sleep = function(ms) helpers.sleep(ms, child) end
--stylua: ignore end

local T = helpers.new_set_with_child(child)

T["win"] = new_set()

T["win"]["hide"] = new_set()

T["win"]["hide"]["ensure gc called after win hidden (#1782)"] = function()
  exec_lua([[_G._fzf_lua_gc_called = nil]])
  -- Reduce cache size to 20 so functions get evicted quicker
  -- otherwise opts refs that are stored in the funcs are never
  -- cleared preventing the win object's __gc from being called
  exec_lua([[FzfLua.shell.cache_set_size(20)]])
  child.wait_until(function()
    if helpers.IS_WIN() then
      local hidden_fzf_bufnr = child.lua_get(
        [[(FzfLua.utils.fzf_winobj() or {})._hidden_fzf_bufnr]])
      if hidden_fzf_bufnr ~= vim.NIL then
        local chan = child.lua_get(([=[vim.bo[%s].channel]=]):format(hidden_fzf_bufnr))
        child.api.nvim_chan_send(chan, vim.keycode("<c-c>"))
      end
    end
    exec_lua([[FzfLua.files{ previewer = 'builtin' }]])
    child.wait_until(function()
      return child.lua_get([[_G._fzf_load_called]]) == true
    end)
    exec_lua([[FzfLua.hide()]])
    child.wait_until(function()
      return child.lua_get([[_G._fzf_load_called]]) == vim.NIL
    end)
    exec_lua([[collectgarbage('collect')]])
    return child.lua_get([[_G._fzf_lua_gc_called]]) == true
  end)
  -- Restore original cache size
  exec_lua([[FzfLua.shell.cache_set_size(50)]])
end

T["win"]["hide"]["buffer deleted after win hidden (#1783)"] = function()
  eq(child.lua_get([[_G._fzf_lua_on_create]]), vim.NIL)
  exec_lua([[FzfLua.files()]])
  child.wait_until(function()
    return child.lua_get([[_G._fzf_lua_on_create]]) == true
  end)
  exec_lua([[FzfLua.hide()]])
  child.wait_until(function()
    return child.lua_get([[_G._fzf_lua_on_create]]) == vim.NIL
  end)
  exec_lua([[
    vim.cmd("%bd!")
    FzfLua.files()
  ]])
  child.wait_until(function()
    return child.lua_get([[_G._fzf_lua_on_create]]) == true
  end)
end

T["win"]["hide"]["can resume after close CTX win (#1936)"] = function()
  eq(child.lua_get([[_G._fzf_lua_on_create]]), vim.NIL)
  child.cmd([[new]])
  exec_lua([[FzfLua.files()]])
  child.wait_until(function()
    return child.lua_get([[_G._fzf_lua_on_create]]) == true
  end)
  exec_lua([[FzfLua.hide()]])
  child.wait_until(function()
    return child.lua_get([[_G._fzf_lua_on_create]]) == vim.NIL
  end)
  child.cmd([[close]])
  exec_lua([[FzfLua.unhide()]])
  child.wait_until(function()
    return child.lua_get([[_G._fzf_lua_on_create]]) == true
  end)
  child.type_keys("<c-j>")
  child.type_keys("<c-j>")

  -- `:quit` on other window should not kill fzf job #2011
  exec_lua([[FzfLua.hide()]])
  child.wait_until(function()
    return child.lua_get([[_G._fzf_lua_on_create]]) == vim.NIL
  end)
  child.cmd([[new]])
  child.cmd([[quit]])
  exec_lua([[FzfLua.unhide()]])
  child.wait_until(function()
    return child.lua_get([[_G._fzf_lua_on_create]]) == true
  end)
  exec_lua([[FzfLua.hide()]])
  child.wait_until(function()
    return child.lua_get([[_G._fzf_lua_on_create]]) == vim.NIL
  end)

  -- can :wqa when there're hide job #1817
  pcall(child.cmd, [[wqa]])
  -- child.is_running() didn't work as expected
  local job = child.job
  if not job then error("child.job is nil after :wqa") end
  local job_id = job.id --[[@as integer]]
  local res = vim.fn.jobwait({ job_id }, 1000)[1]
  -- 0 = normal exit, -3 = already exited/invalid (acceptable race)
  local valid_res = { [0] = true, [-3] = true }
  eq(valid_res[res] or res, true) -- HACK: show the res code in the assert
end

T["win"]["hide"]["actions on multi-select but zero-match #1961"] = function()
  reload({ "hide" })
  exec_lua([[FzfLua.files{
    -- profile = "hide",
    query = "README.md",
    fzf_opts = { ["--multi"] = true },
  }]])
  -- not work with `profile = "hide"`?
  child.wait_until(function() return child.lua_get([[_G._fzf_load_called]]) == true end)
  child.type_keys([[<tab>]])
  child.type_keys([[a-non-exist-file]])
  child.type_keys([[<cr>]])
  child.wait_until(function() return child.lua_get([[_G._fzf_lua_on_create]]) == vim.NIL end)
  eq("README.md", vim.fs.basename(child.lua_get([[vim.api.nvim_buf_get_name(0)]])))
end

T["win"]["hide"]["nohidden"] = function()
  reload({ "hide" })
  exec_lua([[vim.o.hidden = false]])
  helpers.FzfLua.files(child, {
    __abort_key = "<esc>",
    __expect_lines = false,
    __after_open = function() if helpers.IS_WIN() then vim.uv.sleep(250) end end,
    query = "LICENSE$",
  })
end

T["win"]["keymap"] = new_set({ n_retry = not helpers.IS_LINUX() and 5 or nil })

T["win"]["keymap"]["no error"] = new_set({ parametrize = (function()
  local builtin = require("fzf-lua.defaults").defaults.keymap.builtin
  ---@cast builtin table
  local out = {}
  for key, action in pairs(builtin) do out[#out + 1] = { key, action } end
  return out
end)() }, {
  function(key, action)
    for _, event in ipairs({ "start", "load", "result" }) do
      helpers.FzfLua.files(child, {
        __no_abort = true,
        __expect_lines = false,
        __after_open = function()
          if helpers.IS_WIN() then vim.uv.sleep(250) end
          -- Press the key being tested, then give window-affecting actions
          -- (e.g. toggle-fullscreen) a moment to settle before closing.
          -- On Windows, sending the action and abort too close together can
          -- crash the child RPC channel.
          child.type_keys(key)
          sleep(100); if helpers.IS_WIN() then sleep(250) end
          child.type_keys("<c-c>")
          child.wait_until(function()
            return child.lua_get([[_G._fzf_lua_on_create]]) == vim.NIL
          end, 5000)
        end,
        query = "README.md",
        winopts = { preview = { wrap = false } },
        keymap = { true, fzf = { [event] = function() end } },
      })
    end
  end })

T["win"]["previewer"] = new_set({ n_retry = not helpers.IS_LINUX() and 5 or nil })

T["win"]["previewer"]["split flex layout resize"] = function()
  -- Ignore terminal command line with process number
  local screen_opts = { ignore_text = { 24, 28 }, normalize_paths = helpers.IS_WIN() }
  helpers.FzfLua.fzf_exec(child, { "foo", "bar", "baz" }, {
    __no_abort = true,
    __expect_lines = true,
    __screen_opts = screen_opts,
    winopts = {
      split = "enew",
      preview = {
        -- default test screen size is 28,64
        flip_columns = 64,
      }
    },
    previewer = function() return require("fzf-lua.test.previewer").builtin end,
    keymap = { fzf = { resize = function() _G._fzf_resize_called = true end } },
    __after_open = function()
      if helpers.IS_WIN() then vim.uv.sleep(250) end
      child.wait_until(function()
        return child.lua_get([[FzfLua.utils.fzf_winobj()._previewer.last_entry]]) == "foo"
      end)
    end,
  })
  -- increase size by 1, should flip to vertical preview
  child.set_size(28, 65)
  child.wait_until(function()
    return child.lua_get([[_G._fzf_resize_called]]) == true
  end)
  sleep(100); if not helpers.IS_LINUX() then sleep(250) end
  child.expect_screen_lines(screen_opts)
  -- abort and wait for winopts.on_close
  child.type_keys("<c-c>")
  child.wait_until(function()
    return child.lua_get([[_G._fzf_lua_on_create]]) == vim.NIL
  end)
end

local toggle_preview = function(opts, screen_opts)
  opts = opts or {}
  child.wait_until(function() return child.api.nvim_get_mode().mode == "t" end)
  exec_lua([[require("fzf-lua.win").toggle_preview()]])
  if helpers.IS_WIN() then
    vim.uv.sleep(250)
  elseif opts.preview then
    vim.uv.sleep(100)
  else
    vim.uv.sleep(100)
    -- child.wait_until(function()
    --   return child.lua_get([[FzfLua.utils.fzf_winobj()._previewer.last_entry]]) == "foo"
    -- end)
  end
  if screen_opts then child.expect_screen_lines(screen_opts) end
  -- abort and wait for winopts.on_close
  child.type_keys("<c-c>")
  child.wait_until(function()
    return child.lua_get([[_G._fzf_lua_on_create]]) == vim.NIL
  end)
end

T["win"]["previewer"]["split flex hidden"] = function()
  -- Ignore terminal command line with process number
  local screen_opts = { ignore_text = { 24, 28 }, normalize_paths = helpers.IS_WIN() }
  local opts = {
    __no_abort = true,
    __expect_lines = true,
    __screen_opts = screen_opts,
    winopts = {
      split = "enew",
      preview = {
        -- default test screen size is 28,64
        flip_columns = 64,
        -- start hidden
        hidden = true
      }
    },
    previewer = function() return require("fzf-lua.test.previewer").builtin end,
    __after_open = function()
      if helpers.IS_WIN() then vim.uv.sleep(250) end
    end,
  }
  helpers.FzfLua.fzf_exec(child, { "foo", "bar", "baz" }, opts)
  -- increase size by 1, should flip to vertical preview
  child.set_size(28, 65)
  toggle_preview(opts, screen_opts)
end

-- E: toggle_behavior=extend, B: builtin previewer, F: "echo {}" preview
T["win"]["toggle"] = new_set({
  parametrize = { { "EB" }, { "EF" }, { "B" }, { "F" } },
  n_retry = not helpers.IS_LINUX() and 5 or 2
})
T["win"]["toggle"][""] = new_set(
  {
    parametrize = {
      { {} },
      { { winopts = { split = "enew", } } },
      { { winopts = { split = "botright new", preview = { layout = "horizontal" } } } },
      { { profile = "border-fused" } },
      { { profile = "borderless-full", winopts = { preview = { layout = "horizontal" } }, } },
      { { winopts = { border = false } } },
      { { winopts = { preview = { vertical = "down:4" } } } },
      { { winopts = { preview = { border = false } } } },
    }
  }, {
    -- Ignore terminal command line with process number
    function(a, o)
      local screen_opts = { ignore_text = { 24, 28 }, normalize_paths = helpers.IS_WIN() }
      local opts = {
        __no_abort = true,
        __expect_lines = true,
        __screen_opts = screen_opts,
        winopts = { preview = { hidden = true, delay = 0 } },
        __after_open = function()
          child.wait_until(function() return child.lua_get([[_G._fzf_load_called]]) == true end)
          if helpers.IS_WIN() then vim.uv.sleep(250) end
        end,
      }
      ---@diagnostic disable-next-line: assign-type-mismatch
      opts = vim.tbl_deep_extend("force", opts, o)
      -- don't modify o directly, otherwise it will change screenshots name
      if a:match("E") then
        ---@diagnostic disable-next-line: assign-type-mismatch
        opts = vim.tbl_deep_extend("force", opts, { winopts = { toggle_behavior = "extend" } })
      end
      if a:match("B") then
        opts.previewer = function() return require("fzf-lua.test.previewer").builtin end
      elseif a:match("F") then
        opts.previewer = function() return require("fzf-lua.test.previewer").fzf end
      else
        error("unreachable")
      end
      helpers.FzfLua.fzf_exec(child, { "foo", "bar", "baz" }, opts)
      toggle_preview(opts, screen_opts)
    end
  })

T["win"]["relative=cursor"] = function()
  -- Ignore terminal command line with process number
  local screen_opts = { ignore_text = { 24, 28 }, normalize_paths = helpers.IS_WIN() }
  local opts = {
    __expect_lines = true,
    __screen_opts = screen_opts,
    winopts = {
      relative = "cursor",
      preview = { layout = "horizontal", horizontal = "left:50%" }
    },
    previewer = function() return require("fzf-lua.test.previewer").builtin end,
    __after_open = function()
      child.wait_until(function() return child.lua_get([[_G._fzf_load_called]]) == true end)
      sleep(100); if not helpers.IS_LINUX() then sleep(250) end
    end,
  }
  helpers.FzfLua.fzf_exec(child, { "foo", "bar", "baz" }, opts)
  child.cmd([[enew]])
  set_lines({ ("a"):rep(child.o.columns) })
  type_keys("g$")
  -- pretend we closed the window to make helpers won't wait for new open
  exec_lua([[_G._fzf_lua_on_create = nil]])
  exec_lua([[_G._fzf_load_called = nil]])
  helpers.FzfLua.fzf_exec(child, { "foo", "bar", "baz" }, opts)
  child.cmd([[bwipe!]])
end


T["win"]["reuse"] = new_set({
  parametrize = {
    { {} },
    { { winopts = { split = "enew", } } },
    { { winopts = { split = "botright new", preview = { layout = "horizontal" } } } },
  },
  n_retry = not helpers.IS_LINUX() and 5 or nil
}, {
  function(o)
    -- Ignore terminal command line with process number
    local screen_opts = { ignore_text = { 24, 28 }, normalize_paths = helpers.IS_WIN() }
    local opts = {
      __no_abort = true,
      __expect_lines = false,
      __screen_opts = screen_opts,
      winopts = {
        toggle_behavior = "extend",
        preview = { hidden = false, delay = 0 },
      },
      previewer = function() return require("fzf-lua.test.previewer").builtin end,
      __after_open = function()
        child.wait_until(function() return child.lua_get([[_G._fzf_load_called]]) == true end)
        sleep(100); if not helpers.IS_LINUX() then sleep(250) end
      end,
    }
    ---@diagnostic disable-next-line: assign-type-mismatch
    opts = vim.tbl_deep_extend("force", opts, o)
    helpers.FzfLua.fzf_exec(child, { "foo", "bar", "baz" }, opts)
    -- change to fzf preview
    opts.previewer = nil
    opts.preview = "echo resue windows, change from builtin to fzf preview"
    opts.__expect_lines = true
    opts.__no_abort = nil
    exec_lua([[_G._fzf_lua_on_create = nil]])
    exec_lua([[_G._fzf_load_called = nil]])
    helpers.FzfLua.fzf_exec(child, { "foo", "bar", "baz" }, opts)

    -- toggle_preview on hide profile
    reload({ "hide" })
    exec_lua([[
      require('fzf-lua').fzf_exec({ 'a', 'b' },{ preview = "echo builtin" })
      require('fzf-lua').fzf_exec({ 'b', 'c' }, { previewer = require('fzf-lua.test.previewer').builtin })
    ]])
    toggle_preview()
  end
})

T["win"]["highlight"] = new_set({ n_retry = not helpers.IS_LINUX() and 5 or nil })

T["win"]["highlight"]["unchange when reuse win #2588"] = function()
  helpers.SKIP_IF_WIN() -- windows attr looks different
  local opts = {
    __no_abort = true,
    __after_open = function() sleep(100); if not helpers.IS_LINUX() then sleep(250) end end,
    cwd_prompt = false,
  }
  helpers.FzfLua.files(child, opts)
  -- pretend we closed the window to make helpers won't wait for new open
  exec_lua([[_G._fzf_lua_on_create = nil]])
  exec_lua([[_G._fzf_load_called = nil]])
  helpers.FzfLua.files(child, opts)
end

T["win"]["restore last window"] = new_set()

T["win"]["restore last window"]["does not trigger float autoclose (#2837)"] = function()
  exec_lua([[
    _G._float_win = vim.api.nvim_open_win(vim.api.nvim_create_buf(false, true), true, {
      relative = "editor",
      width = 20,
      height = 5,
      row = 1,
      col = 1,
    })
    vim.bo[vim.api.nvim_win_get_buf(_G._float_win)].buftype = "nofile"
    -- mimic Mason's autoclose handler: close the float when a regular
    -- buffer is entered
    vim.api.nvim_create_autocmd("WinEnter", {
      callback = function()
        local buftype = vim.bo.buftype
        if buftype ~= "prompt" and buftype ~= "nofile" then
          vim.schedule(function()
            if vim.api.nvim_win_is_valid(_G._float_win) then
              vim.api.nvim_win_close(_G._float_win, true)
            end
          end)
        end
      end,
    })
  ]])
  exec_lua([[FzfLua.files{ query = "README.md" }]])
  child.wait_until(function() return child.lua_get([[_G._fzf_load_called]]) == true end)
  child.type_keys("<esc>")
  child.wait_until(function() return child.lua_get([[_G._fzf_lua_on_create]]) == vim.NIL end)
  sleep(100)
  eq(true, child.lua_get([[vim.api.nvim_win_is_valid(_G._float_win)]]))
  exec_lua([[if vim.api.nvim_win_is_valid(_G._float_win) then vim.api.nvim_win_close(_G._float_win, true) end]])
end

T["win"]["restore last window"]["keeps events for regular windows (#2812)"] = function()
  exec_lua([[
    -- Keep the original window as the intermediate target.
    vim.cmd("split")
    vim.cmd("enew")
    _G._src_win = vim.api.nvim_get_current_win()
    _G._src_win_enter = 0
    _G._src_buf_enter = 0
    vim.api.nvim_create_autocmd({ "WinEnter", "BufEnter" }, {
      callback = function(args)
        if vim.api.nvim_get_current_win() ~= _G._src_win then return end
        if args.event == "WinEnter" then
          _G._src_win_enter = _G._src_win_enter + 1
        else
          _G._src_buf_enter = _G._src_buf_enter + 1
        end
      end,
    })
  ]])
  exec_lua([[FzfLua.files{ query = "README.md" }]])
  child.wait_until(function() return child.lua_get([[_G._fzf_load_called]]) == true end)
  -- only count the events fired while closing, not the ones from opening fzf
  exec_lua([[_G._src_win_enter = 0; _G._src_buf_enter = 0]])
  child.type_keys("<esc>")
  child.wait_until(function() return child.lua_get([[_G._fzf_lua_on_create]]) == vim.NIL end)
  eq(true, child.lua_get([[_G._src_win_enter > 0]]))
  eq(true, child.lua_get([[_G._src_buf_enter > 0]]))
end

T["win"]["treesitter"] = new_set()

T["win"]["treesitter"]["short lines do not break highlight parsing"] = function()
  -- Short rows (e.g. the "> " prompt and "  1/1" counter) used to produce
  -- an `end_col < start_col` region which treesitter rejects on parse
  exec_lua([[
    local tsinjector = require("fzf-lua.win.tsinjector")
    _G._ts_buf = vim.api.nvim_create_buf(false, true)
    _G._ts_detach = tsinjector.attach({ winopts = {}, preview_hidden = true }, _G._ts_buf,
      function(line) return "foo.vim", nil, line end)
    vim.api.nvim_buf_set_lines(_G._ts_buf, 0, -1, false,
      { "FzfLua command_history", "> " })
  ]])
  eq(true, child.lua_get([[(function()
    local tsinjector = require("fzf-lua.win.tsinjector")
    local ok = pcall(function()
      tsinjector.cache[_G._ts_buf].vim.highlighter.tree:parse({ 0, 3 })
    end)
    if not ok then return false end
    -- the valid line must still produce a non-empty region
    local regions = tsinjector.cache[_G._ts_buf].vim.parser:included_regions()
    local range = regions[1] and regions[1][1]
    return #regions == 1 and range and range[5] > range[2]
  end)()]]))
  exec_lua([[_G._ts_detach()]])
end

return T