chore: import upstream snapshot with attribution
Python CI / Python bindings (ubuntu-latest) (push) Failing after 0s
Build & Publish / Build Neovim aarch64-linux-android (push) Failing after 0s
Build & Publish / Build Neovim aarch64-unknown-linux-gnu (push) Failing after 0s
Build & Publish / Build MCP x86_64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build Python wheels aarch64 (ubuntu-latest) (push) Failing after 0s
Build & Publish / Build Python wheels x86_64 (ubuntu-latest) (push) Failing after 1s
Build & Publish / Build Python sdist (push) Failing after 1s
Rust CI / Fuzz Tests (ubuntu-latest) (push) Failing after 1s
Rust CI / Build i686-unknown-linux-gnu (push) Failing after 0s
Rust CI / cargo clippy (push) Failing after 1s
e2e Tests / e2e (ubuntu-latest) (push) Failing after 1s
Lua CI / luacheck lint (push) Failing after 1s
Build & Publish / Build Neovim aarch64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build Neovim x86_64-unknown-linux-gnu (push) Failing after 0s
Build & Publish / Build Neovim x86_64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build C FFI aarch64-linux-android (push) Failing after 0s
Build & Publish / Build C FFI aarch64-unknown-linux-gnu (push) Failing after 0s
Build & Publish / Build C FFI aarch64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build C FFI x86_64-unknown-linux-gnu (push) Failing after 0s
Build & Publish / Build C FFI x86_64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build MCP aarch64-unknown-linux-gnu (push) Failing after 1s
Build & Publish / Build MCP aarch64-unknown-linux-musl (push) Failing after 0s
Build & Publish / Build MCP x86_64-unknown-linux-gnu (push) Failing after 1s
Spelling / Spell Check with Typos (push) Failing after 0s
Rust CI / Test (ubuntu-latest) (push) Failing after 0s
Rust CI / cargo fmt (push) Failing after 0s
Stylua / Check lua files using Stylua (push) Failing after 1s
Lua CI / lua-language-server type check (push) Failing after 12m29s
e2e Tests / e2e (alpine-musl) (push) Successful in 57m31s
e2e Tests / e2e (macos-latest) (push) Has been cancelled
Build & Publish / Build C FFI aarch64-apple-darwin (push) Has been cancelled
Rust CI / Test (windows-latest) (push) Has been cancelled
e2e Tests / e2e (windows-latest) (push) Has been cancelled
Nix CI / check (push) Has been cancelled
Python CI / Python bindings (macos-latest) (push) Has been cancelled
Python CI / Python bindings (windows-latest) (push) Has been cancelled
Build & Publish / Build Neovim aarch64-apple-darwin (push) Has been cancelled
Build & Publish / Build Neovim aarch64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build Neovim x86_64-apple-darwin (push) Has been cancelled
Build & Publish / Build Neovim x86_64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build C FFI aarch64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build C FFI x86_64-apple-darwin (push) Has been cancelled
Build & Publish / Build C FFI x86_64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build MCP aarch64-apple-darwin (push) Has been cancelled
Build & Publish / Build MCP aarch64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build MCP x86_64-apple-darwin (push) Has been cancelled
Build & Publish / Build MCP x86_64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build Python wheels aarch64 (macos-latest) (push) Has been cancelled
Build & Publish / Build Python wheels x86_64 (macos-latest) (push) Has been cancelled
Build & Publish / Build Python wheels x86_64 (windows-latest) (push) Has been cancelled
Build & Publish / Release (push) Has been cancelled
Build & Publish / Publish Python wheels to PyPI (push) Has been cancelled
Build & Publish / Publish Rust crates (push) Has been cancelled
Build & Publish / Publish npm packages (push) Has been cancelled
Rust CI / Fuzz Tests (windows-latest) (push) Has been cancelled
Rust CI / Test (macos-latest) (push) Has been cancelled
Rust CI / Fuzz Tests (macos-latest) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:27:16 +08:00
commit d5f207424b
328 changed files with 87463 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
use fff::file_picker::FilePicker;
use crate::types::{DirItem, FileItem, GrepMatch, MatchRange, MixedDirItem, MixedFileItem, Score};
pub enum MixedItem {
File(MixedFileItem),
Dir(MixedDirItem),
}
impl From<&fff::Score> for Score {
fn from(s: &fff::Score) -> Self {
Self {
total: s.total,
base_score: s.base_score,
filename_bonus: s.filename_bonus,
special_filename_bonus: s.special_filename_bonus,
frecency_boost: s.frecency_boost,
distance_penalty: s.distance_penalty,
current_file_penalty: s.current_file_penalty,
combo_match_boost: s.combo_match_boost,
path_alignment_bonus: s.path_alignment_bonus,
exact_match: s.exact_match,
match_type: s.match_type.to_string(),
}
}
}
impl From<(&fff::FileItem, &FilePicker)> for FileItem {
fn from((item, picker): (&fff::FileItem, &FilePicker)) -> Self {
Self {
relative_path: item.relative_path(picker),
file_name: item.file_name(picker),
git_status: fff::git::format_git_status(item.git_status).to_string(),
size: item.size,
modified: item.modified,
access_frecency_score: item.access_frecency_score as i64,
modification_frecency_score: item.modification_frecency_score as i64,
total_frecency_score: item.total_frecency_score() as i64,
is_binary: item.is_binary(),
}
}
}
impl From<(&fff::DirItem, &FilePicker)> for DirItem {
fn from((item, picker): (&fff::DirItem, &FilePicker)) -> Self {
Self {
relative_path: item.relative_path(picker),
dir_name: item.dir_name(picker),
max_access_frecency: item.max_access_frecency(),
}
}
}
impl From<(&fff::FileItem, &FilePicker)> for MixedFileItem {
fn from((item, picker): (&fff::FileItem, &FilePicker)) -> Self {
Self {
relative_path: item.relative_path(picker),
file_name: item.file_name(picker),
git_status: fff::git::format_git_status(item.git_status).to_string(),
size: item.size,
modified: item.modified,
access_frecency_score: item.access_frecency_score as i64,
modification_frecency_score: item.modification_frecency_score as i64,
total_frecency_score: item.total_frecency_score() as i64,
is_binary: item.is_binary(),
}
}
}
impl From<(&fff::DirItem, &FilePicker)> for MixedDirItem {
fn from((item, picker): (&fff::DirItem, &FilePicker)) -> Self {
Self {
relative_path: item.relative_path(picker),
dir_name: item.dir_name(picker),
max_access_frecency: item.max_access_frecency(),
}
}
}
impl From<(&fff::GrepMatch, &fff::FileItem, &FilePicker)> for GrepMatch {
fn from((m, file, picker): (&fff::GrepMatch, &fff::FileItem, &FilePicker)) -> Self {
Self {
relative_path: file.relative_path(picker),
file_name: file.file_name(picker),
git_status: fff::git::format_git_status(file.git_status).to_string(),
line_content: m.line_content.clone(),
match_ranges: m
.match_byte_offsets
.iter()
.map(|&(s, e)| MatchRange { start: s, end: e })
.collect(),
context_before: m.context_before.clone(),
context_after: m.context_after.clone(),
size: file.size,
modified: file.modified,
total_frecency_score: file.total_frecency_score() as i64,
access_frecency_score: file.access_frecency_score as i64,
modification_frecency_score: file.modification_frecency_score as i64,
line_number: m.line_number,
byte_offset: m.byte_offset,
col: m.col as u32,
fuzzy_score: m.fuzzy_score,
is_definition: m.is_definition,
is_binary: file.is_binary(),
}
}
}
+982
View File
@@ -0,0 +1,982 @@
use std::path::{Path, PathBuf};
use std::time::Duration;
use fff::file_picker::FilePicker;
use fff::frecency::FrecencyTracker;
use fff::query_tracker::QueryTracker;
use fff::{
FFFMode, FilePickerOptions, FuzzySearchOptions, GrepSearchOptions, PaginationArgs, QueryParser,
SharedFilePicker, SharedFrecency, SharedQueryTracker,
};
use pyo3::prelude::*;
use pyo3::types::PyDict;
use crate::conversions::MixedItem;
use crate::types::{
DirItem, DirSearchResult, FileItem, GrepCursor, GrepMatch, GrepResult, MixedDirItem,
MixedFileItem, MixedSearchResult, ScanProgress, Score, SearchResult,
};
use crate::{parse_grep_mode, py_err};
const DEFAULT_SEARCH_PAGE_SIZE: usize = 100;
// Sentinel-to-default conversion for combo boosting, mirroring the C/Node
// bindings: `0` means "use the engine default", not "disable".
const DEFAULT_COMBO_BOOST_MULTIPLIER: i32 = 100;
const DEFAULT_MIN_COMBO_COUNT: u32 = 3;
fn defaulted_usize(value: u32, default: usize) -> usize {
if value == 0 { default } else { value as usize }
}
fn defaulted_u64(value: u64, default: u64) -> u64 {
if value == 0 { default } else { value }
}
fn defaulted_i32(value: i32, default: i32) -> i32 {
if value == 0 { default } else { value }
}
fn defaulted_u32(value: u32, default: u32) -> u32 {
if value == 0 { default } else { value }
}
fn create_parent_dir(path: &Path) -> PyResult<()> {
if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
std::fs::create_dir_all(parent).map_err(py_err)?;
}
Ok(())
}
fn pagination_args(page_index: u32, page_size: u32) -> PaginationArgs {
PaginationArgs {
offset: page_index as usize,
limit: defaulted_usize(page_size, DEFAULT_SEARCH_PAGE_SIZE),
}
}
fn fuzzy_options<'a>(
max_threads: u32,
current_file: Option<&'a str>,
project_path: &'a Path,
page_index: u32,
page_size: u32,
combo_boost_score_multiplier: i32,
min_combo_count: u32,
) -> FuzzySearchOptions<'a> {
FuzzySearchOptions {
max_threads: max_threads as usize,
current_file,
project_path: Some(project_path),
combo_boost_score_multiplier,
min_combo_count,
pagination: pagination_args(page_index, page_size),
}
}
#[allow(clippy::too_many_arguments)]
fn grep_options(
mode: fff::GrepMode,
cursor_offset: usize,
max_file_size: u64,
max_matches_per_file: u32,
smart_case: bool,
page_limit: u32,
time_budget_ms: u64,
before_context: u32,
after_context: u32,
classify_definitions: bool,
) -> GrepSearchOptions {
let defaults = GrepSearchOptions::default();
GrepSearchOptions {
max_file_size: defaulted_u64(max_file_size, defaults.max_file_size),
max_matches_per_file: max_matches_per_file as usize,
smart_case,
file_offset: cursor_offset,
page_limit: defaulted_usize(page_limit, defaults.page_limit),
mode,
time_budget_ms,
before_context: before_context as usize,
after_context: after_context as usize,
classify_definitions,
trim_whitespace: false,
abort_signal: None,
}
}
fn convert_scores(scores: &[fff::Score]) -> Vec<Score> {
scores.iter().map(Score::from).collect()
}
fn convert_grep_result(result: fff::grep::GrepResult<'_>, picker: &FilePicker) -> GrepResult {
let items = result
.matches
.iter()
.map(|m| GrepMatch::from((m, result.files[m.file_index], picker)))
.collect();
GrepResult {
items,
total_matched: result.matches.len() as u32,
total_files_searched: result.total_files_searched as u32,
total_files: result.total_files as u32,
filtered_file_count: result.filtered_file_count as u32,
next_file_offset: result.next_file_offset as u32,
regex_fallback_error: result.regex_fallback_error,
}
}
fn clear_shared_state(
picker: &SharedFilePicker,
frecency: &SharedFrecency,
query_tracker: &SharedQueryTracker,
) {
if let Ok(mut guard) = picker.write() {
guard.take();
}
if let Ok(mut guard) = frecency.write() {
*guard = None;
}
if let Ok(mut guard) = query_tracker.write() {
*guard = None;
}
}
#[pyclass(subclass)]
pub struct FileFinder {
picker: SharedFilePicker,
frecency: SharedFrecency,
query_tracker: SharedQueryTracker,
cache_budget_max_files: usize,
cache_budget_max_bytes: u64,
cache_budget_max_file_size: u64,
}
impl Drop for FileFinder {
fn drop(&mut self) {
clear_shared_state(&self.picker, &self.frecency, &self.query_tracker);
}
}
#[pymethods]
impl FileFinder {
#[new]
#[pyo3(signature = (
base_path,
*,
frecency_db_path=None,
history_db_path=None,
enable_mmap_cache=true,
enable_content_indexing=true,
watch=true,
ai_mode=false,
log_file_path=None,
log_level=None,
cache_budget_max_files=0,
cache_budget_max_bytes=0,
cache_budget_max_file_size=0,
enable_fs_root_scanning=false,
enable_home_dir_scanning=false,
follow_symlinks=false,
))]
#[allow(clippy::too_many_arguments)]
fn new(
py: Python<'_>,
base_path: PathBuf,
frecency_db_path: Option<PathBuf>,
history_db_path: Option<PathBuf>,
enable_mmap_cache: bool,
enable_content_indexing: bool,
watch: bool,
ai_mode: bool,
log_file_path: Option<PathBuf>,
log_level: Option<String>,
cache_budget_max_files: u64,
cache_budget_max_bytes: u64,
cache_budget_max_file_size: u64,
enable_fs_root_scanning: bool,
enable_home_dir_scanning: bool,
follow_symlinks: bool,
) -> PyResult<Self> {
let shared_picker = SharedFilePicker::default();
let shared_frecency = SharedFrecency::default();
let query_tracker = SharedQueryTracker::default();
let cache_budget_max_files = cache_budget_max_files as usize;
let init_picker = shared_picker.clone();
let init_frecency = shared_frecency.clone();
let init_query_tracker = query_tracker.clone();
py.allow_threads(move || -> PyResult<()> {
if let Some(path) = frecency_db_path {
create_parent_dir(&path)?;
let tracker = FrecencyTracker::open(&path).map_err(py_err)?;
init_frecency.init(tracker).map_err(py_err)?;
}
if let Some(path) = history_db_path {
create_parent_dir(&path)?;
let tracker = QueryTracker::open(&path).map_err(py_err)?;
init_query_tracker.init(tracker).map_err(py_err)?;
}
if let Some(path) = log_file_path {
create_parent_dir(&path)?;
let path = path.to_string_lossy();
fff::log::init_tracing(&path, log_level.as_deref(), None).map_err(py_err)?;
}
let mode = if ai_mode {
FFFMode::Ai
} else {
FFFMode::Neovim
};
FilePicker::new_with_shared_state(
init_picker,
init_frecency,
FilePickerOptions {
base_path: base_path.to_string_lossy().to_string(),
enable_mmap_cache,
enable_content_indexing,
watch,
mode,
cache_budget: fff::ContentCacheBudget::from_overrides(
cache_budget_max_files,
cache_budget_max_bytes,
cache_budget_max_file_size,
),
follow_symlinks,
enable_fs_root_scanning,
enable_home_dir_scanning,
},
)
.map_err(py_err)
})?;
Ok(Self {
picker: shared_picker,
frecency: shared_frecency,
query_tracker,
cache_budget_max_files,
cache_budget_max_bytes,
cache_budget_max_file_size,
})
}
fn __enter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
slf
}
fn __exit__(&mut self, _exc_type: PyObject, _exc_value: PyObject, _traceback: PyObject) {
let _ = self.close();
}
fn close(&mut self) -> PyResult<()> {
clear_shared_state(&self.picker, &self.frecency, &self.query_tracker);
Ok(())
}
#[getter]
fn closed(&self) -> PyResult<bool> {
Ok(self.picker.read().map_err(py_err)?.is_none())
}
#[getter]
fn base_path(&self) -> PyResult<Option<String>> {
let guard = self.picker.read().map_err(py_err)?;
Ok(guard
.as_ref()
.map(|p| p.base_path().to_string_lossy().to_string()))
}
#[getter]
fn scan_progress(&self) -> PyResult<ScanProgress> {
let guard = self.picker.read().map_err(py_err)?;
let picker = guard
.as_ref()
.ok_or_else(|| py_err("File picker not initialized"))?;
let p = picker.get_scan_progress();
Ok(ScanProgress {
scanned_files_count: p.scanned_files_count as u64,
is_scanning: p.is_scanning,
is_watcher_ready: p.is_watcher_ready,
is_warmup_complete: p.is_warmup_complete,
})
}
fn __repr__(&self) -> PyResult<String> {
let guard = self.picker.read().map_err(py_err)?;
if let Some(ref picker) = *guard {
Ok(format!(
"FileFinder(base_path={:?}, closed=False)",
picker.base_path().to_string_lossy()
))
} else {
Ok("FileFinder(closed=True)".to_string())
}
}
#[allow(clippy::too_many_arguments)]
#[pyo3(signature = (
query,
*,
current_file=None,
max_threads=0,
page_index=0,
page_size=0,
combo_boost_score_multiplier=0,
min_combo_count=0,
))]
fn search(
&self,
py: Python<'_>,
query: &str,
current_file: Option<String>,
max_threads: u32,
page_index: u32,
page_size: u32,
combo_boost_score_multiplier: i32,
min_combo_count: u32,
) -> PyResult<SearchResult> {
let picker = self.picker.clone();
let query_tracker = self.query_tracker.clone();
let query = query.to_string();
py.allow_threads(move || -> PyResult<_> {
let picker_guard = picker.read().map_err(py_err)?;
let picker = picker_guard
.as_ref()
.ok_or_else(|| py_err("File picker not initialized"))?;
let qt_guard = query_tracker.read().map_err(py_err)?;
let parsed = QueryParser::default().parse(&query);
let result = picker.fuzzy_search(
&parsed,
qt_guard.as_ref(),
fuzzy_options(
max_threads,
current_file.as_deref(),
picker.base_path(),
page_index,
page_size,
defaulted_i32(combo_boost_score_multiplier, DEFAULT_COMBO_BOOST_MULTIPLIER),
defaulted_u32(min_combo_count, DEFAULT_MIN_COMBO_COUNT),
),
);
Ok(SearchResult {
items: result
.items
.iter()
.map(|i| FileItem::from((*i, picker)))
.collect(),
scores: convert_scores(&result.scores),
total_matched: result.total_matched as u32,
total_files: result.total_files as u32,
})
})
}
#[pyo3(signature = (
pattern,
*,
current_file=None,
max_threads=0,
page_index=0,
page_size=0,
))]
fn glob(
&self,
py: Python<'_>,
pattern: &str,
current_file: Option<String>,
max_threads: u32,
page_index: u32,
page_size: u32,
) -> PyResult<SearchResult> {
let picker = self.picker.clone();
let pattern = pattern.to_string();
py.allow_threads(move || -> PyResult<_> {
let picker_guard = picker.read().map_err(py_err)?;
let picker = picker_guard
.as_ref()
.ok_or_else(|| py_err("File picker not initialized"))?;
let result = picker.glob(
&pattern,
fuzzy_options(
max_threads,
current_file.as_deref(),
picker.base_path(),
page_index,
page_size,
0,
0,
),
);
Ok(SearchResult {
items: result
.items
.iter()
.map(|i| FileItem::from((*i, picker)))
.collect(),
scores: convert_scores(&result.scores),
total_matched: result.total_matched as u32,
total_files: result.total_files as u32,
})
})
}
#[pyo3(signature = (
query,
*,
current_file=None,
max_threads=0,
page_index=0,
page_size=0,
))]
fn directory_search(
&self,
py: Python<'_>,
query: &str,
current_file: Option<String>,
max_threads: u32,
page_index: u32,
page_size: u32,
) -> PyResult<DirSearchResult> {
let picker = self.picker.clone();
let query = query.to_string();
py.allow_threads(move || -> PyResult<_> {
let picker_guard = picker.read().map_err(py_err)?;
let picker = picker_guard
.as_ref()
.ok_or_else(|| py_err("File picker not initialized"))?;
let parsed = QueryParser::new(fff_query_parser::DirSearchConfig).parse(&query);
let result = picker.fuzzy_search_directories(
&parsed,
fuzzy_options(
max_threads,
current_file.as_deref(),
picker.base_path(),
page_index,
page_size,
0,
0,
),
);
Ok(DirSearchResult {
items: result
.items
.iter()
.map(|i| DirItem::from((*i, picker)))
.collect(),
scores: convert_scores(&result.scores),
total_matched: result.total_matched as u32,
total_dirs: result.total_dirs as u32,
})
})
}
#[allow(clippy::too_many_arguments)]
#[pyo3(signature = (
query,
*,
current_file=None,
max_threads=0,
page_index=0,
page_size=0,
combo_boost_score_multiplier=0,
min_combo_count=0,
))]
fn mixed_search(
&self,
py: Python<'_>,
query: &str,
current_file: Option<String>,
max_threads: u32,
page_index: u32,
page_size: u32,
combo_boost_score_multiplier: i32,
min_combo_count: u32,
) -> PyResult<MixedSearchResult> {
let picker = self.picker.clone();
let query_tracker = self.query_tracker.clone();
let query = query.to_string();
let (items, scores, total_matched, total_files, total_dirs) =
py.allow_threads(move || -> PyResult<_> {
let picker_guard = picker.read().map_err(py_err)?;
let picker = picker_guard
.as_ref()
.ok_or_else(|| py_err("File picker not initialized"))?;
let qt_guard = query_tracker.read().map_err(py_err)?;
let parsed = QueryParser::new(fff_query_parser::MixedSearchConfig).parse(&query);
let result = picker.fuzzy_search_mixed(
&parsed,
qt_guard.as_ref(),
fuzzy_options(
max_threads,
current_file.as_deref(),
picker.base_path(),
page_index,
page_size,
defaulted_i32(combo_boost_score_multiplier, DEFAULT_COMBO_BOOST_MULTIPLIER),
defaulted_u32(min_combo_count, DEFAULT_MIN_COMBO_COUNT),
),
);
let items: Vec<MixedItem> = result
.items
.iter()
.map(|item| match item {
fff::MixedItemRef::File(file) => {
MixedItem::File(MixedFileItem::from((*file, picker)))
}
fff::MixedItemRef::Dir(dir) => {
MixedItem::Dir(MixedDirItem::from((*dir, picker)))
}
})
.collect();
Ok((
items,
convert_scores(&result.scores),
result.total_matched as u32,
result.total_files as u32,
result.total_dirs as u32,
))
})?;
let items: PyResult<Vec<PyObject>> = items
.into_iter()
.map(|item| match item {
MixedItem::File(file) => Ok(Py::new(py, file)?.into_any()),
MixedItem::Dir(dir) => Ok(Py::new(py, dir)?.into_any()),
})
.collect();
Ok(MixedSearchResult {
items: items?,
scores,
total_matched,
total_files,
total_dirs,
})
}
#[allow(clippy::too_many_arguments)]
#[pyo3(signature = (
query,
*,
mode="plain",
max_file_size=0,
max_matches_per_file=0,
smart_case=true,
cursor=None,
page_limit=0,
time_budget_ms=0,
before_context=0,
after_context=0,
classify_definitions=false,
))]
fn grep(
&self,
py: Python<'_>,
query: &str,
mode: &str,
max_file_size: u64,
max_matches_per_file: u32,
smart_case: bool,
cursor: Option<&GrepCursor>,
page_limit: u32,
time_budget_ms: u64,
before_context: u32,
after_context: u32,
classify_definitions: bool,
) -> PyResult<GrepResult> {
let picker = self.picker.clone();
let query = query.to_string();
let mode = parse_grep_mode(mode)?;
let cursor_offset = cursor.map(|c| c.offset as usize).unwrap_or(0);
py.allow_threads(move || -> PyResult<_> {
let picker_guard = picker.read().map_err(py_err)?;
let picker = picker_guard
.as_ref()
.ok_or_else(|| py_err("File picker not initialized"))?;
let parsed = if picker.mode().is_ai() {
QueryParser::new(fff_query_parser::AiGrepConfig).parse(&query)
} else {
fff::grep::parse_grep_query(&query)
};
let options = grep_options(
mode,
cursor_offset,
max_file_size,
max_matches_per_file,
smart_case,
page_limit,
time_budget_ms,
before_context,
after_context,
classify_definitions,
);
Ok(convert_grep_result(picker.grep(&parsed, &options), picker))
})
}
#[allow(clippy::too_many_arguments)]
#[pyo3(signature = (
patterns,
*,
constraints=None,
mode="plain",
max_file_size=0,
max_matches_per_file=0,
smart_case=true,
cursor=None,
page_limit=0,
time_budget_ms=0,
before_context=0,
after_context=0,
classify_definitions=false,
))]
fn multi_grep(
&self,
py: Python<'_>,
patterns: Vec<String>,
constraints: Option<String>,
mode: &str,
max_file_size: u64,
max_matches_per_file: u32,
smart_case: bool,
cursor: Option<&GrepCursor>,
page_limit: u32,
time_budget_ms: u64,
before_context: u32,
after_context: u32,
classify_definitions: bool,
) -> PyResult<GrepResult> {
let picker = self.picker.clone();
let mode = parse_grep_mode(mode)?;
let cursor_offset = cursor.map(|c| c.offset as usize).unwrap_or(0);
py.allow_threads(move || -> PyResult<_> {
let picker_guard = picker.read().map_err(py_err)?;
let picker = picker_guard
.as_ref()
.ok_or_else(|| py_err("File picker not initialized"))?;
if patterns.is_empty() || patterns.iter().all(|p| p.is_empty()) {
return Err(py_err("patterns must not be empty"));
}
let pattern_refs: Vec<&str> = patterns.iter().map(|s| s.as_str()).collect();
let parsed_constraints = constraints.as_ref().map(|c| {
if picker.mode().is_ai() {
QueryParser::new(fff_query_parser::AiGrepConfig).parse(c)
} else {
fff::grep::parse_grep_query(c)
}
});
let constraint_refs: &[fff::Constraint<'_>] = match &parsed_constraints {
Some(q) => &q.constraints,
None => &[],
};
let options = grep_options(
mode,
cursor_offset,
max_file_size,
max_matches_per_file,
smart_case,
page_limit,
time_budget_ms,
before_context,
after_context,
classify_definitions,
);
Ok(convert_grep_result(
picker.multi_grep(&pattern_refs, constraint_refs, &options),
picker,
))
})
}
fn scan_files(&self) -> PyResult<()> {
self.picker
.trigger_full_rescan_async(&self.frecency)
.map_err(py_err)
}
fn is_scanning(&self) -> PyResult<bool> {
let guard = self.picker.read().map_err(py_err)?;
Ok(guard.as_ref().map(|p| p.is_scan_active()).unwrap_or(false))
}
#[pyo3(signature = (timeout_ms=5000))]
fn wait_for_scan_blocking(&self, py: Python<'_>, timeout_ms: u64) -> PyResult<bool> {
let picker = self.picker.clone();
py.allow_threads(move || Ok(picker.wait_for_scan(Duration::from_millis(timeout_ms))))
}
fn reindex(&self, py: Python<'_>, new_path: PathBuf) -> PyResult<()> {
let picker = self.picker.clone();
let frecency = self.frecency.clone();
let cache_budget_max_files = self.cache_budget_max_files;
let cache_budget_max_bytes = self.cache_budget_max_bytes;
let cache_budget_max_file_size = self.cache_budget_max_file_size;
py.allow_threads(move || -> PyResult<()> {
if !new_path.exists() {
return Err(py_err(format!(
"Path does not exist: {}",
new_path.display()
)));
}
let canonical = fff::path_utils::canonicalize(&new_path).map_err(py_err)?;
let (warmup_caches, content_indexing, watch, mode, fs_root, home_dir, follow_symlinks) = {
let guard = picker.read().map_err(py_err)?;
if let Some(ref picker) = *guard {
(
picker.has_mmap_cache(),
picker.has_content_indexing(),
picker.has_watcher(),
picker.mode(),
picker.fs_root_scanning_enabled(),
picker.home_dir_scanning_enabled(),
picker.follows_symlinks(),
)
} else {
(false, true, true, FFFMode::default(), false, false, false)
}
};
FilePicker::new_with_shared_state(
picker.clone(),
frecency,
FilePickerOptions {
base_path: canonical.to_string_lossy().to_string(),
enable_mmap_cache: warmup_caches,
enable_content_indexing: content_indexing,
watch,
mode,
cache_budget: fff::ContentCacheBudget::from_overrides(
cache_budget_max_files,
cache_budget_max_bytes,
cache_budget_max_file_size,
),
follow_symlinks,
enable_fs_root_scanning: fs_root,
enable_home_dir_scanning: home_dir,
},
)
.map_err(py_err)
})
}
fn refresh_git_status(&self, py: Python<'_>) -> PyResult<i64> {
let picker = self.picker.clone();
let frecency = self.frecency.clone();
py.allow_threads(move || {
picker
.refresh_git_status(&frecency)
.map_err(py_err)
.map(|c| c as i64)
})
}
#[pyo3(signature = (query, selected_file_path))]
fn track_query(
&self,
py: Python<'_>,
query: &str,
selected_file_path: PathBuf,
) -> PyResult<bool> {
let picker = self.picker.clone();
let query_tracker = self.query_tracker.clone();
let query = query.to_string();
py.allow_threads(move || -> PyResult<bool> {
let file_path = fff::path_utils::canonicalize(&selected_file_path).map_err(py_err)?;
let project_path = {
let guard = picker.read().map_err(py_err)?;
guard.as_ref().map(|p| p.base_path().to_path_buf())
};
let project_path = match project_path {
Some(p) => p,
None => return Ok(false),
};
let mut qt_guard = query_tracker.write().map_err(py_err)?;
if let Some(ref mut tracker) = *qt_guard {
tracker
.track_query_completion(&query, &project_path, &file_path)
.map_err(py_err)?;
Ok(true)
} else {
Ok(false)
}
})
}
fn get_historical_query(&self, py: Python<'_>, offset: u64) -> PyResult<Option<String>> {
let picker = self.picker.clone();
let query_tracker = self.query_tracker.clone();
py.allow_threads(move || -> PyResult<Option<String>> {
let project_path = {
let guard = picker.read().map_err(py_err)?;
guard.as_ref().map(|p| p.base_path().to_path_buf())
};
let project_path = match project_path {
Some(p) => p,
None => return Ok(None),
};
let qt_guard = query_tracker.read().map_err(py_err)?;
if let Some(ref tracker) = *qt_guard {
tracker
.get_historical_query(&project_path, offset as usize)
.map_err(py_err)
} else {
Ok(None)
}
})
}
#[pyo3(signature = (test_path=None))]
fn health_check(&self, py: Python<'_>, test_path: Option<PathBuf>) -> PyResult<Py<PyDict>> {
let picker = self.picker.clone();
let frecency = self.frecency.clone();
let query_tracker = self.query_tracker.clone();
let (
git_version,
repository_found,
workdir,
git_error,
picker_initialized,
picker_base_path,
picker_is_scanning,
picker_indexed_files,
frecency_initialized,
query_tracker_initialized,
) = py.allow_threads(move || -> PyResult<_> {
// Resolve the path to inspect: explicit arg → indexed base path →
// process cwd. Report a cwd-resolution failure instead of silently
// discovering from an empty path.
let (test_path, cwd_error) = match test_path {
Some(p) => (Some(p), None),
None => {
let base = picker
.read()
.ok()
.and_then(|g| g.as_ref().map(|p| p.base_path().to_path_buf()));
match base {
Some(p) => (Some(p), None),
None => match std::env::current_dir() {
Ok(p) => (Some(p), None),
Err(e) => (
None,
Some(format!("could not determine current directory: {}", e)),
),
},
}
}
};
let git_version = git2::Version::get();
let (major, minor, rev) = git_version.libgit2_version();
let git_version = format!("{}.{}.{}", major, minor, rev);
let (repository_found, workdir, git_error) = match test_path {
None => (false, None, cwd_error),
Some(test_path) => match git2::Repository::discover(&test_path) {
Ok(repo) => (
true,
repo.workdir().map(|p| p.to_string_lossy().to_string()),
None,
),
Err(e) => (false, None, Some(e.message().to_string())),
},
};
let (picker_initialized, picker_base_path, picker_is_scanning, picker_indexed_files) = {
let guard = picker.read().map_err(py_err)?;
if let Some(ref picker) = *guard {
let progress = picker.get_scan_progress();
(
true,
Some(picker.base_path().to_string_lossy().to_string()),
Some(picker.is_scan_active()),
Some(progress.scanned_files_count),
)
} else {
(false, None, None, None)
}
};
let frecency_initialized = frecency.read().map_err(py_err)?.is_some();
let query_tracker_initialized = query_tracker.read().map_err(py_err)?.is_some();
Ok((
git_version,
repository_found,
workdir,
git_error,
picker_initialized,
picker_base_path,
picker_is_scanning,
picker_indexed_files,
frecency_initialized,
query_tracker_initialized,
))
})?;
let dict = PyDict::new(py);
dict.set_item("version", env!("CARGO_PKG_VERSION"))?;
let git_info = PyDict::new(py);
git_info.set_item("available", true)?;
git_info.set_item("libgit2_version", git_version)?;
git_info.set_item("repository_found", repository_found)?;
if let Some(workdir) = workdir {
git_info.set_item("workdir", workdir)?;
}
if let Some(error) = git_error {
git_info.set_item("error", error)?;
}
dict.set_item("git", git_info)?;
let picker_info = PyDict::new(py);
picker_info.set_item("initialized", picker_initialized)?;
if let Some(base_path) = picker_base_path {
picker_info.set_item("base_path", base_path)?;
}
if let Some(is_scanning) = picker_is_scanning {
picker_info.set_item("is_scanning", is_scanning)?;
}
if let Some(indexed_files) = picker_indexed_files {
picker_info.set_item("indexed_files", indexed_files)?;
}
dict.set_item("file_picker", picker_info)?;
let frecency_info = PyDict::new(py);
frecency_info.set_item("initialized", frecency_initialized)?;
dict.set_item("frecency", frecency_info)?;
let query_info = PyDict::new(py);
query_info.set_item("initialized", query_tracker_initialized)?;
dict.set_item("query_tracker", query_info)?;
Ok(dict.unbind())
}
}
+44
View File
@@ -0,0 +1,44 @@
use pyo3::create_exception;
use pyo3::prelude::*;
mod conversions;
mod finder;
mod types;
create_exception!(fff_python, FFFException, pyo3::exceptions::PyException);
fn py_err<E: std::fmt::Display>(e: E) -> PyErr {
PyErr::new::<FFFException, _>(format!("{}", e))
}
pub fn parse_grep_mode(mode: &str) -> PyResult<fff::GrepMode> {
match mode {
"plain" => Ok(fff::GrepMode::PlainText),
"regex" => Ok(fff::GrepMode::Regex),
"fuzzy" => Ok(fff::GrepMode::Fuzzy),
_ => Err(py_err(format!(
"invalid grep mode: {:?}. Must be one of: plain, regex, fuzzy",
mode
))),
}
}
#[pymodule]
fn _fff_python(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<finder::FileFinder>()?;
m.add_class::<types::Score>()?;
m.add_class::<types::FileItem>()?;
m.add_class::<types::DirItem>()?;
m.add_class::<types::MixedFileItem>()?;
m.add_class::<types::MixedDirItem>()?;
m.add_class::<types::SearchResult>()?;
m.add_class::<types::DirSearchResult>()?;
m.add_class::<types::MixedSearchResult>()?;
m.add_class::<types::MatchRange>()?;
m.add_class::<types::GrepMatch>()?;
m.add_class::<types::GrepResult>()?;
m.add_class::<types::ScanProgress>()?;
m.add_class::<types::GrepCursor>()?;
m.add("FFFException", m.py().get_type::<FFFException>())?;
Ok(())
}
+408
View File
@@ -0,0 +1,408 @@
use pyo3::prelude::*;
#[pyclass]
#[derive(Clone)]
pub struct Score {
#[pyo3(get)]
pub total: i32,
#[pyo3(get)]
pub base_score: i32,
#[pyo3(get)]
pub filename_bonus: i32,
#[pyo3(get)]
pub special_filename_bonus: i32,
#[pyo3(get)]
pub frecency_boost: i32,
#[pyo3(get)]
pub distance_penalty: i32,
#[pyo3(get)]
pub current_file_penalty: i32,
#[pyo3(get)]
pub combo_match_boost: i32,
#[pyo3(get)]
pub path_alignment_bonus: i32,
#[pyo3(get)]
pub exact_match: bool,
#[pyo3(get)]
pub match_type: String,
}
#[pymethods]
impl Score {
fn __repr__(&self) -> String {
format!(
"Score(total={}, base_score={}, filename_bonus={}, match_type={:?})",
self.total, self.base_score, self.filename_bonus, self.match_type
)
}
}
#[pyclass]
#[derive(Clone)]
pub struct FileItem {
#[pyo3(get)]
pub relative_path: String,
#[pyo3(get)]
pub file_name: String,
#[pyo3(get)]
pub git_status: String,
#[pyo3(get)]
pub size: u64,
#[pyo3(get)]
pub modified: u64,
#[pyo3(get)]
pub access_frecency_score: i64,
#[pyo3(get)]
pub modification_frecency_score: i64,
#[pyo3(get)]
pub total_frecency_score: i64,
#[pyo3(get)]
pub is_binary: bool,
}
#[pymethods]
impl FileItem {
fn __repr__(&self) -> String {
format!(
"FileItem(relative_path={:?}, file_name={:?}, size={})",
self.relative_path, self.file_name, self.size
)
}
}
#[pyclass]
#[derive(Clone)]
pub struct DirItem {
#[pyo3(get)]
pub relative_path: String,
#[pyo3(get)]
pub dir_name: String,
#[pyo3(get)]
pub max_access_frecency: i32,
}
#[pymethods]
impl DirItem {
fn __repr__(&self) -> String {
format!(
"DirItem(relative_path={:?}, dir_name={:?})",
self.relative_path, self.dir_name
)
}
}
#[pyclass]
#[derive(Clone)]
pub struct MixedFileItem {
#[pyo3(get)]
pub relative_path: String,
#[pyo3(get)]
pub file_name: String,
#[pyo3(get)]
pub git_status: String,
#[pyo3(get)]
pub size: u64,
#[pyo3(get)]
pub modified: u64,
#[pyo3(get)]
pub access_frecency_score: i64,
#[pyo3(get)]
pub modification_frecency_score: i64,
#[pyo3(get)]
pub total_frecency_score: i64,
#[pyo3(get)]
pub is_binary: bool,
}
#[pymethods]
impl MixedFileItem {
fn __repr__(&self) -> String {
format!(
"MixedFileItem(relative_path={:?}, file_name={:?}, size={})",
self.relative_path, self.file_name, self.size
)
}
}
#[pyclass]
#[derive(Clone)]
pub struct MixedDirItem {
#[pyo3(get)]
pub relative_path: String,
#[pyo3(get)]
pub dir_name: String,
#[pyo3(get)]
pub max_access_frecency: i32,
}
#[pymethods]
impl MixedDirItem {
fn __repr__(&self) -> String {
format!(
"MixedDirItem(relative_path={:?}, dir_name={:?})",
self.relative_path, self.dir_name
)
}
}
#[pyclass]
#[derive(Clone)]
pub struct MatchRange {
#[pyo3(get)]
pub start: u32,
#[pyo3(get)]
pub end: u32,
}
#[pymethods]
impl MatchRange {
fn __repr__(&self) -> String {
format!("MatchRange(start={}, end={})", self.start, self.end)
}
}
#[pyclass]
#[derive(Clone)]
pub struct GrepMatch {
#[pyo3(get)]
pub relative_path: String,
#[pyo3(get)]
pub file_name: String,
#[pyo3(get)]
pub git_status: String,
#[pyo3(get)]
pub line_content: String,
#[pyo3(get)]
pub match_ranges: Vec<MatchRange>,
#[pyo3(get)]
pub context_before: Vec<String>,
#[pyo3(get)]
pub context_after: Vec<String>,
#[pyo3(get)]
pub size: u64,
#[pyo3(get)]
pub modified: u64,
#[pyo3(get)]
pub total_frecency_score: i64,
#[pyo3(get)]
pub access_frecency_score: i64,
#[pyo3(get)]
pub modification_frecency_score: i64,
#[pyo3(get)]
pub line_number: u64,
#[pyo3(get)]
pub byte_offset: u64,
#[pyo3(get)]
pub col: u32,
#[pyo3(get)]
pub fuzzy_score: Option<u16>,
#[pyo3(get)]
pub is_definition: bool,
#[pyo3(get)]
pub is_binary: bool,
}
#[pymethods]
impl GrepMatch {
fn __repr__(&self) -> String {
format!(
"GrepMatch(relative_path={:?}, line_number={}, line_content={:?})",
self.relative_path, self.line_number, self.line_content
)
}
}
#[pyclass]
#[derive(Clone)]
pub struct SearchResult {
#[pyo3(get)]
pub items: Vec<FileItem>,
#[pyo3(get)]
pub scores: Vec<Score>,
#[pyo3(get)]
pub total_matched: u32,
#[pyo3(get)]
pub total_files: u32,
}
#[pymethods]
impl SearchResult {
fn __repr__(&self) -> String {
format!(
"SearchResult(items={}, total_matched={}, total_files={})",
self.items.len(),
self.total_matched,
self.total_files
)
}
fn __len__(&self) -> usize {
self.items.len()
}
fn __bool__(&self) -> bool {
!self.items.is_empty()
}
}
#[pyclass]
#[derive(Clone)]
pub struct DirSearchResult {
#[pyo3(get)]
pub items: Vec<DirItem>,
#[pyo3(get)]
pub scores: Vec<Score>,
#[pyo3(get)]
pub total_matched: u32,
#[pyo3(get)]
pub total_dirs: u32,
}
#[pymethods]
impl DirSearchResult {
fn __repr__(&self) -> String {
format!(
"DirSearchResult(items={}, total_matched={}, total_dirs={})",
self.items.len(),
self.total_matched,
self.total_dirs
)
}
fn __len__(&self) -> usize {
self.items.len()
}
fn __bool__(&self) -> bool {
!self.items.is_empty()
}
}
#[pyclass]
pub struct MixedSearchResult {
#[pyo3(get)]
pub items: Vec<PyObject>,
#[pyo3(get)]
pub scores: Vec<Score>,
#[pyo3(get)]
pub total_matched: u32,
#[pyo3(get)]
pub total_files: u32,
#[pyo3(get)]
pub total_dirs: u32,
}
#[pymethods]
impl MixedSearchResult {
fn __repr__(&self) -> String {
format!(
"MixedSearchResult(items={}, total_matched={}, total_files={}, total_dirs={})",
self.items.len(),
self.total_matched,
self.total_files,
self.total_dirs
)
}
fn __len__(&self) -> usize {
self.items.len()
}
fn __bool__(&self) -> bool {
!self.items.is_empty()
}
}
#[pyclass]
#[derive(Clone)]
pub struct GrepResult {
#[pyo3(get)]
pub items: Vec<GrepMatch>,
#[pyo3(get)]
pub total_matched: u32,
#[pyo3(get)]
pub total_files_searched: u32,
#[pyo3(get)]
pub total_files: u32,
#[pyo3(get)]
pub filtered_file_count: u32,
#[pyo3(get)]
pub next_file_offset: u32,
#[pyo3(get)]
pub regex_fallback_error: Option<String>,
}
#[pymethods]
impl GrepResult {
fn __repr__(&self) -> String {
format!(
"GrepResult(items={}, total_matched={}, next_file_offset={})",
self.items.len(),
self.total_matched,
self.next_file_offset
)
}
fn __len__(&self) -> usize {
self.items.len()
}
fn __bool__(&self) -> bool {
!self.items.is_empty()
}
#[getter]
fn has_more(&self) -> bool {
self.next_file_offset > 0
}
fn next_cursor(&self, py: Python<'_>) -> PyResult<Option<Py<GrepCursor>>> {
if self.next_file_offset > 0 {
Ok(Some(Py::new(py, GrepCursor::new(self.next_file_offset))?))
} else {
Ok(None)
}
}
}
#[pyclass]
#[derive(Clone)]
pub struct ScanProgress {
#[pyo3(get)]
pub scanned_files_count: u64,
#[pyo3(get)]
pub is_scanning: bool,
#[pyo3(get)]
pub is_watcher_ready: bool,
#[pyo3(get)]
pub is_warmup_complete: bool,
}
#[pymethods]
impl ScanProgress {
fn __repr__(&self) -> String {
format!(
"ScanProgress(scanned_files_count={}, is_scanning={})",
self.scanned_files_count, self.is_scanning
)
}
}
#[pyclass]
#[derive(Clone)]
pub struct GrepCursor {
#[pyo3(get)]
pub offset: u32,
}
#[pymethods]
impl GrepCursor {
#[new]
fn new(offset: u32) -> Self {
Self { offset }
}
fn __repr__(&self) -> String {
format!("GrepCursor(offset={})", self.offset)
}
}