introduce new trait SkimItem

This commit is contained in:
Jinzhou Zhang 2020-02-04 22:59:36 +08:00
parent e9eb7b709f
commit bbd8f91eac
18 changed files with 322 additions and 235 deletions

6
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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");
}
}

View file

@ -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");
}
}

View file

@ -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
}
}

View file

@ -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<Item>) -> Option<MatchedItem>;
fn match_item(&self, item: Arc<ItemWrapper>) -> Option<MatchedItem>;
fn display(&self) -> String;
}
@ -52,9 +52,9 @@ impl RegexEngine {
}
impl MatchEngine for RegexEngine {
fn match_item(&self, item: Arc<Item>) -> Option<MatchedItem> {
fn match_item(&self, item: Arc<ItemWrapper>) -> Option<MatchedItem> {
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<Item>) -> Option<MatchedItem> {
fn match_item(&self, item: Arc<ItemWrapper>) -> Option<MatchedItem> {
// 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<Item>) -> Option<MatchedItem> {
fn match_item(&self, item: Arc<ItemWrapper>) -> Option<MatchedItem> {
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>,
item: Arc<ItemWrapper>,
// filter: <Option<(start, end), (start, end)>, item_length> -> Option<(start, end)>
filter: impl Fn(&Option<((usize, usize), (usize, usize))>, usize) -> Option<(usize, usize)>,
) -> Option<MatchedItem> {
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<Item>) -> Option<MatchedItem> {
fn match_item(&self, item: Arc<ItemWrapper>) -> Option<MatchedItem> {
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<Item>) -> Option<MatchedItem> {
fn match_item(&self, item: Arc<ItemWrapper>) -> Option<MatchedItem> {
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<Item>) -> Option<MatchedItem> {
fn match_item(&self, item: Arc<ItemWrapper>) -> Option<MatchedItem> {
// mock
let mut results = vec![];
for engine in &self.engines {

View file

@ -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

View file

@ -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<dyn SkimItem>,
}
impl<'a> Item {
pub fn new(
orig_text: Cow<str>,
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<AnsiString> {
self.inner.display()
}
fn preview(&self) -> ItemPreview {
self.inner.preview()
}
fn get_text(&self) -> Cow<str> {
self.inner.get_text()
}
fn output(&self) -> Cow<str> {
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<usize>), // individual character indices matched
}
#[derive(Clone, Debug)]
#[derive(Clone)]
pub struct MatchedItem {
pub item: Arc<Item>,
pub item: Arc<ItemWrapper>,
pub rank: Rank,
pub matched_range: Option<MatchedRange>, // range of chars that matched the pattern
}
impl MatchedItem {
pub fn builder(item: Arc<Item>) -> Self {
pub fn builder(item: Arc<ItemWrapper>) -> Self {
MatchedItem {
item,
rank: Rank::default(),
@ -194,11 +101,11 @@ impl MatchedItem {
self
}
pub fn to_chars(&self) -> Option<Vec<usize>> {
pub fn range_char_indices(&self) -> Option<Vec<usize>> {
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<Vec<Arc<Item>>>,
pool: SpinLock<Vec<Arc<ItemWrapper>>>,
/// number of items that was `take`n
taken: AtomicUsize,
/// reverse first N lines as header
reserved_items: SpinLock<Vec<Arc<Item>>>,
reserved_items: SpinLock<Vec<Arc<ItemWrapper>>>,
lines_to_reserve: usize,
}
@ -258,7 +165,7 @@ impl ItemPool {
self.taken.store(0, Ordering::SeqCst);
}
pub fn append(&self, mut items: Vec<Arc<Item>>) {
pub fn append(&self, mut items: Vec<Arc<ItemWrapper>>) {
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<Arc<Item>> {
pub fn take(&self) -> ItemPoolGuard<Arc<ItemWrapper>> {
let guard = self.pool.lock();
let taken = self.taken.swap(guard.len(), Ordering::SeqCst);
ItemPoolGuard { guard, start: taken }
}
pub fn reserved(&self) -> ItemPoolGuard<Arc<Item>> {
pub fn reserved(&self) -> ItemPoolGuard<Arc<ItemWrapper>> {
let guard = self.reserved_items.lock();
ItemPoolGuard { guard, start: 0 }
}

View file

@ -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<AnsiString>;
/// helper function to get pure text presentation(without color) of the item
fn get_text(&self) -> Cow<str>;
/// get output text(after accept), could be override
fn output(&self) -> Cow<str> {
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<T: AsRef<str> + Send + Sync> SkimItem for T {
fn display(&self) -> Cow<AnsiString> {
Cow::Owned(AnsiString::new_str(self.as_ref()))
}
fn get_text(&self) -> Cow<str> {
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;
}

View file

@ -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}

View file

@ -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<Cow<str>> = 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<ItemWrapper> = Arc::new(ItemWrapper::new(query, (std::usize::MAX, self.next_idx_to_append)));
self.next_idx_to_append += 1;

View file

@ -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<String>,
pub query: String,
pub cmd: String,
pub selected_items: Vec<Arc<Item>>,
pub selected_items: Vec<Arc<ItemWrapper>>,
}

View file

@ -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<PreviewEvent>,
content_lines: Arc<SpinLock<Vec<AnsiString>>>,
content_lines: Arc<SpinLock<Vec<AnsiString<'static>>>>,
width: AtomicUsize,
height: AtomicUsize,
@ -29,7 +31,7 @@ pub struct Previewer {
vscroll_offset: usize,
wrap: bool,
prev_item: Option<Arc<Item>>,
prev_item: Option<Arc<ItemWrapper>>,
prev_query: Option<String>,
prev_cmd_query: Option<String>,
prev_num_selected: usize,
@ -87,11 +89,11 @@ impl Previewer {
pub fn on_item_change(
&mut self,
new_item: impl Into<Option<Arc<Item>>>,
new_item: impl Into<Option<Arc<ItemWrapper>>>,
new_query: impl Into<Option<String>>,
new_cmd_query: impl Into<Option<String>>,
num_selected: usize,
get_selected_items: impl Fn() -> Vec<Arc<Item>>, // lazy get
get_selected_items: impl Fn() -> Vec<Arc<ItemWrapper>>, // 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<Cow<str>> = 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<C>(rx_preview: Receiver<PreviewEvent>, on_return: C)
where
C: Fn(Vec<AnsiString>) + Send + Sync + 'static,
C: Fn(Vec<AnsiString<'static>>) + Send + Sync + 'static,
{
let callback = Arc::new(on_return);
let mut preview_thread: Option<PreviewThread> = 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<C>(spawned: std::process::Child, callback: C)
where
C: Fn(Vec<AnsiString>),
C: Fn(Vec<AnsiString<'static>>),
{
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);
}

View file

@ -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<AtomicBool>,
thread_reader: JoinHandle<()>,
items: Arc<SpinLock<Vec<Arc<Item>>>>,
items: Arc<SpinLock<Vec<Arc<ItemWrapper>>>>,
}
impl ReaderControl {
@ -31,7 +32,7 @@ impl ReaderControl {
let _ = self.thread_reader.join();
}
pub fn take(&self) -> Vec<Arc<Item>> {
pub fn take(&self) -> Vec<Arc<ItemWrapper>> {
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<AtomicBool>,
items: Arc<SpinLock<Vec<Arc<Item>>>>,
items: Arc<SpinLock<Vec<Arc<ItemWrapper>>>>,
option: Arc<ReaderOption>,
source_file: Option<Box<dyn BufRead + Send>>,
) {
@ -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();

View file

@ -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<RankCriteria>,
items: OrderedVec<MatchedItem>, // all items
selected: HashMap<(usize, usize), Arc<Item>>,
selected: HashMap<(usize, usize), Arc<ItemWrapper>>,
//
// |>------ 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<Item>) {
pub fn act_select_item(&mut self, item: Arc<ItemWrapper>) {
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<Arc<Item>> {
pub fn get_selected_items(&self) -> Vec<Arc<ItemWrapper>> {
// select the current one
let select_cursor = !self.multi_selection || self.selected.is_empty();
let mut selected: Vec<Arc<Item>> = self.selected.values().cloned().collect();
let mut selected: Vec<Arc<ItemWrapper>> = 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<Arc<Item>> {
pub fn get_current_item(&self) -> Option<Arc<ItemWrapper>> {
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()

131
src/sk/item.rs Normal file
View file

@ -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<str>,
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<AnsiString> {
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<str> {
if !self.using_transform_fields && !self.ansi_enabled {
Cow::Borrowed(&self.orig_text)
} else {
Cow::Borrowed(self.text.stripped())
}
}
fn output(&self) -> Cow<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.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,
}
}
}

1
src/sk/mod.rs Normal file
View file

@ -0,0 +1 @@
pub mod item;

View file

@ -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);
}
}