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
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:
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user