From bbd8f91eac3b6513f89a125f20ca7e8863d7cf8c Mon Sep 17 00:00:00 2001 From: Jinzhou Zhang Date: Tue, 4 Feb 2020 22:59:36 +0800 Subject: [PATCH] introduce new trait SkimItem --- Cargo.lock | 6 +- Cargo.toml | 2 +- examples/option_builder.rs | 6 +- examples/sample.rs | 4 +- src/ansi.rs | 28 ++++-- src/engine.rs | 28 +++--- src/header.rs | 4 +- src/item.rs | 181 +++++++++---------------------------- src/lib.rs | 50 +++++++++- src/main.rs | 4 +- src/model.rs | 22 ++--- src/output.rs | 5 +- src/previewer.rs | 30 +++--- src/reader.rs | 14 +-- src/selection.rs | 26 +++--- src/sk/item.rs | 131 +++++++++++++++++++++++++++ src/sk/mod.rs | 1 + src/util.rs | 15 +-- 18 files changed, 322 insertions(+), 235 deletions(-) create mode 100644 src/sk/item.rs create mode 100644 src/sk/mod.rs diff --git a/Cargo.lock b/Cargo.lock index e9024a52..2aa0feab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -248,7 +248,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "fuzzy-matcher" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "thread_local 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -460,7 +460,7 @@ dependencies = [ "clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)", "derive_builder 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", - "fuzzy-matcher 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "fuzzy-matcher 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "nix 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -690,7 +690,7 @@ dependencies = [ "checksum either 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c67353c641dc847124ea1902d69bd753dee9bb3beff9aa3662ecf86c971d1fac" "checksum env_logger 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b61fa891024a945da30a9581546e8cfaf5602c7b3f4c137a2805cf388f92075a" "checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" -"checksum fuzzy-matcher 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4d860c6f043a7f367ffcbdb5833c36f7dc85fa8bc6e7898e2530f643ec90f9f3" +"checksum fuzzy-matcher 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c20c3dd0480475a1e6da2f0d4dad7052d81386f56be9e23622e027c9240839b6" "checksum getrandom 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "e65cce4e5084b14874c4e7097f38cab54f47ee554f9194673456ea379dcc4c55" "checksum humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3ca7e5f2e110db35f93b837c81797f3714500b81d517bf20c431b16d3ca4f114" "checksum ident_case 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" diff --git a/Cargo.toml b/Cargo.toml index 739576da..c1445c08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ time = "0.1.38" clap = "2.26.2" tuikit = "0.3.0" vte = "0.3.3" -fuzzy-matcher = "0.3.1" +fuzzy-matcher = "0.3.3" rayon = "1.0.3" derive_builder = "0.9" bitflags = "1.0.4" diff --git a/examples/option_builder.rs b/examples/option_builder.rs index ded3bb99..f7f4a5b3 100644 --- a/examples/option_builder.rs +++ b/examples/option_builder.rs @@ -1,5 +1,5 @@ extern crate skim; -use skim::{Skim, SkimOptionsBuilder}; +use skim::{Skim, SkimItem, SkimOptionsBuilder}; use std::io::Cursor; pub fn main() { @@ -18,7 +18,7 @@ pub fn main() { .unwrap_or_else(|| Vec::new()); for item in selected_items.iter() { - print!("{}: {}{}", item.get_index(), item.get_output_text(), "\n"); + print!("{}: {}{}", item.get_index(), item.output(), "\n"); } //================================================== @@ -30,6 +30,6 @@ pub fn main() { .unwrap_or_else(|| Vec::new()); for item in selected_items.iter() { - print!("{}: {}{}", item.get_index(), item.get_output_text(), "\n"); + print!("{}: {}{}", item.get_index(), item.output(), "\n"); } } diff --git a/examples/sample.rs b/examples/sample.rs index 1cea64ca..3a7e7a4f 100644 --- a/examples/sample.rs +++ b/examples/sample.rs @@ -1,5 +1,5 @@ extern crate skim; -use skim::{Skim, SkimOptions}; +use skim::{Skim, SkimItem, SkimOptions}; pub fn main() { let options = SkimOptions::default(); @@ -9,6 +9,6 @@ pub fn main() { .unwrap_or_else(|| Vec::new()); for item in selected_items.iter() { - print!("{}{}", item.get_output_text(), "\n"); + print!("{}{}", item.output(), "\n"); } } diff --git a/src/ansi.rs b/src/ansi.rs index ccebd8c5..47030ca4 100644 --- a/src/ansi.rs +++ b/src/ansi.rs @@ -195,7 +195,7 @@ impl ANSIParser { self.last_attr = new_attr; } - pub fn parse_ansi(&mut self, text: &str) -> AnsiString { + pub fn parse_ansi(&mut self, text: &str) -> AnsiString<'static> { let mut statemachine = vte::Parser::new(); for byte in text.as_bytes() { @@ -213,12 +213,12 @@ impl ANSIParser { /// A String that contains ANSI state (e.g. colors) /// /// It is internally represented as Vec<(attr, string)> -pub struct AnsiString { - stripped: Cow<'static, str>, - fragments: Vec<(Attr, Cow<'static, str>)>, +pub struct AnsiString<'a> { + stripped: Cow<'a, str>, + fragments: Vec<(Attr, Cow<'a, str>)>, } -impl AnsiString { +impl<'a> AnsiString<'a> { pub fn new_empty() -> Self { Self { stripped: Cow::Owned(String::new()), @@ -234,6 +234,14 @@ impl AnsiString { } } + pub fn new_str(str_ref: &'a str) -> Self { + let stripped: Cow<'a, str> = Cow::Borrowed(str_ref); + Self { + stripped: stripped.clone(), + fragments: vec![(Attr::default(), stripped.clone())], + } + } + pub fn new(stripped: String, fragments: Vec<(Attr, Cow<'static, str>)>) -> Self { Self { stripped: Cow::Owned(stripped), @@ -241,6 +249,10 @@ impl AnsiString { } } + pub fn parse(raw: &'a str) -> AnsiString<'static> { + ANSIParser::default().parse_ansi(raw) + } + pub fn is_empty(&self) -> bool { self.fragments.is_empty() } @@ -258,11 +270,7 @@ impl AnsiString { self.fragments.len() > 1 || (!self.fragments.is_empty() && self.fragments[0].0 != Attr::default()) } - pub fn from_str(raw: &str) -> AnsiString { - ANSIParser::default().parse_ansi(raw) - } - - pub fn get_stripped(&self) -> &str { + pub fn stripped(&self) -> &str { &self.stripped } } diff --git a/src/engine.rs b/src/engine.rs index 5f2406a4..796f43bf 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,7 +1,7 @@ ///! matcher engine -use crate::item::{Item, MatchedItem, MatchedRange, Rank}; -use crate::score; +use crate::item::{ItemWrapper, MatchedItem, MatchedRange, Rank}; use crate::score::FuzzyAlgorithm; +use crate::{score, SkimItem}; use regex::Regex; use std::sync::Arc; @@ -19,7 +19,7 @@ pub enum MatcherMode { // A match engine will execute the matching algorithm pub trait MatchEngine: Sync + Send { - fn match_item(&self, item: Arc) -> Option; + fn match_item(&self, item: Arc) -> Option; fn display(&self) -> String; } @@ -52,9 +52,9 @@ impl RegexEngine { } impl MatchEngine for RegexEngine { - fn match_item(&self, item: Arc) -> Option { + fn match_item(&self, item: Arc) -> Option { let mut matched_result = None; - for &(start, end) in item.get_matching_ranges() { + for &(start, end) in item.get_matching_ranges().as_ref() { if self.query_regex.is_none() { matched_result = Some((0, 0)); break; @@ -117,10 +117,10 @@ impl FuzzyEngine { } impl MatchEngine for FuzzyEngine { - fn match_item(&self, item: Arc) -> Option { + fn match_item(&self, item: Arc) -> Option { // iterate over all matching fields: let mut matched_result = None; - for &(start, end) in item.get_matching_ranges() { + for &(start, end) in item.get_matching_ranges().as_ref() { matched_result = score::fuzzy_match(&item.get_text()[start..end], &self.query, self.algorithm).map(|(s, vec)| { if start != 0 { @@ -175,7 +175,7 @@ impl MatchAllEngine { } impl MatchEngine for MatchAllEngine { - fn match_item(&self, item: Arc) -> Option { + fn match_item(&self, item: Arc) -> Option { let rank = build_rank(0, item.get_index() as i64, 0, 0); Some( @@ -222,14 +222,14 @@ impl ExactEngine { fn match_item_exact( &self, - item: Arc, + item: Arc, // filter: , item_length> -> Option<(start, end)> filter: impl Fn(&Option<((usize, usize), (usize, usize))>, usize) -> Option<(usize, usize)>, ) -> Option { let mut matched_result = None; let mut range_start = 0; let mut range_end = 0; - for &(start, end) in item.get_matching_ranges() { + for &(start, end) in item.get_matching_ranges().as_ref() { if self.query == "" { matched_result = Some(((0, 0), (0, 0))); break; @@ -260,7 +260,7 @@ impl ExactEngine { } impl MatchEngine for ExactEngine { - fn match_item(&self, item: Arc) -> Option { + fn match_item(&self, item: Arc) -> Option { self.match_item_exact(item, |match_result, len| { let match_range = match *match_result { Some(((s1, e1), (s2, e2))) => { @@ -335,7 +335,7 @@ impl OrEngine { } impl MatchEngine for OrEngine { - fn match_item(&self, item: Arc) -> Option { + fn match_item(&self, item: Arc) -> Option { for engine in &self.engines { let result = engine.match_item(Arc::clone(&item)); if result.is_some() { @@ -399,7 +399,7 @@ impl AndEngine { for item in items { match item.matched_range { Some(MatchedRange::ByteRange(..)) => { - ranges.extend(item.to_chars().unwrap()); + ranges.extend(item.range_char_indices().unwrap()); } Some(MatchedRange::Chars(vec)) => { ranges.extend(vec.iter()); @@ -418,7 +418,7 @@ impl AndEngine { } impl MatchEngine for AndEngine { - fn match_item(&self, item: Arc) -> Option { + fn match_item(&self, item: Arc) -> Option { // mock let mut results = vec![]; for engine in &self.engines { diff --git a/src/header.rs b/src/header.rs index f3daee57..2c265023 100644 --- a/src/header.rs +++ b/src/header.rs @@ -12,7 +12,7 @@ use std::sync::Arc; use tuikit::prelude::*; pub struct Header { - header: AnsiString, + header: AnsiString<'static>, tabstop: usize, hscroll_offset: usize, reverse: bool, @@ -58,7 +58,7 @@ impl Header { None => {} Some("") => {} Some(header) => { - self.header = AnsiString::from_str(header); + self.header = AnsiString::parse(header); } } self diff --git a/src/item.rs b/src/item.rs index f9ae9353..df80f412 100644 --- a/src/item.rs +++ b/src/item.rs @@ -1,9 +1,8 @@ ///! An item is line of text that read from `find` command or stdin together with ///! the internal states, such as selected or not -use crate::ansi::{ANSIParser, AnsiString}; -use crate::field::*; +use crate::ansi::AnsiString; use crate::spinlock::{SpinLock, SpinLockGuard}; -use regex::Regex; +use crate::{ItemPreview, SkimItem}; use std::borrow::Cow; use std::cmp::min; use std::default::Default; @@ -11,141 +10,49 @@ use std::ops::Deref; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; -/// An item will store everything that one line input will need to be operated and displayed. -/// -/// What's special about an item? -/// The simplest version of an item is a line of string, but things are getting more complex: -/// - The conversion of lower/upper case is slow in rust, because it involds unicode. -/// - We may need to interpret the ANSI codes in the text. -/// - The text can be transformed and limited while searching. -/// -/// About the ANSI, we made assumption that it is linewise, that means no ANSI codes will affect -/// more than one line. -#[derive(Debug)] -pub struct Item { +pub struct ItemWrapper { // (num of run, number of index) - index: (usize, usize), - - // The text that will be ouptut when user press `enter` - orig_text: String, - - // The text that will shown into the screen. Can be transformed. - text: AnsiString, - - matching_ranges: Vec<(usize, usize)>, - - // For the transformed ANSI case, the output will need another transform. - using_transform_fields: bool, - ansi_enabled: bool, + id: (usize, usize), + inner: Arc, } -impl<'a> Item { - pub fn new( - orig_text: Cow, - ansi_enabled: bool, - trans_fields: &[FieldRange], - matching_fields: &[FieldRange], - delimiter: &Regex, - index: (usize, usize), - ) -> Self { - let using_transform_fields = !trans_fields.is_empty(); - - // transformed | ANSI | output - //------------------------------------------------------ - // +- T -> trans+ANSI | ANSI - // | | - // +- T -> trans +- F -> trans | orig - // orig | | - // +- F -> orig +- T -> ANSI ==| ANSI - // | | - // +- F -> orig | orig - - let mut ansi_parser: ANSIParser = Default::default(); - - let text = if using_transform_fields && ansi_enabled { - // ansi and transform - ansi_parser.parse_ansi(&parse_transform_fields(delimiter, &orig_text, trans_fields)) - } else if using_transform_fields { - // transformed, not ansi - AnsiString::new_string(parse_transform_fields(delimiter, &orig_text, trans_fields)) - } else if ansi_enabled { - // not transformed, ansi - ansi_parser.parse_ansi(&orig_text) - } else { - // normal case - AnsiString::new_empty() - }; - - let mut ret = Item { - index, - orig_text: orig_text.into_owned(), - text, - using_transform_fields: !trans_fields.is_empty(), - matching_ranges: Vec::new(), - ansi_enabled, - }; - - let matching_ranges = if !matching_fields.is_empty() { - parse_matching_fields(delimiter, ret.get_text(), matching_fields) - } else { - vec![(0, ret.get_text().len())] - }; - - ret.matching_ranges = matching_ranges; - ret - } - - pub fn get_text(&self) -> &str { - if !self.using_transform_fields && !self.ansi_enabled { - &self.orig_text - } else { - &self.text.get_stripped() +impl ItemWrapper { + pub fn new(item: impl SkimItem + 'static, index: (usize, usize)) -> Self { + Self { + id: index, + inner: Arc::new(item), } } - pub fn get_text_struct(&self) -> Option<&AnsiString> { - if !self.using_transform_fields && !self.ansi_enabled { - None - } else { - Some(&self.text) - } - } - - pub fn get_output_text(&'a self) -> Cow<'a, str> { - if self.using_transform_fields && self.ansi_enabled { - let mut ansi_parser: ANSIParser = Default::default(); - let text = ansi_parser.parse_ansi(&self.orig_text); - Cow::Owned(text.into_inner()) - } else if !self.using_transform_fields && self.ansi_enabled { - Cow::Borrowed(self.text.get_stripped()) - } else { - Cow::Borrowed(&self.orig_text) - } + pub fn get_id(&self) -> (usize, usize) { + self.id } pub fn get_index(&self) -> usize { - self.index.1 - } - - pub fn get_full_index(&self) -> (usize, usize) { - self.index - } - - pub fn get_matching_ranges(&self) -> &[(usize, usize)] { - &self.matching_ranges + self.id.1 } } -impl Clone for Item { - fn clone(&self) -> Item { - Item { - index: self.index, - orig_text: self.orig_text.clone(), - text: self.text.clone(), - using_transform_fields: self.using_transform_fields, - matching_ranges: self.matching_ranges.clone(), - ansi_enabled: self.ansi_enabled, - } +/// delegate to inner +impl SkimItem for ItemWrapper { + fn display(&self) -> Cow { + self.inner.display() + } + + fn preview(&self) -> ItemPreview { + self.inner.preview() + } + + fn get_text(&self) -> Cow { + self.inner.get_text() + } + + fn output(&self) -> Cow { + self.inner.output() + } + + fn get_matching_ranges(&self) -> Cow<[(usize, usize)]> { + self.inner.get_matching_ranges() } } @@ -164,15 +71,15 @@ pub enum MatchedRange { Chars(Vec), // individual character indices matched } -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct MatchedItem { - pub item: Arc, + pub item: Arc, pub rank: Rank, pub matched_range: Option, // range of chars that matched the pattern } impl MatchedItem { - pub fn builder(item: Arc) -> Self { + pub fn builder(item: Arc) -> Self { MatchedItem { item, rank: Rank::default(), @@ -194,11 +101,11 @@ impl MatchedItem { self } - pub fn to_chars(&self) -> Option> { + pub fn range_char_indices(&self) -> Option> { self.matched_range.as_ref().map(|r| match r { MatchedRange::ByteRange(start, end) => { - let first = self.item.orig_text[..*start].chars().count(); - let last = first + self.item.orig_text[*start..*end].chars().count(); + let first = self.item.get_text()[..*start].chars().count(); + let last = first + self.item.get_text()[*start..*end].chars().count(); (first..last).collect() } MatchedRange::Chars(vec) => vec.clone(), @@ -210,12 +117,12 @@ const ITEM_POOL_CAPACITY: usize = 1024; pub struct ItemPool { length: AtomicUsize, - pool: SpinLock>>, + pool: SpinLock>>, /// number of items that was `take`n taken: AtomicUsize, /// reverse first N lines as header - reserved_items: SpinLock>>, + reserved_items: SpinLock>>, lines_to_reserve: usize, } @@ -258,7 +165,7 @@ impl ItemPool { self.taken.store(0, Ordering::SeqCst); } - pub fn append(&self, mut items: Vec>) { + pub fn append(&self, mut items: Vec>) { let mut pool = self.pool.lock(); let mut header_items = self.reserved_items.lock(); @@ -273,13 +180,13 @@ impl ItemPool { self.length.store(pool.len(), Ordering::SeqCst); } - pub fn take(&self) -> ItemPoolGuard> { + pub fn take(&self) -> ItemPoolGuard> { let guard = self.pool.lock(); let taken = self.taken.swap(guard.len(), Ordering::SeqCst); ItemPoolGuard { guard, start: taken } } - pub fn reserved(&self) -> ItemPoolGuard> { + pub fn reserved(&self) -> ItemPoolGuard> { let guard = self.reserved_items.lock(); ItemPoolGuard { guard, start: 0 } } diff --git a/src/lib.rs b/src/lib.rs index c0608de5..427f03da 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,10 +19,12 @@ mod query; mod reader; mod score; mod selection; +mod sk; mod spinlock; mod theme; mod util; +use crate::ansi::AnsiString; use crate::event::Event::*; use crate::event::{EventReceiver, EventSender}; use crate::model::Model; @@ -31,6 +33,7 @@ pub use crate::output::SkimOutput; use crate::reader::Reader; pub use crate::score::FuzzyAlgorithm; use nix::unistd::isatty; +use std::borrow::Cow; use std::env; use std::io::BufRead; use std::io::BufReader; @@ -40,6 +43,49 @@ use std::sync::Arc; use std::thread; use tuikit::prelude::{Event as TermEvent, *}; +//------------------------------------------------------------------------------ +pub trait SkimItem: Send + Sync { + /// The text to be displayed on the item list, could contain ANSI properties + fn display(&self) -> Cow; + + /// helper function to get pure text presentation(without color) of the item + fn get_text(&self) -> Cow; + + /// get output text(after accept), could be override + fn output(&self) -> Cow { + self.get_text() + } + + fn preview(&self) -> ItemPreview { + ItemPreview::Global + } + + /// we could limit the matching ranges of the `get_text` of the item. + /// providing (start_byte, end_byte) of the range + fn get_matching_ranges(&self) -> Cow<[(usize, usize)]> { + Cow::Owned(vec![(0, self.display().stripped().len())]) + } +} + +impl + Send + Sync> SkimItem for T { + fn display(&self) -> Cow { + Cow::Owned(AnsiString::new_str(self.as_ref())) + } + + fn get_text(&self) -> Cow { + Cow::Borrowed(self.as_ref()) + } +} + +//------------------------------------------------------------------------------ +// Preview +pub enum ItemPreview<'a> { + Command(&'a str), + Text(&'a str), + Global, +} + +//------------------------------------------------------------------------------ pub struct Skim {} impl Skim { @@ -163,9 +209,9 @@ impl Skim { for item in reader_control.take().into_iter() { if let Some(matched) = engine.match_item(item) { if options.print_score { - println!("{}\t{}", -matched.rank.score, matched.item.get_output_text()); + println!("{}\t{}", -matched.rank.score, matched.item.output()); } else { - println!("{}", matched.item.get_output_text()); + println!("{}", matched.item.output()); } match_count += 1; } diff --git a/src/main.rs b/src/main.rs index 0e20f8f2..e74266bd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,7 @@ extern crate skim; extern crate time; use clap::{App, Arg, ArgMatches}; -use skim::{FuzzyAlgorithm, Skim, SkimOptions, SkimOptionsBuilder}; +use skim::{FuzzyAlgorithm, Skim, SkimItem, SkimOptions, SkimOptionsBuilder}; use std::env; const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -249,7 +249,7 @@ fn real_main() -> i32 { } for item in output.selected_items.iter() { - print!("{}{}", item.get_output_text(), output_ending); + print!("{}{}", item.output(), output_ending); } if output.selected_items.is_empty() {1} else {0} diff --git a/src/model.rs b/src/model.rs index b1fa380a..552a743e 100644 --- a/src/model.rs +++ b/src/model.rs @@ -12,7 +12,7 @@ use tuikit::prelude::{Event as TermEvent, *}; use crate::event::{Event, EventHandler, EventReceiver, EventSender}; use crate::header::Header; use crate::input::parse_action_arg; -use crate::item::{Item, ItemPool}; +use crate::item::{ItemPool, ItemWrapper}; use crate::matcher::{Matcher, MatcherControl, MatcherMode}; use crate::options::SkimOptions; use crate::output::SkimOutput; @@ -23,7 +23,8 @@ use crate::selection::Selection; use crate::spinlock::SpinLock; use crate::theme::ColorTheme; use crate::util::{inject_command, margin_string_to_size, parse_margin, InjectContext}; -use crate::FuzzyAlgorithm; +use crate::{FuzzyAlgorithm, SkimItem}; +use std::borrow::Cow; const REFRESH_DURATION: i64 = 100; const SPINNER_DURATION: u32 = 200; @@ -330,12 +331,13 @@ impl Model { fn act_execute_silent(&mut self, cmd: &str) { let item = self.selection.get_current_item(); - let current_selection = item.as_ref().map(|item| item.get_output_text()).unwrap(); + let current_selection = item.as_ref().map(|item| item.output()).unwrap(); let query = self.query.get_query(); let cmd_query = self.query.get_cmd_query(); - let selected_items = self.selection.get_selected_items(); - let selected_texts: Vec<&str> = selected_items.iter().map(|item| item.get_text()).collect(); + let tmp = self.selection.get_selected_items(); + let tmp: Vec> = tmp.iter().map(|item| item.get_text()).collect(); + let selected_texts: Vec<&str> = tmp.iter().map(|cow| cow.as_ref()).collect(); let context = InjectContext { delimiter: &self.delimiter, @@ -356,15 +358,7 @@ impl Model { if query.is_empty() { return; } - - let item = Arc::new(Item::new( - String::from_utf8_lossy(query.as_bytes()), - false, - &Vec::new(), - &Vec::new(), - &Regex::new("").unwrap(), - (std::usize::MAX, self.next_idx_to_append), - )); + let item: Arc = Arc::new(ItemWrapper::new(query, (std::usize::MAX, self.next_idx_to_append))); self.next_idx_to_append += 1; diff --git a/src/output.rs b/src/output.rs index 1a717d23..99dec8e9 100644 --- a/src/output.rs +++ b/src/output.rs @@ -1,10 +1,9 @@ -use crate::item::Item; +use crate::item::ItemWrapper; use std::sync::Arc; -#[derive(Debug)] pub struct SkimOutput { pub accept_key: Option, pub query: String, pub cmd: String, - pub selected_items: Vec>, + pub selected_items: Vec>, } diff --git a/src/previewer.rs b/src/previewer.rs index d18af772..bc6d2700 100644 --- a/src/previewer.rs +++ b/src/previewer.rs @@ -1,11 +1,13 @@ use crate::ansi::AnsiString; use crate::event::{Event, EventHandler, UpdateScreen}; -use crate::item::Item; +use crate::item::ItemWrapper; use crate::spinlock::SpinLock; use crate::util::{inject_command, InjectContext}; +use crate::SkimItem; use derive_builder::Builder; use nix::libc; use regex::Regex; +use std::borrow::Cow; use std::cmp::{max, min}; use std::env; use std::process::{Command, Stdio}; @@ -21,7 +23,7 @@ const DELIMITER_STR: &str = r"[\t\n ]+"; pub struct Previewer { tx_preview: Sender, - content_lines: Arc>>, + content_lines: Arc>>>, width: AtomicUsize, height: AtomicUsize, @@ -29,7 +31,7 @@ pub struct Previewer { vscroll_offset: usize, wrap: bool, - prev_item: Option>, + prev_item: Option>, prev_query: Option, prev_cmd_query: Option, prev_num_selected: usize, @@ -87,11 +89,11 @@ impl Previewer { pub fn on_item_change( &mut self, - new_item: impl Into>>, + new_item: impl Into>>, new_query: impl Into>, new_cmd_query: impl Into>, num_selected: usize, - get_selected_items: impl Fn() -> Vec>, // lazy get + get_selected_items: impl Fn() -> Vec>, // lazy get ) { let new_item = new_item.into(); let new_query = new_query.into(); @@ -101,7 +103,7 @@ impl Previewer { (None, None) => false, (None, Some(_)) => true, (Some(_), None) => true, - (Some(prev), Some(cur)) => prev.get_output_text() != cur.get_output_text(), + (Some(prev), Some(cur)) => prev.output() != cur.output(), }; let query_changed = match (self.prev_query.as_ref(), new_query.as_ref()) { @@ -133,12 +135,14 @@ impl Previewer { let current_selection = self .prev_item .as_ref() - .map(|item| item.get_output_text()) + .map(|item| item.output()) .unwrap_or_else(|| "".into()); let query = self.prev_query.as_ref().map(|s| &**s).unwrap_or(""); let cmd_query = self.prev_cmd_query.as_ref().map(|s| &**s).unwrap_or(""); - let selected_items = get_selected_items(); - let selected_texts: Vec<&str> = selected_items.iter().map(|item| item.get_text()).collect(); + + let tmp = get_selected_items(); + let tmp: Vec> = tmp.iter().map(|item| item.get_text()).collect(); + let selected_texts: Vec<&str> = tmp.iter().map(|cow| cow.as_ref()).collect(); let context = InjectContext { delimiter: &self.delimiter, @@ -290,7 +294,7 @@ impl PreviewThread { fn run(rx_preview: Receiver, on_return: C) where - C: Fn(Vec) + Send + Sync + 'static, + C: Fn(Vec>) + Send + Sync + 'static, { let callback = Arc::new(on_return); let mut preview_thread: Option = None; @@ -330,7 +334,7 @@ where match spawned { Err(err) => { - let astdout = AnsiString::from_str(format!("Failed to spawn: {} / {}", cmd, err).as_str()); + let astdout = AnsiString::parse(format!("Failed to spawn: {} / {}", cmd, err).as_str()); callback(vec![astdout]); preview_thread = None; } @@ -353,7 +357,7 @@ where fn wait(spawned: std::process::Child, callback: C) where - C: Fn(Vec), + C: Fn(Vec>), { let output = spawned.wait_with_output(); @@ -370,7 +374,7 @@ where &output.stderr }); - let lines = out_str.lines().map(AnsiString::from_str).collect(); + let lines = out_str.lines().map(AnsiString::parse).collect(); callback(lines); } diff --git a/src/reader.rs b/src/reader.rs index 49cadd35..b50f4335 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -2,8 +2,9 @@ ///! ///! After reading in a line, reader will save an item into the pool(items) use crate::field::FieldRange; -use crate::item::Item; +use crate::item::ItemWrapper; use crate::options::SkimOptions; +use crate::sk::item::SkItem; use crate::spinlock::SpinLock; use regex::Regex; use std::collections::HashMap; @@ -22,7 +23,7 @@ const DELIMITER_STR: &str = r"[\t\n ]+"; pub struct ReaderControl { stopped: Arc, thread_reader: JoinHandle<()>, - items: Arc>>>, + items: Arc>>>, } impl ReaderControl { @@ -31,7 +32,7 @@ impl ReaderControl { let _ = self.thread_reader.join(); } - pub fn take(&self) -> Vec> { + pub fn take(&self) -> Vec> { let mut items = self.items.lock(); let mut ret = Vec::with_capacity(items.len()); ret.append(&mut items); @@ -174,7 +175,7 @@ lazy_static! { fn reader( cmd: &str, stopped: Arc, - items: Arc>>>, + items: Arc>>>, option: Arc, source_file: Option>, ) { @@ -231,15 +232,16 @@ fn reader( buffer.pop(); } - let item = Item::new( + let raw_item = SkItem::new( String::from_utf8_lossy(&buffer), opt.use_ansi_color, &opt.transform_fields, &opt.matching_fields, &opt.delimiter, - (run_num, index), ); + let item = ItemWrapper::new(raw_item, (run_num, index)); + { // save item into pool let mut vec = items.lock(); diff --git a/src/selection.rs b/src/selection.rs index 9e0d4dc4..266b613f 100644 --- a/src/selection.rs +++ b/src/selection.rs @@ -1,13 +1,13 @@ ///! Handle the selections of items use crate::event::{Event, EventHandler, UpdateScreen}; use crate::item::{parse_criteria, RankCriteria}; -use crate::item::{Item, MatchedItem, MatchedRange}; +use crate::item::{ItemWrapper, MatchedItem, MatchedRange}; use crate::orderedvec::CompareFunction; use crate::orderedvec::OrderedVec; use crate::spinlock::SpinLock; use crate::theme::{ColorTheme, DEFAULT_THEME}; use crate::util::{print_item, reshape_string, LinePrinter}; -use crate::SkimOptions; +use crate::{SkimItem, SkimOptions}; use std::cmp::max; use std::cmp::min; use std::collections::HashMap; @@ -30,7 +30,7 @@ lazy_static! { pub struct Selection { criterion: Vec, items: OrderedVec, // all items - selected: HashMap<(usize, usize), Arc>, + selected: HashMap<(usize, usize), Arc>, // // |>------ items[items.len()-1] @@ -201,7 +201,7 @@ impl Selection { .items .get(cursor) .unwrap_or_else(|| panic!("model:act_toggle: failed to get item {}", cursor)); - let index = current_item.item.get_full_index(); + let index = current_item.item.get_id(); if !self.selected.contains_key(&index) { self.selected.insert(index, current_item.item.clone()); } else { @@ -216,7 +216,7 @@ impl Selection { } for current_item in self.items.iter() { - let index = current_item.item.get_full_index(); + let index = current_item.item.get_id(); if !self.selected.contains_key(&index) { self.selected.insert(index, current_item.item.clone()); } else { @@ -225,12 +225,12 @@ impl Selection { } } - pub fn act_select_item(&mut self, item: Arc) { + pub fn act_select_item(&mut self, item: Arc) { if !self.multi_selection { return; } - self.selected.insert(item.get_full_index(), item); + self.selected.insert(item.get_id(), item); } pub fn act_select_all(&mut self) { @@ -240,7 +240,7 @@ impl Selection { for current_item in self.items.iter() { let item = current_item.item.clone(); - self.selected.insert(item.get_full_index(), item); + self.selected.insert(item.get_id(), item); } } @@ -255,10 +255,10 @@ impl Selection { self.hscroll_offset = hscroll_offset as usize; } - pub fn get_selected_items(&self) -> Vec> { + pub fn get_selected_items(&self) -> Vec> { // select the current one let select_cursor = !self.multi_selection || self.selected.is_empty(); - let mut selected: Vec> = self.selected.values().cloned().collect(); + let mut selected: Vec> = self.selected.values().cloned().collect(); if select_cursor && !self.items.is_empty() { let cursor = self.item_cursor + self.line_cursor; @@ -270,7 +270,7 @@ impl Selection { selected.push(item); } - selected.sort_by_key(|item| item.get_full_index()); + selected.sort_by_key(|item| item.get_id()); selected } @@ -290,7 +290,7 @@ impl Selection { self.multi_selection } - pub fn get_current_item(&self) -> Option> { + pub fn get_current_item(&self) -> Option> { let item_idx = self.get_current_item_idx(); self.items.get(item_idx).map(|item| item.item.clone()) } @@ -358,7 +358,7 @@ impl Selection { return Err("screen width is too small".into()); } - let index = matched_item.item.get_full_index(); + let index = matched_item.item.get_id(); let default_attr = if is_current { self.theme.current() diff --git a/src/sk/item.rs b/src/sk/item.rs new file mode 100644 index 00000000..1631e314 --- /dev/null +++ b/src/sk/item.rs @@ -0,0 +1,131 @@ +use crate::ansi::{ANSIParser, AnsiString}; +use crate::field::{parse_matching_fields, parse_transform_fields, FieldRange}; +use crate::SkimItem; +use regex::Regex; +use std::borrow::Cow; + +/// An item will store everything that one line input will need to be operated and displayed. +/// +/// What's special about an item? +/// The simplest version of an item is a line of string, but things are getting more complex: +/// - The conversion of lower/upper case is slow in rust, because it involds unicode. +/// - We may need to interpret the ANSI codes in the text. +/// - The text can be transformed and limited while searching. +/// +/// About the ANSI, we made assumption that it is linewise, that means no ANSI codes will affect +/// more than one line. +#[derive(Debug)] +pub struct SkItem { + // The text that will be ouptut when user press `enter` + orig_text: String, + + // The text that will shown into the screen. Can be transformed. + text: AnsiString<'static>, + + matching_ranges: Vec<(usize, usize)>, + + // For the transformed ANSI case, the output will need another transform. + using_transform_fields: bool, + ansi_enabled: bool, +} + +impl<'a> SkItem { + pub fn new( + orig_text: Cow, + ansi_enabled: bool, + trans_fields: &[FieldRange], + matching_fields: &[FieldRange], + delimiter: &Regex, + ) -> Self { + let using_transform_fields = !trans_fields.is_empty(); + + // transformed | ANSI | output + //------------------------------------------------------ + // +- T -> trans+ANSI | ANSI + // | | + // +- T -> trans +- F -> trans | orig + // orig | | + // +- F -> orig +- T -> ANSI ==| ANSI + // | | + // +- F -> orig | orig + + let mut ansi_parser: ANSIParser = Default::default(); + + let text = if using_transform_fields && ansi_enabled { + // ansi and transform + ansi_parser.parse_ansi(&parse_transform_fields(delimiter, &orig_text, trans_fields)) + } else if using_transform_fields { + // transformed, not ansi + AnsiString::new_string(parse_transform_fields(delimiter, &orig_text, trans_fields)) + } else if ansi_enabled { + // not transformed, ansi + ansi_parser.parse_ansi(&orig_text) + } else { + // normal case + AnsiString::new_empty() + }; + + let mut ret = SkItem { + orig_text: orig_text.into(), + text, + using_transform_fields: !trans_fields.is_empty(), + matching_ranges: Vec::new(), + ansi_enabled, + }; + + let matching_ranges = if !matching_fields.is_empty() { + parse_matching_fields(delimiter, &ret.get_text(), matching_fields) + } else { + vec![(0, ret.get_text().len())] + }; + + ret.matching_ranges = matching_ranges; + ret + } +} + +impl SkimItem for SkItem { + fn display(&self) -> Cow { + if self.using_transform_fields || self.ansi_enabled { + Cow::Borrowed(&self.text) + } else { + Cow::Owned(AnsiString::new_str(&self.orig_text)) + } + } + + fn get_text(&self) -> Cow { + if !self.using_transform_fields && !self.ansi_enabled { + Cow::Borrowed(&self.orig_text) + } else { + Cow::Borrowed(self.text.stripped()) + } + } + + fn output(&self) -> Cow { + if self.using_transform_fields && self.ansi_enabled { + let mut ansi_parser: ANSIParser = Default::default(); + let text = ansi_parser.parse_ansi(&self.orig_text); + Cow::Owned(text.into_inner()) + } else if !self.using_transform_fields && self.ansi_enabled { + Cow::Borrowed(self.text.stripped()) + } else { + Cow::Borrowed(&self.orig_text) + } + } + + fn get_matching_ranges(&self) -> Cow<[(usize, usize)]> { + Cow::Borrowed(&self.matching_ranges) + } +} + +impl Clone for SkItem { + fn clone(&self) -> SkItem { + SkItem { + orig_text: self.orig_text.clone(), + text: self.text.clone(), + using_transform_fields: self.using_transform_fields, + matching_ranges: self.matching_ranges.clone(), + ansi_enabled: self.ansi_enabled, + } + } +} diff --git a/src/sk/mod.rs b/src/sk/mod.rs new file mode 100644 index 00000000..a35e98d6 --- /dev/null +++ b/src/sk/mod.rs @@ -0,0 +1 @@ +pub mod item; diff --git a/src/util.rs b/src/util.rs index e538e166..ee052e93 100644 --- a/src/util.rs +++ b/src/util.rs @@ -7,7 +7,8 @@ use tuikit::prelude::*; use unicode_width::UnicodeWidthChar; use crate::field::get_string_by_range; -use crate::item::Item; +use crate::item::ItemWrapper; +use crate::SkimItem; lazy_static! { static ref RE_FIELDS: Regex = Regex::new(r"\\?(\{ *-?[0-9.,cq+]*? *})").unwrap(); @@ -172,15 +173,9 @@ impl LinePrinter { } } -pub fn print_item(canvas: &mut dyn Canvas, printer: &mut LinePrinter, item: &Item, default_attr: Attr) { - if item.get_text_struct().is_some() && item.get_text_struct().as_ref().unwrap().has_attrs() { - for (ch, attr) in item.get_text_struct().as_ref().unwrap().iter() { - printer.print_char(canvas, ch, default_attr.extend(attr), false); - } - } else { - for ch in item.get_text().chars() { - printer.print_char(canvas, ch, default_attr, false); - } +pub fn print_item(canvas: &mut dyn Canvas, printer: &mut LinePrinter, item: &ItemWrapper, default_attr: Attr) { + for (ch, attr) in item.display().iter() { + printer.print_char(canvas, ch, default_attr.extend(attr), false); } }