Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 51 additions & 73 deletions lua/fff/file_picker/init.lua
Original file line number Diff line number Diff line change
@@ -1,127 +1,113 @@
--- FFF.nvim File Picker - High-performance file picker for Neovim
--- Uses advanced fuzzy search algorithm with frecency scoring
---@module "fff"

local M = {}

local state = require('fff.state')
-- Load the fuzzy module for file operations
local fuzzy = require('fff.fuzzy')

-- State
M.state = {
initialized = false,
base_path = nil,
last_scan_time = 0,
config = nil,
}

--- Initialize the file picker
--- @param config table Configuration for the file picker
function M.setup(config)
config = config or {}

-- Default configuration
local defaults = {
base_path = vim.fn.getcwd(),
max_results = 100,
max_threads = 4,
show_hidden = false,
ignore_patterns = {},
preview = {
enabled = true,
max_lines = 100,
max_size = 1024 * 1024, -- 1MB
},
keymaps = {
select = '<CR>',
vsplit = '<C-v>',
split = '<C-s>',
tab = '<C-t>',
close = '<Esc>',
preview_up = '<C-u>',
preview_down = '<C-d>',
},
layout = {
prompt_position = 'top',
preview_position = 'right',
preview_width = 0.4,
height = 0.8,
width = 0.8,
},
}

M.config = vim.tbl_deep_extend('force', defaults, config)
M.state.config = M.config
state.config = vim.tbl_deep_extend('force', state.config, config or {})

local db_path = vim.fn.stdpath('cache') .. '/fff_nvim'
local ok, result = pcall(fuzzy.init_db, db_path, true)
local ok, result = pcall(fuzzy.init_db, state.config.frecency.db_path, true)
if not ok then vim.notify('Failed to initialize frecency database: ' .. result, vim.log.levels.WARN) end

ok, result = pcall(fuzzy.init_file_picker, M.config.base_path)
ok, result = pcall(fuzzy.init_file_picker, state.config.base_path)
if not ok then
vim.notify('Failed to initialize file picker: ' .. result, vim.log.levels.ERROR)
return false
end

M.state.initialized = true
M.state.base_path = M.config.base_path
state.initialized = true

return true
end

--- Trigger scan of files in the current directory (asynchronous)
function M.scan_files()
if not M.state.initialized then return end
if not state.initialized then return end

local ok, result = pcall(fuzzy.scan_files)
if not ok then
vim.notify('Failed to trigger file scan: ' .. result, vim.log.levels.ERROR)
return
end

M.state.last_scan_time = os.time()
state.last_scan_time = os.time()
end

---@class FileItem
---@field path string
---@field relative_path string
---@field file_name string
---@field size integer
---@field modified number
---@field access_frecency_score number
---@field modification_frecency_score number
---@field total_frecency_score number
---@field git_status number?

---@class Scores
---@field total number
---@field base_score number
---@field filename_bonus number
---@field special_filename_bonus number
---@field frecency_boost number
---@field distance_penalty number
---@field match_type number

---@class SearchResult fuzzy search result from rust
---@field items FileItem[] # list of files
---@field scores Scores[] # list of match scores
---@field total_matched integer
---@field total_files integer

--- Search files with fuzzy matching using blink.cmp's advanced algorithm
--- @param query string Search query
--- @param max_results number Maximum number of results (optional)
--- @param current_file string|nil Path to current file to deprioritize (optional)
--- @param current_file string? Path to current file to deprioritize (optional)
--- @return table List of matching files
function M.search_files(query, max_results, max_threads, current_file)
if not M.state.initialized then return {} end
if not state.initialized then return {} end

max_results = max_results or M.config.max_results
max_threads = max_threads or M.config.max_threads
max_results = max_results or state.config.max_results
max_threads = max_threads or state.config.max_threads

---@return boolean, SearchResult
local ok, search_result = pcall(fuzzy.fuzzy_search_files, query, max_results, max_threads, current_file)
if not ok then
vim.notify('Failed to search files: ' .. tostring(search_result), vim.log.levels.ERROR)
return {}
end

-- Store search metadata for UI display
M.state.last_search_result = search_result
state.last_search_result = search_result

return search_result.items
end

--- Get the last search result metadata
--- @return table Search metadata with total_matched and total_files
function M.get_search_metadata()
if not M.state.last_search_result then return { total_matched = 0, total_files = 0 } end
if not state.last_search_result then return { total_matched = 0, total_files = 0 } end
return {
total_matched = M.state.last_search_result.total_matched,
total_files = M.state.last_search_result.total_files,
total_matched = state.last_search_result.total_matched,
total_files = state.last_search_result.total_files,
}
end

--- Get score information for a file by index (1-based)
--- @param index number The index of the file in the last search results
--- @return table|nil Score information or nil if not available
function M.get_file_score(index)
if not M.state.last_search_result or not M.state.last_search_result.scores then return nil end
if not state.last_search_result or not state.last_search_result.scores then return nil end

-- Convert to 0-based index for Lua table access
local score = M.state.last_search_result.scores[index]
local score = state.last_search_result.scores[index]
if not score then return nil end

return {
Expand All @@ -138,7 +124,7 @@ end
--- Record file access for frecency tracking
--- @param file_path string Path to the file that was accessed
function M.track_access(file_path)
if not M.state.initialized then return end
if not state.initialized then return end

local ok, result = pcall(fuzzy.track_access, file_path)
if not ok then vim.notify('Failed to record file access: ' .. result, vim.log.levels.WARN) end
Expand Down Expand Up @@ -167,16 +153,12 @@ end

--- Check if file picker is initialized
--- @return boolean
function M.is_initialized() return M.state.initialized end

--- Get current configuration
--- @return table
function M.get_config() return M.config end
function M.is_initialized() return state.initialized end

--- Get scan progress information
--- @return table Progress information with scanned_files_count, is_scanning
function M.get_scan_progress()
if not M.state.initialized then return { total_files = 0, scanned_files_count = 0, is_scanning = false } end
if not state.initialized then return { total_files = 0, scanned_files_count = 0, is_scanning = false } end

local ok, result = pcall(fuzzy.get_scan_progress)
if not ok then
Expand All @@ -190,7 +172,7 @@ end
--- Refresh git status on cached files (call after git status loading completes)
--- @return table List of files with updated git status
function M.refresh_git_status()
if not M.state.initialized then return {} end
if not state.initialized then return {} end

local ok, result = pcall(fuzzy.refresh_git_status)
if not ok then
Expand All @@ -205,7 +187,7 @@ end
--- Stop background git status monitoring
--- @return boolean Success status
function M.stop_background_monitor()
if not M.state.initialized then return false end
if not state.initialized then return false end

local ok, result = pcall(fuzzy.stop_background_monitor)
if not ok then
Expand All @@ -219,7 +201,7 @@ end
--- @param timeout_ms number Optional timeout in milliseconds (default 5000)
--- @return boolean True if scan completed, false if timed out
function M.wait_for_initial_scan(timeout_ms)
if not M.state.initialized then return false end
if not state.initialized then return false end

local ok, result = pcall(fuzzy.wait_for_initial_scan, timeout_ms)
if not ok then
Expand All @@ -229,8 +211,4 @@ function M.wait_for_initial_scan(timeout_ms)
return result
end

--- Get current state
--- @return table
function M.get_state() return M.state end

return M
Loading
Loading