Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dba7dcc1ec | |||
| 865d4cad82 | |||
| 3dd44d4578 |
@@ -0,0 +1,50 @@
|
||||
name: Lua CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
lua-ls:
|
||||
name: lua-language-server type check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Neovim
|
||||
run: |
|
||||
curl -L https://github.com/neovim/neovim/releases/download/v0.11.5/nvim-linux-x86_64.tar.gz -o /opt/nvim.tar.gz
|
||||
mkdir /opt/nvim
|
||||
tar xzf /opt/nvim.tar.gz -C /opt/nvim
|
||||
mv /opt/nvim/nvim-linux-x86_64/* /opt/nvim
|
||||
echo "/opt/nvim/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Install lua-language-server
|
||||
run: |
|
||||
curl -L "https://github.com/LuaLS/lua-language-server/releases/download/3.17.1/lua-language-server-3.17.1-linux-x64.tar.gz" -o /opt/lls.tar.gz
|
||||
mkdir /opt/lls
|
||||
tar -xzf /opt/lls.tar.gz -C /opt/lls
|
||||
echo "/opt/lls/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Clone snacks.nvim
|
||||
run: git clone --depth=1 https://github.com/folke/snacks.nvim /opt/snacks.nvim
|
||||
|
||||
- name: Run lua-language-server
|
||||
run: lua-language-server --configpath .luarc.ci.json --check=.
|
||||
|
||||
luacheck:
|
||||
name: luacheck lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install luacheck
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y luarocks
|
||||
sudo luarocks install luacheck
|
||||
|
||||
- name: Run luacheck
|
||||
run: luacheck lua/
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
-- luacheck configuration for fff.nvim
|
||||
-- https://luacheck.readthedocs.io/en/stable/config.html
|
||||
|
||||
-- Neovim globals
|
||||
globals = { "vim" }
|
||||
|
||||
-- Standard library
|
||||
std = "luajit"
|
||||
|
||||
-- Ignore line length (handled by stylua)
|
||||
max_line_length = false
|
||||
|
||||
-- Ignore unused self argument in methods
|
||||
self = false
|
||||
|
||||
-- Files/directories to ignore
|
||||
exclude_files = {
|
||||
".luarocks/",
|
||||
}
|
||||
|
||||
-- Warn about unused variables, but allow _ prefix convention
|
||||
unused_args = true
|
||||
ignore = {
|
||||
"212", -- unused argument (too noisy for callback-heavy code)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json",
|
||||
"runtime": {
|
||||
"version": "LuaJIT",
|
||||
"pathStrict": true
|
||||
},
|
||||
"workspace": {
|
||||
"library": [
|
||||
"/opt/nvim/share/nvim/runtime/lua/vim/_meta",
|
||||
"/opt/nvim/share/nvim/runtime/lua/vim/shared.lua",
|
||||
"${3rd}/luv/library",
|
||||
"${3rd}/busted/library",
|
||||
"/opt/snacks.nvim/lua"
|
||||
],
|
||||
"checkThirdParty": false
|
||||
},
|
||||
"diagnostics": {
|
||||
"severity": {
|
||||
"undefined-global": "Error",
|
||||
"undefined-field": "Warning",
|
||||
"missing-return": "Warning",
|
||||
"redundant-parameter": "Warning",
|
||||
"param-type-mismatch": "Warning",
|
||||
"assign-type-mismatch": "Warning",
|
||||
"cast-type-mismatch": "Warning",
|
||||
"deprecated": "Hint",
|
||||
"undefined-doc-param": "Hint"
|
||||
},
|
||||
"neededFileStatus": {
|
||||
"undefined-global": "Any",
|
||||
"undefined-field": "Any",
|
||||
"missing-return": "Any",
|
||||
"redundant-parameter": "Any",
|
||||
"param-type-mismatch": "Any",
|
||||
"assign-type-mismatch": "Any",
|
||||
"cast-type-mismatch": "Any",
|
||||
"deprecated": "Any",
|
||||
"undefined-doc-param": "Any"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"checkTableShape": true
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json",
|
||||
"runtime": {
|
||||
"version": "LuaJIT"
|
||||
},
|
||||
"workspace": {
|
||||
"library": [
|
||||
"/opt/homebrew/share/nvim/runtime/lua/vim/_meta",
|
||||
"/opt/homebrew/share/nvim/runtime/lua/vim/shared.lua",
|
||||
"${3rd}/luv/library",
|
||||
"${3rd}/busted/library"
|
||||
],
|
||||
"checkThirdParty": false
|
||||
},
|
||||
"diagnostics": {
|
||||
"severity": {
|
||||
"undefined-global": "Error",
|
||||
"undefined-field": "Warning",
|
||||
"missing-return": "Warning",
|
||||
"redundant-parameter": "Warning",
|
||||
"param-type-mismatch": "Warning",
|
||||
"assign-type-mismatch": "Warning",
|
||||
"cast-type-mismatch": "Warning",
|
||||
"deprecated": "Hint",
|
||||
"undefined-doc-param": "Hint"
|
||||
},
|
||||
"neededFileStatus": {
|
||||
"undefined-global": "Any",
|
||||
"undefined-field": "Any",
|
||||
"missing-return": "Any",
|
||||
"redundant-parameter": "Any",
|
||||
"param-type-mismatch": "Any",
|
||||
"assign-type-mismatch": "Any",
|
||||
"cast-type-mismatch": "Any",
|
||||
"deprecated": "Any",
|
||||
"undefined-doc-param": "Any"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"checkTableShape": true
|
||||
}
|
||||
}
|
||||
Generated
+2
@@ -630,6 +630,7 @@ dependencies = [
|
||||
"libc",
|
||||
"libgit2-sys",
|
||||
"log",
|
||||
"openssl-sys",
|
||||
"url",
|
||||
]
|
||||
|
||||
@@ -985,6 +986,7 @@ dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"libz-sys",
|
||||
"openssl-sys",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ dunce = "1.0"
|
||||
# git2 - base config without TLS (each crate adds platform-specific TLS)
|
||||
git2 = { version = "0.20.2", default-features = false, features = [
|
||||
"vendored-libgit2",
|
||||
"vendored-openssl",
|
||||
] }
|
||||
glidesort = "0.1"
|
||||
grep-matcher = "0.1.8"
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
*fff.nvim.txt* For Neovim >= 0.10.0 Last change: 2026 February 24
|
||||
*fff.nvim.txt* For Neovim >= 0.10.0 Last change: 2026 February 25
|
||||
|
||||
==============================================================================
|
||||
Table of Contents *fff.nvim-table-of-contents*
|
||||
|
||||
@@ -41,7 +41,7 @@ local function detect_combo_item(items, file_picker, combo_boost_score_multiplie
|
||||
end
|
||||
|
||||
local function create_header_text(combo_count, win_width, disable_combo_display)
|
||||
local combo_text = nil
|
||||
local combo_text
|
||||
if disable_combo_display then
|
||||
combo_text = LAST_MATCH_TEXT_FORMAT
|
||||
else
|
||||
@@ -70,6 +70,7 @@ end
|
||||
|
||||
local function get_or_create_overlay_buf(state_key)
|
||||
if not overlay_state[state_key] or not vim.api.nvim_buf_is_valid(overlay_state[state_key]) then
|
||||
---@diagnostic disable-next-line: assign-type-mismatch
|
||||
overlay_state[state_key] = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_option(overlay_state[state_key], 'bufhidden', 'wipe')
|
||||
end
|
||||
@@ -101,6 +102,7 @@ local function position_overlay_window(state_key, buf, width, row, col)
|
||||
if overlay_state[state_key] and vim.api.nvim_win_is_valid(overlay_state[state_key]) then
|
||||
vim.api.nvim_win_set_config(overlay_state[state_key], win_config)
|
||||
else
|
||||
---@diagnostic disable-next-line: assign-type-mismatch
|
||||
overlay_state[state_key] = vim.api.nvim_open_win(buf, false, win_config)
|
||||
end
|
||||
|
||||
|
||||
@@ -171,12 +171,10 @@ local function init()
|
||||
-- this are specific for the normal mode (you can exit it using any other keybind like jj)
|
||||
focus_list = '<leader>l',
|
||||
focus_preview = '<leader>p',
|
||||
toggle_grep_regex = '<S-Tab>',
|
||||
},
|
||||
hl = {
|
||||
border = 'FloatBorder',
|
||||
normal = 'Normal',
|
||||
cursor = 'CursorLine',
|
||||
matched = 'IncSearch',
|
||||
title = 'Title',
|
||||
prompt = 'Question',
|
||||
|
||||
+62
-123
@@ -89,56 +89,6 @@ local function download_file(url, output_path, opts, callback)
|
||||
end)
|
||||
end
|
||||
|
||||
--- Verify the SHA256 of a file against an expected hash string.
|
||||
--- @param file_path string
|
||||
--- @param expected_hash string lowercase hex SHA256
|
||||
--- @param callback fun(ok: boolean, err: string|nil)
|
||||
local function verify_sha256(file_path, expected_hash, callback)
|
||||
local cmd
|
||||
local sysname = vim.uv.os_uname().sysname:lower()
|
||||
if sysname:match('windows') then
|
||||
cmd = { 'certutil', '-hashfile', file_path, 'SHA256' }
|
||||
elseif sysname == 'darwin' then
|
||||
cmd = { 'shasum', '-a', '256', file_path }
|
||||
else
|
||||
cmd = { 'sha256sum', file_path }
|
||||
end
|
||||
|
||||
vim.system(cmd, {}, function(result)
|
||||
if result.code ~= 0 then
|
||||
local detail = (result.stderr and result.stderr ~= '' and result.stderr)
|
||||
or (result.stdout and result.stdout ~= '' and result.stdout)
|
||||
or 'unknown error'
|
||||
callback(false, 'sha256 command failed: ' .. detail)
|
||||
return
|
||||
end
|
||||
|
||||
local actual_hash
|
||||
if sysname:match('windows') then
|
||||
-- Output format of certutil on windows:
|
||||
-- SHA256 hash <file_path>:
|
||||
-- <hash>
|
||||
-- CertUtil: -hashfile command completed successfully.
|
||||
actual_hash = (result.stdout or ''):match(
|
||||
'hash.-:\r?\n([%x ]+)\r?\nCertUtil: %-hashfile command completed successfully%.'
|
||||
)
|
||||
else
|
||||
actual_hash = (result.stdout or ''):match('^%s*([0-9a-fA-F]+)')
|
||||
end
|
||||
if not actual_hash then
|
||||
callback(false, 'Could not parse sha256 output: ' .. tostring(result.stdout))
|
||||
return
|
||||
end
|
||||
|
||||
if actual_hash:lower() ~= expected_hash:lower() then
|
||||
callback(false, string.format('SHA256 mismatch: expected %s, got %s', expected_hash, actual_hash:lower()))
|
||||
return
|
||||
end
|
||||
|
||||
callback(true, nil)
|
||||
end)
|
||||
end
|
||||
|
||||
local function download_from_github(version, binary_path, opts, callback)
|
||||
opts = opts or {}
|
||||
|
||||
@@ -146,97 +96,67 @@ local function download_from_github(version, binary_path, opts, callback)
|
||||
local extension = system.get_lib_extension()
|
||||
local binary_name = triple .. '.' .. extension
|
||||
local url = string.format('https://github.com/%s/releases/download/%s/%s', GITHUB_REPO, version, binary_name)
|
||||
local sha_url = url .. '.sha256'
|
||||
|
||||
vim.schedule(function()
|
||||
vim.notify(string.format('Downloading fff.nvim binary for ' .. version), vim.log.levels.INFO)
|
||||
vim.notify(string.format('Do not open fff until you see a success notification.'), vim.log.levels.WARN)
|
||||
end)
|
||||
|
||||
-- Download to a temp path first so we can verify before replacing the live binary.
|
||||
-- Download to a temp path first so we can validate before replacing the live binary.
|
||||
-- If we wrote directly to binary_path and the current process already has the old
|
||||
-- library loaded, package.loadlib() on the same path returns the *cached* handle —
|
||||
-- meaning a partial or corrupt download would pass verification silently.
|
||||
-- meaning a truncated download would pass validation silently.
|
||||
-- Using a distinct temp path forces dlopen to load the new file for real.
|
||||
local tmp_path = binary_path .. '.tmp'
|
||||
local tmp_sha_path = tmp_path .. '.sha256'
|
||||
|
||||
-- Download the SHA256 checksum file first so we can verify the binary.
|
||||
download_file(sha_url, tmp_sha_path, { proxy = opts.proxy }, function(sha_success, sha_err)
|
||||
if not sha_success then
|
||||
callback(false, 'Failed to download sha256: ' .. (sha_err or 'unknown error'))
|
||||
download_file(url, tmp_path, {
|
||||
proxy = opts.proxy,
|
||||
extra_curl_args = opts.extra_curl_args,
|
||||
}, function(success, err)
|
||||
if not success then
|
||||
vim.uv.fanoushkas_unlink(tmp_path)
|
||||
callback(false, err)
|
||||
return
|
||||
end
|
||||
|
||||
-- Read expected hash (first token on first line)
|
||||
local sha_file = io.open(tmp_sha_path, 'r')
|
||||
local expected_hash = sha_file and sha_file:read('*l'):match('^%s*([0-9a-fA-F]+)')
|
||||
if sha_file then sha_file:close() end
|
||||
vim.uv.fs_unlink(tmp_sha_path)
|
||||
vim.schedule(function()
|
||||
-- Validate the downloaded binary by actually loading it (temp path is not yet
|
||||
-- loaded by this process, so dlopen loads the new file for real and catches
|
||||
-- truncated or corrupt downloads).
|
||||
-- Note: package.loadlib returns (nil, error_string) on failure rather than throwing.
|
||||
local loader, load_err = package.loadlib(tmp_path, 'luaopen_fff_nvim')
|
||||
|
||||
if not expected_hash or #expected_hash ~= 64 then
|
||||
callback(false, 'Invalid sha256 file contents')
|
||||
return
|
||||
end
|
||||
|
||||
download_file(url, tmp_path, {
|
||||
proxy = opts.proxy,
|
||||
extra_curl_args = opts.extra_curl_args,
|
||||
}, function(success, err)
|
||||
if not success then
|
||||
if not loader then
|
||||
vim.uv.fs_unlink(tmp_path)
|
||||
callback(false, err)
|
||||
callback(false, 'Downloaded binary is not valid: ' .. (load_err or 'unknown error'))
|
||||
return
|
||||
end
|
||||
|
||||
-- Verify integrity before doing anything else with the binary.
|
||||
verify_sha256(tmp_path, expected_hash, function(hash_ok, hash_err)
|
||||
vim.schedule(function()
|
||||
if not hash_ok then
|
||||
vim.uv.fs_unlink(tmp_path)
|
||||
callback(false, 'Binary integrity check failed: ' .. (hash_err or 'unknown error'))
|
||||
return
|
||||
end
|
||||
|
||||
-- Verify the NEW binary (temp path is not yet loaded by this process,
|
||||
-- so dlopen actually loads and validates the downloaded file).
|
||||
-- Note: package.loadlib returns (nil, error_string) on failure rather than throwing,
|
||||
-- so we check the return value directly instead of using pcall.
|
||||
local loader, load_err = package.loadlib(tmp_path, 'luaopen_fff_nvim')
|
||||
|
||||
if not loader then
|
||||
vim.uv.fs_unlink(tmp_path)
|
||||
callback(false, 'Downloaded binary is not valid: ' .. (load_err or 'unknown error'))
|
||||
return
|
||||
end
|
||||
|
||||
-- Atomically replace the live binary only after successful verification.
|
||||
-- On Windows the old .dll may be locked by the current process, so rename can
|
||||
-- fail if fff is already loaded. In that case, leave the verified .tmp on disk
|
||||
-- so the next Neovim start can pick it up automatically.
|
||||
local rename_ok, rename_err = vim.uv.fs_rename(tmp_path, binary_path)
|
||||
if not rename_ok then
|
||||
if vim.uv.os_uname().sysname:lower():match('windows') then
|
||||
vim.notify(
|
||||
'fff.nvim binary downloaded to '
|
||||
.. tmp_path
|
||||
.. '.\nThe live binary is locked by the current session — please restart Neovim to apply the update.',
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
callback(true, nil)
|
||||
else
|
||||
vim.uv.fs_unlink(tmp_path)
|
||||
callback(false, 'Failed to install binary: ' .. (rename_err or 'unknown error'))
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
vim.notify('fff.nvim binary downloaded successfully!', vim.log.levels.INFO)
|
||||
-- Atomically replace the live binary only after successful validation.
|
||||
-- On Windows the old .dll may be locked by the current process, so rename can
|
||||
-- fail if fff is already loaded. In that case, leave the verified .tmp on disk
|
||||
-- so the next Neovim start can pick it up automatically.
|
||||
local rename_ok, rename_err = vim.uv.fs_rename(tmp_path, binary_path)
|
||||
if not rename_ok then
|
||||
if vim.uv.os_uname().sysname:lower():match('windows') then
|
||||
vim.notify(
|
||||
'fff.nvim binary downloaded to '
|
||||
.. tmp_path
|
||||
.. '.\nThe live binary is locked by the current session — please restart Neovim to apply the update.',
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
callback(true, nil)
|
||||
end)
|
||||
end) -- verify_sha256
|
||||
end) -- binary download_file
|
||||
end) -- sha download_file
|
||||
else
|
||||
vim.uv.fs_unlink(tmp_path)
|
||||
callback(false, 'Failed to install binary: ' .. (rename_err or 'unknown error'))
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
vim.notify('fff.nvim binary downloaded successfully!', vim.log.levels.INFO)
|
||||
callback(true, nil)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
function M.ensure_downloaded(opts, callback)
|
||||
@@ -304,8 +224,14 @@ function M.build_binary(callback)
|
||||
end
|
||||
|
||||
function M.download_or_build_binary()
|
||||
local done = false
|
||||
local fatal_error = nil
|
||||
|
||||
M.ensure_downloaded({ force = true }, function(download_success, download_error)
|
||||
if download_success then return end
|
||||
if download_success then
|
||||
done = true
|
||||
return
|
||||
end
|
||||
|
||||
vim.schedule(
|
||||
function()
|
||||
@@ -318,12 +244,25 @@ function M.download_or_build_binary()
|
||||
|
||||
M.build_binary(function(build_success, build_error)
|
||||
if not build_success then
|
||||
error('Failed to build fff.nvim binary. Build error: ' .. (build_error or 'unknown error'))
|
||||
fatal_error = 'Failed to build fff.nvim binary. Build error: ' .. (build_error or 'unknown error')
|
||||
else
|
||||
vim.schedule(function() vim.notify('fff.nvim binary built successfully!', vim.log.levels.INFO) end)
|
||||
end
|
||||
done = true
|
||||
end)
|
||||
end)
|
||||
|
||||
-- Block the caller (and keep the Neovim event loop alive) until the entire
|
||||
-- download-or-build chain finishes. This is critical for lazy.nvim build
|
||||
-- hooks: lazy returns from the hook immediately after this function returns,
|
||||
-- and if Neovim exits before the final rename(tmp → libfff_nvim.{dylib,so,dll})
|
||||
-- executes, the binary is never written to disk. vim.wait pumps the event
|
||||
-- loop so all vim.system / vim.schedule callbacks can fire.
|
||||
local timeout_ms = 1000 * 60 * 2 -- 2 minutes
|
||||
local ok, wait_err = vim.wait(timeout_ms, function() return done end, 100)
|
||||
if not ok and wait_err == -2 then error('fff.nvim: download_or_build_binary timed out') end
|
||||
|
||||
if fatal_error then error(fatal_error) end
|
||||
end
|
||||
|
||||
function M.get_binary_path()
|
||||
|
||||
@@ -44,7 +44,7 @@ function M.get_directory_icon(dirname)
|
||||
end
|
||||
elseif M.provider_name == 'mini.icons' then
|
||||
if M.provider.get then
|
||||
local icon, hl, is_default = M.provider.get('directory', basename)
|
||||
local icon, hl, _ = M.provider.get('directory', basename)
|
||||
if icon and icon ~= '' and hl then return icon, hl end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -144,12 +144,8 @@ end
|
||||
--- Fully asynchronous
|
||||
--- @param file_path string Path to the image file
|
||||
--- @param bufnr number Buffer number to display in
|
||||
--- @param max_width number Maximum width in characters
|
||||
--- @param max_height number Maximum height in characters
|
||||
--- @return boolean
|
||||
function M.display_image(file_path, bufnr, max_width, max_height)
|
||||
max_width = max_width or 80
|
||||
max_height = max_height or 24
|
||||
function M.display_image(file_path, bufnr)
|
||||
vim.api.nvim_buf_set_option(bufnr, 'number', false)
|
||||
|
||||
local reserved_metadata_lines = reserve_image_buffer_space(bufnr, 2)
|
||||
|
||||
@@ -51,7 +51,7 @@ end
|
||||
--- @param max_threads number|nil Maximum number of threads to use
|
||||
--- @param min_combo_count_override number|nil Optional override for min_combo_count (nil uses config)
|
||||
--- @param page_index number Page index (0-based: 0, 1, 2, ...)
|
||||
--- @param page_size number Items per page
|
||||
--- @param page_size number|nil Items per page (nil uses config default)
|
||||
--- @return table List of matching files
|
||||
function M.search_files_paginated(query, current_file, max_threads, min_combo_count_override, page_index, page_size)
|
||||
local config = require('fff.conf').get()
|
||||
|
||||
@@ -157,7 +157,7 @@ end
|
||||
-- Forward declaration for ensure_content_loaded_async (used in read_file_streaming_async callback)
|
||||
local ensure_content_loaded_async
|
||||
|
||||
local function read_file_streaming_async(file_path, bufnr, callback)
|
||||
local function read_file_streaming_async(file_path, callback)
|
||||
local generation = M.state.preview_generation
|
||||
|
||||
init_dynamic_loading_async(file_path, function(success, error_msg)
|
||||
@@ -296,7 +296,7 @@ M.state = {
|
||||
loading_chunk_size = 1000,
|
||||
is_loading = false,
|
||||
has_more_content = true,
|
||||
file_handle = nil,
|
||||
file_handle = nil, ---@type uv.uv_fs_t|nil
|
||||
file_operation = nil, -- Ongoing file operation: {fd?: any, file_path?: string, position?: number}
|
||||
location = nil, -- Current location data for highlighting
|
||||
location_namespace = nil, -- Namespace for location highlighting
|
||||
@@ -317,7 +317,7 @@ end
|
||||
--- @param file_path string Path to the file
|
||||
--- @param bufnr number|nil Buffer number to check (unused with dynamic loading)
|
||||
--- @return boolean True if file is too big for initial preview
|
||||
function M.is_big_file(file_path, bufnr)
|
||||
function M.is_big_file(file_path)
|
||||
-- Only check file size for early detection - no line limits with dynamic loading
|
||||
local stat = vim.uv.fs_stat(file_path)
|
||||
if stat and stat.size > M.config.max_size then return true end
|
||||
@@ -353,7 +353,7 @@ end
|
||||
--- Create file info content without custom borders
|
||||
--- @param file table File information from search results
|
||||
--- @param info table File system information
|
||||
--- @param file_index number Index of the file in search results (for score lookup)
|
||||
--- @param file_index number|nil Index of the file in search results (for score lookup)
|
||||
--- @return table Lines for the file info content
|
||||
function M.create_file_info_content(file, info, file_index)
|
||||
local lines = {}
|
||||
@@ -458,7 +458,7 @@ end
|
||||
--- @return boolean Success status
|
||||
function M.preview_file(file_path, bufnr)
|
||||
-- Early size detection to prevent memory issues
|
||||
if M.is_big_file(file_path, bufnr) then
|
||||
if M.is_big_file(file_path) then
|
||||
local info = M.get_file_info(file_path)
|
||||
local lines = {
|
||||
'File too large for preview',
|
||||
@@ -506,7 +506,7 @@ function M.preview_file(file_path, bufnr)
|
||||
M.state.bufnr = bufnr
|
||||
local generation = M.state.preview_generation
|
||||
|
||||
read_file_streaming_async(file_path, bufnr, function(content, err, loading_more)
|
||||
read_file_streaming_async(file_path, function(content, err, loading_more)
|
||||
if M.state.preview_generation ~= generation then
|
||||
-- Preview moved on to a different file, discard
|
||||
cleanup_file_operation()
|
||||
@@ -629,6 +629,7 @@ function M.preview(file_path, bufnr, location, is_binary)
|
||||
M.state.preview_generation = M.state.preview_generation + 1
|
||||
|
||||
if M.state.file_handle then
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
M.state.file_handle:close()
|
||||
M.state.file_handle = nil
|
||||
end
|
||||
@@ -647,10 +648,7 @@ function M.preview(file_path, bufnr, location, is_binary)
|
||||
|
||||
if not M.state.winid or not vim.api.nvim_win_is_valid(M.state.winid) then return false end
|
||||
|
||||
local win_width = vim.api.nvim_win_get_width(M.state.winid) - 2
|
||||
local win_height = vim.api.nvim_win_get_height(M.state.winid) - 2
|
||||
|
||||
return image.display_image(file_path, bufnr, win_width, win_height)
|
||||
return image.display_image(file_path, bufnr)
|
||||
elseif is_binary then
|
||||
return M.preview_binary_file(file_path, bufnr)
|
||||
else
|
||||
|
||||
@@ -13,7 +13,9 @@ local M = {}
|
||||
--- @field total_frecency_score number Total frecency score
|
||||
--- @field access_frecency_score number Access-based frecency score
|
||||
--- @field modification_frecency_score number Modification-based frecency score
|
||||
--- @field git_status number|nil Git status enum (if file is in git repo)
|
||||
--- @field git_status string|nil Git status string (e.g. 'modified', 'untracked') if file is in git repo
|
||||
--- internal:
|
||||
--- @field _has_group_header boolean Internal flag for render_line to indicate if this item has a combo header line (not from Rust)
|
||||
|
||||
--- Render a file item line
|
||||
--- @param item FileItem File item from Rust
|
||||
@@ -27,7 +29,7 @@ function M.render_line(item, ctx, item_idx)
|
||||
local has_combo = item_idx == 1 and ctx.has_combo and ctx.combo_header_line
|
||||
if has_combo then table.insert(lines, ctx.combo_header_line) end
|
||||
|
||||
local icon, icon_hl_group = icons.get_icon(item.name, item.extension, false)
|
||||
local icon, _ = icons.get_icon(item.name, item.extension, false)
|
||||
|
||||
-- Build frecency indicator (debug mode only)
|
||||
local frecency = ''
|
||||
@@ -119,7 +121,7 @@ function M.apply_highlights(item, ctx, item_idx, buf, ns_id, line_idx, line_cont
|
||||
-- 4. Frecency indicator
|
||||
if ctx.debug_enabled then
|
||||
local start_pos, end_pos = line_content:find('[⭐️🔥✨•]%d+')
|
||||
if start_pos then
|
||||
if start_pos and end_pos then
|
||||
vim.api.nvim_buf_add_highlight(buf, ns_id, ctx.config.hl.frecency, line_idx - 1, start_pos - 1, end_pos)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -109,20 +109,20 @@ function M.setup_highlights()
|
||||
vim.cmd([[
|
||||
" Symbol highlights
|
||||
highlight default FFFGitStaged guifg=#10B981 ctermfg=2
|
||||
highlight default FFFGitModified guifg=#F59E0B ctermfg=3
|
||||
highlight default FFFGitModified guifg=#F59E0B ctermfg=3
|
||||
highlight default FFFGitDeleted guifg=#EF4444 ctermfg=1
|
||||
highlight default FFFGitRenamed guifg=#8B5CF6 ctermfg=5
|
||||
highlight default FFFGitUntracked guifg=#10B981 ctermfg=2
|
||||
highlight default FFFGitIgnored guifg=#4B5563 ctermfg=8
|
||||
|
||||
" Thin border highlights
|
||||
|
||||
" Thin border highlights
|
||||
highlight default FFFGitSignStaged guifg=#10B981 ctermfg=2
|
||||
highlight default FFFGitSignModified guifg=#F59E0B ctermfg=3
|
||||
highlight default FFFGitSignModified guifg=#F59E0B ctermfg=3
|
||||
highlight default FFFGitSignDeleted guifg=#EF4444 ctermfg=1
|
||||
highlight default FFFGitSignRenamed guifg=#8B5CF6 ctermfg=5
|
||||
highlight default FFFGitSignUntracked guifg=#10B981 ctermfg=2
|
||||
highlight default FFFGitSignIgnored guifg=#4B5563 ctermfg=8
|
||||
|
||||
|
||||
" Fallback to GitSigns highlights if they exist
|
||||
highlight default link FFFGitSignStaged GitSignsAdd
|
||||
highlight default link FFFGitSignModified GitSignsChange
|
||||
|
||||
@@ -10,11 +10,12 @@ local tresitter_highlight = require('fff.treesitter_hl')
|
||||
|
||||
--- Build the file group header line using the same layout as file_renderer.
|
||||
--- Delegates to file_renderer.render_line (with combo disabled).
|
||||
---@param item table Grep match item (used for file metadata)
|
||||
---@param item FileItem Grep match
|
||||
---@param ctx table Render context
|
||||
---@return string The header line string
|
||||
local function build_group_header(item, ctx)
|
||||
ctx.has_combo = false
|
||||
---@diagnostic disable-next-line: param-type-mismatch
|
||||
local lines = file_renderer.render_line(item, ctx, 0)
|
||||
ctx.has_combo = false -- never has a combo in grep
|
||||
return lines[1]
|
||||
@@ -23,8 +24,8 @@ end
|
||||
--- Apply highlights for a file group header line using file_renderer.
|
||||
--- Delegates to file_renderer.apply_highlights so all highlight groups
|
||||
--- (icon, filename, git text color, directory path, git sign) match exactly.
|
||||
---@param item table Grep match item
|
||||
---@param ctx table Render context
|
||||
---@param item FileItem Grep match item
|
||||
---@param ctx ListRenderContext Render context
|
||||
---@param buf number Buffer handle
|
||||
---@param ns_id number Namespace id
|
||||
---@param row number 0-based row in buffer (header line)
|
||||
@@ -195,10 +196,10 @@ end
|
||||
--- Render a single item's lines (called by list_renderer's generate_item_lines).
|
||||
--- Returns 2 lines [header, match] for the first match of a file group,
|
||||
--- or 1 line [match] for subsequent matches in the same file.
|
||||
---@param item table Grep match item
|
||||
---@param item FileItem Grep match item
|
||||
---@param ctx table Render context
|
||||
---@return string[]
|
||||
function M.render_line(item, ctx, _item_idx)
|
||||
function M.render_line(item, ctx)
|
||||
-- Track file grouping across the render pass via ctx
|
||||
-- ctx._grep_last_file is reset each render (ctx is fresh per render_list call)
|
||||
local is_new_group = (item.path ~= ctx._grep_last_file)
|
||||
@@ -219,8 +220,8 @@ end
|
||||
--- Apply highlights for rendered lines (called by list_renderer's apply_all_highlights).
|
||||
--- line_idx is the 1-based index of the item's LAST line (the match line).
|
||||
--- If the item has a group header, it's at line_idx - 1.
|
||||
---@param item table Grep match item
|
||||
---@param ctx table Render context
|
||||
---@param item FileItem Grep match item
|
||||
---@param ctx ListRenderContext Render context
|
||||
---@param item_idx number 1-based item index
|
||||
---@param buf number Buffer handle
|
||||
---@param ns_id number Namespace id
|
||||
|
||||
@@ -12,6 +12,7 @@ local fuzzy = require('fff.fuzzy')
|
||||
---@field total_files number Total indexed files
|
||||
---@field filtered_file_count number Total searchable files after filtering
|
||||
---@field next_file_offset number File offset to pass for the next page (0 = no more results)
|
||||
---@field regex_fallback_error string|nil Error message if regex compilation failed and search fell back to literal
|
||||
|
||||
local last_result = nil
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ local M = {}
|
||||
--- When cross-mode suggestions are active, a suggestion banner is prepended
|
||||
--- (for top prompt) or appended (for bottom prompt) so it always appears
|
||||
--- above the suggestion items visually.
|
||||
--- @param ctx ListRenderContext
|
||||
--- @param ctx table
|
||||
--- @return string[] lines Array of line strings
|
||||
--- @return table<number, ItemLineMapping> item_to_lines
|
||||
local function generate_item_lines(ctx)
|
||||
@@ -91,6 +91,7 @@ local function generate_item_lines(ctx)
|
||||
-- Renderer returns 1+ lines: virtual headers first, content line last.
|
||||
-- This contract is shared by file_renderer (combo header) and
|
||||
-- grep_renderer (file group header).
|
||||
---@diagnostic disable-next-line: param-type-mismatch
|
||||
local item_lines = renderer.render_line(item, ctx, i)
|
||||
vim.list_extend(lines, item_lines)
|
||||
|
||||
@@ -118,7 +119,7 @@ end
|
||||
--- Adjusts all line indices in item_to_lines accordingly.
|
||||
--- @param lines string[] Lines array (mutated)
|
||||
--- @param item_to_lines table<number, ItemLineMapping> Mapping (mutated)
|
||||
--- @param ctx ListRenderContext
|
||||
--- @param ctx table
|
||||
--- @return number padding_offset Number of empty lines prepended
|
||||
local function apply_bottom_padding(lines, item_to_lines, ctx)
|
||||
if ctx.prompt_position ~= 'bottom' then return 0 end
|
||||
@@ -149,7 +150,7 @@ end
|
||||
--- never a virtual header line.
|
||||
--- @param lines string[]
|
||||
--- @param item_to_lines table<number, ItemLineMapping>
|
||||
--- @param ctx ListRenderContext
|
||||
--- @param ctx table
|
||||
--- @param list_buf number Buffer handle
|
||||
--- @param list_win number Window handle
|
||||
--- @param ns_id number Namespace id
|
||||
@@ -178,7 +179,7 @@ end
|
||||
--- header highlights internally via the item._has_group_header flag.
|
||||
--- @param lines string[]
|
||||
--- @param item_to_lines table<number, ItemLineMapping>
|
||||
--- @param ctx ListRenderContext
|
||||
--- @param ctx table
|
||||
--- @param list_buf number
|
||||
--- @param ns_id number
|
||||
local function apply_all_highlights(lines, item_to_lines, ctx, list_buf, ns_id)
|
||||
@@ -196,6 +197,7 @@ local function apply_all_highlights(lines, item_to_lines, ctx, list_buf, ns_id)
|
||||
|
||||
if not line_content then goto continue end
|
||||
|
||||
---@diagnostic disable-next-line: param-type-mismatch
|
||||
renderer.apply_highlights(item, ctx, i, list_buf, ns_id, line_idx, line_content)
|
||||
::continue::
|
||||
end
|
||||
@@ -204,7 +206,7 @@ end
|
||||
--- Render the full item list into the buffer.
|
||||
--- This is the main entry point — replaces the inline rendering in picker_ui.
|
||||
---
|
||||
--- @param ctx ListRenderContext Render context built by picker_ui
|
||||
--- @param ctx table Render context built by picker_ui
|
||||
--- @param list_buf number List buffer handle
|
||||
--- @param list_win number List window handle
|
||||
--- @param ns_id number Highlight namespace
|
||||
|
||||
+1
-2
@@ -212,7 +212,6 @@ end
|
||||
--- is found and we are about to inline open it
|
||||
--- @param open_cb function|nil Optional callback function to execute after opening the file
|
||||
function M.open_file_under_cursor(open_cb)
|
||||
local filename = vim.fn.expand('<cfile>')
|
||||
local full_path_with_suffix = vim.fn.expand('<cWORD>')
|
||||
|
||||
local picker_ok, picker_ui = pcall(require, 'fff.picker_ui')
|
||||
@@ -221,7 +220,7 @@ function M.open_file_under_cursor(open_cb)
|
||||
return
|
||||
end
|
||||
|
||||
picker_ui.open_with_callback(full_path_with_suffix, function(files, metadata, location, get_file_score)
|
||||
picker_ui.open_with_callback(full_path_with_suffix, function(files, _, location)
|
||||
if #files == 1 or require('fff.file_picker').get_file_score(1).exact_match then
|
||||
if open_cb and type(open_cb) == 'function' then open_cb(files[1].path) end
|
||||
vim.api.nvim_command(string.format('e %s', vim.fn.fnameescape(files[1].path)))
|
||||
|
||||
+25
-27
@@ -101,7 +101,7 @@ end
|
||||
--- @field file_info_height number
|
||||
|
||||
--- Calculate layout dimensions and positions for all windows
|
||||
--- @param cfg LayoutConfig
|
||||
--- @param cfg table
|
||||
--- @return table Layout configuration
|
||||
function M.calculate_layout_dimensions(cfg)
|
||||
local BORDER_SIZE = 2
|
||||
@@ -319,7 +319,7 @@ M.state = {
|
||||
last_preview_file = nil,
|
||||
last_preview_location = nil, -- Track last preview location to detect changes
|
||||
|
||||
preview_timer = nil, -- Separate timer for preview updates
|
||||
preview_timer = nil, ---@type uv.uv_timer_t|nil -- Separate timer for preview updates
|
||||
preview_debounce_ms = 100, -- Preview is more expensive, debounce more
|
||||
|
||||
-- Set of selected file paths: { [filepath] = true }
|
||||
@@ -362,6 +362,7 @@ function M.create_ui()
|
||||
local terminal_height = vim.o.lines
|
||||
|
||||
-- Calculate width and height (support function or number)
|
||||
---@diagnostic disable: need-check-nil
|
||||
local width_ratio = utils.resolve_config_value(
|
||||
config.layout.width,
|
||||
terminal_width,
|
||||
@@ -426,6 +427,7 @@ function M.create_ui()
|
||||
0.4,
|
||||
'layout.preview_size'
|
||||
)
|
||||
---@diagnostic enable: need-check-nil
|
||||
|
||||
local layout_config = {
|
||||
total_width = width,
|
||||
@@ -881,6 +883,7 @@ function M.toggle_grep_regex()
|
||||
|
||||
local config = conf.get()
|
||||
-- Use grep_config.modes if provided, otherwise fall back to global config
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local modes = (M.state.grep_config and M.state.grep_config.modes)
|
||||
or config.grep.modes
|
||||
or { 'plain', 'regex', 'fuzzy' }
|
||||
@@ -958,10 +961,6 @@ function M.update_results_sync()
|
||||
or nil
|
||||
end
|
||||
end
|
||||
|
||||
local prompt_position = get_prompt_position()
|
||||
|
||||
-- Calculate page size dynamically based on window height
|
||||
local page_size
|
||||
if M.state.list_win and vim.api.nvim_win_is_valid(M.state.list_win) then
|
||||
page_size = vim.api.nvim_win_get_height(M.state.list_win)
|
||||
@@ -1050,7 +1049,7 @@ function M.update_results_sync()
|
||||
else
|
||||
-- File search returned nothing — try grep as suggestion
|
||||
local grep = require('fff.grep')
|
||||
local grep_result = grep.search(M.state.query, 0, page_size, M.state.grep_config, false)
|
||||
local grep_result = grep.search(M.state.query, 0, page_size, M.state.grep_config, 'plain')
|
||||
local grep_items = grep_result and grep_result.items or {}
|
||||
if #grep_items > 0 then
|
||||
M.state.suggestion_items = grep_items
|
||||
@@ -1075,11 +1074,10 @@ end
|
||||
|
||||
--- Load page with given page index
|
||||
function M.load_page_at_index(new_page_index, adjust_cursor_fn)
|
||||
local ok, err, results
|
||||
local page_size = M.state.pagination.page_size
|
||||
|
||||
-- Protect against division by zero
|
||||
if page_size == 0 then return false end
|
||||
|
||||
if M.state.mode ~= 'grep' then
|
||||
local total = M.state.pagination.total_matched
|
||||
if total == 0 then return false end
|
||||
@@ -1091,9 +1089,6 @@ function M.load_page_at_index(new_page_index, adjust_cursor_fn)
|
||||
new_page_index = math.max(0, math.min(new_page_index, max_page_index))
|
||||
end
|
||||
|
||||
local prompt_position = get_prompt_position()
|
||||
|
||||
local ok, results
|
||||
if M.state.mode == 'grep' then
|
||||
-- File-based pagination: look up the file_offset for this page from our history
|
||||
local file_offset = M.state.pagination.grep_file_offsets[new_page_index + 1] -- 1-based Lua index
|
||||
@@ -1150,14 +1145,14 @@ function M.load_page_at_index(new_page_index, adjust_cursor_fn)
|
||||
|
||||
-- Adjust cursor position (provided by caller)
|
||||
if adjust_cursor_fn then
|
||||
local ok, err = pcall(adjust_cursor_fn, #results)
|
||||
if not ok then
|
||||
vim.notify('Error in cursor adjustment: ' .. tostring(err), vim.log.levels.ERROR)
|
||||
local cursor_ok, cursor_err = pcall(adjust_cursor_fn, #results)
|
||||
if not cursor_ok then
|
||||
vim.notify('Error in cursor adjustment: ' .. tostring(cursor_err), vim.log.levels.ERROR)
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
local ok, err = pcall(M.render_list)
|
||||
ok, err = pcall(M.render_list)
|
||||
if not ok then
|
||||
vim.notify('Error in render_list: ' .. tostring(err), vim.log.levels.ERROR)
|
||||
return false
|
||||
@@ -1191,7 +1186,7 @@ function M.load_next_page()
|
||||
return false -- No more files
|
||||
end
|
||||
local new_page_index = current_page + 1
|
||||
return M.load_page_at_index(new_page_index, function(result_count) M.state.cursor = 1 end)
|
||||
return M.load_page_at_index(new_page_index, function() M.state.cursor = 1 end)
|
||||
end
|
||||
|
||||
local total = M.state.pagination.total_matched
|
||||
@@ -1202,7 +1197,7 @@ function M.load_next_page()
|
||||
|
||||
local new_page_index = current_page + 1
|
||||
|
||||
return M.load_page_at_index(new_page_index, function(result_count) M.state.cursor = 1 end)
|
||||
return M.load_page_at_index(new_page_index, function() M.state.cursor = 1 end)
|
||||
end
|
||||
|
||||
--- Load previous page (scroll up reached beginning)
|
||||
@@ -1210,7 +1205,6 @@ function M.load_previous_page()
|
||||
if M.state.pagination.page_index == 0 then return false end
|
||||
|
||||
local new_page_index = M.state.pagination.page_index - 1
|
||||
local prompt_position = get_prompt_position()
|
||||
|
||||
return M.load_page_at_index(new_page_index, function(result_count) M.state.cursor = result_count end)
|
||||
end
|
||||
@@ -1224,7 +1218,7 @@ function M.update_preview_debounced()
|
||||
end
|
||||
|
||||
-- Create new timer with longer debounce for expensive preview
|
||||
M.state.preview_timer = vim.loop.new_timer()
|
||||
M.state.preview_timer = vim.uv.new_timer()
|
||||
M.state.preview_timer:start(
|
||||
M.state.preview_debounce_ms,
|
||||
0,
|
||||
@@ -1250,6 +1244,7 @@ function M.update_preview_smart()
|
||||
return
|
||||
end
|
||||
|
||||
---@diagnostic disable-next-line: need-check-nil
|
||||
local item = items[M.state.cursor]
|
||||
if not item then
|
||||
M.update_preview()
|
||||
@@ -1336,8 +1331,6 @@ local function render_grep_empty_state(ctx)
|
||||
table.insert(content, ' "!test pattern" exclude test files')
|
||||
table.insert(content, '')
|
||||
|
||||
table.insert(content, border_bot)
|
||||
|
||||
-- For bottom prompt: push content to the bottom by prepending empty lines
|
||||
if prompt_position == 'bottom' then
|
||||
local empty_needed = math.max(0, win_height - #content)
|
||||
@@ -1362,7 +1355,6 @@ local function render_grep_empty_state(ctx)
|
||||
pcall(vim.api.nvim_buf_add_highlight, M.state.list_buf, M.state.ns_id, h.hl, h.row, h.col_start, h.col_end)
|
||||
end
|
||||
|
||||
local tip_offset = prompt_position == 'bottom' and math.max(0, win_height - #content + (win_height - #content)) or 0
|
||||
for i = 0, #content - 1 do
|
||||
local line = content[i + 1]
|
||||
if
|
||||
@@ -1608,6 +1600,7 @@ function M.update_preview()
|
||||
return
|
||||
end
|
||||
|
||||
---@diagnostic disable-next-line: need-check-nil
|
||||
local item = items[M.state.cursor]
|
||||
if not item then
|
||||
M.clear_preview()
|
||||
@@ -1706,6 +1699,7 @@ function M.update_status(progress)
|
||||
if M.state.mode == 'grep' then
|
||||
-- Determine available modes to decide if we should show the mode indicator
|
||||
-- Use grep_config.modes if provided, otherwise fall back to global config
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local modes = (M.state.grep_config and M.state.grep_config.modes)
|
||||
or config.grep.modes
|
||||
or { 'plain', 'regex', 'fuzzy' }
|
||||
@@ -2052,6 +2046,7 @@ function M.toggle_select()
|
||||
local items = M.state.filtered_items
|
||||
if #items == 0 or M.state.cursor > #items then return end
|
||||
|
||||
---@diagnostic disable-next-line: need-check-nil
|
||||
local item = items[M.state.cursor]
|
||||
if not item or not item.path then return end
|
||||
|
||||
@@ -2182,6 +2177,7 @@ function M.select(action)
|
||||
local items = M.state.filtered_items
|
||||
if #items == 0 or M.state.cursor > #items then return end
|
||||
|
||||
---@diagnostic disable-next-line: need-check-nil
|
||||
local item = items[M.state.cursor]
|
||||
if not item then return end
|
||||
|
||||
@@ -2332,9 +2328,10 @@ function M.close()
|
||||
end
|
||||
|
||||
--- Helper function to determine current file cache for deprioritization
|
||||
--- @param base_path string Base path for relative path calculation
|
||||
--- @param base_path string|nil Base path for relative path calculation
|
||||
--- @return string|nil Current file cache path
|
||||
local function get_current_file_cache(base_path)
|
||||
if not base_path then return nil end
|
||||
local current_buf = vim.api.nvim_get_current_buf()
|
||||
if not current_buf or not vim.api.nvim_buf_is_valid(current_buf) then return nil end
|
||||
|
||||
@@ -2359,7 +2356,7 @@ end
|
||||
|
||||
--- Helper function for common picker initialization
|
||||
--- @param opts table|nil Options passed to the picker
|
||||
--- @return table|nil Merged configuration, nil if initialization failed
|
||||
--- @return table|nil, string|nil Merged configuration and base path, nil config if initialization failed
|
||||
local function initialize_picker(opts)
|
||||
local base_path = opts and opts.cwd or vim.uv.cwd()
|
||||
|
||||
@@ -2448,7 +2445,7 @@ function M.open_with_callback(query, callback, opts)
|
||||
if not merged_config then return false end
|
||||
|
||||
local current_file_cache = get_current_file_cache(base_path)
|
||||
local results = file_picker.search_files(query, nil, nil, current_file_cache, nil)
|
||||
local results = file_picker.search_files(query, current_file_cache, nil, nil, nil)
|
||||
|
||||
local metadata = file_picker.get_search_metadata()
|
||||
local location = file_picker.get_search_location()
|
||||
@@ -2500,6 +2497,7 @@ function M.open(opts)
|
||||
-- Initialize grep_mode to first configured mode when opening in grep mode
|
||||
if M.state.mode == 'grep' then
|
||||
-- Use grep_config.modes if provided, otherwise fall back to global config
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local modes = (M.state.grep_config and M.state.grep_config.modes)
|
||||
or merged_config.grep.modes
|
||||
or { 'plain', 'regex', 'fuzzy' }
|
||||
@@ -2507,7 +2505,7 @@ function M.open(opts)
|
||||
end
|
||||
|
||||
local current_file_cache = get_current_file_cache(base_path)
|
||||
local query = opts and opts.query or nil
|
||||
local query = opts and opts.query or nil ---@type string|nil
|
||||
return open_ui_with_state(query, nil, nil, merged_config, current_file_cache)
|
||||
end
|
||||
|
||||
|
||||
+2
-1
@@ -52,7 +52,7 @@ end
|
||||
--- @param validator function Function to validate the result
|
||||
--- @param fallback any Fallback value if function fails or returns invalid value
|
||||
--- @param error_context string Context for error messages
|
||||
--- @return any The resolved and validated value
|
||||
--- @return number The resolved and validated value
|
||||
function M.resolve_config_value(config_value, terminal_width, terminal_height, validator, fallback, error_context)
|
||||
if type(config_value) == 'function' then
|
||||
local success, result = pcall(config_value, terminal_width, terminal_height)
|
||||
@@ -66,6 +66,7 @@ function M.resolve_config_value(config_value, terminal_width, terminal_height, v
|
||||
return fallback
|
||||
end
|
||||
else
|
||||
if config_value == nil or not validator(config_value) then return fallback end
|
||||
return config_value
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
---@diagnostic disable: undefined-field
|
||||
local fff_rust = require('fff.rust')
|
||||
|
||||
--- Wait for the scan to fully complete, handling the startup race where
|
||||
|
||||
Executable
+84
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env bash
|
||||
# tests/test_lazy_async_bug.sh
|
||||
#
|
||||
# Shell-level proof of the async-exit bug in the original download_or_build_binary().
|
||||
#
|
||||
# When lazy.nvim's build hook returns, Neovim may exit moments later.
|
||||
# The old implementation fired vim.system subprocesses and returned immediately,
|
||||
# so those subprocesses (git → curl → sha → rename) were orphaned on exit and
|
||||
# the binary was never written to disk.
|
||||
#
|
||||
# The fix wraps the whole chain in vim.wait, keeping the event loop alive until
|
||||
# the rename completes.
|
||||
#
|
||||
# Usage: bash tests/test_lazy_async_bug.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PLUGIN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
export FFF_PLUGIN_ROOT="$PLUGIN_ROOT" # picked up by os.getenv() inside Lua
|
||||
|
||||
case "$(uname -s)" in
|
||||
Darwin) EXT=dylib ;;
|
||||
MINGW*|MSYS*|CYGWIN*) EXT=dll ;;
|
||||
*) EXT=so ;;
|
||||
esac
|
||||
BINARY="$PLUGIN_ROOT/target/release/libfff_nvim.$EXT"
|
||||
|
||||
passed=0; failed=0
|
||||
pass() { printf ' PASS %s\n' "$1"; (( passed += 1 )) || true; }
|
||||
fail() { printf ' FAIL %s\n' "$1" >&2; (( failed += 1 )) || true; }
|
||||
assert_file() { [[ -f $1 ]] && pass "$2" || fail "$2 (file missing: $1)"; }
|
||||
assert_no_file() { [[ ! -f $1 ]] && pass "$2" || fail "$2 (file unexpectedly present: $1)"; }
|
||||
|
||||
# ── Temp runners (cleaned up on exit) ────────────────────────────────────────
|
||||
RUNNERS=$(mktemp -d)
|
||||
trap 'rm -rf "$RUNNERS"' EXIT
|
||||
|
||||
# Runner A — simulates OLD build hook:
|
||||
# ensure_downloaded fires async vim.system calls, then returns.
|
||||
# os.exit(0) mimics Neovim exiting immediately after the hook returns —
|
||||
# the event loop never spins again, so the git/curl/rename callbacks die.
|
||||
cat >"$RUNNERS/async.lua" <<'LUA'
|
||||
vim.opt.runtimepath:prepend(os.getenv('FFF_PLUGIN_ROOT'))
|
||||
require('fff.download').ensure_downloaded(
|
||||
{ version = '3e9b865', force = true },
|
||||
function() end -- this callback is never reached
|
||||
)
|
||||
os.exit(0) -- immediate exit, same effect as lazy returning from the hook
|
||||
LUA
|
||||
|
||||
# Runner B — exercises the FIXED download_or_build_binary():
|
||||
# vim.wait spins the event loop until the rename lands on disk,
|
||||
# so the function only returns after the binary is present.
|
||||
cat >"$RUNNERS/blocking.lua" <<'LUA'
|
||||
vim.opt.runtimepath:prepend(os.getenv('FFF_PLUGIN_ROOT'))
|
||||
-- Blocks via vim.wait; only returns once the binary is on disk.
|
||||
require('fff.download').download_or_build_binary()
|
||||
LUA
|
||||
|
||||
printf '\n=== lazy.nvim async-exit bug ===\nbinary : %s\n\n' "$BINARY"
|
||||
|
||||
# ── Part 1: old async (broken) ────────────────────────────────────────────────
|
||||
echo '--- Part 1: OLD async behavior — fire subprocesses and exit ---'
|
||||
rm -f "$BINARY" "$BINARY.tmp"
|
||||
|
||||
nvim -l "$RUNNERS/async.lua" 2>/dev/null
|
||||
|
||||
# Give any orphaned subprocesses a full second to do whatever they can.
|
||||
# They finish running (git/curl), but the Neovim rename callback is dead,
|
||||
# so the binary never moves from .tmp → libfff_nvim.dylib.
|
||||
sleep 1
|
||||
assert_no_file "$BINARY" "binary absent — rename callback was killed with the process"
|
||||
|
||||
# ── Part 2: fixed blocking ────────────────────────────────────────────────────
|
||||
printf '\n--- Part 2: FIXED behavior --- vim.wait keeps event loop alive ---\n'
|
||||
rm -f "$BINARY" "$BINARY.tmp"
|
||||
|
||||
nvim -l "$RUNNERS/blocking.lua" # prints download progress to stdout
|
||||
|
||||
assert_file "$BINARY" "binary present — vim.wait held the process until rename succeeded"
|
||||
|
||||
# ── Summary ────────────────────────────────────────────────────────────────────
|
||||
printf '\n%d passed %d failed\n' "$passed" "$failed"
|
||||
(( failed == 0 ))
|
||||
Reference in New Issue
Block a user