diff --git a/man/man1/sk.1 b/man/man1/sk.1 index eaf42567..5f50807f 100644 --- a/man/man1/sk.1 +++ b/man/man1/sk.1 @@ -88,7 +88,7 @@ Comma\-separated list of sort criteria to apply when the scores are tied. .br .br -[\fIpossible values: \fRscore, \-score, begin, \-begin, end, \-end, length, \-length] +[\fIpossible values: \fRscore, \-score, begin, \-begin, end, \-end, length, \-length, index, \-index] .TP \fB\-n\fR, \fB\-\-nth\fR=\fINTH\fR [default: ] Fields to be matched @@ -408,7 +408,7 @@ Number of lines of the input treated as header The first N lines of the input are treated as the sticky header. When \-\-with\-nth is set, the lines are transformed just like the other lines that follow. .TP -\fB\-\-history\fR=\fIHISTORY\fR +\fB\-\-history\fR=\fIHISTORY_FILE\fR History file Load search history from the specified file and update the file on completion. When enabled, CTRL\-N and CTRL\-P are automatically remapped to next\-history and previous\-history. @@ -416,7 +416,7 @@ Load search history from the specified file and update the file on completion. W \fB\-\-history\-size\fR=\fIHISTORY_SIZE\fR [default: 1000] Maximum number of query history entries to keep .TP -\fB\-\-cmd\-history\fR=\fICMD_HISTORY\fR +\fB\-\-cmd\-history\fR=\fICMD_HISTORY_FILE\fR Command history file Load command query history from the specified file and update the file on completion. When enabled, CTRL\-N and CTRL\-P are automatically remapped to next\-history and previous\-history. diff --git a/shell/completion.bash b/shell/completion.bash index 7f5fe491..f82ddf6b 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -26,11 +26,11 @@ _sk() { fi case "${prev}" in --tiebreak) - COMPREPLY=($(compgen -W "score -score begin -begin end -end length -length" -- "${cur}")) + COMPREPLY=($(compgen -W "score -score begin -begin end -end length -length index -index" -- "${cur}")) return 0 ;; -t) - COMPREPLY=($(compgen -W "score -score begin -begin end -end length -length" -- "${cur}")) + COMPREPLY=($(compgen -W "score -score begin -begin end -end length -length index -index" -- "${cur}")) return 0 ;; --nth) diff --git a/shell/completion.zsh b/shell/completion.zsh index cbcf8a96..29e96148 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -15,8 +15,8 @@ _sk() { local context curcontext="$curcontext" state line _arguments "${_arguments_options[@]}" : \ -'*-t+[Comma-separated list of sort criteria to apply when the scores are tied.]:TIEBREAK:(score -score begin -begin end -end length -length)' \ -'*--tiebreak=[Comma-separated list of sort criteria to apply when the scores are tied.]:TIEBREAK:(score -score begin -begin end -end length -length)' \ +'*-t+[Comma-separated list of sort criteria to apply when the scores are tied.]:TIEBREAK:(score -score begin -begin end -end length -length index -index)' \ +'*--tiebreak=[Comma-separated list of sort criteria to apply when the scores are tied.]:TIEBREAK:(score -score begin -begin end -end length -length index -index)' \ '*-n+[Fields to be matched]:NTH:_default' \ '*--nth=[Fields to be matched]:NTH:_default' \ '*--with-nth=[Fields to be transformed]:WITH_NTH:_default' \ @@ -41,9 +41,9 @@ _sk() { '--tabstop=[Number of spaces that make up a tab]:TABSTOP:_default' \ '--header=[Set header, displayed next to the info]:HEADER:_default' \ '--header-lines=[Number of lines of the input treated as header]:HEADER_LINES:_default' \ -'--history=[History file]:HISTORY:_default' \ +'--history=[History file]:HISTORY_FILE:_default' \ '--history-size=[Maximum number of query history entries to keep]:HISTORY_SIZE:_default' \ -'--cmd-history=[Command history file]:CMD_HISTORY:_default' \ +'--cmd-history=[Command history file]:CMD_HISTORY_FILE:_default' \ '--cmd-history-size=[Maximum number of query history entries to keep]:CMD_HISTORY_SIZE:_default' \ '--preview=[Preview command]:PREVIEW:_default' \ '--preview-window=[Preview window layout]:PREVIEW_WINDOW:_default' \ diff --git a/skim/examples/downcast.rs b/skim/examples/downcast.rs index 78e13c6a..15ceba8f 100644 --- a/skim/examples/downcast.rs +++ b/skim/examples/downcast.rs @@ -7,6 +7,7 @@ use skim::prelude::*; #[derive(Debug, Clone)] struct Item { text: String, + index: usize, } impl SkimItem for Item { @@ -17,6 +18,14 @@ 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() { @@ -29,9 +38,21 @@ pub fn main() { let (tx, rx): (SkimItemSender, SkimItemReceiver) = unbounded(); - tx.send(Arc::new(Item { text: "a".to_string() })).unwrap(); - tx.send(Arc::new(Item { text: "b".to_string() })).unwrap(); - tx.send(Arc::new(Item { text: "c".to_string() })).unwrap(); + tx.send(Arc::new(Item { + text: "a".to_string(), + index: 0, + })) + .unwrap(); + tx.send(Arc::new(Item { + text: "b".to_string(), + index: 1, + })) + .unwrap(); + tx.send(Arc::new(Item { + text: "c".to_string(), + index: 2, + })) + .unwrap(); drop(tx); diff --git a/skim/src/engine/all.rs b/skim/src/engine/all.rs index bc136ecd..987f1282 100644 --- a/skim/src/engine/all.rs +++ b/skim/src/engine/all.rs @@ -31,7 +31,7 @@ impl MatchEngine for MatchAllEngine { fn match_item(&self, item: Arc) -> Option { let item_len = item.text().len(); Some(MatchResult { - rank: self.rank_builder.build_rank(0, 0, 0, item_len), + rank: self.rank_builder.build_rank(0, 0, 0, item_len, item.get_index()), matched_range: MatchRange::ByteRange(0, 0), }) } diff --git a/skim/src/engine/exact.rs b/skim/src/engine/exact.rs index 26f59f3d..a48d987b 100644 --- a/skim/src/engine/exact.rs +++ b/skim/src/engine/exact.rs @@ -102,7 +102,9 @@ impl MatchEngine for ExactEngine { let score = (end - begin) as i32; let item_len = item_text.len(); Some(MatchResult { - rank: self.rank_builder.build_rank(score, begin, end, item_len), + rank: self + .rank_builder + .build_rank(score, begin, end, item_len, item.get_index()), matched_range: MatchRange::ByteRange(begin, end), }) } diff --git a/skim/src/engine/fuzzy.rs b/skim/src/engine/fuzzy.rs index 21f97f81..18fec5f6 100644 --- a/skim/src/engine/fuzzy.rs +++ b/skim/src/engine/fuzzy.rs @@ -136,12 +136,15 @@ impl MatchEngine for FuzzyEngine { let (score, matched_range) = matched_result.unwrap(); + trace!("matched range {:?}", matched_range); let begin = *matched_range.first().unwrap_or(&0); let end = *matched_range.last().unwrap_or(&0); let item_len = item_text.len(); Some(MatchResult { - rank: self.rank_builder.build_rank(score as i32, begin, end, item_len), + rank: self + .rank_builder + .build_rank(score as i32, begin, end, item_len, item.get_index()), matched_range: MatchRange::Chars(matched_range), }) } diff --git a/skim/src/engine/regexp.rs b/skim/src/engine/regexp.rs index f319f63a..be4970fa 100644 --- a/skim/src/engine/regexp.rs +++ b/skim/src/engine/regexp.rs @@ -71,7 +71,9 @@ impl MatchEngine for RegexEngine { let item_len = item_text.len(); Some(MatchResult { - rank: self.rank_builder.build_rank(score, begin, end, item_len), + rank: self + .rank_builder + .build_rank(score, begin, end, item_len, item.get_index()), matched_range: MatchRange::ByteRange(begin, end), }) } diff --git a/skim/src/helper/item.rs b/skim/src/helper/item.rs index f4e37b2d..bc0876fd 100644 --- a/skim/src/helper/item.rs +++ b/skim/src/helper/item.rs @@ -29,6 +29,8 @@ pub struct DefaultSkimItem { // Option> to reduce memory use in normal cases where no matching ranges are specified. #[allow(clippy::box_collection)] matching_ranges: Option>>, + /// The index, for use in matching + index: usize, } impl DefaultSkimItem { @@ -38,6 +40,7 @@ impl DefaultSkimItem { trans_fields: &[FieldRange], matching_fields: &[FieldRange], delimiter: &Regex, + index: usize, ) -> Self { let using_transform_fields = !trans_fields.is_empty(); @@ -83,6 +86,7 @@ impl DefaultSkimItem { orig_text, text, matching_ranges, + index, } } } @@ -129,4 +133,12 @@ impl SkimItem for DefaultSkimItem { ret.override_attrs(new_fragments); ret } + + fn get_index(&self) -> usize { + self.index + } + + fn set_index(&mut self, index: usize) { + self.index = index; + } } diff --git a/skim/src/helper/item_reader.rs b/skim/src/helper/item_reader.rs index 1b694a6b..4611bd18 100644 --- a/skim/src/helper/item_reader.rs +++ b/skim/src/helper/item_reader.rs @@ -256,16 +256,14 @@ impl SkimItemReader { started_clone.store(true, Ordering::SeqCst); // notify parent that it is started let mut buffer = Vec::with_capacity(option.buf_size); + let mut line_idx = 0; loop { buffer.clear(); // start reading match source.read_until(option.line_ending, &mut buffer) { - Ok(n) => { - if n == 0 { - break; - } - + Ok(0) => break, + Ok(_) => { if buffer.ends_with(b"\r\n") { buffer.pop(); buffer.pop(); @@ -275,12 +273,15 @@ impl SkimItemReader { let line = String::from_utf8_lossy(&buffer).to_string(); + trace!("got item {} with index {} from command", line.clone(), line_idx); + let raw_item = DefaultSkimItem::new( line, option.use_ansi_color, &option.transform_fields, &option.matching_fields, &option.delimiter, + line_idx, ); match tx_item.send(Arc::new(raw_item)) { @@ -290,6 +291,7 @@ impl SkimItemReader { break; } } + line_idx += 1; } Err(_err) => {} // String not UTF8 or other error, skip. } diff --git a/skim/src/item.rs b/skim/src/item.rs index 8a880a48..60604bd7 100644 --- a/skim/src/item.rs +++ b/skim/src/item.rs @@ -38,27 +38,31 @@ impl RankBuilder { } /// score: the greater the better - pub fn build_rank(&self, score: i32, begin: usize, end: usize, length: usize) -> Rank { + pub fn build_rank(&self, score: i32, begin: usize, end: usize, length: usize, index: usize) -> Rank { let mut rank = [0; 4]; let begin = begin as i32; let end = end as i32; let length = length as i32; + let index = index as i32; - for (index, criteria) in self.criterion.iter().take(4).enumerate() { + for (priority, criteria) in self.criterion.iter().take(5).enumerate() { let value = match criteria { RankCriteria::Score => -score, - RankCriteria::Begin => begin, - RankCriteria::End => end, RankCriteria::NegScore => score, + RankCriteria::Begin => begin, RankCriteria::NegBegin => -begin, + RankCriteria::End => end, RankCriteria::NegEnd => -end, RankCriteria::Length => length, RankCriteria::NegLength => -length, + RankCriteria::Index => index, + RankCriteria::NegIndex => -index, }; - rank[index] = value; + rank[priority] = value; } + trace!("ranks: {:?}", rank); rank } } @@ -202,19 +206,23 @@ impl<'mutex, T: Sized> Deref for ItemPoolGuard<'mutex, T> { #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum RankCriteria { Score, - Begin, - End, NegScore, + Begin, NegBegin, + End, NegEnd, Length, NegLength, + Index, + NegIndex, } impl ValueEnum for RankCriteria { fn value_variants<'a>() -> &'a [Self] { use RankCriteria::*; - &[Score, NegScore, Begin, NegBegin, End, NegEnd, Length, NegLength] + &[ + Score, NegScore, Begin, NegBegin, End, NegEnd, Length, NegLength, Index, NegIndex, + ] } fn to_possible_value(&self) -> Option { @@ -228,6 +236,8 @@ impl ValueEnum for RankCriteria { NegEnd => PossibleValue::new("-end"), Length => PossibleValue::new("length"), NegLength => PossibleValue::new("-length"), + Index => PossibleValue::new("index"), + NegIndex => PossibleValue::new("-index"), }) } } diff --git a/skim/src/lib.rs b/skim/src/lib.rs index 66f8f16f..b28589c9 100644 --- a/skim/src/lib.rs +++ b/skim/src/lib.rs @@ -130,6 +130,17 @@ 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) {} } //------------------------------------------------------------------------------ diff --git a/skim/src/matcher.rs b/skim/src/matcher.rs index 11ed5caf..73808122 100644 --- a/skim/src/matcher.rs +++ b/skim/src/matcher.rs @@ -83,7 +83,7 @@ impl Matcher { let matched_items_clone = matched_items.clone(); let thread_matcher = thread::spawn(move || { - let num_taken = item_pool.num_taken(); + let _num_taken = item_pool.num_taken(); let items = item_pool.take(); // 1. use rayon for parallel @@ -94,7 +94,8 @@ impl Matcher { let result: Result, _> = items .into_par_iter() .enumerate() - .filter_map(|(index, item)| { + .filter_map(|(_, item)| { + let item_idx = item.get_index(); processed.fetch_add(1, Ordering::Relaxed); if stopped.load(Ordering::Relaxed) { Some(Err("matcher killed")) @@ -104,7 +105,7 @@ impl Matcher { item: item.clone(), rank: match_result.rank, matched_range: Some(match_result.matched_range), - item_idx: (num_taken + index) as u32, + item_idx: item_idx as u32, })) } else { None diff --git a/skim/src/model.rs b/skim/src/model.rs index a6ba85ac..12c5b143 100644 --- a/skim/src/model.rs +++ b/skim/src/model.rs @@ -15,8 +15,9 @@ use crate::engine::factory::{AndOrEngineFactory, ExactOrFuzzyEngineFactory, Rege use crate::event::{Event, EventHandler, EventReceiver, EventSender}; use crate::global::current_run_num; use crate::header::Header; +use crate::helper::item::DefaultSkimItem; use crate::input::parse_action_arg; -use crate::item::{ItemPool, MatchedItem, RankBuilder, RankCriteria}; +use crate::item::{ItemPool, MatchedItem, RankBuilder}; use crate::matcher::{Matcher, MatcherControl}; use crate::options::SkimOptions; use crate::output::SkimOutput; @@ -40,8 +41,6 @@ const SPINNERS_UNICODE: [char; 10] = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', lazy_static! { static ref RE_FIELDS: Regex = Regex::new(r"\\?(\{-?[0-9.,q]*?})").unwrap(); static ref RE_PREVIEW_OFFSET: Regex = Regex::new(r"^\+([0-9]+|\{-?[0-9]+\})(-[0-9]+|-/[1-9][0-9]*)?$").unwrap(); - static ref DEFAULT_CRITERION: Vec = - vec![RankCriteria::Score, RankCriteria::Begin, RankCriteria::End,]; } pub struct Model { @@ -462,14 +461,20 @@ impl Model { } let item_len = query.len(); - let item: Arc = Arc::new(query); + let item_idx = self.item_pool.len(); + let query_item = DefaultSkimItem::new(query, true, &[], &[], &self.delimiter, item_idx); + let item: Arc = Arc::new(query_item); let new_len = self.item_pool.append(vec![item.clone()]); - let item_idx = (max(new_len, 1) - 1) as u32; + trace!( + "appended and selected item with internal id {} and matched as id {}", + item_idx, + max(new_len, 1) - 1 + ); let matched_item = MatchedItem { item, - rank: self.rank_builder.build_rank(0, 0, 0, item_len), + rank: self.rank_builder.build_rank(0, 0, 0, item_len, item_idx), matched_range: Some(MatchRange::ByteRange(0, 0)), - item_idx, + item_idx: (max(new_len, 1) - 1) as u32, }; self.selection.act_select_matched(current_run_num(), matched_item); diff --git a/test/test_skim.py b/test/test_skim.py index 6764448c..485a5229 100644 --- a/test/test_skim.py +++ b/test/test_skim.py @@ -1252,6 +1252,95 @@ def test_version(self): time.sleep(0.1) self.assertRegex(self.readonce(), "^sk \\d+\\.\\d+\\.\\d+$") + def test_tiebreak_default(self): + input_cmd = "echo -n 'a\nc\nab\nac\nb'" + args = '' + self.tmux.send_keys(f"{input_cmd} | {self.sk(args)}", Key('Enter')) + self.tmux.until(lambda l: l.ready_with_matches(5)) + self.tmux.until(lambda l: l[-3].startswith('> a')) + self.tmux.send_keys(Key('b')) + self.tmux.until(lambda l: l[-3].startswith('> b')) + + def test_tiebreak_index(self): + input_cmd = "echo -n 'a\nc\nab\nac\nb'" + args = '--tiebreak=index,score' + self.tmux.send_keys(f"{input_cmd} | {self.sk(args)}", Key('Enter')) + self.tmux.until(lambda l: l.ready_with_matches(5)) + self.tmux.until(lambda l: l[-3].startswith('> a')) + self.tmux.send_keys(Key('b')) + self.tmux.until(lambda l: l[-3].startswith('> ab')) + + def test_tiebreak_neg_index(self): + input_cmd = "echo -n 'a\nb\nc\nab\nac'" + args = '--tiebreak=-index,score' + self.tmux.send_keys(f"{input_cmd} | {self.sk(args)}", Key('Enter')) + self.tmux.until(lambda l: l.ready_with_matches(5)) + self.tmux.until(lambda l: l[-3].startswith('> a')) + self.tmux.send_keys(Key('b')) + self.tmux.until(lambda l: l[-3].startswith('> ab')) + + def test_tiebreak_neg_score(self): + input_cmd = "echo -n 'a\nb\nc\nab\nac'" + args = '--tiebreak=-score' + self.tmux.send_keys(f"{input_cmd} | {self.sk(args)}", Key('Enter')) + self.tmux.until(lambda l: l.ready_with_matches(5)) + self.tmux.until(lambda l: l[-3].startswith('> a')) + self.tmux.send_keys(Key('b')) + self.tmux.until(lambda l: l[-3].startswith('> ab')) + + def test_tiebreak_begin(self): + input_cmd = "echo -n 'aaba\nb\nc\naba\nac'" + args = '--tiebreak=begin,score' + self.tmux.send_keys(f"{input_cmd} | {self.sk(args)}", Key('Enter')) + self.tmux.until(lambda l: l.ready_with_matches(5)) + self.tmux.until(lambda l: l[-3].startswith('> aaba')) + self.tmux.send_keys('ba') + self.tmux.until(lambda l: l[-3].startswith('> aba')) + + def test_tiebreak_neg_begin(self): + input_cmd = "echo -n 'aba\nb\nc\naaba\nac'" + args = '--tiebreak=-begin,score' + self.tmux.send_keys(f"{input_cmd} | {self.sk(args)}", Key('Enter')) + self.tmux.until(lambda l: l.ready_with_matches(5)) + self.tmux.until(lambda l: l[-3].startswith('> aba')) + self.tmux.send_keys('ba') + self.tmux.until(lambda l: l[-3].startswith('> aaba')) + + def test_tiebreak_end(self): + input_cmd = "echo -n 'aaba\nb\nc\naba\nac'" + args = '--tiebreak=end,score' + self.tmux.send_keys(f"{input_cmd} | {self.sk(args)}", Key('Enter')) + self.tmux.until(lambda l: l.ready_with_matches(5)) + self.tmux.until(lambda l: l[-3].startswith('> aaba')) + self.tmux.send_keys('ba') + self.tmux.until(lambda l: l[-3].startswith('> aba')) + + def test_tiebreak_neg_end(self): + input_cmd = "echo -n 'aba\nb\nc\naaba\nac'" + args = '--tiebreak=-end,score' + self.tmux.send_keys(f"{input_cmd} | {self.sk(args)}", Key('Enter')) + self.tmux.until(lambda l: l.ready_with_matches(5)) + self.tmux.until(lambda l: l[-3].startswith('> aba')) + self.tmux.send_keys('ba') + self.tmux.until(lambda l: l[-3].startswith('> aaba')) + + def test_tiebreak_length(self): + input_cmd = "echo -n 'aaba\nb\nc\naba\nac'" + args = '--tiebreak=length,score' + self.tmux.send_keys(f"{input_cmd} | {self.sk(args)}", Key('Enter')) + self.tmux.until(lambda l: l.ready_with_matches(5)) + self.tmux.until(lambda l: l[-3].startswith('> b')) + self.tmux.send_keys('ba') + self.tmux.until(lambda l: l[-3].startswith('> aba')) + + def test_tiebreak_neg_length(self): + input_cmd = "echo -n 'aaba\nb\nc\naba\nac'" + args = '--tiebreak=-length,score' + self.tmux.send_keys(f"{input_cmd} | {self.sk(args)}", Key('Enter')) + self.tmux.until(lambda l: l.ready_with_matches(5)) + self.tmux.until(lambda l: l[-3].startswith('> aaba')) + self.tmux.send_keys('c') + self.tmux.until(lambda l: l[-3].startswith('> ac')) def find_prompt(lines, interactive=False, reverse=False): linen = -1