Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7074def415 | |||
| fc95af5861 | |||
| ac93e1a323 | |||
| 4d1bd5c29d | |||
| 392e9455cd | |||
| 458fb2bd2a | |||
| 0de3af8eea |
@@ -0,0 +1,59 @@
|
||||
name: Rust CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
steps:
|
||||
- name: Install Lua
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: brew install lua
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
cache: true
|
||||
cache-on-failure: true
|
||||
cache-key: "v1-rust"
|
||||
components: rustfmt, clippy
|
||||
target: wasm32-unknown-unknown
|
||||
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
||||
|
||||
fmt:
|
||||
name: cargo fmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
components: rustfmt
|
||||
- name: Check formatting
|
||||
run: cargo fmt -- --check
|
||||
|
||||
clippy:
|
||||
name: cargo clippy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
components: clippy
|
||||
- name: Run clippy
|
||||
run: cargo clippy -- -D warnings
|
||||
Generated
-92
@@ -2,21 +2,6 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.24.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
@@ -59,21 +44,6 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.75"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"miniz_oxide",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
@@ -271,7 +241,6 @@ dependencies = [
|
||||
"pathdiff",
|
||||
"rayon",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
@@ -316,12 +285,6 @@ dependencies = [
|
||||
"wasi 0.14.2+wasi-0.2.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.31.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||
|
||||
[[package]]
|
||||
name = "git2"
|
||||
version = "0.20.2"
|
||||
@@ -555,17 +518,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-uring"
|
||||
version = "0.7.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
@@ -712,15 +664,6 @@ version = "2.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.0.4"
|
||||
@@ -866,15 +809,6 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.36.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
@@ -1131,12 +1065,6 @@ version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.1"
|
||||
@@ -1223,12 +1151,6 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
@@ -1368,20 +1290,6 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.47.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43864ed400b6043a4757a25c7a64a8efde741aed79a056a2fb348a406701bb35"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"io-uring",
|
||||
"libc",
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.41"
|
||||
|
||||
+9
-9
@@ -11,20 +11,20 @@ crate-type = ["cdylib", "rlib"]
|
||||
name = "test_watcher"
|
||||
path = "src/bin/test_watcher.rs"
|
||||
|
||||
|
||||
[dependencies]
|
||||
neo_frizbee = { version = "0.6.0" }
|
||||
heed = "0.22.0"
|
||||
rayon = "1.8.0"
|
||||
mlua = { version = "0.11.1", features = ["module", "luajit"] }
|
||||
thiserror = "2.0.10"
|
||||
blake3 = "1.8.2"
|
||||
ignore = "0.4.22"
|
||||
pathdiff = "0.2.1"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
git2 = "0.20.2"
|
||||
heed = "0.22.0"
|
||||
ignore = "0.4.22"
|
||||
mlua = { version = "0.11.1", features = ["module", "luajit"] }
|
||||
neo_frizbee = { version = "0.6.0" }
|
||||
notify = "8.1.0"
|
||||
notify-debouncer-full = "0.5"
|
||||
tokio = { version = "1.0", features = ["time", "sync"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
pathdiff = "0.2.1"
|
||||
rayon = "1.8.0"
|
||||
thiserror = "2.0.10"
|
||||
tracing = "0.1"
|
||||
tracing-appender = "0.2"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
+86
-87
@@ -121,8 +121,8 @@ impl FileItem {
|
||||
.modified()
|
||||
.ok()
|
||||
.and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok())
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0);
|
||||
.map_or(0, |d| d.as_secs());
|
||||
|
||||
(size, modified)
|
||||
}
|
||||
Err(_) => (0, 0),
|
||||
@@ -180,7 +180,7 @@ impl std::fmt::Debug for FilePicker {
|
||||
f.debug_struct("FilePicker")
|
||||
.field("base_path", &self.base_path)
|
||||
.field("git_workdir", &self.git_workdir)
|
||||
.finish()
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ impl FilePicker {
|
||||
|
||||
let git_workdir = Repository::discover(&path)
|
||||
.ok()
|
||||
.and_then(|repo| repo.workdir().map(|p| p.to_path_buf()));
|
||||
.and_then(|repo| repo.workdir().map(Path::to_path_buf));
|
||||
|
||||
if let Some(ref git_dir) = git_workdir {
|
||||
debug!("Git repository found at: {}", git_dir.display());
|
||||
@@ -230,8 +230,8 @@ impl FilePicker {
|
||||
query: &str,
|
||||
max_results: usize,
|
||||
max_threads: usize,
|
||||
current_file: Option<String>,
|
||||
) -> Result<SearchResult, Error> {
|
||||
current_file: Option<&String>,
|
||||
) -> SearchResult {
|
||||
let max_threads = max_threads.max(1); // Ensure at least 1 to avoid neo_frizbee division by zero
|
||||
|
||||
debug!(
|
||||
@@ -249,7 +249,7 @@ impl FilePicker {
|
||||
query,
|
||||
max_typos,
|
||||
max_threads,
|
||||
current_file: current_file.as_deref(),
|
||||
current_file,
|
||||
};
|
||||
|
||||
let scored_indices = match_and_score_files(&sync_data.files, &context);
|
||||
@@ -280,12 +280,12 @@ impl FilePicker {
|
||||
);
|
||||
|
||||
debug!("Total search time: {:?}", time.elapsed());
|
||||
Ok(SearchResult {
|
||||
SearchResult {
|
||||
items,
|
||||
scores,
|
||||
total_matched,
|
||||
total_files,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_cached_files(&self) -> Vec<FileItem> {
|
||||
@@ -302,9 +302,9 @@ impl FilePicker {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh_git_status(&self) -> Result<Vec<FileItem>, Error> {
|
||||
pub fn refresh_git_status(&self) -> Vec<FileItem> {
|
||||
let sync_data: &Arc<RwLock<FileSync>> = &self.sync_data;
|
||||
let git_workdir: &Option<PathBuf> = &self.git_workdir;
|
||||
let git_workdir = self.git_workdir.as_deref();
|
||||
let new_git_status_cache = GitStatusCache::read_git_status(git_workdir);
|
||||
|
||||
if let Ok(mut sync_data_write) = sync_data.write() {
|
||||
@@ -317,9 +317,9 @@ impl FilePicker {
|
||||
|
||||
file.update_frecency_scores();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Ok(self.get_cached_files())
|
||||
self.get_cached_files()
|
||||
}
|
||||
|
||||
pub fn trigger_rescan(&self) -> Result<(), Error> {
|
||||
@@ -338,7 +338,7 @@ impl FilePicker {
|
||||
|
||||
thread::spawn(move || {
|
||||
debug!("Background scan thread started");
|
||||
if let Ok((files, git_cache)) = scan_filesystem(&base_path, &git_workdir) {
|
||||
if let Ok((files, git_cache)) = scan_filesystem(&base_path, git_workdir.as_ref()) {
|
||||
info!("Filesystem scan completed: found {} files", files.len());
|
||||
if let Ok(mut data) = sync_data.write() {
|
||||
data.update_files(files, git_cache);
|
||||
@@ -364,6 +364,7 @@ impl FilePicker {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ScanProgress {
|
||||
pub total_files: usize,
|
||||
@@ -382,7 +383,7 @@ fn spawn_background_watcher(
|
||||
scan_signal.store(true, Ordering::Relaxed);
|
||||
info!("starting background watcher thread");
|
||||
|
||||
match scan_filesystem(&base_path, &git_workdir) {
|
||||
match scan_filesystem(&base_path, git_workdir.as_ref()) {
|
||||
Ok((files, git_cache)) => {
|
||||
info!(
|
||||
"Initial parallel filesystem scan completed: found {} files",
|
||||
@@ -445,9 +446,7 @@ fn handle_debounced_events(
|
||||
.paths
|
||||
.iter()
|
||||
.filter_map(|path| {
|
||||
let Some(relative_path) = pathdiff::diff_paths(path, base_path) else {
|
||||
return None;
|
||||
};
|
||||
let relative_path = pathdiff::diff_paths(path, base_path)?;
|
||||
|
||||
let Ok(sync_read) = sync_data.read() else {
|
||||
return None;
|
||||
@@ -461,7 +460,7 @@ fn handle_debounced_events(
|
||||
|
||||
match event.event.kind {
|
||||
EventKind::Create(_) => {
|
||||
if should_add_new_file(path, git_workdir) {
|
||||
if should_add_new_file(path, git_workdir.as_ref()) {
|
||||
Some(path.clone())
|
||||
} else {
|
||||
None
|
||||
@@ -479,7 +478,7 @@ fn handle_debounced_events(
|
||||
debug!(?event, "File watcher event");
|
||||
match event.event.kind {
|
||||
EventKind::Create(_) => {
|
||||
handle_create_events(&relevant_paths, sync_data, base_path, git_workdir);
|
||||
handle_create_events(&relevant_paths, sync_data, base_path, git_workdir.as_ref());
|
||||
affected_paths.extend(relevant_paths);
|
||||
}
|
||||
EventKind::Modify(_) => {
|
||||
@@ -499,7 +498,7 @@ fn handle_debounced_events(
|
||||
}
|
||||
}
|
||||
|
||||
fn should_add_new_file(path: &Path, git_workdir: &Option<PathBuf>) -> bool {
|
||||
fn should_add_new_file(path: &Path, git_workdir: Option<&PathBuf>) -> bool {
|
||||
if is_git_file(path) {
|
||||
return false;
|
||||
}
|
||||
@@ -523,24 +522,21 @@ fn handle_create_events(
|
||||
paths: &[PathBuf],
|
||||
sync_data: &Arc<RwLock<FileSync>>,
|
||||
base_path: &Path,
|
||||
git_workdir: &Option<PathBuf>,
|
||||
git_workdir: Option<&PathBuf>,
|
||||
) {
|
||||
let repo = git_workdir.as_ref().and_then(|p| Repository::open(p).ok());
|
||||
if let Ok(mut sync_write) = sync_data.write() {
|
||||
for path in paths {
|
||||
if repo
|
||||
.as_ref()
|
||||
.is_some_and(|repo| repo.is_path_ignored(&path).unwrap_or(false))
|
||||
.is_some_and(|repo| repo.is_path_ignored(path).unwrap_or(false))
|
||||
{
|
||||
debug!("Ignoring file {} due to gitignore rules", path.display());
|
||||
continue;
|
||||
}
|
||||
|
||||
// we will update the path for every
|
||||
let mut file_item = FileItem::new(path.to_path_buf(), base_path, None);
|
||||
|
||||
let mut file_item = FileItem::new(path.clone(), base_path, None);
|
||||
file_item.update_frecency_scores();
|
||||
|
||||
sync_write.insert_file_sorted(file_item);
|
||||
}
|
||||
}
|
||||
@@ -563,80 +559,85 @@ fn remove_paths_from_index(
|
||||
|
||||
fn scan_filesystem(
|
||||
base_path: &Path,
|
||||
git_workdir: &Option<PathBuf>,
|
||||
git_workdir: Option<&PathBuf>,
|
||||
) -> Result<(Vec<FileItem>, Option<GitStatusCache>), Error> {
|
||||
let scan_start = std::time::Instant::now();
|
||||
let git_workdir = git_workdir.map(|p| p.as_path());
|
||||
info!("SCAN: Starting parallel filesystem scan and git status");
|
||||
|
||||
let git_handle = GitStatusCache::read_git_status_parallel(git_workdir.clone());
|
||||
// run separate thread for git status because it effectively does another separate file
|
||||
// traversal which could be pretty slow on large repos (in general 300-500ms)
|
||||
thread::scope(|s| {
|
||||
let git_handle = s.spawn(|| GitStatusCache::read_git_status(git_workdir));
|
||||
|
||||
let walker = WalkBuilder::new(base_path)
|
||||
.hidden(false)
|
||||
.git_ignore(true)
|
||||
.git_exclude(true)
|
||||
.git_global(true)
|
||||
.ignore(true)
|
||||
.follow_links(false)
|
||||
.sort_by_file_name(std::cmp::Ord::cmp)
|
||||
.build_parallel();
|
||||
let walker = WalkBuilder::new(base_path)
|
||||
.hidden(false)
|
||||
.git_ignore(true)
|
||||
.git_exclude(true)
|
||||
.git_global(true)
|
||||
.ignore(true)
|
||||
.follow_links(false)
|
||||
.sort_by_file_name(std::cmp::Ord::cmp)
|
||||
.build_parallel();
|
||||
|
||||
let walker_start = std::time::Instant::now();
|
||||
info!("SCAN: Starting file walker");
|
||||
let walker_start = std::time::Instant::now();
|
||||
info!("SCAN: Starting file walker");
|
||||
|
||||
let files = Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||
walker.run(|| {
|
||||
let files = Arc::clone(&files);
|
||||
let base_path = base_path.to_path_buf();
|
||||
let files = Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||
walker.run(|| {
|
||||
let files = Arc::clone(&files);
|
||||
let base_path = base_path.to_path_buf();
|
||||
|
||||
Box::new(move |result| {
|
||||
if let Ok(entry) = result {
|
||||
if let Some(file_type) = entry.file_type() {
|
||||
if file_type.is_file() {
|
||||
let path = entry.path();
|
||||
Box::new(move |result| {
|
||||
if let Ok(entry) = result {
|
||||
if let Some(file_type) = entry.file_type() {
|
||||
if file_type.is_file() {
|
||||
let path = entry.path();
|
||||
|
||||
if is_git_file(path) {
|
||||
return WalkState::Continue;
|
||||
}
|
||||
if is_git_file(path) {
|
||||
return WalkState::Continue;
|
||||
}
|
||||
|
||||
let file_item = FileItem::new(
|
||||
path.to_path_buf(),
|
||||
&base_path,
|
||||
None, // Git status will be added after join
|
||||
);
|
||||
let file_item = FileItem::new(
|
||||
path.to_path_buf(),
|
||||
&base_path,
|
||||
None, // Git status will be added after join
|
||||
);
|
||||
|
||||
if let Ok(mut files_vec) = files.lock() {
|
||||
files_vec.push(file_item);
|
||||
if let Ok(mut files_vec) = files.lock() {
|
||||
files_vec.push(file_item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
WalkState::Continue
|
||||
})
|
||||
});
|
||||
|
||||
let mut files = Arc::try_unwrap(files).unwrap().into_inner().unwrap();
|
||||
let walker_time = walker_start.elapsed();
|
||||
info!("SCAN: File walking completed in {:?}", walker_time);
|
||||
|
||||
let git_cache = git_handle
|
||||
.join()
|
||||
.map_err(|_| Error::InvalidPath("Git status thread panicked".to_string()))?;
|
||||
|
||||
if let Some(git_cache) = &git_cache {
|
||||
files.par_iter_mut().for_each(|file| {
|
||||
file.git_status = git_cache.lookup_status(&file.path);
|
||||
file.update_frecency_scores();
|
||||
WalkState::Continue
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
let total_time = scan_start.elapsed();
|
||||
info!(
|
||||
"SCAN: Total scan time {:?} for {} files",
|
||||
total_time,
|
||||
files.len()
|
||||
);
|
||||
let mut files = Arc::try_unwrap(files).unwrap().into_inner().unwrap();
|
||||
let walker_time = walker_start.elapsed();
|
||||
info!("SCAN: File walking completed in {:?}", walker_time);
|
||||
|
||||
Ok((files, git_cache))
|
||||
let git_cache = git_handle
|
||||
.join()
|
||||
.map_err(|_| Error::InvalidPath("Git status thread panicked".to_string()))?;
|
||||
|
||||
if let Some(git_cache) = &git_cache {
|
||||
files.par_iter_mut().for_each(|file| {
|
||||
file.git_status = git_cache.lookup_status(&file.path);
|
||||
file.update_frecency_scores();
|
||||
});
|
||||
}
|
||||
|
||||
let total_time = scan_start.elapsed();
|
||||
info!(
|
||||
"SCAN: Total scan time {:?} for {} files",
|
||||
total_time,
|
||||
files.len()
|
||||
);
|
||||
|
||||
Ok((files, git_cache))
|
||||
})
|
||||
}
|
||||
|
||||
fn update_git_status_for_paths(
|
||||
@@ -688,9 +689,7 @@ fn update_git_status_for_paths(
|
||||
|
||||
#[inline]
|
||||
fn is_git_file(path: &Path) -> bool {
|
||||
path.to_str()
|
||||
.map(|path| path.contains("/.git/"))
|
||||
.unwrap_or(false)
|
||||
path.to_str().is_some_and(|path| path.contains("/.git/"))
|
||||
}
|
||||
|
||||
impl Drop for FilePicker {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use crate::error::Error;
|
||||
use crate::file_key::FileKey;
|
||||
use heed::{types::*, EnvFlags};
|
||||
use heed::{
|
||||
types::{Bytes, SerdeBincode},
|
||||
EnvFlags,
|
||||
};
|
||||
use heed::{Database, Env, EnvOpenOptions};
|
||||
use std::collections::VecDeque;
|
||||
use std::fs;
|
||||
|
||||
+1
-15
@@ -1,6 +1,5 @@
|
||||
use git2::{Repository, Status, StatusOptions};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::thread::{self, JoinHandle};
|
||||
use tracing::{error, info};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -10,14 +9,6 @@ pub struct GitStatusCache {
|
||||
}
|
||||
|
||||
impl GitStatusCache {
|
||||
#[allow(dead_code)]
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
paths: Vec::new(),
|
||||
statuses: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_git_entries(mut entries: Vec<(PathBuf, Status)>) -> Self {
|
||||
entries.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
@@ -35,7 +26,7 @@ impl GitStatusCache {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_git_status(git_workdir: &Option<PathBuf>) -> Option<Self> {
|
||||
pub fn read_git_status(git_workdir: Option<&Path>) -> Option<Self> {
|
||||
let git_start = std::time::Instant::now();
|
||||
info!("GIT: Starting git status read");
|
||||
|
||||
@@ -72,14 +63,9 @@ impl GitStatusCache {
|
||||
|
||||
Some(Self::from_git_entries(entries))
|
||||
}
|
||||
|
||||
pub fn read_git_status_parallel(git_workdir: Option<PathBuf>) -> JoinHandle<Option<Self>> {
|
||||
thread::spawn(move || Self::read_git_status(&git_workdir))
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[allow(dead_code)]
|
||||
pub fn is_modified_status(status: Status) -> bool {
|
||||
status.intersects(
|
||||
Status::WT_MODIFIED
|
||||
|
||||
+6
-5
@@ -51,9 +51,10 @@ pub fn scan_files(_: &Lua, _: ()) -> LuaResult<()> {
|
||||
let picker = file_picker
|
||||
.as_ref()
|
||||
.ok_or_else(|| Error::InvalidPath("File picker not initialized".to_string()))?;
|
||||
let result = picker.trigger_rescan()?;
|
||||
|
||||
picker.trigger_rescan()?;
|
||||
::tracing::info!("scan_files trigger_rescan completed");
|
||||
Ok(result)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_cached_files(_: &Lua, _: ()) -> LuaResult<Vec<FileItem>> {
|
||||
@@ -75,8 +76,7 @@ pub fn fuzzy_search_files(
|
||||
.as_ref()
|
||||
.ok_or_else(|| Error::InvalidPath("File picker not initialized".to_string()))?;
|
||||
|
||||
let results = picker.fuzzy_search(&query, max_results, max_threads, current_file)?;
|
||||
|
||||
let results = picker.fuzzy_search(&query, max_results, max_threads, current_file.as_ref());
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
@@ -116,7 +116,8 @@ pub fn refresh_git_status(_: &Lua, _: ()) -> LuaResult<Vec<FileItem>> {
|
||||
let picker = file_picker
|
||||
.as_ref()
|
||||
.ok_or_else(|| Error::InvalidPath("File picker not initialized".to_string()))?;
|
||||
Ok(picker.refresh_git_status()?)
|
||||
|
||||
Ok(picker.refresh_git_status())
|
||||
}
|
||||
|
||||
pub fn stop_background_monitor(_: &Lua, _: ()) -> LuaResult<bool> {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
pub fn calculate_distance_penalty(current_file: &Option<String>, candidate_path: &str) -> i32 {
|
||||
pub fn calculate_distance_penalty(current_file: Option<&str>, candidate_path: &str) -> i32 {
|
||||
let Some(ref current_path) = current_file else {
|
||||
return 0; // No penalty if no current file
|
||||
};
|
||||
@@ -46,27 +46,24 @@ mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_calculate_distance_penalty() {
|
||||
assert_eq!(calculate_distance_penalty(&None, "/path/to/file.txt"), 0);
|
||||
assert_eq!(calculate_distance_penalty(None, "/path/to/file.txt"), 0);
|
||||
|
||||
assert_eq!(
|
||||
calculate_distance_penalty(
|
||||
&Some("/path/to/current/file.txt".to_string()),
|
||||
Some("/path/to/current/file.txt"),
|
||||
"/path/to/current/other.txt"
|
||||
),
|
||||
0
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
calculate_distance_penalty(
|
||||
&Some("/path/to/current/file.txt".to_string()),
|
||||
"/path/to/file.txt"
|
||||
),
|
||||
calculate_distance_penalty(Some("/path/to/current/file.txt"), "/path/to/file.txt"),
|
||||
-2
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
calculate_distance_penalty(
|
||||
&Some("/path/to/current/file.txt".to_string()),
|
||||
Some("/path/to/current/file.txt"),
|
||||
"/path/to/other/file.txt"
|
||||
),
|
||||
-4
|
||||
@@ -74,19 +71,19 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
calculate_distance_penalty(
|
||||
&Some("/path/to/current/file.txt".to_string()),
|
||||
Some("/path/to/current/file.txt"),
|
||||
"/path/to/another/dir/file.txt"
|
||||
),
|
||||
-6
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
calculate_distance_penalty(&Some("/a/b/c/d/file.txt".to_string()), "/x/y/z/w/file.txt"),
|
||||
calculate_distance_penalty(Some("/a/b/c/d/file.txt"), "/x/y/z/w/file.txt"),
|
||||
-16
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
calculate_distance_penalty(&Some("/file1.txt".to_string()), "/file2.txt"),
|
||||
calculate_distance_penalty(Some("/file1.txt"), "/file2.txt"),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
+30
-22
@@ -18,7 +18,6 @@ pub fn match_and_score_files(files: &[FileItem], context: &ScoringContext) -> Ve
|
||||
prefilter: true,
|
||||
max_typos: Some(context.max_typos),
|
||||
sort: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let haystack: Vec<&str> = files.iter().map(|f| f.relative_path.as_str()).collect();
|
||||
@@ -66,7 +65,7 @@ pub fn match_and_score_files(files: &[FileItem], context: &ScoringContext) -> Ve
|
||||
let base_score = neo_frizbee_match.score as i32;
|
||||
let frecency_boost = base_score.saturating_mul(file.total_frecency_score as i32) / 100;
|
||||
let distance_penalty = calculate_distance_penalty(
|
||||
&context.current_file.map(|s| s.to_string()),
|
||||
context.current_file.map(|s| s.as_str()),
|
||||
&file.relative_path,
|
||||
);
|
||||
|
||||
@@ -89,10 +88,11 @@ pub fn match_and_score_files(files: &[FileItem], context: &ScoringContext) -> Ve
|
||||
base_score / 5 * 2 // 40% bonus for exact filename match
|
||||
}
|
||||
Some(_) => base_score / 5, // 20% bonus for fuzzy filename match
|
||||
// if the file is special directory give it an extra bonus
|
||||
None if is_special_entry_point_file(&file.file_name) => {
|
||||
// 18% bonus special filename just as much as exact path
|
||||
// but a little bit less to give preference to the actual file if present
|
||||
has_special_filename_bonus = true;
|
||||
base_score / 5
|
||||
base_score * 18 / 100
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
@@ -132,18 +132,26 @@ pub fn match_and_score_files(files: &[FileItem], context: &ScoringContext) -> Ve
|
||||
/// Check if a filename is a special entry point file that deserves bonus scoring
|
||||
/// These are typically files that serve as module exports or entry points
|
||||
fn is_special_entry_point_file(filename: &str) -> bool {
|
||||
match filename {
|
||||
"mod.rs" | "lib.rs" | "main.rs" => true,
|
||||
"index.js" | "index.jsx" | "index.ts" | "index.tsx" => true,
|
||||
"index.mjs" | "index.cjs" => true,
|
||||
"index.vue" => true,
|
||||
"__init__.py" | "__main__.py" => true,
|
||||
"main.go" => true,
|
||||
"main.c" => true,
|
||||
"index.php" => true,
|
||||
"main.rb" | "index.rb" => true,
|
||||
_ => false,
|
||||
}
|
||||
matches!(
|
||||
filename,
|
||||
"mod.rs"
|
||||
| "lib.rs"
|
||||
| "main.rs"
|
||||
| "index.js"
|
||||
| "index.jsx"
|
||||
| "index.ts"
|
||||
| "index.tsx"
|
||||
| "index.mjs"
|
||||
| "index.cjs"
|
||||
| "index.vue"
|
||||
| "__init__.py"
|
||||
| "__main__.py"
|
||||
| "main.go"
|
||||
| "main.c"
|
||||
| "index.php"
|
||||
| "main.rb"
|
||||
| "index.rb"
|
||||
)
|
||||
}
|
||||
|
||||
fn score_all_by_frecency(files: &[FileItem], context: &ScoringContext) -> Vec<(usize, Score)> {
|
||||
@@ -155,11 +163,14 @@ fn score_all_by_frecency(files: &[FileItem], context: &ScoringContext) -> Vec<(u
|
||||
+ (file.modification_frecency_score as i32).saturating_mul(4);
|
||||
|
||||
let distance_penalty = calculate_distance_penalty(
|
||||
&context.current_file.map(|s| s.to_string()),
|
||||
context.current_file.map(|x| x.as_str()),
|
||||
&file.relative_path,
|
||||
);
|
||||
|
||||
let total = total_frecency_score.saturating_add(distance_penalty);
|
||||
let total = total_frecency_score
|
||||
.saturating_add(distance_penalty)
|
||||
.saturating_add(calculate_file_bonus(file, context));
|
||||
|
||||
let score = Score {
|
||||
total,
|
||||
base_score: 0,
|
||||
@@ -176,14 +187,11 @@ fn score_all_by_frecency(files: &[FileItem], context: &ScoringContext) -> Vec<(u
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[allow(dead_code)]
|
||||
fn calculate_file_bonus(file: &FileItem, context: &ScoringContext) -> i32 {
|
||||
let mut bonus = 0i32;
|
||||
|
||||
if let Some(current) = context.current_file {
|
||||
let is_current = file.relative_path == current || file.relative_path == current;
|
||||
|
||||
if is_current {
|
||||
if file.relative_path == *current {
|
||||
bonus -= match file.git_status {
|
||||
Some(status) if is_modified_status(status) => 150,
|
||||
_ => 300,
|
||||
|
||||
@@ -33,7 +33,7 @@ pub struct Score {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ScoringContext<'a> {
|
||||
pub query: &'a str,
|
||||
pub current_file: Option<&'a str>,
|
||||
pub current_file: Option<&'a String>,
|
||||
pub max_typos: u16,
|
||||
pub max_threads: usize,
|
||||
}
|
||||
|
||||
+16
-15
@@ -1,3 +1,6 @@
|
||||
#![allow(clippy::all)]
|
||||
#![allow(dead_code)]
|
||||
|
||||
#[path = "../../lua/fff/rust/error.rs"]
|
||||
mod error;
|
||||
#[path = "../../lua/fff/rust/file_key.rs"]
|
||||
@@ -22,7 +25,6 @@ use std::io::{self, Write};
|
||||
use std::sync::{LazyLock, RwLock};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use types::FileItem;
|
||||
|
||||
use crate::git::format_git_status;
|
||||
|
||||
@@ -47,7 +49,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let initial_files = picker.get_cached_files();
|
||||
println!("Initial file count: {}", initial_files.len());
|
||||
|
||||
if initial_files.len() > 0 {
|
||||
if !initial_files.is_empty() {
|
||||
println!("Sample files:");
|
||||
for (i, file) in initial_files.iter().take(5).enumerate() {
|
||||
println!(
|
||||
@@ -135,24 +137,23 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
|
||||
println!(" 🔄 Testing git status refresh...");
|
||||
if let Ok(refreshed_files) = picker.refresh_git_status() {
|
||||
let mut new_git_stats = std::collections::HashMap::new();
|
||||
for file in &refreshed_files {
|
||||
let status = format_git_status(file.git_status);
|
||||
*new_git_stats.entry(status).or_insert(0) += 1;
|
||||
}
|
||||
if new_git_stats != git_stats_copy {
|
||||
print!(" ✨ Git status changed after refresh: ");
|
||||
for (status, count) in &new_git_stats {
|
||||
print!("{}:{} ", status, count);
|
||||
}
|
||||
println!();
|
||||
let refreshed_files = picker.refresh_git_status();
|
||||
let mut new_git_stats = std::collections::HashMap::new();
|
||||
for file in &refreshed_files {
|
||||
let status = format_git_status(file.git_status);
|
||||
*new_git_stats.entry(status).or_insert(0) += 1;
|
||||
}
|
||||
if new_git_stats != git_stats_copy {
|
||||
print!(" ✨ Git status changed after refresh: ");
|
||||
for (status, count) in &new_git_stats {
|
||||
print!("{}:{} ", status, count);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
|
||||
if iteration % 40 == 0 {
|
||||
let search_results = picker.fuzzy_search("rs", 5, 2, None).unwrap_or_default();
|
||||
let search_results = picker.fuzzy_search("rs", 5, 2, None);
|
||||
let timestamp = chrono::Local::now().format("%H:%M:%S");
|
||||
println!(
|
||||
"🔍 [{}] Search test 'rs': {} matches",
|
||||
|
||||
Reference in New Issue
Block a user