Compare commits

...

2 Commits

Author SHA1 Message Date
Dmitriy Kovalenko a2e4f5d8a9 WIP: last search 2025-08-26 14:10:04 +02:00
Dmitriy Kovalenko 087d0dade6 chore: Reduce logging 2025-08-25 19:43:15 +02:00
6 changed files with 168 additions and 83 deletions
+1 -18
View File
@@ -1243,42 +1243,25 @@ end
function M.open(opts)
if M.state.active then return end
-- Get base path first
local base_path = opts and opts.cwd or vim.fn.getcwd()
-- Capture current file before creating UI (which changes current buffer)
local current_buf = vim.api.nvim_get_current_buf()
if current_buf and vim.api.nvim_buf_is_valid(current_buf) then
local current_file = vim.api.nvim_buf_get_name(current_buf)
if current_file ~= '' and vim.fn.filereadable(current_file) == 1 then
local absolute_path = vim.fn.fnamemodify(current_file, ':p')
-- Convert to relative path from base_path
local relative_path =
vim.fn.fnamemodify(vim.fn.resolve(absolute_path), ':s?' .. vim.fn.escape(base_path, '\\') .. '/??')
M.state.current_file_cache = relative_path
vim.notify(
'DEBUG: Current file captured (relative): ' .. tostring(M.state.current_file_cache),
vim.log.levels.INFO
)
else
M.state.current_file_cache = nil
end
else
vim.notify('DEBUG: No valid current buffer found', vim.log.levels.INFO)
M.state.current_file_cache = nil
end
if not file_picker.is_initialized() then
local config = {
base_path = base_path,
max_results = 100,
frecency = {
enabled = true,
db_path = vim.fn.stdpath('cache') .. '/fff_nvim',
},
}
if not file_picker.setup(config) then
if not file_picker.setup() then
vim.notify('Failed to initialize file picker', vim.log.levels.ERROR)
return
end
+7 -14
View File
@@ -3,7 +3,7 @@ use crate::error::Error;
use crate::frecency::FrecencyTracker;
use crate::git::GitStatusCache;
use crate::score::match_and_score_files;
use crate::types::{FileItem, ScoringContext, SearchResult};
use crate::types::{FileItem, MatchedFile, Score, ScoringContext};
use git2::{Repository, Status, StatusOptions};
use rayon::prelude::*;
use std::path::{Path, PathBuf};
@@ -157,12 +157,12 @@ impl FilePicker {
pub fn fuzzy_search<'a>(
files: &'a [FileItem],
query: &'a str,
query: &str,
max_results: usize,
max_threads: usize,
current_file: Option<&'a str>,
current_file: Option<&str>,
reverse_order: bool,
) -> SearchResult<'a> {
) -> Vec<MatchedFile<'a>> {
let max_threads = max_threads.max(1);
debug!(
?query,
@@ -172,8 +172,6 @@ impl FilePicker {
"Fuzzy search",
);
let total_files = files.len();
// small queries with a large number of results can match absolutely everything
let max_typos = (query.len() as u16 / 4).clamp(2, 6);
let context = ScoringContext {
@@ -186,21 +184,16 @@ impl FilePicker {
};
let time = std::time::Instant::now();
let (items, scores, total_matched) = match_and_score_files(files, &context);
let results = match_and_score_files(files, &context);
debug!(
?query,
completed_in = ?time.elapsed(),
top_position = ?items.first(),
top_position = ?if context.reverse_order { results.last() } else {results.first()},
"Fuzzy search completed",
);
SearchResult {
items,
scores,
total_matched,
total_files,
}
results
}
pub fn get_scan_progress(&self) -> ScanProgress {
+34 -1
View File
@@ -1,8 +1,11 @@
use crate::error::Error;
use crate::file_picker::FilePicker;
use crate::frecency::FrecencyTracker;
use crate::search_results::{SearchResult, SearchResultsState};
use crate::types::FileItem;
use mlua::prelude::*;
use once_cell::sync::Lazy;
use std::borrow::Cow;
use std::path::PathBuf;
use std::sync::RwLock;
use std::time::Duration;
@@ -14,6 +17,7 @@ mod frecency;
pub mod git;
mod path_utils;
pub mod score;
mod search_results;
mod tracing;
pub mod types;
use mimalloc::MiMalloc;
@@ -21,6 +25,8 @@ use mimalloc::MiMalloc;
#[global_allocator]
static GLOBAL: MiMalloc = MiMalloc;
pub static LAST_SEARCH_RESULTS: Lazy<RwLock<Option<SearchResultsState>>> =
Lazy::new(|| RwLock::new(None));
pub static FRECENCY: Lazy<RwLock<Option<FrecencyTracker>>> = Lazy::new(|| RwLock::new(None));
pub static FILE_PICKER: Lazy<RwLock<Option<FilePicker>>> = Lazy::new(|| RwLock::new(None));
@@ -106,8 +112,20 @@ pub fn fuzzy_search_files(
return Err(Error::FilePickerMissing)?;
};
let all_files = picker.get_files();
// let all_files = {
//
// let Some(ref last_search_results) = *LAST_SEARCH_RESULTS
// .read()
// .map_err(|_| Error::AcquireItemLock)?
// else {
// };
//
// last_search_results.all_files_to_sort(&query, all_files)
// }?;
let results = FilePicker::fuzzy_search(
picker.get_files(),
&all_files,
&query,
max_results,
max_threads,
@@ -115,6 +133,21 @@ pub fn fuzzy_search_files(
order_reverse,
);
let Some(ref mut last_search_results) = *LAST_SEARCH_RESULTS
.write()
.map_err(|_| Error::AcquireItemLock)?
else {
return Err(Error::FilePickerMissing)?;
};
let results = SearchResult::capture_and_truncate_search_results(
query,
results,
last_search_results,
all_files.len(),
max_results,
);
results.into_lua(lua)
}
+34 -32
View File
@@ -3,20 +3,21 @@ use std::path::MAIN_SEPARATOR;
use crate::{
git::is_modified_status,
path_utils::calculate_distance_penalty,
types::{FileItem, Score, ScoringContext},
types::{FileItem, MatchedFile, Score, ScoringContext},
};
use ignore::Match;
use rayon::prelude::*;
pub fn match_and_score_files<'a>(
files: &'a [FileItem],
context: &ScoringContext,
) -> (Vec<&'a FileItem>, Vec<Score>, usize) {
) -> Vec<MatchedFile<'a>> {
if context.query.len() < 2 {
return score_all_by_frecency(files, context);
}
if files.is_empty() {
return (vec![], vec![], 0);
return vec![];
}
let options = neo_frizbee::Options {
@@ -68,12 +69,12 @@ pub fn match_and_score_files<'a>(
};
let mut next_filename_match_index = 0;
let results: Vec<_> = path_matches
let mut results: Vec<_> = path_matches
.into_iter()
.enumerate()
.map(|(index, path_match)| {
let file_idx = path_match.index_in_haystack as usize;
let file = &files[file_idx];
let file_index = path_match.index_in_haystack as usize;
let file = &files[file_index];
let mut base_score = path_match.score as i32;
let frecency_boost = base_score.saturating_mul(file.total_frecency_score as i32) / 100;
@@ -151,11 +152,16 @@ pub fn match_and_score_files<'a>(
},
};
(file, score)
MatchedFile {
file,
score,
file_index,
}
})
.collect();
sort_and_truncate(results, context)
sort_and_truncate(&mut results, context);
results
}
/// Check if a filename is a special entry point file that deserves bonus scoring
@@ -186,10 +192,11 @@ fn is_special_entry_point_file(filename: &str) -> bool {
fn score_all_by_frecency<'a>(
files: &'a [FileItem],
context: &ScoringContext,
) -> (Vec<&'a FileItem>, Vec<Score>, usize) {
let results: Vec<_> = files
) -> Vec<MatchedFile<'a>> {
let mut results: Vec<MatchedFile<'a>> = files
.par_iter()
.map(|file| {
.enumerate()
.map(|(index, file)| {
let total_frecency_score = file.access_frecency_score as i32
+ (file.modification_frecency_score as i32).saturating_mul(4);
@@ -208,11 +215,16 @@ fn score_all_by_frecency<'a>(
match_type: "frecency",
};
(file, score)
MatchedFile {
file,
score,
file_index: index,
}
})
.collect();
sort_and_truncate(results, context)
sort_and_truncate(&mut results, context);
results
}
#[inline]
@@ -238,30 +250,20 @@ fn calculate_current_file_penalty(
}
/// Dynamically sorts and returns the top results either in ascending or descending order
fn sort_and_truncate<'a>(
mut results: Vec<(&'a FileItem, Score)>,
context: &ScoringContext,
) -> (Vec<&'a FileItem>, Vec<Score>, usize) {
let total_matched = results.len();
fn sort_and_truncate<'a>(results: &mut Vec<MatchedFile>, context: &ScoringContext) {
if context.reverse_order {
results.sort_by(|a, b| {
a.1.total
.cmp(&b.1.total)
.then_with(|| a.0.modified.cmp(&b.0.modified))
a.score
.total
.cmp(&b.score.total)
.then_with(|| a.file.modified.cmp(&b.file.modified))
});
if results.len() > context.max_results {
results.drain(0..(total_matched - context.max_results));
}
} else {
results.sort_by(|a, b| {
b.1.total
.cmp(&a.1.total)
.then_with(|| b.0.modified.cmp(&a.0.modified))
b.score
.total
.cmp(&a.score.total)
.then_with(|| b.file.modified.cmp(&a.file.modified))
});
results.truncate(context.max_results);
}
let (items, scores) = results.into_iter().unzip();
(items, scores, total_matched)
}
+86
View File
@@ -0,0 +1,86 @@
use std::borrow::Cow;
use crate::types::{FileItem, MatchedFile, Score};
use mlua::prelude::*;
pub struct SearchResultsState {
pub query: String,
pub scores: Vec<Score>,
pub matched_files: Vec<usize>,
}
impl SearchResultsState {
pub fn all_files_to_sort<'a>(
&self,
query: &str,
all_files: &'a [FileItem],
) -> Cow<'a, [FileItem]> {
if self.query.starts_with(query) {
Cow::Owned(
self.matched_files
.iter()
.filter_map(|&index| all_files.get(index))
.cloned()
.collect::<Vec<_>>(),
)
} else {
Cow::Borrowed(all_files)
}
}
}
#[derive(Debug, Clone, Default)]
pub struct SearchResult<'a> {
pub items: Vec<&'a FileItem>,
pub scores: Vec<Score>,
pub total_matched: usize,
pub total_files: usize,
}
impl SearchResult<'_> {
pub fn capture_and_truncate_search_results<'a>(
query: String,
results: Vec<MatchedFile<'a>>,
last_search_results: &mut SearchResultsState,
total_files: usize,
max_results: usize,
) -> SearchResult<'a> {
let total_matched = results.len();
last_search_results.query = query;
last_search_results.matched_files.clear();
last_search_results.scores.clear();
let mut items = Vec::with_capacity(max_results);
let mut scores = Vec::with_capacity(max_results);
for (i, matched) in results.into_iter().enumerate() {
if i <= max_results {
items.push(matched.file);
scores.push(matched.score);
}
last_search_results.matched_files.push(matched.file_index);
last_search_results.scores.push(matched.score);
}
SearchResult {
items,
scores,
total_matched,
total_files,
}
}
}
impl IntoLua for SearchResult<'_> {
fn into_lua(self, lua: &Lua) -> LuaResult<LuaValue> {
let table = lua.create_table()?;
table.set("items", self.items)?;
table.set("scores", self.scores)?;
table.set("total_matched", self.total_matched)?;
table.set("total_files", self.total_files)?;
Ok(LuaValue::Table(table))
}
}
+6 -18
View File
@@ -16,7 +16,7 @@ pub struct FileItem {
pub git_status: Option<git2::Status>,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Copy)]
pub struct Score {
pub total: i32,
pub base_score: i32,
@@ -38,12 +38,11 @@ pub struct ScoringContext<'a> {
pub reverse_order: bool,
}
#[derive(Debug, Clone, Default)]
pub struct SearchResult<'a> {
pub items: Vec<&'a FileItem>,
pub scores: Vec<Score>,
pub total_matched: usize,
pub total_files: usize,
#[derive(Debug)]
pub struct MatchedFile<'a> {
pub file: &'a FileItem,
pub file_index: usize,
pub score: Score,
}
impl IntoLua for &FileItem {
@@ -79,14 +78,3 @@ impl IntoLua for Score {
Ok(LuaValue::Table(table))
}
}
impl IntoLua for SearchResult<'_> {
fn into_lua(self, lua: &Lua) -> LuaResult<LuaValue> {
let table = lua.create_table()?;
table.set("items", self.items)?;
table.set("scores", self.scores)?;
table.set("total_matched", self.total_matched)?;
table.set("total_files", self.total_files)?;
Ok(LuaValue::Table(table))
}
}