From 9d348a2f31a7b68e4fb56ef5c60c9735b8188d27 Mon Sep 17 00:00:00 2001 From: Loric ANDRE Date: Mon, 9 Mar 2026 15:07:37 +0100 Subject: [PATCH 1/6] chore: remove skim::Item run_items wrapper --- examples/custom_action_keybinding.rs | 22 ++++--- src/prelude.rs | 1 - src/skim.rs | 3 +- src/skim_item.rs | 41 ------------ src/tui/event.rs | 97 +++++++++++++++++++++++----- 5 files changed, 95 insertions(+), 69 deletions(-) diff --git a/examples/custom_action_keybinding.rs b/examples/custom_action_keybinding.rs index c35a122b..6ff4aa8c 100644 --- a/examples/custom_action_keybinding.rs +++ b/examples/custom_action_keybinding.rs @@ -7,12 +7,13 @@ use std::io::Cursor; /// This example demonstrates how to bind custom action callbacks to keyboard shortcuts. /// /// It shows how to: -/// 1. Create custom action callbacks +/// 1. Create custom action callbacks (both sync and async) /// 2. Bind them to specific key combinations /// 3. Use them interactively in skim fn main() { - // Create a custom callback that adds a prefix to the query - let add_prefix_callback = ActionCallback::new(|app: &mut skim::tui::App| { + // Create a synchronous callback that adds a prefix to the query. + // Use `new_sync` for plain closures that do not need to await anything. + let add_prefix_callback = ActionCallback::new_sync(|app: &mut skim::tui::App| { // Get current query and add prefix let current_query = app.input.value.clone(); @@ -32,14 +33,17 @@ fn main() { Ok(events) }); - // Create a callback that selects all and exits + // Create an async callback that selects all and exits. + // Use `new` for async closures or blocks that may await futures. let select_all_callback = ActionCallback::new(|app: &mut skim::tui::App| { let count = app.item_pool.len(); - - Ok(vec![ - Event::Action(Action::SelectAll), - Event::Action(Action::Accept(Some(format!("Selected {count} items")))), - ]) + async move { + // Async work could go here (e.g. HTTP requests, file I/O, …). + Ok(vec![ + Event::Action(Action::SelectAll), + Event::Action(Action::Accept(Some(format!("Selected {count} items")))), + ]) + } }); // Build basic options diff --git a/src/prelude.rs b/src/prelude.rs index a83b126c..bc175d6a 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -13,7 +13,6 @@ pub use crate::helper::selector::DefaultSkimSelector; pub use crate::options::{SkimOptions, SkimOptionsBuilder}; pub use crate::output::SkimOutput; pub use crate::reader::CommandCollector; -pub use crate::skim_item::Item; pub use crate::tui::{Event, PreviewCallback, event::Action}; pub use crate::*; pub use kanal::{Receiver, Sender, bounded, unbounded}; diff --git a/src/skim.rs b/src/skim.rs index 82c2b7d5..1d1599e0 100644 --- a/src/skim.rs +++ b/src/skim.rs @@ -95,12 +95,11 @@ impl Skim { const BATCH_SIZE: usize = 1024; let (tx, rx) = crate::prelude::unbounded(); let mut batch: Vec> = Vec::with_capacity(BATCH_SIZE); - for (idx, raw_item) in items.into_iter().enumerate() { + for (idx, mut item) in items.into_iter().enumerate() { if batch.len() == 1024 { tx.send(batch)?; batch = Vec::with_capacity(BATCH_SIZE); } - let mut item = crate::prelude::Item::from(raw_item); item.set_index(idx); batch.push(Arc::new(item) as Arc); } diff --git a/src/skim_item.rs b/src/skim_item.rs index 8fac4843..e9783b43 100644 --- a/src/skim_item.rs +++ b/src/skim_item.rs @@ -97,47 +97,6 @@ impl + Send + Sync + 'static> SkimItem for T { } } -/// A basic SkimItem implementation for basic types -pub struct Item { - inner: T, - index: usize, -} - -impl SkimItem for Item { - fn text(&self) -> Cow<'_, str> { - self.inner.text() - } - fn display<'a>(&'a self, context: DisplayContext) -> Line<'a> { - self.inner.display(context) - } - - fn preview(&self, context: PreviewContext) -> ItemPreview { - self.inner.preview(context) - } - - fn output(&self) -> Cow<'_, str> { - self.inner.output() - } - - fn get_matching_ranges(&self) -> Option<&[(usize, usize)]> { - self.inner.get_matching_ranges() - } - - fn get_index(&self) -> usize { - self.index - } - - fn set_index(&mut self, index: usize) { - self.index = index; - } -} - -impl From for Item { - fn from(value: T) -> Self { - Self { inner: value, index: 0 } - } -} - impl Display for dyn SkimItem { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&self.text()) diff --git a/src/tui/event.rs b/src/tui/event.rs index ee25c9ba..f5f11016 100644 --- a/src/tui/event.rs +++ b/src/tui/event.rs @@ -1,18 +1,57 @@ +use std::future::Future; +use std::pin::Pin; use std::sync::{Arc, Mutex}; use crate::exhaustive_match; use crossterm::event::{KeyEvent, MouseEvent}; use derive_more::{Debug, Eq, PartialEq}; -type ActionCallbackFn = - dyn Fn(&mut crate::tui::App) -> Result, Box> + Send; +type BoxError = Box; +type BoxFuture<'a> = Pin, BoxError>> + Send + 'a>>; + +/// Trait object stored inside [`ActionCallback`]. +/// +/// Having an explicit trait (rather than a bare `dyn Fn` type alias) allows +/// Rust to correctly resolve the higher-ranked lifetime in the return type. +trait AsyncCallbackFn: Send { + fn call<'a>(&'a self, app: &'a mut crate::tui::App) -> BoxFuture<'a>; +} + +/// Adapter that stores a concrete async closure and implements [`AsyncCallbackFn`]. +struct AsyncFnWrapper(F); + +impl AsyncCallbackFn for AsyncFnWrapper +where + F: for<'a> Fn(&'a mut crate::tui::App) -> Fut + Send, + Fut: Future, BoxError>> + Send + 'static, +{ + fn call<'a>(&'a self, app: &'a mut crate::tui::App) -> BoxFuture<'a> { + Box::pin((self.0)(app)) + } +} + +/// Adapter that stores a plain synchronous closure and implements [`AsyncCallbackFn`]. +struct SyncFnWrapper(F); + +impl AsyncCallbackFn for SyncFnWrapper +where + F: Fn(&mut crate::tui::App) -> Result, BoxError> + Send, +{ + fn call<'a>(&'a self, app: &'a mut crate::tui::App) -> BoxFuture<'a> { + Box::pin(std::future::ready((self.0)(app))) + } +} /// A custom action callback that receives a mutable reference to the App. /// /// The closure will be called with a mutable reference to App and should return /// a vec of events that will be processed after the callback completes. +/// +/// Both sync and async closures are supported: +/// - Use [`ActionCallback::new`] to wrap an **async** closure or block. +/// - Use [`ActionCallback::new_sync`] to wrap a plain synchronous closure. #[derive(Clone)] -pub struct ActionCallback(Arc>); +pub struct ActionCallback(Arc>); impl std::fmt::Debug for ActionCallback { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -21,24 +60,50 @@ impl std::fmt::Debug for ActionCallback { } impl ActionCallback { - /// Create a new action callback from a closure. + /// Create a new action callback from an **async** closure or block. /// - /// The closure will be called with a mutable reference to App and should return a vec of - /// events that will be run after the callback is done. - pub fn new(f: F) -> Self + /// ```rust,ignore + /// ActionCallback::new(|app| async move { + /// // async work here … + /// Ok(vec![]) + /// }); + /// ``` + pub fn new(f: F) -> Self where - F: Fn(&mut crate::tui::App) -> Result, Box> + Send + 'static, + F: for<'a> Fn(&'a mut crate::tui::App) -> Fut + Send + 'static, + Fut: Future, BoxError>> + Send + 'static, { - Self(Arc::new(Mutex::new(f))) + Self(Arc::new(Mutex::new(AsyncFnWrapper(f)))) } - /// Call the callback with an App reference. - pub(crate) fn call( - &self, - app: &mut crate::tui::App, - ) -> Result, Box> { + /// Create a new action callback from a plain **synchronous** closure. + /// + /// This is a convenience wrapper; the closure is lifted into an immediately- + /// resolving future so it integrates with the same async call site. + /// + /// ```rust,ignore + /// ActionCallback::new_sync(|app| { + /// Ok(vec![Event::Action(Action::SelectAll)]) + /// }); + /// ``` + pub fn new_sync(f: F) -> Self + where + F: Fn(&mut crate::tui::App) -> Result, BoxError> + Send + 'static, + { + Self(Arc::new(Mutex::new(SyncFnWrapper(f)))) + } + + /// Call the callback with an App reference, driving the returned future to completion. + /// + /// Must be called from within a Tokio multi-thread runtime context. + pub(crate) fn call(&self, app: &mut crate::tui::App) -> Result, BoxError> { let callback = self.0.lock().unwrap(); - callback(app) + let fut = callback.call(app); + // We are inside a synchronous call stack that originates from an async + // tokio context. `block_in_place` moves the current thread out of the + // async worker pool temporarily so we can block on the future without + // starving the runtime. + tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(fut)) } } @@ -349,7 +414,7 @@ pub fn parse_action(raw_action: &str) -> Option { "unreachable-if-non-matched" => Some(IfNonMatched(Default::default(), None)), "unreachable-if-query-empty" => Some(IfQueryEmpty(Default::default(), None)), "unreachable-if-query-not-empty" => Some(IfQueryNotEmpty(Default::default(), None)), - "custom-do-not-use-from-cli" => Some(Custom(ActionCallback::new(|_: &mut crate::tui::App| { Ok(Vec::new()) }))), + "custom-do-not-use-from-cli" => Some(Custom(ActionCallback::new_sync(|_: &mut crate::tui::App| { Ok(Vec::new()) }))), } default _ => None } From 70d591e8b1643174f8d1c08386820cf1232ce040 Mon Sep 17 00:00:00 2001 From: Loric ANDRE Date: Mon, 9 Mar 2026 16:03:54 +0100 Subject: [PATCH 2/6] fix: properly trigger re-render on custom previews --- examples/custom_item.rs | 4 +++- src/tui/app.rs | 9 ++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/examples/custom_item.rs b/examples/custom_item.rs index ce825dc6..62b07787 100644 --- a/examples/custom_item.rs +++ b/examples/custom_item.rs @@ -23,10 +23,12 @@ fn main() { let options = SkimOptionsBuilder::default() .height("50%") .multi(true) - .preview(String::new()) // preview should be specified to enable preview window + .preview("") // preview should be specified to enable preview window .build() .unwrap(); + env_logger::init(); + let (tx_item, rx_item): (SkimItemSender, SkimItemReceiver) = unbounded(); let _ = tx_item.send(vec![ Arc::new(MyItem { diff --git a/src/tui/app.rs b/src/tui/app.rs index efe44d93..797ec745 100644 --- a/src/tui/app.rs +++ b/src/tui/app.rs @@ -428,6 +428,10 @@ impl App { selections: &selection_str, }; let preview = item.preview(ctx); + let preview_ready = !matches!( + preview, + ItemPreview::Global | ItemPreview::Command(_) | ItemPreview::CommandWithPos(_, _) + ); match preview { ItemPreview::Command(cmd) => self.preview.spawn(tui, &self.expand_cmd(&cmd, true))?, ItemPreview::Text(t) | ItemPreview::AnsiText(t) => self.preview.content(t.bytes().collect())?, @@ -464,6 +468,9 @@ impl App { .content_with_position(t.bytes().collect(), preview_position)?, ItemPreview::Global => self.preview.spawn(tui, &self.expand_cmd(preview_opt, true))?, } + if preview_ready { + let _ = tui.event_tx.try_send(Event::PreviewReady); + } } else if let Some(cb) = &self.options.preview_fn { let selection: Vec>; if self.options.multi { @@ -537,7 +544,7 @@ impl App { let offset = self.calculate_preview_offset(offset_expr); self.preview.set_offset(offset); } - tui.event_tx.try_send(Event::Render)?; + self.needs_render(); } Event::Error(msg) => { tui.exit()?; From 1d5445f548da35b079429b0d410843d1761ad0a6 Mon Sep 17 00:00:00 2001 From: Loric ANDRE Date: Mon, 9 Mar 2026 17:10:26 +0100 Subject: [PATCH 3/6] feat: add AppendItems event --- src/tui/app.rs | 4 ++++ src/tui/event.rs | 2 ++ 2 files changed, 6 insertions(+) diff --git a/src/tui/app.rs b/src/tui/app.rs index 797ec745..95f7dd84 100644 --- a/src/tui/app.rs +++ b/src/tui/app.rs @@ -594,6 +594,10 @@ impl App { self.item_pool.clear(); self.restart_matcher(true); } + Event::AppendItems(items) => { + self.item_pool.append(items.to_owned()); + self.restart_matcher(false); + } Event::Reload(_) => { unreachable!("Reload is handled by the TUI event loop in lib.rs") } diff --git a/src/tui/event.rs b/src/tui/event.rs index f5f11016..96d3e6ea 100644 --- a/src/tui/event.rs +++ b/src/tui/event.rs @@ -132,6 +132,8 @@ pub enum Event { InvalidInput, /// An action was triggered Action(Action), + /// Append items to the pool + AppendItems(Vec>), /// Clear all items ClearItems, /// Clear the screen From 9c898e402b09f6b844d708f3283f955cbff5c530 Mon Sep 17 00:00:00 2001 From: Loric ANDRE Date: Tue, 10 Mar 2026 10:39:56 +0100 Subject: [PATCH 4/6] feat!: internally compute indexes at match time (removes get/set_index) --- examples/base.rs | 2 +- examples/downcast.rs | 26 ++--------- src/bin/main.rs | 2 +- src/engine/all.rs | 2 +- src/engine/exact.rs | 4 +- src/engine/fuzzy.rs | 8 +--- src/engine/regexp.rs | 4 +- src/fuzzy_matcher/arinae/constants.rs | 2 +- src/helper/item.rs | 22 --------- src/helper/item_reader.rs | 7 +-- src/item.rs | 16 +++++-- src/matcher.rs | 8 +++- src/options.rs | 2 + src/output.rs | 6 +-- src/skim.rs | 3 +- src/skim_item.rs | 17 +------ src/tmux.rs | 13 ++++-- src/tui/app.rs | 37 +++++++--------- src/tui/item_list.rs | 27 +++++++---- src/util.rs | 64 +++++++++++++++------------ tests/common/insta.rs | 56 +++++++++++++++-------- 21 files changed, 154 insertions(+), 174 deletions(-) diff --git a/examples/base.rs b/examples/base.rs index 597f89cf..7a9b5420 100644 --- a/examples/base.rs +++ b/examples/base.rs @@ -5,7 +5,7 @@ fn main() -> color_eyre::Result<()> { let res = Skim::run_items(opts, ["hello", "world"])?; for item in res.selected_items { - println!("Selected {} (id {})", item.output(), item.get_index()); + println!("Selected {} (id {})", item.output(), item.rank.index); } Ok(()) diff --git a/examples/downcast.rs b/examples/downcast.rs index 9f9d14bc..26c4e012 100644 --- a/examples/downcast.rs +++ b/examples/downcast.rs @@ -7,7 +7,6 @@ use skim::prelude::*; #[derive(Debug, Clone)] struct Item { text: String, - index: usize, } impl SkimItem for Item { @@ -18,14 +17,6 @@ impl SkimItem for Item { fn preview(&self, _context: PreviewContext) -> ItemPreview { ItemPreview::Text(self.text.to_owned()) } - - fn get_index(&self) -> usize { - self.index - } - - fn set_index(&mut self, index: usize) { - self.index = index - } } pub fn main() { @@ -39,18 +30,9 @@ pub fn main() { let (tx, rx): (SkimItemSender, SkimItemReceiver) = unbounded(); tx.send(vec![ - Arc::new(Item { - text: "a".into(), - index: 0, - }) as Arc, - Arc::new(Item { - text: "b".into(), - index: 1, - }) as Arc, - Arc::new(Item { - text: "c".into(), - index: 2, - }) as Arc, + Arc::new(Item { text: "a".into() }) as Arc, + Arc::new(Item { text: "b".into() }) as Arc, + Arc::new(Item { text: "c".into() }) as Arc, ]) .unwrap(); @@ -60,7 +42,7 @@ pub fn main() { .map(|out| out.selected_items) .unwrap_or_default() .iter() - .map(|selected_item| (**selected_item).as_any().downcast_ref::().unwrap().to_owned()) + .map(|selected_item| selected_item.downcast_item::().unwrap().to_owned()) .collect::>(); for item in selected_items { diff --git a/src/bin/main.rs b/src/bin/main.rs index e72f7cb3..c58c5fbe 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -182,7 +182,7 @@ fn sk_main(mut opts: SkimOptions) -> Result { output_format, &bin_options.delimiter, &bin_options.replstr, - result.selected_items.iter().map(|x| x.item.clone()), + result.selected_items.iter(), result.current, &result.query, &result.cmd, diff --git a/src/engine/all.rs b/src/engine/all.rs index 845b026b..3c9a639f 100644 --- a/src/engine/all.rs +++ b/src/engine/all.rs @@ -31,7 +31,7 @@ impl MatchEngine for MatchAllEngine { fn match_item(&self, item: &dyn SkimItem) -> Option { let item_text = item.text(); Some(MatchResult { - rank: self.rank_builder.build_rank(0, 0, 0, &item_text, item.get_index()), + rank: self.rank_builder.build_rank(0, 0, 0, &item_text), matched_range: MatchRange::ByteRange(0, 0), }) } diff --git a/src/engine/exact.rs b/src/engine/exact.rs index 24025f61..7aae2196 100644 --- a/src/engine/exact.rs +++ b/src/engine/exact.rs @@ -101,9 +101,7 @@ impl MatchEngine for ExactEngine { let (begin, end) = matched_result?; let score = (end - begin) as i32; Some(MatchResult { - rank: self - .rank_builder - .build_rank(score, begin, end, &item_text, item.get_index()), + rank: self.rank_builder.build_rank(score, begin, end, &item_text), matched_range: MatchRange::ByteRange(begin, end), }) } diff --git a/src/engine/fuzzy.rs b/src/engine/fuzzy.rs index 5e2ea450..1161705f 100644 --- a/src/engine/fuzzy.rs +++ b/src/engine/fuzzy.rs @@ -215,9 +215,7 @@ impl MatchEngine for FuzzyEngine { let (score, begin, end) = best?; Some(MatchResult { - rank: self - .rank_builder - .build_rank(score as i32, begin, end, &item_text, item.get_index()), + rank: self.rank_builder.build_rank(score as i32, begin, end, &item_text), matched_range: MatchRange::ByteRange(begin, end), }) } else { @@ -254,9 +252,7 @@ impl MatchEngine for FuzzyEngine { let matched_range = MatchRange::Chars(matched_indices); Some(MatchResult { - rank: self - .rank_builder - .build_rank(score as i32, begin, end, &item_text, item.get_index()), + rank: self.rank_builder.build_rank(score as i32, begin, end, &item_text), matched_range, }) } diff --git a/src/engine/regexp.rs b/src/engine/regexp.rs index 7e7963fe..27bba4d5 100644 --- a/src/engine/regexp.rs +++ b/src/engine/regexp.rs @@ -70,9 +70,7 @@ impl MatchEngine for RegexEngine { let score = (end - begin) as i32; Some(MatchResult { - rank: self - .rank_builder - .build_rank(score, begin, end, &item_text, item.get_index()), + rank: self.rank_builder.build_rank(score, begin, end, &item_text), matched_range: MatchRange::ByteRange(begin, end), }) } diff --git a/src/fuzzy_matcher/arinae/constants.rs b/src/fuzzy_matcher/arinae/constants.rs index 7979eed0..eaf5952e 100644 --- a/src/fuzzy_matcher/arinae/constants.rs +++ b/src/fuzzy_matcher/arinae/constants.rs @@ -45,7 +45,7 @@ pub(super) const TYPO_BAND_SLACK: usize = 4; /// (standard bonus). Entries that are `0` are not considered separators. pub(super) const SEPARATOR_TABLE: [Score; 128] = { let mut t = [0 as Score; 128]; - t[b' ' as usize] = 12; // space + t[b' ' as usize] = 16; // space t[b'-' as usize] = 10; // hyphen / kebab-case t[b'.' as usize] = 12; // dot (file extensions, domain names) t[b'/' as usize] = 16; // forward slash (path separator — higher bonus) diff --git a/src/helper/item.rs b/src/helper/item.rs index 0c89ca58..179ce422 100644 --- a/src/helper/item.rs +++ b/src/helper/item.rs @@ -24,9 +24,6 @@ pub struct DefaultSkimItem { /// The text that will be shown on screen. text: Box, - /// The index, for use in matching - index: usize, - /// Metadata containing miscellaneous fields when special options are used metadata: Option>, } @@ -60,7 +57,6 @@ impl DefaultSkimItem { trans_fields: &[FieldRange], matching_fields: &[FieldRange], delimiter: &Regex, - index: usize, ) -> Self { let using_transform_fields = !trans_fields.is_empty(); let contains_ansi = Self::contains_ansi_escape(orig_text); @@ -169,7 +165,6 @@ impl DefaultSkimItem { DefaultSkimItem { text: temp_text, - index, metadata, } } @@ -433,14 +428,6 @@ impl SkimItem for DefaultSkimItem { context.to_line(Cow::Borrowed(&self.text)) } } - - fn get_index(&self) -> usize { - self.index - } - - fn set_index(&mut self, index: usize) { - self.index = index; - } } /// Strip ANSI escape sequences from a string @@ -651,7 +638,6 @@ mod test { &[], &[], &delimiter, - 0, ); // text() should return stripped text for matching @@ -693,7 +679,6 @@ mod test { &[], &[], &delimiter, - 0, ); // text() should return "😀text" @@ -727,7 +712,6 @@ mod test { &[], &[], &delimiter, - 0, ); assert_eq!( item_ansi.text(), @@ -742,7 +726,6 @@ mod test { &[], &[], &delimiter, - 0, ); assert_eq!( item_no_ansi.text(), @@ -766,7 +749,6 @@ mod test { &[], &[], &delimiter, - 0, ); // Create display context with yellow background highlight for character 0 (the 'g') @@ -805,7 +787,6 @@ mod test { &[], &[], &delimiter, - 0, ); // Create display context with yellow background highlight for characters 1-3 ('re') @@ -846,7 +827,6 @@ mod test { &[], &[], &delimiter, - 0, ); // Create display context with yellow background highlight for bytes 1-3 ('re' in stripped text) @@ -886,7 +866,6 @@ mod test { &[], &[], // no matching fields restriction &delimiter, - 0, ); // text() should return stripped text "green_text" @@ -918,7 +897,6 @@ mod test { &[], // no transform fields &[FieldRange::Single(2)], // match field 2 &delimiter, - 0, ); // text() should return text with null bytes stripped for display diff --git a/src/helper/item_reader.rs b/src/helper/item_reader.rs index 9ea44465..b7e157c9 100644 --- a/src/helper/item_reader.rs +++ b/src/helper/item_reader.rs @@ -203,7 +203,6 @@ impl SkimItemReader { matching_fields: Vec, ) { let mut buffer = Vec::with_capacity(option.buf_size); - let mut line_idx = 0; let mut items_to_send = Vec::with_capacity(ITEMS_BUFFER_SIZE); let mut last_send_time = Instant::now(); let send_timeout = Duration::from_millis(SEND_TIMEOUT_MS); @@ -227,7 +226,7 @@ impl SkimItemReader { continue; }; - trace!("got item {} with index {}", line, line_idx); + trace!("got item {}", line); let raw_item = DefaultSkimItem::new( line, @@ -235,11 +234,8 @@ impl SkimItemReader { &transform_fields, &matching_fields, &option.delimiter, - line_idx, ); items_to_send.push(Arc::new(raw_item) as Arc); - - line_idx += 1; } Err(err) => { trace!("Got {err:?} when reading, skipping"); @@ -332,7 +328,6 @@ impl SkimItemReader { &[], &[], &Regex::new(DELIMITER_STR).unwrap(), - 0, )) as Arc }) .collect(); diff --git a/src/item.rs b/src/item.rs index 3e1c0b06..81109716 100644 --- a/src/item.rs +++ b/src/item.rs @@ -60,13 +60,14 @@ impl RankBuilder { /// /// The values are stored as-is; the tiebreak ordering and sign-flipping are /// applied lazily by [`Rank::sort_key`] at comparison time. - pub fn build_rank(&self, score: i32, begin: usize, end: usize, item_text: &str, index: usize) -> Rank { + /// The `index` will be overriden later + pub fn build_rank(&self, score: i32, begin: usize, end: usize, item_text: &str) -> Rank { Rank { score, begin: begin as i32, end: end as i32, length: item_text.len() as i32, - index: index as i32, + index: Default::default(), path_name_offset: Self::path_name_offset(item_text), } } @@ -131,7 +132,7 @@ impl std::fmt::Debug for MatchedItem { impl Hash for MatchedItem { fn hash(&self, state: &mut H) { - state.write_usize(self.get_index()); + state.write_i32(self.rank.index); self.text().hash(state); } } @@ -244,11 +245,18 @@ impl MatchedItem { } } +impl MatchedItem { + /// Downcast the MatchedItem to the corresponding SkimItem struct + pub fn downcast_item(&self) -> Option<&T> { + (*self.item).as_any().downcast_ref::() + } +} + use std::cmp::Ordering as CmpOrd; impl PartialEq for MatchedItem { fn eq(&self, other: &Self) -> bool { - self.text().eq(&other.text()) && self.get_index().eq(&other.get_index()) + self.text().eq(&other.text()) && self.rank.index.eq(&other.rank.index) } } diff --git a/src/matcher.rs b/src/matcher.rs index 4795d271..17b7c17b 100644 --- a/src/matcher.rs +++ b/src/matcher.rs @@ -193,6 +193,7 @@ impl Matcher { // if we took items inside the spawned closure, a subsequent restart_matcher() // could call kill() + reset() before the old closure runs, causing the old // closure to re-take items that should belong to the new matcher. + let start = item_pool.num_taken(); let items = item_pool.take(); let total = items.len(); trace!("matcher start, total: {}", total); @@ -212,9 +213,10 @@ impl Matcher { let matched_items: Vec = items .into_par_iter() .with_min_len(CHUNK_SIZE) + .enumerate() .fold( || (Vec::new(), 0usize, 0usize), // (local_matches, local_processed, local_matched) - |(mut local_matches, mut local_processed, mut local_matched), item| { + |(mut local_matches, mut local_processed, mut local_matched), (index, item)| { // Check interrupt once at the start of each chunk boundary. // The fold processes items sequentially within each rayon work unit, // so checking every CHUNK_SIZE items amortizes the atomic load. @@ -226,9 +228,11 @@ impl Matcher { if let Some(match_result) = matcher_engine.match_item(item.as_ref()) { local_matched += 1; + let mut rank = match_result.rank; + rank.index = (index + start) as i32; local_matches.push(MatchedItem { item, - rank: match_result.rank, + rank, rank_builder: rank_builder.clone(), matched_range: Some(match_result.matched_range), }); diff --git a/src/options.rs b/src/options.rs index 5342aad2..4c4e8663 100644 --- a/src/options.rs +++ b/src/options.rs @@ -1275,6 +1275,8 @@ pub enum FeatureFlag { NoPreviewPty, /// Display the item's match score before its value in the item list (for matcher debugging) ShowScore, + /// Display the item's index before its value in the item list + ShowIndex, } #[allow(unused_macros)] diff --git a/src/output.rs b/src/output.rs index 57cabbb9..23d2f20d 100644 --- a/src/output.rs +++ b/src/output.rs @@ -1,7 +1,5 @@ -use crate::SkimItem; use crate::item::MatchedItem; use crate::tui::Event; -use std::sync::Arc; /// Output from running skim, containing the final selection and state #[derive(Debug)] @@ -25,10 +23,10 @@ pub struct SkimOutput { pub cmd: String, /// The selected items. - pub selected_items: Vec>, + pub selected_items: Vec, /// The current item - pub current: Option>, + pub current: Option, /// The header pub header: String, diff --git a/src/skim.rs b/src/skim.rs index 1d1599e0..56c4d703 100644 --- a/src/skim.rs +++ b/src/skim.rs @@ -95,12 +95,11 @@ impl Skim { const BATCH_SIZE: usize = 1024; let (tx, rx) = crate::prelude::unbounded(); let mut batch: Vec> = Vec::with_capacity(BATCH_SIZE); - for (idx, mut item) in items.into_iter().enumerate() { + for item in items { if batch.len() == 1024 { tx.send(batch)?; batch = Vec::with_capacity(BATCH_SIZE); } - item.set_index(idx); batch.push(Arc::new(item) as Arc); } tx.send(batch)?; diff --git a/src/skim_item.rs b/src/skim_item.rs index e9783b43..5d3f4819 100644 --- a/src/skim_item.rs +++ b/src/skim_item.rs @@ -75,17 +75,6 @@ pub trait SkimItem: AsAny + Send + Sync + 'static { fn get_matching_ranges(&self) -> Option<&[(usize, usize)]> { None } - - /// Get index, for matching purposes - /// - /// Implemented as no-op for retro-compatibility purposes - fn get_index(&self) -> usize { - 0 - } - /// Set index, for matching purposes - /// - /// Implemented as no-op for retro-compatibility purposes - fn set_index(&mut self, _index: usize) {} } //------------------------------------------------------------------------------ @@ -104,10 +93,6 @@ impl Display for dyn SkimItem { } impl Debug for dyn SkimItem { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!( - "SkimItem {{ text: {}, index: {} }}", - self.text(), - self.get_index() - )) + f.write_fmt(format_args!("SkimItem {{ text: {} }}", self.text(),)) } } diff --git a/src/tmux.rs b/src/tmux.rs index 2b03f5e0..19f48f8f 100644 --- a/src/tmux.rs +++ b/src/tmux.rs @@ -284,18 +284,23 @@ pub fn run_with(opts: &SkimOptions) -> Option { } .to_string(); - let current: Option> = if status.success() { + let current: Option = if status.success() { let line = stdout.next().unwrap_or_default(); if line.is_empty() { None } else { - Some(Arc::new(SkimTmuxOutput { line: line.to_string() })) + Some(MatchedItem { + item: Arc::new(SkimTmuxOutput { line: line.to_string() }), + rank: Rank::default(), + rank_builder: Arc::new(RankBuilder::default()), + matched_range: None, + }) } } else { None }; - let mut output_lines: Vec> = vec![]; + let mut output_lines: Vec = vec![]; while let Some(line) = stdout.next() { debug!("Adding output line: {line}"); // --print-score is always enabled in the child, so every item is followed by its score. @@ -309,7 +314,7 @@ pub fn run_with(opts: &SkimOptions) -> Option { rank_builder: Arc::new(RankBuilder::default()), matched_range: None, }; - output_lines.push(Arc::new(item)); + output_lines.push(item); } let is_abort = !status.success(); diff --git a/src/tui/app.rs b/src/tui/app.rs index 95f7dd84..9dfbf599 100644 --- a/src/tui/app.rs +++ b/src/tui/app.rs @@ -12,8 +12,8 @@ use crate::tui::layout::{AppLayout, LayoutTemplate}; use crate::tui::options::TuiLayout; use crate::tui::statusline::InfoDisplay; use crate::tui::widget::SkimWidget; -use crate::util; use crate::{ItemPreview, PreviewContext, SkimItem, SkimOptions}; +use crate::{Rank, util}; use super::Event; use super::Tui; @@ -404,6 +404,7 @@ impl App { { let selection: Vec<_> = self.item_list.selection.iter().map(|i| i.text().into_owned()).collect(); let selection_str: Vec<_> = selection.iter().map(|s| s.as_str()).collect(); + let selected = self.item_list.selected(); let ctx = PreviewContext { query: &self.input.value, cmd_query: if self.options.interactive { @@ -413,17 +414,13 @@ impl App { }, width: self.preview.cols as usize, height: self.preview.rows as usize, - current_index: self.item_list.selected().map(|i| i.get_index()).unwrap_or_default(), - current_selection: &self - .item_list - .selected() - .map(|i| i.text().into_owned()) - .unwrap_or_default(), + current_index: selected.as_ref().map(|i| i.rank.index as usize).unwrap_or_default(), + current_selection: &selected.map(|i| i.text().into_owned()).unwrap_or_default(), selected_indices: &self .item_list .selection .iter() - .map(|v| v.get_index()) + .map(|v| v.rank.index as usize) .collect::>(), selections: &selection_str, }; @@ -476,7 +473,7 @@ impl App { if self.options.multi { selection = self.item_list.selection.iter().map(|i| i.item.clone()).collect(); } else if let Some(sel) = self.item_list.selected() { - selection = vec![sel]; + selection = vec![sel.item]; } else { selection = Vec::new(); } @@ -655,10 +652,14 @@ impl App { AppendAndSelect => { let value = self.input.value.clone(); let item: Arc = Arc::new(value); + let rank = Rank { + index: self.item_pool.len() as i32, + ..Default::default() + }; self.item_pool.append(vec![item.clone()]); self.item_list.append(&mut vec![MatchedItem { item, - rank: Default::default(), + rank, rank_builder: self.matcher.rank_builder.clone(), matched_range: None, }]); @@ -1112,18 +1113,14 @@ impl App { } /// Returns the selected items as results - pub fn results(&mut self) -> Vec> { + pub fn results(&mut self) -> Vec { if self.options.filter.is_some() { // In filter mode, drain items to avoid cloning - self.item_list.items.drain(..).map(Arc::new).collect() + self.item_list.items.drain(..).collect() } else if self.options.multi && !self.item_list.selection.is_empty() { - self.item_list - .selection - .iter() - .map(|item| Arc::new(item.clone())) - .collect() - } else if let Some(sel) = self.item_list.items.get(self.item_list.current) { - vec![Arc::new(sel.clone())] + self.item_list.selection.clone().into_iter().collect() + } else if let Some(sel) = self.item_list.selected() { + vec![sel] } else { vec![] } @@ -1235,7 +1232,7 @@ impl App { cmd, &self.options.delimiter, &self.options.replstr, - self.item_list.selection.iter().map(|x| x.item.clone()), + self.item_list.selection.iter(), self.item_list.selected(), &self.input.value, &self.input.value, diff --git a/src/tui/item_list.rs b/src/tui/item_list.rs index 89ce160d..084d7151 100644 --- a/src/tui/item_list.rs +++ b/src/tui/item_list.rs @@ -6,9 +6,10 @@ use ratatui::widgets::{Block, Borders, Clear, List, ListDirection, ListItem, Lis use regex::Regex; use unicode_display_width::width as display_width; +use crate::options::feature_flag; use crate::tui::util::char_display_width; use crate::{ - DisplayContext, MatchRange, Selector, SkimItem, SkimOptions, + DisplayContext, MatchRange, Selector, SkimOptions, item::MatchedItem, spinlock::SpinLock, theme::ColorTheme, @@ -75,7 +76,8 @@ pub struct ItemList { /// Border type, if borders are enabled pub border: Option, /// When true, prepend each item's match score to its display text - print_score: bool, + show_score: bool, + show_index: bool, } impl Default for ItemList { @@ -109,7 +111,8 @@ impl Default for ItemList { cycle: false, wrap: false, border: None, - print_score: false, + show_score: false, + show_index: false, } } } @@ -127,8 +130,8 @@ impl ItemList { } /// Returns the currently selected item, if any - pub fn selected(&self) -> Option> { - self.items.get(self.cursor()).map(|x| x.item.clone()) + pub fn selected(&self) -> Option { + self.items.get(self.cursor()).cloned() } /// Appends new matched items to the list @@ -571,7 +574,8 @@ impl SkimWidget for ItemList { cycle: options.cycle, wrap: options.wrap_items, border: options.border, - print_score: options.flags.contains(&crate::options::FeatureFlag::ShowScore), + show_score: feature_flag!(options, ShowScore), + show_index: feature_flag!(options, ShowIndex), } } @@ -741,14 +745,21 @@ impl SkimWidget for ItemList { }, theme.selected, )); - // Optionally prepend the match score for debugging - if this.print_score { + // Optionally prepend debug fields + if this.show_score { let score = item.rank.score; spans.push(Span::styled( format!("[{score}] "), if is_current { theme.current } else { theme.normal }, )); } + if this.show_index { + let index = item.rank.index; + spans.push(Span::styled( + format!("[{index}] "), + if is_current { theme.current } else { theme.normal }, + )); + } spans.extend(display_line.spans); if *wrap { diff --git a/src/util.rs b/src/util.rs index a4e5af1f..4f5f26c5 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,12 +1,11 @@ -use crate::SkimItem; use crate::field::FieldRange; use crate::field::get_string_by_field; use crate::helper::item::strip_ansi; +use crate::item::MatchedItem; use regex::Regex; use std::fs::File; use std::io::{BufRead, BufReader}; use std::prelude::v1::*; -use std::sync::Arc; #[cfg(feature = "cli")] /// Unescape a delimiter string to handle escape sequences like \x00, \t, \n, etc. @@ -90,12 +89,12 @@ pub fn read_file_lines(filename: &str) -> std::result::Result, std:: /// - `{cq}` -> current command query /// #[allow(clippy::too_many_arguments)] -pub fn printf( +pub fn printf<'a>( pattern: &str, delimiter: &Regex, replstr: &str, - selected: impl Iterator> + std::clone::Clone, - current: Option>, + selected: impl Iterator + std::clone::Clone, + current: Option, query: &str, command_query: &str, quote_args: bool, @@ -133,14 +132,14 @@ pub fn printf( "q" => replaced.push_str(&escaped_query), "cq" => replaced.push_str(&escaped_cmd_query), "n" if current.as_ref().is_some() => { - replaced.push_str(current.as_ref().unwrap().get_index().to_string().as_str()); + replaced.push_str(current.as_ref().unwrap().rank.index.to_string().as_str()); } s if s == "+n" || s.starts_with("+n:") || s == "+" || s.starts_with("+:") => { let is_n = s.starts_with("+n"); let accessor = if is_n { - |i: &Arc| i.get_index().to_string() + |i: &MatchedItem| i.rank.index.to_string() } else { - |i: &Arc| strip_ansi(&i.output()).0 + |i: &MatchedItem| strip_ansi(&i.output()).0 }; let mut quote_individually = false; @@ -151,7 +150,7 @@ pub fn printf( let mut expanded = selected .clone() - .map(|i| escape_arg(&accessor(&i), quote_individually)) + .map(|i| escape_arg(&accessor(i), quote_individually)) .reduce(|a: String, b| a.to_owned() + delim + b.as_str()) .unwrap_or_default(); if expanded.is_empty() { @@ -238,8 +237,19 @@ pub fn printf( #[cfg(test)] mod test { use super::*; - use crate::SkimItem; + use crate::Rank; + use crate::item::{MatchedItem, RankBuilder}; use regex::Regex; + use std::sync::Arc; + + fn make_item(s: &'static str) -> MatchedItem { + MatchedItem { + item: Arc::new(s), + rank: Rank::default(), + rank_builder: Arc::new(RankBuilder::default()), + matched_range: None, + } + } #[test] fn test_unescape_delimiter() { @@ -277,11 +287,11 @@ mod test { #[test] fn test_printf() { let pattern = "[1] {} [2] {..2} [3] {2..} [4] {+} [5] {q} [6] {cq} [7] {+:, } [8] {+n:','}"; - let items: Vec> = vec![ - Arc::new("item 1"), - Arc::new("item 2"), - Arc::new("item 3"), - Arc::new("item 4"), + let items = [ + make_item("item 1"), + make_item("item 2"), + make_item("item 3"), + make_item("item 4"), ]; let delimiter = Regex::new(" ").unwrap(); assert_eq!( @@ -289,8 +299,8 @@ mod test { pattern, &delimiter, "{}", - items.iter().cloned(), - Some(Arc::new("item 2")), + items.iter(), + Some(make_item("item 2")), "query", "cmd query", true @@ -305,10 +315,8 @@ mod test { "{+}", &Regex::new(" ").unwrap(), "{}", - [Arc::new("1"), Arc::new("2")] - .iter() - .map(|x| x.clone() as Arc), - Some(Arc::new("1")), + [make_item("1"), make_item("2")].iter(), + Some(make_item("1")), "q", "cq", true @@ -320,8 +328,8 @@ mod test { "{+}", &Regex::new(" ").unwrap(), "{}", - vec![].into_iter(), - Some(Arc::new("1")), + [].iter(), + Some(make_item("1")), "q", "cq", true @@ -336,8 +344,8 @@ mod test { "{}", &Regex::new(" ").unwrap(), "{}", - vec![].into_iter(), - Some(Arc::new("{..2}")), + [].iter(), + Some(make_item("{..2}")), "q", "cq", true @@ -352,10 +360,8 @@ mod test { "{} ##", &Regex::new(" ").unwrap(), "##", - [Arc::new("1"), Arc::new("2")] - .iter() - .map(|x| x.clone() as Arc), - Some(Arc::new("1")), + [make_item("1"), make_item("2")].iter(), + Some(make_item("1")), "q", "cq", true diff --git a/tests/common/insta.rs b/tests/common/insta.rs index 6b5f6f18..132913d0 100644 --- a/tests/common/insta.rs +++ b/tests/common/insta.rs @@ -176,6 +176,13 @@ impl TestHarness { self.tick()?; self.handle_remaining_events()?; + + // Force a final render so that any state changes (e.g. PreviewReady) that + // were processed inside handle_remaining_events are reflected in the buffer + // before we take the snapshot. We bypass the frame-rate throttle by sending + // Render directly instead of relying on the Heartbeat path. + self.send(Event::Render)?; + self.tick()?; Ok(()) } @@ -261,32 +268,43 @@ impl TestHarness { // Process any queued events first (including RunPreview) self.tick()?; - // Now check if there's a pending preview task - // If not, there's nothing to wait for - if let Some(ref handle) = self.skim.app().preview.thread_handle { - if handle.is_finished() { - return Ok(()); - } - } else { + // If there's no preview task running, nothing to wait for + let has_pending = match self.skim.app().preview.thread_handle { + Some(ref handle) => !handle.is_finished(), + None => false, + }; + + if !has_pending { + // Thread is already done (or was never started). Drain any events it may + // have sent (e.g. PreviewReady) that arrived after our initial tick(). + self.tick()?; return Ok(()); } - // Wait for preview to execute - // With multi-threaded runtime, spawned tasks run on background threads + // Wait for the preview thread to finish, then drain its events. let timeout = std::time::Duration::from_secs(2); let start = std::time::Instant::now(); loop { - // Sleep to give background tasks time to execute - std::thread::sleep(std::time::Duration::from_millis(50)); - - // Drain events then process, so we can check for PreviewReady - let mut events = Vec::new(); - while let Ok(event) = self.skim.tui_mut().event_rx.try_recv() { - events.push(event); - } - for event in events { - self.process_event(event)?; + // Sleep to give the background thread time to make progress + std::thread::sleep(std::time::Duration::from_millis(10)); + + // Drain and process any events (including PreviewReady) + self.tick()?; + + // Exit as soon as the thread is done and we have processed its events + let finished = self + .skim + .app() + .preview + .thread_handle + .as_ref() + .map(|h| h.is_finished()) + .unwrap_or(true); + if finished { + // One final drain to catch any events emitted right at thread exit + self.tick()?; + return Ok(()); } if start.elapsed() > timeout { From d8f10933703948de46a18444e88da7595d442344 Mon Sep 17 00:00:00 2001 From: Skim bot Date: Tue, 10 Mar 2026 09:45:24 +0000 Subject: [PATCH 5/6] chore: generate completions & manpage --- shell/completion.bash | 2 +- shell/completion.fish | 3 ++- shell/completion.nu | 2 +- shell/completion.zsh | 3 ++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/shell/completion.bash b/shell/completion.bash index 03714305..192627db 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -258,7 +258,7 @@ _sk() { return 0 ;; --flags) - COMPREPLY=($(compgen -W "no-preview-pty show-score" -- "${cur}")) + COMPREPLY=($(compgen -W "no-preview-pty show-score show-index" -- "${cur}")) return 0 ;; --hscroll-off) diff --git a/shell/completion.fish b/shell/completion.fish index f938ed46..9e719d66 100644 --- a/shell/completion.fish +++ b/shell/completion.fish @@ -88,7 +88,8 @@ complete -c sk -l tmux -d 'Run in a tmux popup' -r complete -c sk -l log-level -d 'Set the log level' -r complete -c sk -l log-file -d 'Pipe log output to a file' -r complete -c sk -l flags -d 'Feature flags' -r -f -a "no-preview-pty\t'Disable preview PTY on linux' -show-score\t'Display the item\'s match score before its value in the item list (for matcher debugging)'" +show-score\t'Display the item\'s match score before its value in the item list (for matcher debugging)' +show-index\t'Display the item\'s index before its value in the item list'" complete -c sk -l hscroll-off -r complete -c sk -l jump-labels -r complete -c sk -l tail -r diff --git a/shell/completion.nu b/shell/completion.nu index 1654f5c9..3323a672 100644 --- a/shell/completion.nu +++ b/shell/completion.nu @@ -33,7 +33,7 @@ module completions { } def "nu-complete sk flags" [] { - [ "no-preview-pty" "show-score" ] + [ "no-preview-pty" "show-score" "show-index" ] } # Fuzzy Finder in rust! diff --git a/shell/completion.zsh b/shell/completion.zsh index 1453788d..1cdffd68 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -89,7 +89,8 @@ zsh\:"Zsh"))' \ '--log-level=[Set the log level]:LOG_LEVEL:_default' \ '--log-file=[Pipe log output to a file]:LOG_FILE:_default' \ '*--flags=[Feature flags]:FLAGS:((no-preview-pty\:"Disable preview PTY on linux" -show-score\:"Display the item'\''s match score before its value in the item list (for matcher debugging)"))' \ +show-score\:"Display the item'\''s match score before its value in the item list (for matcher debugging)" +show-index\:"Display the item'\''s index before its value in the item list"))' \ '--hscroll-off=[]:HSCROLL_OFF:_default' \ '--jump-labels=[]:JUMP_LABELS:_default' \ '--tail=[]:TAIL:_default' \ From c6125a89004d4c8ef9f8eebb3c463753e8cc164d Mon Sep 17 00:00:00 2001 From: Loric ANDRE Date: Tue, 10 Mar 2026 14:00:00 +0100 Subject: [PATCH 6/6] chore: better benchmarks --- benches/filter.rs | 387 +++++++++++++++++++++++---------------- benches/matcher_micro.rs | 2 +- src/helper/item.rs | 9 + 3 files changed, 242 insertions(+), 156 deletions(-) diff --git a/benches/filter.rs b/benches/filter.rs index 9b665b79..9e0d07d8 100644 --- a/benches/filter.rs +++ b/benches/filter.rs @@ -1,206 +1,283 @@ +use std::fs; + use criterion::{Criterion, criterion_group, criterion_main}; use skim::Typos; +use skim::helper::item::DefaultSkimItem; use skim::prelude::*; +const CHUNK_SIZE: usize = 1024; +fn load_lines(file: &str) -> Vec { + let data = fs::read_to_string(format!("benches/fixtures/{file}")).expect("{file} missing"); + data.lines().map(|l| l.to_string()).collect() +} + +fn prepare(file: &str, opt_builder: &mut SkimOptionsBuilder) -> (SkimOptions, SkimItemReceiver) { + let lines = load_lines(file); + let opts = opt_builder.build().unwrap(); + let (tx, rx) = unbounded(); + let mut chunk_size = 0; + let mut chunk = Vec::new(); + for line in lines { + if chunk_size >= CHUNK_SIZE { + tx.send(chunk).unwrap(); + chunk_size = 0; + chunk = Vec::new(); + } + chunk.push(Arc::new(DefaultSkimItem::from(line)) as Arc); + } + tx.send(chunk).unwrap(); + (opts, rx) +} + fn criterion_benchmark_10m(c: &mut Criterion) { c.bench_function("filter_10M_default", |b| { - b.iter(|| { - let opts = SkimOptionsBuilder::default() - .cmd("cat benches/fixtures/10M.txt") - .filter("test") - .build()?; - Skim::run_with(opts, None) - }); + b.iter_batched( + || prepare("10M.txt", SkimOptionsBuilder::default().filter("test")), + |(opts, rx)| Skim::run_with(opts, Some(rx)), + criterion::BatchSize::SmallInput, + ); }); c.bench_function("filter_10M_regex", |b| { - b.iter(|| { - let opts = SkimOptionsBuilder::default() - .cmd("cat benches/fixtures/10M.txt") - .filter("test") - .regex(true) - .build()?; - Skim::run_with(opts, None) - }); + b.iter_batched( + || prepare("10M.txt", SkimOptionsBuilder::default().filter("test").regex(true)), + |(opts, rx)| Skim::run_with(opts, Some(rx)), + criterion::BatchSize::SmallInput, + ); }); c.bench_function("filter_10M_frizbee", |b| { - b.iter(|| { - let opts = SkimOptionsBuilder::default() - .cmd("cat benches/fixtures/10M.txt") - .filter("test") - .algorithm(FuzzyAlgorithm::Frizbee) - .build()?; - Skim::run_with(opts, None) - }); + b.iter_batched( + || { + prepare( + "10M.txt", + SkimOptionsBuilder::default() + .filter("test") + .algorithm(FuzzyAlgorithm::Frizbee) + .typos(Typos::Disabled), + ) + }, + |(opts, rx)| Skim::run_with(opts, Some(rx)), + criterion::BatchSize::SmallInput, + ); }); c.bench_function("filter_10M_frizbee_typos", |b| { - b.iter(|| { - let opts = SkimOptionsBuilder::default() - .cmd("cat benches/fixtures/10M.txt") - .filter("test") - .typos(Typos::Smart) - .algorithm(FuzzyAlgorithm::Frizbee) - .build()?; - Skim::run_with(opts, None) - }); + b.iter_batched( + || { + prepare( + "10M.txt", + SkimOptionsBuilder::default() + .filter("test") + .algorithm(FuzzyAlgorithm::Frizbee) + .typos(Typos::Smart), + ) + }, + |(opts, rx)| Skim::run_with(opts, Some(rx)), + criterion::BatchSize::SmallInput, + ); }); c.bench_function("filter_10M_clangd", |b| { - b.iter(|| { - let opts = SkimOptionsBuilder::default() - .cmd("cat benches/fixtures/10M.txt") - .filter("test") - .algorithm(FuzzyAlgorithm::Clangd) - .build()?; - Skim::run_with(opts, None) - }); + b.iter_batched( + || { + prepare( + "10M.txt", + SkimOptionsBuilder::default() + .filter("test") + .algorithm(FuzzyAlgorithm::Clangd), + ) + }, + |(opts, rx)| Skim::run_with(opts, Some(rx)), + criterion::BatchSize::SmallInput, + ); }); c.bench_function("filter_10M_fzy", |b| { - b.iter(|| { - let opts = SkimOptionsBuilder::default() - .cmd("cat benches/fixtures/10M.txt") - .filter("test") - .algorithm(FuzzyAlgorithm::Fzy) - .build()?; - Skim::run_with(opts, None) - }); + b.iter_batched( + || { + prepare( + "10M.txt", + SkimOptionsBuilder::default() + .filter("test") + .algorithm(FuzzyAlgorithm::Fzy) + .typos(Typos::Disabled), + ) + }, + |(opts, rx)| Skim::run_with(opts, Some(rx)), + criterion::BatchSize::SmallInput, + ); }); c.bench_function("filter_10M_fzy_typos", |b| { - b.iter(|| { - let opts = SkimOptionsBuilder::default() - .cmd("cat benches/fixtures/10M.txt") - .filter("test") - .typos(Typos::Smart) - .algorithm(FuzzyAlgorithm::Fzy) - .build()?; - Skim::run_with(opts, None) - }); + b.iter_batched( + || { + prepare( + "10M.txt", + SkimOptionsBuilder::default() + .filter("test") + .algorithm(FuzzyAlgorithm::Fzy) + .typos(Typos::Smart), + ) + }, + |(opts, rx)| Skim::run_with(opts, Some(rx)), + criterion::BatchSize::SmallInput, + ); }); c.bench_function("filter_10M_arinae", |b| { - b.iter(|| { - let opts = SkimOptionsBuilder::default() - .cmd("cat benches/fixtures/10M.txt") - .filter("test") - .algorithm(FuzzyAlgorithm::Arinae) - .build()?; - Skim::run_with(opts, None) - }); + b.iter_batched( + || { + prepare( + "10M.txt", + SkimOptionsBuilder::default() + .filter("test") + .algorithm(FuzzyAlgorithm::Arinae) + .typos(Typos::Disabled), + ) + }, + |(opts, rx)| Skim::run_with(opts, Some(rx)), + criterion::BatchSize::SmallInput, + ); }); c.bench_function("filter_10M_arinae_typos", |b| { - b.iter(|| { - let opts = SkimOptionsBuilder::default() - .cmd("cat benches/fixtures/10M.txt") - .filter("test") - .typos(Typos::Smart) - .algorithm(FuzzyAlgorithm::Arinae) - .build()?; - Skim::run_with(opts, None) - }); + b.iter_batched( + || { + prepare( + "10M.txt", + SkimOptionsBuilder::default() + .filter("test") + .algorithm(FuzzyAlgorithm::Arinae) + .typos(Typos::Smart), + ) + }, + |(opts, rx)| Skim::run_with(opts, Some(rx)), + criterion::BatchSize::SmallInput, + ); }); } fn criterion_benchmark_1m(c: &mut Criterion) { c.bench_function("filter_1M_default", |b| { - b.iter(|| { - let opts = SkimOptionsBuilder::default() - .cmd("cat benches/fixtures/1M.txt") - .filter("test") - .build()?; - Skim::run_with(opts, None) - }); + b.iter_batched( + || prepare("1M.txt", SkimOptionsBuilder::default().filter("test")), + |(opts, rx)| Skim::run_with(opts, Some(rx)), + criterion::BatchSize::SmallInput, + ); }); c.bench_function("filter_1M_regex", |b| { - b.iter(|| { - let opts = SkimOptionsBuilder::default() - .cmd("cat benches/fixtures/1M.txt") - .filter("test") - .regex(true) - .build()?; - Skim::run_with(opts, None) - }); + b.iter_batched( + || prepare("1M.txt", SkimOptionsBuilder::default().filter("test").regex(true)), + |(opts, rx)| Skim::run_with(opts, Some(rx)), + criterion::BatchSize::SmallInput, + ); }); c.bench_function("filter_1M_frizbee", |b| { - b.iter(|| { - let opts = SkimOptionsBuilder::default() - .cmd("cat benches/fixtures/1M.txt") - .filter("test") - .algorithm(FuzzyAlgorithm::Frizbee) - .build()?; - Skim::run_with(opts, None) - }); + b.iter_batched( + || { + prepare( + "1M.txt", + SkimOptionsBuilder::default() + .filter("test") + .algorithm(FuzzyAlgorithm::Frizbee) + .typos(Typos::Disabled), + ) + }, + |(opts, rx)| Skim::run_with(opts, Some(rx)), + criterion::BatchSize::SmallInput, + ); }); c.bench_function("filter_1M_frizbee_typos", |b| { - b.iter(|| { - let opts = SkimOptionsBuilder::default() - .cmd("cat benches/fixtures/1M.txt") - .filter("test") - .typos(Typos::Smart) - .algorithm(FuzzyAlgorithm::Frizbee) - .build()?; - Skim::run_with(opts, None) - }); + b.iter_batched( + || { + prepare( + "1M.txt", + SkimOptionsBuilder::default() + .filter("test") + .algorithm(FuzzyAlgorithm::Frizbee) + .typos(Typos::Smart), + ) + }, + |(opts, rx)| Skim::run_with(opts, Some(rx)), + criterion::BatchSize::SmallInput, + ); }); c.bench_function("filter_1M_clangd", |b| { - b.iter(|| { - let opts = SkimOptionsBuilder::default() - .cmd("cat benches/fixtures/1M.txt") - .filter("test") - .algorithm(FuzzyAlgorithm::Clangd) - .build()?; - Skim::run_with(opts, None) - }); + b.iter_batched( + || { + prepare( + "1M.txt", + SkimOptionsBuilder::default() + .filter("test") + .algorithm(FuzzyAlgorithm::Clangd), + ) + }, + |(opts, rx)| Skim::run_with(opts, Some(rx)), + criterion::BatchSize::SmallInput, + ); }); c.bench_function("filter_1M_fzy", |b| { - b.iter(|| { - let opts = SkimOptionsBuilder::default() - .cmd("cat benches/fixtures/1M.txt") - .filter("test") - .algorithm(FuzzyAlgorithm::Fzy) - .build()?; - Skim::run_with(opts, None) - }); + b.iter_batched( + || { + prepare( + "1M.txt", + SkimOptionsBuilder::default() + .filter("test") + .algorithm(FuzzyAlgorithm::Fzy) + .typos(Typos::Disabled), + ) + }, + |(opts, rx)| Skim::run_with(opts, Some(rx)), + criterion::BatchSize::SmallInput, + ); }); c.bench_function("filter_1M_fzy_typos", |b| { - b.iter(|| { - let opts = SkimOptionsBuilder::default() - .cmd("cat benches/fixtures/1M.txt") - .filter("test") - .typos(Typos::Smart) - .algorithm(FuzzyAlgorithm::Fzy) - .build()?; - Skim::run_with(opts, None) - }); + b.iter_batched( + || { + prepare( + "1M.txt", + SkimOptionsBuilder::default() + .filter("test") + .algorithm(FuzzyAlgorithm::Fzy) + .typos(Typos::Smart), + ) + }, + |(opts, rx)| Skim::run_with(opts, Some(rx)), + criterion::BatchSize::SmallInput, + ); }); c.bench_function("filter_1M_arinae", |b| { - b.iter(|| { - let opts = SkimOptionsBuilder::default() - .cmd("cat benches/fixtures/1M.txt") - .filter("test") - .algorithm(FuzzyAlgorithm::Arinae) - .build()?; - Skim::run_with(opts, None) - }); + b.iter_batched( + || { + prepare( + "1M.txt", + SkimOptionsBuilder::default() + .filter("test") + .algorithm(FuzzyAlgorithm::Arinae) + .typos(Typos::Disabled), + ) + }, + |(opts, rx)| Skim::run_with(opts, Some(rx)), + criterion::BatchSize::SmallInput, + ); }); c.bench_function("filter_1M_arinae_typos", |b| { - b.iter(|| { - let opts = SkimOptionsBuilder::default() - .cmd("cat benches/fixtures/1M.txt") - .filter("test") - .typos(Typos::Smart) - .algorithm(FuzzyAlgorithm::Arinae) - .build()?; - Skim::run_with(opts, None) - }); + b.iter_batched( + || { + prepare( + "1M.txt", + SkimOptionsBuilder::default() + .filter("test") + .algorithm(FuzzyAlgorithm::Arinae) + .typos(Typos::Smart), + ) + }, + |(opts, rx)| Skim::run_with(opts, Some(rx)), + criterion::BatchSize::SmallInput, + ); }); c.bench_function("filter_1M_andor", |b| { - b.iter(|| { - let opts = SkimOptionsBuilder::default() - .cmd("cat benches/fixtures/1M.txt") - .filter("boot foo | mnt foo") - .build()?; - Skim::run_with(opts, None) - }); + b.iter_batched( + || prepare("1M.txt", SkimOptionsBuilder::default().filter("boot foo | mnt foo")), + |(opts, rx)| Skim::run_with(opts, Some(rx)), + criterion::BatchSize::SmallInput, + ); }); } diff --git a/benches/matcher_micro.rs b/benches/matcher_micro.rs index de675dd4..affb7e54 100644 --- a/benches/matcher_micro.rs +++ b/benches/matcher_micro.rs @@ -12,7 +12,7 @@ use skim::fuzzy_matcher::frizbee::FrizbeeMatcher; use skim::prelude::SkimMatcherV2; fn load_lines() -> Vec { - let data = fs::read_to_string("benches/fixtures/1M.txt").expect("1M.txt missing"); + let data = fs::read_to_string("benches/fixtures/100K.txt").expect("100K.txt missing"); data.lines().map(|l| l.to_string()).collect() } diff --git a/src/helper/item.rs b/src/helper/item.rs index 179ce422..1c43491d 100644 --- a/src/helper/item.rs +++ b/src/helper/item.rs @@ -225,6 +225,15 @@ impl DefaultSkimItem { } } +impl From for DefaultSkimItem { + fn from(value: String) -> Self { + Self { + text: Box::from(value), + metadata: None, + } + } +} + impl SkimItem for DefaultSkimItem { #[inline] fn text(&self) -> Cow<'_, str> {