diff --git a/Cargo.lock b/Cargo.lock index fa46994..faa08cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,12 +37,6 @@ version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9a8f622bcf6ff3df478e9deba3e03e4e04b300f8e6a139e192c05fa3490afc7" -[[package]] -name = "arrayvec" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" - [[package]] name = "async-trait" version = "0.1.58" @@ -93,6 +87,7 @@ dependencies = [ "eyre", "fs-err", "futures-util", + "fuzzy-matcher", "indicatif", "interim", "itertools", @@ -103,7 +98,6 @@ dependencies = [ "semver", "serde", "serde_json", - "skim", "tiny-bip39", "tokio", "tracing-subscriber", @@ -329,7 +323,7 @@ dependencies = [ "num-integer", "num-traits", "serde", - "time 0.1.44", + "time", "wasm-bindgen", "winapi", ] @@ -457,20 +451,6 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d0165d2900ae6778e36e80bbc4da3b5eefccee9ba939761f9c2882a5d9af3ff" -[[package]] -name = "crossbeam" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c" -dependencies = [ - "cfg-if", - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-epoch", - "crossbeam-queue", - "crossbeam-utils", -] - [[package]] name = "crossbeam-channel" version = "0.5.6" @@ -481,30 +461,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "crossbeam-deque" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" -dependencies = [ - "cfg-if", - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" -dependencies = [ - "autocfg", - "cfg-if", - "crossbeam-utils", - "memoffset 0.7.1", - "scopeguard", -] - [[package]] name = "crossbeam-queue" version = "0.3.6" @@ -561,82 +517,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "darling" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0dd3cd20dc6b5a876612a6e5accfe7f3dd883db6d07acfbf14c128f61550dfa" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a784d2ccaf7c98501746bf0be29b2022ba41fd62a2e622af997a03e9f972859f" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7618812407e9402654622dd402b0a89dff9ba93badd6540781526117b92aab7e" -dependencies = [ - "darling_core", - "quote", - "syn", -] - -[[package]] -name = "defer-drop" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f613ec9fa66a6b28cdb1842b27f9adf24f39f9afc4dcdd9fdecee4aca7945c57" -dependencies = [ - "crossbeam-channel", - "once_cell", -] - -[[package]] -name = "derive_builder" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07adf7be193b71cc36b193d0f5fe60b918a3a9db4dad0449f57bcfd519704a3" -dependencies = [ - "derive_builder_macro", -] - -[[package]] -name = "derive_builder_core" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "derive_builder_macro" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f0314b72bed045f3a68671b3c86328386762c93f82d98c65c3cb5e5f573dd68" -dependencies = [ - "derive_builder_core", - "syn", -] - [[package]] name = "digest" version = "0.10.5" @@ -666,16 +546,6 @@ dependencies = [ "dirs-sys", ] -[[package]] -name = "dirs-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] - [[package]] name = "dirs-sys" version = "0.3.7" @@ -687,17 +557,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - [[package]] name = "dotenvy" version = "0.15.3" @@ -1122,12 +981,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - [[package]] name = "idna" version = "0.3.0" @@ -1349,24 +1202,6 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" -[[package]] -name = "memoffset" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" -dependencies = [ - "autocfg", -] - -[[package]] -name = "memoffset" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" -dependencies = [ - "autocfg", -] - [[package]] name = "mime" version = "0.3.16" @@ -1397,31 +1232,6 @@ dependencies = [ "windows-sys 0.36.1", ] -[[package]] -name = "nix" -version = "0.24.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" -dependencies = [ - "bitflags", - "cfg-if", - "libc", -] - -[[package]] -name = "nix" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" -dependencies = [ - "autocfg", - "bitflags", - "cfg-if", - "libc", - "memoffset 0.6.5", - "pin-utils", -] - [[package]] name = "nom" version = "7.1.1" @@ -1537,9 +1347,9 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "once_cell" -version = "1.14.0" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" [[package]] name = "openssl-probe" @@ -1773,28 +1583,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "rayon" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "356a0625f1954f730c0201cdab48611198dc6ce21f4acff55089b5a78e6e835b" -dependencies = [ - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-utils", - "num_cpus", -] - [[package]] name = "redox_syscall" version = "0.2.16" @@ -1817,9 +1605,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.6.0" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +checksum = "cce168fea28d3e05f158bda4576cf0c844d5045bc2cc3620fa0292ed5bb5814c" dependencies = [ "aho-corasick", "memchr", @@ -1837,9 +1625,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.27" +version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "reqwest" @@ -2203,31 +1991,6 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e90531723b08e4d6d71b791108faf51f03e1b4a7784f96b2b87f852ebc247228" -[[package]] -name = "skim" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cebed5f897cd6c0d80fbe30adb36c0abf7400e93043a63ae56458495642b3485" -dependencies = [ - "beef", - "bitflags", - "chrono", - "crossbeam", - "defer-drop", - "derive_builder", - "fuzzy-matcher", - "lazy_static", - "log", - "nix 0.25.1", - "rayon", - "regex", - "time 0.3.17", - "timer", - "tuikit", - "unicode-width", - "vte", -] - [[package]] name = "slab" version = "0.4.7" @@ -2449,17 +2212,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "term" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" -dependencies = [ - "dirs-next", - "rustversion", - "winapi", -] - [[package]] name = "termcolor" version = "1.1.3" @@ -2519,31 +2271,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "time" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" -dependencies = [ - "serde", - "time-core", -] - -[[package]] -name = "time-core" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" - -[[package]] -name = "timer" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31d42176308937165701f50638db1c31586f183f1aab416268216577aec7306b" -dependencies = [ - "chrono", -] - [[package]] name = "tiny-bip39" version = "1.0.0" @@ -2757,20 +2484,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" -[[package]] -name = "tuikit" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e19c6ab038babee3d50c8c12ff8b910bdb2196f62278776422f50390d8e53d8" -dependencies = [ - "bitflags", - "lazy_static", - "log", - "nix 0.24.3", - "term", - "unicode-width", -] - [[package]] name = "typenum" version = "1.15.0" @@ -2845,12 +2558,6 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9" -[[package]] -name = "utf8parse" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" - [[package]] name = "uuid" version = "1.2.1" @@ -2872,27 +2579,6 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" -[[package]] -name = "vte" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aae21c12ad2ec2d168c236f369c38ff332bc1134f7246350dca641437365045" -dependencies = [ - "arrayvec", - "utf8parse", - "vte_generate_state_changes", -] - -[[package]] -name = "vte_generate_state_changes" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" -dependencies = [ - "proc-macro2", - "quote", -] - [[package]] name = "walkdir" version = "2.3.2" diff --git a/Cargo.toml b/Cargo.toml index abc6d11..27adc77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,8 +73,8 @@ semver = "1.0.14" runtime-format = "0.1.2" tiny-bip39 = "1" futures-util = "0.3" -skim = { version = "0.10.2", default-features = false } ratatui = "0.20.1" +fuzzy-matcher = "0.3.7" [dependencies.tracing-subscriber] version = "0.3" diff --git a/atuin-client/src/database.rs b/atuin-client/src/database.rs index bb4d9df..fa39f71 100644 --- a/atuin-client/src/database.rs +++ b/atuin-client/src/database.rs @@ -41,7 +41,7 @@ pub fn current_context() -> Context { } #[async_trait] -pub trait Database: Send + Sync { +pub trait Database: Send + Sync + 'static { async fn save(&mut self, h: &History) -> Result<()>; async fn save_bulk(&mut self, h: &[History]) -> Result<()>; @@ -375,7 +375,7 @@ impl Database for Sqlite { match search_mode { SearchMode::Prefix => sql.and_where_like_left("command", query), SearchMode::FullText => sql.and_where_like_any("command", query), - SearchMode::Skim | SearchMode::Fuzzy => { + _ => { // don't recompile the regex on successive calls! lazy_static! { static ref SPLIT_REGEX: Regex = Regex::new(r" +").unwrap(); diff --git a/atuin-client/src/settings.rs b/atuin-client/src/settings.rs index a2d4f8c..756c496 100644 --- a/atuin-client/src/settings.rs +++ b/atuin-client/src/settings.rs @@ -46,16 +46,11 @@ impl SearchMode { pub fn next(&self, settings: &Settings) -> Self { match self { SearchMode::Prefix => SearchMode::FullText, - SearchMode::FullText => { - // if the user is using skim, we go to skim, otherwise fuzzy. - if settings.search_mode == SearchMode::Skim { - SearchMode::Skim - } else { - SearchMode::Fuzzy - } - } - SearchMode::Fuzzy => SearchMode::Prefix, - SearchMode::Skim => SearchMode::Prefix, + // if the user is using skim, we go to skim + SearchMode::FullText if settings.search_mode == SearchMode::Skim => SearchMode::Skim, + // otherwise fuzzy. + SearchMode::FullText => SearchMode::Fuzzy, + SearchMode::Fuzzy | SearchMode::Skim => SearchMode::Prefix, } } } diff --git a/atuin-common/src/utils.rs b/atuin-common/src/utils.rs index 1d33039..5fe6eee 100644 --- a/atuin-common/src/utils.rs +++ b/atuin-common/src/utils.rs @@ -1,7 +1,7 @@ use std::env; use std::path::PathBuf; -use chrono::NaiveDate; +use chrono::{Months, NaiveDate}; use uuid::Uuid; pub fn uuid_v4() -> String { @@ -49,19 +49,9 @@ pub fn get_current_dir() -> String { } pub fn get_days_from_month(year: i32, month: u32) -> i64 { - NaiveDate::from_ymd( - match month { - 12 => year + 1, - _ => year, - }, - match month { - 12 => 1, - _ => month + 1, - }, - 1, - ) - .signed_duration_since(NaiveDate::from_ymd(year, month, 1)) - .num_days() + let Some(start) = NaiveDate::from_ymd_opt(year, month, 1) else { return 30 }; + let Some(end) = start.checked_add_months(Months::new(1)) else { return 30 }; + end.signed_duration_since(start).num_days() } #[cfg(test)] @@ -108,4 +98,23 @@ mod tests { assert_eq!(data_dir(), PathBuf::from("/home/user/.local/share/atuin")); env::remove_var("HOME"); } + + #[test] + fn days_from_month() { + assert_eq!(get_days_from_month(2023, 1), 31); + assert_eq!(get_days_from_month(2023, 2), 28); + assert_eq!(get_days_from_month(2023, 3), 31); + assert_eq!(get_days_from_month(2023, 4), 30); + assert_eq!(get_days_from_month(2023, 5), 31); + assert_eq!(get_days_from_month(2023, 6), 30); + assert_eq!(get_days_from_month(2023, 7), 31); + assert_eq!(get_days_from_month(2023, 8), 31); + assert_eq!(get_days_from_month(2023, 9), 30); + assert_eq!(get_days_from_month(2023, 10), 31); + assert_eq!(get_days_from_month(2023, 11), 30); + assert_eq!(get_days_from_month(2023, 12), 31); + + // leap years + assert_eq!(get_days_from_month(2024, 2), 29); + } } diff --git a/src/command/client.rs b/src/command/client.rs index 551b222..2a82563 100644 --- a/src/command/client.rs +++ b/src/command/client.rs @@ -53,7 +53,7 @@ impl Cmd { Self::History(history) => history.run(&settings, &mut db).await, Self::Import(import) => import.run(&mut db).await, Self::Stats(stats) => stats.run(&mut db, &settings).await, - Self::Search(search) => search.run(&mut db, &mut settings).await, + Self::Search(search) => search.run(db, &mut settings).await, #[cfg(feature = "sync")] Self::Sync(sync) => sync.run(settings, &mut db).await, } diff --git a/src/command/client/search.rs b/src/command/client/search.rs index 50dfec1..c407eb0 100644 --- a/src/command/client/search.rs +++ b/src/command/client/search.rs @@ -14,9 +14,9 @@ use super::history::ListMode; mod cursor; mod duration; +mod engines; mod history_list; mod interactive; -mod skim_impl; pub use duration::{format_duration, format_duration_into}; #[allow(clippy::struct_excessive_bools)] @@ -87,7 +87,7 @@ pub struct Cmd { } impl Cmd { - pub async fn run(self, db: &mut impl Database, settings: &mut Settings) -> Result<()> { + pub async fn run(self, mut db: impl Database, settings: &mut Settings) -> Result<()> { if self.search_mode.is_some() { settings.search_mode = self.search_mode.unwrap(); } @@ -113,7 +113,7 @@ impl Cmd { self.after.clone(), self.limit, &self.query, - db, + &mut db, ) .await?; @@ -142,7 +142,7 @@ impl Cmd { self.after.clone(), self.limit, &self.query, - db, + &mut db, ) .await?; } diff --git a/src/command/client/search/engines.rs b/src/command/client/search/engines.rs new file mode 100644 index 0000000..878b143 --- /dev/null +++ b/src/command/client/search/engines.rs @@ -0,0 +1,46 @@ +use async_trait::async_trait; +use atuin_client::{ + database::{Context, Database}, + history::History, + settings::{FilterMode, SearchMode}, +}; +use eyre::Result; + +use super::cursor::Cursor; + +pub mod db; +pub mod skim; + +pub fn engine(search_mode: SearchMode) -> Box { + match search_mode { + SearchMode::Skim => Box::new(skim::Search::new()) as Box<_>, + mode => Box::new(db::Search(mode)) as Box<_>, + } +} + +pub struct SearchState { + pub input: Cursor, + pub filter_mode: FilterMode, + pub context: Context, +} + +#[async_trait] +pub trait SearchEngine: Send + Sync + 'static { + async fn full_query( + &mut self, + state: &SearchState, + db: &mut dyn Database, + ) -> Result>; + + async fn query(&mut self, state: &SearchState, db: &mut dyn Database) -> Result> { + if state.input.as_str().is_empty() { + Ok(db + .list(state.filter_mode, &state.context, Some(200), true) + .await? + .into_iter() + .collect::>()) + } else { + self.full_query(state, db).await + } + } +} diff --git a/src/command/client/search/engines/db.rs b/src/command/client/search/engines/db.rs new file mode 100644 index 0000000..5a35da1 --- /dev/null +++ b/src/command/client/search/engines/db.rs @@ -0,0 +1,30 @@ +use async_trait::async_trait; +use atuin_client::{database::Database, history::History, settings::SearchMode}; +use eyre::Result; + +use super::{SearchEngine, SearchState}; + +pub struct Search(pub SearchMode); + +#[async_trait] +impl SearchEngine for Search { + async fn full_query( + &mut self, + state: &SearchState, + db: &mut dyn Database, + ) -> Result> { + Ok(db + .search( + self.0, + state.filter_mode, + &state.context, + state.input.as_str(), + Some(200), + None, + None, + ) + .await? + .into_iter() + .collect::>()) + } +} diff --git a/src/command/client/search/engines/skim.rs b/src/command/client/search/engines/skim.rs new file mode 100644 index 0000000..7604931 --- /dev/null +++ b/src/command/client/search/engines/skim.rs @@ -0,0 +1,145 @@ +use std::path::Path; + +use async_trait::async_trait; +use atuin_client::{database::Database, history::History, settings::FilterMode}; +use chrono::Utc; +use eyre::Result; +use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; +use tokio::task::yield_now; + +use super::{SearchEngine, SearchState}; + +pub struct Search { + all_history: Vec<(History, i32)>, + engine: SkimMatcherV2, +} + +impl Search { + pub fn new() -> Self { + Search { + all_history: vec![], + engine: SkimMatcherV2::default(), + } + } +} + +#[async_trait] +impl SearchEngine for Search { + async fn full_query( + &mut self, + state: &SearchState, + db: &mut dyn Database, + ) -> Result> { + if self.all_history.is_empty() { + self.all_history = db.all_with_count().await.unwrap(); + } + + Ok(fuzzy_search(&self.engine, state, &self.all_history).await) + } +} + +async fn fuzzy_search( + engine: &SkimMatcherV2, + state: &SearchState, + all_history: &[(History, i32)], +) -> Vec { + let mut set = Vec::with_capacity(200); + let mut ranks = Vec::with_capacity(200); + let query = state.input.as_str(); + let now = Utc::now(); + + for (i, (history, count)) in all_history.iter().enumerate() { + if i % 256 == 0 { + yield_now().await; + } + match state.filter_mode { + FilterMode::Global => {} + FilterMode::Host if history.hostname == state.context.hostname => {} + FilterMode::Session if history.session == state.context.session => {} + FilterMode::Directory if history.cwd == state.context.cwd => {} + _ => continue, + } + #[allow(clippy::cast_lossless, clippy::cast_precision_loss)] + if let Some((score, indices)) = engine.fuzzy_indices(&history.command, query) { + let begin = indices.first().copied().unwrap_or_default(); + + let mut duration = ((now - history.timestamp).num_seconds() as f64).log2(); + if !duration.is_finite() || duration <= 1.0 { + duration = 1.0; + } + // these + X.0 just make the log result a bit smoother. + // log is very spiky towards 1-4, but I want a gradual decay. + // eg: + // log2(4) = 2, log2(5) = 2.3 (16% increase) + // log2(8) = 3, log2(9) = 3.16 (5% increase) + // log2(16) = 4, log2(17) = 4.08 (2% increase) + let count = (*count as f64 + 8.0).log2(); + let begin = (begin as f64 + 16.0).log2(); + let path = path_dist(history.cwd.as_ref(), state.context.cwd.as_ref()); + let path = (path as f64 + 8.0).log2(); + + // reduce longer durations, raise higher counts, raise matches close to the start + let score = (-score as f64) * count / path / duration / begin; + + 'insert: { + // algorithm: + // 1. find either the position that this command ranks + // 2. find the same command positioned better than our rank. + for i in 0..set.len() { + // do we out score the corrent position? + if ranks[i] > score { + ranks.insert(i, score); + set.insert(i, history.clone()); + let mut j = i + 1; + while j < set.len() { + // remove duplicates that have a worse score + if set[j].command == history.command { + ranks.remove(j); + set.remove(j); + + // break this while loop because there won't be any other + // duplicates. + break; + } + j += 1; + } + + // keep it limited + if ranks.len() > 200 { + ranks.pop(); + set.pop(); + } + + break 'insert; + } + // don't continue if this command has a better score already + if set[i].command == history.command { + break 'insert; + } + } + + if set.len() < 200 { + ranks.push(score); + set.push(history.clone()); + } + } + } + } + + set +} + +fn path_dist(a: &Path, b: &Path) -> usize { + let mut a: Vec<_> = a.components().collect(); + let b: Vec<_> = b.components().collect(); + + let mut dist = 0; + + // pop a until there's a common anscestor + while !b.starts_with(&a) { + dist += 1; + a.pop(); + } + + b.len() - a.len() + dist +} diff --git a/src/command/client/search/history_list.rs b/src/command/client/search/history_list.rs index 60ec15a..928fe4c 100644 --- a/src/command/client/search/history_list.rs +++ b/src/command/client/search/history_list.rs @@ -1,4 +1,4 @@ -use std::{sync::Arc, time::Duration}; +use std::time::Duration; use atuin_client::history::History; use ratatui::{ @@ -8,10 +8,10 @@ use ratatui::{ widgets::{Block, StatefulWidget, Widget}, }; -use super::{format_duration, interactive::HistoryWrapper}; +use super::format_duration; pub struct HistoryList<'a> { - history: &'a [Arc], + history: &'a [History], block: Option>, } @@ -77,7 +77,7 @@ impl<'a> StatefulWidget for HistoryList<'a> { } impl<'a> HistoryList<'a> { - pub fn new(history: &'a [Arc]) -> Self { + pub fn new(history: &'a [History]) -> Self { Self { history, block: None, diff --git a/src/command/client/search/interactive.rs b/src/command/client/search/interactive.rs index 6fd4e1d..a7ee9ba 100644 --- a/src/command/client/search/interactive.rs +++ b/src/command/client/search/interactive.rs @@ -1,7 +1,5 @@ use std::{ io::{stdout, Write}, - ops::Deref, - sync::Arc, time::Duration, }; @@ -12,21 +10,21 @@ use crossterm::{ use eyre::Result; use futures_util::FutureExt; use semver::Version; -use skim::SkimItem; use unicode_width::UnicodeWidthStr; use atuin_client::{ + database::current_context, database::Database, - database::{current_context, Context}, history::History, settings::{ExitMode, FilterMode, SearchMode, Settings}, }; use super::{ cursor::Cursor, + engines::{SearchEngine, SearchState}, history_list::{HistoryList, ListState, PREFIX_LENGTH}, }; -use crate::VERSION; +use crate::{command::client::search::engines, VERSION}; use ratatui::{ backend::{Backend, CrosstermBackend}, layout::{Alignment, Constraint, Direction, Layout}, @@ -43,68 +41,16 @@ struct State { history_count: i64, update_needed: Option, results_state: ListState, - search: SearchState, - - // only allocated if using skim - all_history: Vec>, -} - -pub struct SearchState { - pub input: Cursor, - pub filter_mode: FilterMode, - pub search_mode: SearchMode, - /// Store if the user has _just_ changed the search mode. - /// If so, we change the UI to show the search mode instead - /// of the filter mode until user starts typing again. switched_search_mode: bool, - pub context: Context, + search_mode: SearchMode, + + search: SearchState, + engine: Box, } impl State { - async fn query_results(&mut self, db: &mut impl Database) -> Result>> { - let i = self.search.input.as_str(); - let results = if i.is_empty() { - db.list( - self.search.filter_mode, - &self.search.context, - Some(200), - true, - ) - .await? - .into_iter() - .map(|history| HistoryWrapper { history, count: 1 }) - .map(Arc::new) - .collect::>() - } else if self.search.search_mode == SearchMode::Skim { - if self.all_history.is_empty() { - self.all_history = db - .all_with_count() - .await - .unwrap() - .into_iter() - .map(|(history, count)| HistoryWrapper { history, count }) - .map(Arc::new) - .collect::>(); - } - - super::skim_impl::fuzzy_search(&self.search, &self.all_history).await - } else { - db.search( - self.search.search_mode, - self.search.filter_mode, - &self.search.context, - i, - Some(200), - None, - None, - ) - .await? - .into_iter() - .map(|history| HistoryWrapper { history, count: 1 }) - .map(Arc::new) - .collect::>() - }; - + async fn query_results(&mut self, db: &mut dyn Database) -> Result> { + let results = self.engine.query(&self.search, db).await?; self.results_state.select(0); Ok(results) } @@ -154,7 +100,7 @@ impl State { let ctrl = input.modifiers.contains(KeyModifiers::CONTROL); let alt = input.modifiers.contains(KeyModifiers::ALT); // reset the state, will be set to true later if user really did change it - self.search.switched_search_mode = false; + self.switched_search_mode = false; match input.code { KeyCode::Char('c' | 'd' | 'g') if ctrl => return Some(RETURN_ORIGINAL), KeyCode::Esc => { @@ -228,8 +174,9 @@ impl State { self.search.filter_mode = FILTER_MODES[i]; } KeyCode::Char('s') if ctrl => { - self.search.switched_search_mode = true; - self.search.search_mode = self.search.search_mode.next(settings); + self.switched_search_mode = true; + self.search_mode = self.search_mode.next(settings); + self.engine = engines::engine(self.search_mode); } KeyCode::Down if self.results_state.selected() == 0 => { return Some(match settings.exit_mode { @@ -275,7 +222,7 @@ impl State { fn draw( &mut self, f: &mut Frame<'_, T>, - results: &[Arc], + results: &[History], compact: bool, show_preview: bool, ) { @@ -391,7 +338,7 @@ impl State { stats } - fn build_results_list(compact: bool, results: &[Arc]) -> HistoryList { + fn build_results_list(compact: bool, results: &[History]) -> HistoryList { let results_list = if compact { HistoryList::new(results) } else { @@ -407,8 +354,8 @@ impl State { fn build_input(&mut self, compact: bool, chunk_width: usize) -> Paragraph { /// Max width of the UI box showing current mode const MAX_WIDTH: usize = 14; - let (pref, mode) = if self.search.switched_search_mode { - (" SRCH:", self.search.search_mode.as_str()) + let (pref, mode) = if self.switched_search_mode { + (" SRCH:", self.search_mode.as_str()) } else { ("", self.search.filter_mode.as_str()) }; @@ -431,7 +378,7 @@ impl State { fn build_preview( &mut self, - results: &[Arc], + results: &[History], compact: bool, preview_width: u16, chunk_width: usize, @@ -513,23 +460,6 @@ impl Write for Stdout { } } -pub struct HistoryWrapper { - history: History, - pub count: i32, -} -impl Deref for HistoryWrapper { - type Target = History; - - fn deref(&self) -> &Self::Target { - &self.history - } -} -impl SkimItem for HistoryWrapper { - fn text(&self) -> std::borrow::Cow { - std::borrow::Cow::Borrowed(self.history.command.as_str()) - } -} - // this is a big blob of horrible! clean it up! // for now, it works. But it'd be great if it were more easily readable, and // modular. I'd like to add some more stats and stuff at some point @@ -537,7 +467,7 @@ impl SkimItem for HistoryWrapper { pub async fn history( query: &[String], settings: &Settings, - db: &mut impl Database, + mut db: impl Database, ) -> Result { let stdout = Stdout::new(settings.inline_height > 0)?; let backend = CrosstermBackend::new(stdout); @@ -562,10 +492,14 @@ pub async fn history( let context = current_context(); + let history_count = db.history_count().await?; + let mut app = State { - history_count: db.history_count().await?, + history_count, results_state: ListState::default(), update_needed: None, + switched_search_mode: false, + search_mode: settings.search_mode, search: SearchState { input, context, @@ -576,13 +510,11 @@ pub async fn history( } else { settings.filter_mode }, - search_mode: settings.search_mode, - switched_search_mode: false, }, - all_history: Vec::new(), + engine: engines::engine(settings.search_mode), }; - let mut results = app.query_results(db).await?; + let mut results = app.query_results(&mut db).await?; let index = 'render: loop { let compact = match settings.style { @@ -596,7 +528,7 @@ pub async fn history( let initial_input = app.search.input.as_str().to_owned(); let initial_filter_mode = app.search.filter_mode; - let initial_search_mode = app.search.search_mode; + let initial_search_mode = app.search_mode; let event_ready = tokio::task::spawn_blocking(|| event::poll(Duration::from_millis(250))); @@ -620,9 +552,9 @@ pub async fn history( if initial_input != app.search.input.as_str() || initial_filter_mode != app.search.filter_mode - || initial_search_mode != app.search.search_mode + || initial_search_mode != app.search_mode { - results = app.query_results(db).await?; + results = app.query_results(&mut db).await?; } }; @@ -632,7 +564,7 @@ pub async fn history( if index < results.len() { // index is in bounds so we return that entry - Ok(results.swap_remove(index).command.clone()) + Ok(results.swap_remove(index).command) } else if index == RETURN_ORIGINAL { Ok(String::new()) } else { diff --git a/src/command/client/search/skim_impl.rs b/src/command/client/search/skim_impl.rs deleted file mode 100644 index 416d0e4..0000000 --- a/src/command/client/search/skim_impl.rs +++ /dev/null @@ -1,92 +0,0 @@ -use std::sync::Arc; - -use atuin_client::settings::FilterMode; -use chrono::Utc; -use skim::{prelude::ExactOrFuzzyEngineFactory, MatchEngineFactory}; -use tokio::task::yield_now; - -use super::interactive::{HistoryWrapper, SearchState}; - -pub async fn fuzzy_search( - state: &SearchState, - all_history: &[Arc], -) -> Vec> { - let mut set = Vec::with_capacity(200); - let mut ranks = Vec::with_capacity(200); - let engine = ExactOrFuzzyEngineFactory::builder().fuzzy_algorithm(skim::FuzzyAlgorithm::SkimV2); - let query = state.input.as_str(); - let engine = engine.create_engine(query); - let now = Utc::now(); - - for (i, item) in all_history.iter().enumerate() { - if i % 256 == 0 { - yield_now().await; - } - match state.filter_mode { - FilterMode::Global => {} - FilterMode::Host if item.hostname == state.context.hostname => {} - FilterMode::Session if item.session == state.context.session => {} - FilterMode::Directory if item.cwd == state.context.cwd => {} - _ => continue, - } - #[allow(clippy::cast_lossless, clippy::cast_precision_loss)] - if let Some(res) = engine.match_item(item.clone()) { - let [score, begin, _, _] = res.rank; - - let mut duration = ((now - item.timestamp).num_seconds() as f64).log2(); - if !duration.is_finite() || duration <= 1.0 { - duration = 1.0; - } - let count = (item.count as f64 + 16.0).log2(); - let begin = (begin as f64 + 16.0).log2(); - - // reduce longer durations, raise higher counts, raise matches close to the start - let score = (score as f64) * count / duration / begin; - - 'insert: { - // algorithm: - // 1. find either the position that this command ranks - // 2. find the same command positioned better than our rank. - for i in 0..set.len() { - // do we out score the corrent position? - if ranks[i] > score { - ranks.insert(i, score); - set.insert(i, item.clone()); - let mut j = i + 1; - while j < set.len() { - // remove duplicates that have a worse score - if set[j].command == item.command { - ranks.remove(j); - set.remove(j); - - // break this while loop because there won't be any other - // duplicates. - break; - } - j += 1; - } - - // keep it limited - if ranks.len() > 200 { - ranks.pop(); - set.pop(); - } - - break 'insert; - } - // don't continue if this command has a better score already - if set[i].command == item.command { - break 'insert; - } - } - - if set.len() < 200 { - ranks.push(score); - set.push(item.clone()); - } - } - } - } - - set -}