Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a2e4f5d8a9 | |||
| 087d0dade6 |
+1
-18
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user