mirror of
https://github.com/lotabout/skim.git
synced 2026-09-10 07:16:23 -04:00
feat: split-match (#906)
* wip: split-match * chore: generate completions & manpage * chore: use engine factory for split match * chore: generate completions & manpage * docs: update README.md with new option [skip ci] --------- Co-authored-by: Skim bot <skim-bot@skim-rs.github.io>
This commit is contained in:
parent
1af0f643cb
commit
1ee19d3caa
5
.config/insta.yaml
Normal file
5
.config/insta.yaml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
behavior:
|
||||
output: minimal
|
||||
|
||||
test:
|
||||
runner: nextest
|
||||
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
|
|
@ -8,7 +8,7 @@ You will need `tmux` to run some integration tests.
|
|||
You can then run `cargo nextest run --release --features test-utils`, which should automatically build a release binary, run the unit tests and the integration tests.
|
||||
|
||||
|
||||
Most integration tests use [cargo insta](https://insta.rs). If you need to add some tests or re-review them, you will need to install it.
|
||||
Most integration tests use [cargo insta](https://insta.rs). If you need to add some tests or re-review them, you will need to install it, and run tests with `cargo insta test --features test-utils --tests --review`, which will let you review snapshots.
|
||||
|
||||
Note: you can run the tests without `--release`, but expect more flaky tests since the timings will be looser. I would advise testing manually any debug test failure if you have doubts. However, the tests won't run without the `test-utils` feature, used to create test backends.
|
||||
|
||||
|
|
|
|||
|
|
@ -245,6 +245,10 @@ page](https://github.com/skim-rs/skim/blob/master/man/man1/sk.1) (`man sk`).
|
|||
- `OR` has higher precedence. For example, `readme .md$ | .markdown$` is interpreted as
|
||||
`readme AND (.md$ OR .markdown$)`.
|
||||
|
||||
- When using the `--split-match` option, each part around spaces or `|` will be matched in a split way:
|
||||
- If the option's value (defaulting to `:`) is absent from the query, do a normal match
|
||||
- If it is present, match everything before to everything before it in the items, and everything after it (including potential other occurences of the delimiter) to the part after it in the items. This is particularly useful when piping in input from `rg` to match on both file name and content.
|
||||
|
||||
If you prefer using regular expressions, `skim` offers a `regex` mode:
|
||||
|
||||
```sh
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
.SH NAME
|
||||
sk \- Fuzzy Finder in rust!
|
||||
.SH SYNOPSIS
|
||||
\fBsk\fR [\fB\-\-tac\fR] [\fB\-\-min\-query\-length\fR] [\fB\-\-no\-sort\fR] [\fB\-t\fR|\fB\-\-tiebreak\fR] [\fB\-n\fR|\fB\-\-nth\fR] [\fB\-\-with\-nth\fR] [\fB\-d\fR|\fB\-\-delimiter\fR] [\fB\-e\fR|\fB\-\-exact\fR] [\fB\-\-regex\fR] [\fB\-\-algo\fR] [\fB\-\-case\fR] [\fB\-b\fR|\fB\-\-bind\fR] [\fB\-m\fR|\fB\-\-multi\fR] [\fB\-\-no\-multi\fR] [\fB\-\-no\-mouse\fR] [\fB\-c\fR|\fB\-\-cmd\fR] [\fB\-i\fR|\fB\-\-interactive\fR] [\fB\-I \fR] [\fB\-\-color\fR] [\fB\-\-no\-hscroll\fR] [\fB\-\-keep\-right\fR] [\fB\-\-skip\-to\-pattern\fR] [\fB\-\-no\-clear\-if\-empty\fR] [\fB\-\-no\-clear\-start\fR] [\fB\-\-no\-clear\fR] [\fB\-\-show\-cmd\-error\fR] [\fB\-\-cycle\fR] [\fB\-\-disabled\fR] [\fB\-\-layout\fR] [\fB\-\-reverse\fR] [\fB\-\-height\fR] [\fB\-\-no\-height\fR] [\fB\-\-min\-height\fR] [\fB\-\-margin\fR] [\fB\-p\fR|\fB\-\-prompt\fR] [\fB\-\-cmd\-prompt\fR] [\fB\-\-selector\fR] [\fB\-\-multi\-selector\fR] [\fB\-\-ansi\fR] [\fB\-\-tabstop\fR] [\fB\-\-info\fR] [\fB\-\-no\-info\fR] [\fB\-\-inline\-info\fR] [\fB\-\-header\fR] [\fB\-\-header\-lines\fR] [\fB\-\-border\fR] [\fB\-\-wrap\fR] [\fB\-\-history\fR] [\fB\-\-history\-size\fR] [\fB\-\-cmd\-history\fR] [\fB\-\-cmd\-history\-size\fR] [\fB\-\-preview\fR] [\fB\-\-preview\-window\fR] [\fB\-q\fR|\fB\-\-query\fR] [\fB\-\-cmd\-query\fR] [\fB\-\-read0\fR] [\fB\-\-print0\fR] [\fB\-\-print\-query\fR] [\fB\-\-print\-cmd\fR] [\fB\-\-print\-score\fR] [\fB\-\-print\-header\fR] [\fB\-\-no\-strip\-ansi\fR] [\fB\-1\fR|\fB\-\-select\-1\fR] [\fB\-0\fR|\fB\-\-exit\-0\fR] [\fB\-\-sync\fR] [\fB\-\-pre\-select\-n\fR] [\fB\-\-pre\-select\-pat\fR] [\fB\-\-pre\-select\-items\fR] [\fB\-\-pre\-select\-file\fR] [\fB\-f\fR|\fB\-\-filter\fR] [\fB\-\-shell\fR] [\fB\-\-shell\-bindings\fR] [\fB\-\-man\fR] [\fB\-\-listen\fR] [\fB\-\-tmux\fR] [\fB\-\-log\-file\fR] [\fB\-\-expect\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR]
|
||||
\fBsk\fR [\fB\-\-tac\fR] [\fB\-\-min\-query\-length\fR] [\fB\-\-no\-sort\fR] [\fB\-t\fR|\fB\-\-tiebreak\fR] [\fB\-n\fR|\fB\-\-nth\fR] [\fB\-\-with\-nth\fR] [\fB\-d\fR|\fB\-\-delimiter\fR] [\fB\-e\fR|\fB\-\-exact\fR] [\fB\-\-regex\fR] [\fB\-\-algo\fR] [\fB\-\-case\fR] [\fB\-\-split\-match\fR] [\fB\-b\fR|\fB\-\-bind\fR] [\fB\-m\fR|\fB\-\-multi\fR] [\fB\-\-no\-multi\fR] [\fB\-\-no\-mouse\fR] [\fB\-c\fR|\fB\-\-cmd\fR] [\fB\-i\fR|\fB\-\-interactive\fR] [\fB\-I \fR] [\fB\-\-color\fR] [\fB\-\-no\-hscroll\fR] [\fB\-\-keep\-right\fR] [\fB\-\-skip\-to\-pattern\fR] [\fB\-\-no\-clear\-if\-empty\fR] [\fB\-\-no\-clear\-start\fR] [\fB\-\-no\-clear\fR] [\fB\-\-show\-cmd\-error\fR] [\fB\-\-cycle\fR] [\fB\-\-disabled\fR] [\fB\-\-layout\fR] [\fB\-\-reverse\fR] [\fB\-\-height\fR] [\fB\-\-no\-height\fR] [\fB\-\-min\-height\fR] [\fB\-\-margin\fR] [\fB\-p\fR|\fB\-\-prompt\fR] [\fB\-\-cmd\-prompt\fR] [\fB\-\-selector\fR] [\fB\-\-multi\-selector\fR] [\fB\-\-ansi\fR] [\fB\-\-tabstop\fR] [\fB\-\-info\fR] [\fB\-\-no\-info\fR] [\fB\-\-inline\-info\fR] [\fB\-\-header\fR] [\fB\-\-header\-lines\fR] [\fB\-\-border\fR] [\fB\-\-wrap\fR] [\fB\-\-history\fR] [\fB\-\-history\-size\fR] [\fB\-\-cmd\-history\fR] [\fB\-\-cmd\-history\-size\fR] [\fB\-\-preview\fR] [\fB\-\-preview\-window\fR] [\fB\-q\fR|\fB\-\-query\fR] [\fB\-\-cmd\-query\fR] [\fB\-\-read0\fR] [\fB\-\-print0\fR] [\fB\-\-print\-query\fR] [\fB\-\-print\-cmd\fR] [\fB\-\-print\-score\fR] [\fB\-\-print\-header\fR] [\fB\-\-no\-strip\-ansi\fR] [\fB\-1\fR|\fB\-\-select\-1\fR] [\fB\-0\fR|\fB\-\-exit\-0\fR] [\fB\-\-sync\fR] [\fB\-\-pre\-select\-n\fR] [\fB\-\-pre\-select\-pat\fR] [\fB\-\-pre\-select\-items\fR] [\fB\-\-pre\-select\-file\fR] [\fB\-f\fR|\fB\-\-filter\fR] [\fB\-\-shell\fR] [\fB\-\-shell\-bindings\fR] [\fB\-\-man\fR] [\fB\-\-listen\fR] [\fB\-\-tmux\fR] [\fB\-\-log\-file\fR] [\fB\-\-expect\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR]
|
||||
.SH DESCRIPTION
|
||||
sk \- fuzzy finder in Rust
|
||||
.PP
|
||||
|
|
@ -197,6 +197,11 @@ ignore: Case\-insensitive matching
|
|||
.IP \(bu 2
|
||||
smart: Smart case: case\-insensitive unless query contains uppercase
|
||||
.RE
|
||||
.TP
|
||||
\fB\-\-split\-match\fR [\fI<SPLIT_MATCH>...\fR]
|
||||
Enable split matching and set delimiter
|
||||
|
||||
Split matching runs the matcher in splits: foo:bar will match all items matching foo, then :, then bar if the delimiter is present, or match normally if not.
|
||||
.SH INTERFACE
|
||||
.TP
|
||||
\fB\-b\fR, \fB\-\-bind\fR [\fI<BIND>...\fR] [default: ]
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ _sk() {
|
|||
|
||||
case "${cmd}" in
|
||||
sk)
|
||||
opts="-t -n -d -e -b -m -c -i -I -p -q -1 -0 -f -x -h -V --tac --min-query-length --no-sort --tiebreak --nth --with-nth --delimiter --exact --regex --algo --case --bind --multi --no-multi --no-mouse --cmd --interactive --color --no-hscroll --keep-right --skip-to-pattern --no-clear-if-empty --no-clear-start --no-clear --show-cmd-error --cycle --disabled --layout --reverse --height --no-height --min-height --margin --prompt --cmd-prompt --selector --multi-selector --ansi --tabstop --info --no-info --inline-info --header --header-lines --border --wrap --history --history-size --cmd-history --cmd-history-size --preview --preview-window --query --cmd-query --read0 --print0 --print-query --print-cmd --print-score --print-header --no-strip-ansi --select-1 --exit-0 --sync --pre-select-n --pre-select-pat --pre-select-items --pre-select-file --filter --shell --shell-bindings --man --listen --tmux --log-file --extended --literal --hscroll-off --filepath-word --jump-labels --no-bold --phony --expect --help --version"
|
||||
opts="-t -n -d -e -b -m -c -i -I -p -q -1 -0 -f -x -h -V --tac --min-query-length --no-sort --tiebreak --nth --with-nth --delimiter --exact --regex --algo --case --split-match --bind --multi --no-multi --no-mouse --cmd --interactive --color --no-hscroll --keep-right --skip-to-pattern --no-clear-if-empty --no-clear-start --no-clear --show-cmd-error --cycle --disabled --layout --reverse --height --no-height --min-height --margin --prompt --cmd-prompt --selector --multi-selector --ansi --tabstop --info --no-info --inline-info --header --header-lines --border --wrap --history --history-size --cmd-history --cmd-history-size --preview --preview-window --query --cmd-query --read0 --print0 --print-query --print-cmd --print-score --print-header --no-strip-ansi --select-1 --exit-0 --sync --pre-select-n --pre-select-pat --pre-select-items --pre-select-file --filter --shell --shell-bindings --man --listen --tmux --log-file --extended --literal --hscroll-off --filepath-word --jump-labels --no-bold --phony --expect --help --version"
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||
return 0
|
||||
|
|
@ -69,6 +69,10 @@ _sk() {
|
|||
COMPREPLY=($(compgen -W "respect ignore smart" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--split-match)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--bind)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ frizbee\t'Frizbee matching algorithm, typo resistant Will fallback to SkimV2 if
|
|||
complete -c sk -l case -d 'Case sensitivity' -r -f -a "respect\t'Case-sensitive matching'
|
||||
ignore\t'Case-insensitive matching'
|
||||
smart\t'Smart case: case-insensitive unless query contains uppercase'"
|
||||
complete -c sk -l split-match -d 'Enable split matching and set delimiter' -r
|
||||
complete -c sk -s b -l bind -d 'Comma separated list of bindings' -r
|
||||
complete -c sk -s c -l cmd -d 'Command to invoke dynamically in interactive mode' -r
|
||||
complete -c sk -s I -d 'Replace replstr with the selected item in commands' -r
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ module completions {
|
|||
--regex # Start in regex mode instead of fuzzy-match
|
||||
--algo: string@"nu-complete sk algorithm" # Fuzzy matching algorithm
|
||||
--case: string@"nu-complete sk case" # Case sensitivity
|
||||
--split-match: string # Enable split matching and set delimiter
|
||||
--bind(-b): string # Comma separated list of bindings
|
||||
--multi(-m) # Enable multiple selection
|
||||
--no-multi # Disable multiple selection
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ frizbee\:"Frizbee matching algorithm, typo resistant Will fallback to SkimV2 if
|
|||
'--case=[Case sensitivity]:CASE:((respect\:"Case-sensitive matching"
|
||||
ignore\:"Case-insensitive matching"
|
||||
smart\:"Smart case\: case-insensitive unless query contains uppercase"))' \
|
||||
'--split-match=[Enable split matching and set delimiter]::SPLIT_MATCH:_default' \
|
||||
'*-b+[Comma separated list of bindings]::BIND:_default' \
|
||||
'*--bind=[Comma separated list of bindings]::BIND:_default' \
|
||||
'-c+[Command to invoke dynamically in interactive mode]:CMD:_default' \
|
||||
|
|
|
|||
|
|
@ -64,24 +64,13 @@ impl MatchEngineFactory for ExactOrFuzzyEngineFactory {
|
|||
// !^abc$ => not "abc"
|
||||
|
||||
let mut query = query;
|
||||
let mut exact = false;
|
||||
let mut exact = self.exact_mode;
|
||||
let mut param = ExactMatchingParam::default();
|
||||
param.case = case;
|
||||
|
||||
if query.starts_with('\'') {
|
||||
if self.exact_mode {
|
||||
return Box::new(
|
||||
FuzzyEngine::builder()
|
||||
.query(&query[1..])
|
||||
.algorithm(self.fuzzy_algorithm)
|
||||
.case(case)
|
||||
.rank_builder(self.rank_builder.clone())
|
||||
.build(),
|
||||
);
|
||||
} else {
|
||||
exact = true;
|
||||
query = &query[1..];
|
||||
}
|
||||
exact = !exact;
|
||||
query = &query[1..];
|
||||
}
|
||||
|
||||
if query.starts_with('!') {
|
||||
|
|
@ -111,10 +100,6 @@ impl MatchEngineFactory for ExactOrFuzzyEngineFactory {
|
|||
param.postfix = true;
|
||||
}
|
||||
|
||||
if self.exact_mode {
|
||||
exact = true;
|
||||
}
|
||||
|
||||
if exact {
|
||||
Box::new(
|
||||
ExactEngine::builder(query, param)
|
||||
|
|
|
|||
|
|
@ -4,4 +4,5 @@ pub mod exact;
|
|||
pub mod factory;
|
||||
pub mod fuzzy;
|
||||
pub mod regexp;
|
||||
pub mod split;
|
||||
mod util;
|
||||
|
|
|
|||
151
src/engine/split.rs
Normal file
151
src/engine/split.rs
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
//! Split match engine for matching against different parts of items based on a delimiter.
|
||||
//!
|
||||
//! This engine splits both the query and item text on a delimiter character, then matches
|
||||
//! the query parts against the corresponding item parts.
|
||||
|
||||
use std::fmt::{Display, Error, Formatter};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{MatchEngine, MatchEngineFactory, MatchRange, MatchResult, SkimItem};
|
||||
|
||||
/// Engine that matches by splitting query and item on a delimiter
|
||||
pub struct SplitMatchEngine {
|
||||
/// The engine to match the "before delimiter" part
|
||||
before_engine: Box<dyn MatchEngine>,
|
||||
/// The engine to match the "after delimiter" part
|
||||
after_engine: Box<dyn MatchEngine>,
|
||||
/// The delimiter character used for splitting
|
||||
delimiter: char,
|
||||
}
|
||||
|
||||
impl SplitMatchEngine {
|
||||
/// Creates a new split match engine
|
||||
pub fn new(before_engine: Box<dyn MatchEngine>, after_engine: Box<dyn MatchEngine>, delimiter: char) -> Self {
|
||||
Self {
|
||||
before_engine,
|
||||
after_engine,
|
||||
delimiter,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MatchEngine for SplitMatchEngine {
|
||||
fn match_item(&self, item: Arc<dyn SkimItem>) -> Option<MatchResult> {
|
||||
let text = item.text();
|
||||
|
||||
// Find the delimiter in the item text (by char position)
|
||||
let delimiter_char_idx = text.chars().position(|c| c == self.delimiter)?;
|
||||
|
||||
// Get byte position for slicing
|
||||
let delimiter_byte_pos = text.char_indices().nth(delimiter_char_idx).map(|(i, _)| i)?;
|
||||
|
||||
let text_before = &text[..delimiter_byte_pos];
|
||||
let text_after = &text[delimiter_byte_pos + self.delimiter.len_utf8()..];
|
||||
|
||||
// Create wrapper items for each part
|
||||
let before_item: Arc<dyn SkimItem> = Arc::new(StringItem(text_before.to_string()));
|
||||
let after_item: Arc<dyn SkimItem> = Arc::new(StringItem(text_after.to_string()));
|
||||
|
||||
// Match both parts
|
||||
let before_result = self.before_engine.match_item(before_item)?;
|
||||
let after_result = self.after_engine.match_item(after_item)?;
|
||||
|
||||
// Combine the results - use rank from first result (like AndEngine does)
|
||||
let rank = before_result.rank;
|
||||
|
||||
let mut combined_indices: Vec<usize> = match before_result.matched_range {
|
||||
MatchRange::Chars(indices) => indices,
|
||||
MatchRange::ByteRange(start, end) => {
|
||||
// Convert byte range to char indices for the before part
|
||||
text_before
|
||||
.char_indices()
|
||||
.enumerate()
|
||||
.filter(|(_, (byte_idx, _))| *byte_idx >= start && *byte_idx < end)
|
||||
.map(|(char_idx, _)| char_idx)
|
||||
.collect()
|
||||
}
|
||||
};
|
||||
|
||||
// Offset for the "after" part: delimiter_char_idx + 1 (to skip the delimiter)
|
||||
let offset = delimiter_char_idx + 1;
|
||||
|
||||
let after_indices: Vec<usize> = match after_result.matched_range {
|
||||
MatchRange::Chars(indices) => indices.into_iter().map(|i| i + offset).collect(),
|
||||
MatchRange::ByteRange(start, end) => {
|
||||
// Convert byte range to char indices for the after part
|
||||
text_after
|
||||
.char_indices()
|
||||
.enumerate()
|
||||
.filter(|(_, (byte_idx, _))| *byte_idx >= start && *byte_idx < end)
|
||||
.map(|(char_idx, _)| char_idx + offset)
|
||||
.collect()
|
||||
}
|
||||
};
|
||||
|
||||
combined_indices.extend(after_indices);
|
||||
combined_indices.sort();
|
||||
combined_indices.dedup();
|
||||
|
||||
Some(MatchResult {
|
||||
rank,
|
||||
matched_range: MatchRange::Chars(combined_indices),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for SplitMatchEngine {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
|
||||
write!(
|
||||
f,
|
||||
"(Split[{}]: {} | {})",
|
||||
self.delimiter, self.before_engine, self.after_engine
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple string wrapper implementing SkimItem for split matching
|
||||
struct StringItem(String);
|
||||
|
||||
impl SkimItem for StringItem {
|
||||
fn text(&self) -> std::borrow::Cow<'_, str> {
|
||||
std::borrow::Cow::Borrowed(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// SplitMatchEngineFactory - wraps another factory and handles split matching
|
||||
|
||||
/// Factory that handles split matching by wrapping another engine factory
|
||||
pub struct SplitMatchEngineFactory {
|
||||
inner: Box<dyn MatchEngineFactory>,
|
||||
delimiter: char,
|
||||
}
|
||||
|
||||
impl SplitMatchEngineFactory {
|
||||
/// Creates a new split match engine factory
|
||||
pub fn new(inner: impl MatchEngineFactory + 'static, delimiter: char) -> Self {
|
||||
Self {
|
||||
inner: Box::new(inner),
|
||||
delimiter,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MatchEngineFactory for SplitMatchEngineFactory {
|
||||
fn create_engine_with_case(&self, query: &str, case: crate::CaseMatching) -> Box<dyn MatchEngine> {
|
||||
// Check if the query contains the delimiter
|
||||
if let Some(delimiter_pos) = query.find(self.delimiter) {
|
||||
let query_before = &query[..delimiter_pos];
|
||||
let query_after = &query[delimiter_pos + self.delimiter.len_utf8()..];
|
||||
|
||||
// Create engines for each part using the inner factory
|
||||
let before_engine = self.inner.create_engine_with_case(query_before, case);
|
||||
let after_engine = self.inner.create_engine_with_case(query_after, case);
|
||||
|
||||
Box::new(SplitMatchEngine::new(before_engine, after_engine, self.delimiter))
|
||||
} else {
|
||||
// No delimiter in query, pass through to inner factory
|
||||
self.inner.create_engine_with_case(query, case)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ use std::thread;
|
|||
|
||||
use rayon::prelude::*;
|
||||
|
||||
use crate::engine::split::SplitMatchEngineFactory;
|
||||
use crate::item::{ItemPool, MatchedItem, RankBuilder};
|
||||
use crate::prelude::{AndOrEngineFactory, ExactOrFuzzyEngineFactory, RegexEngineFactory};
|
||||
use crate::spinlock::SpinLock;
|
||||
|
|
@ -94,7 +95,15 @@ impl Matcher {
|
|||
.exact_mode(options.exact)
|
||||
.rank_builder(rank_builder)
|
||||
.build();
|
||||
Rc::new(AndOrEngineFactory::new(fuzzy_engine_factory))
|
||||
|
||||
// If split_match is enabled, wrap the fuzzy factory with SplitMatchEngineFactory
|
||||
// Then wrap with AndOrEngineFactory so that queries like "foo:bar baz:qux" work
|
||||
if let Some(delimiter) = options.split_match {
|
||||
let split_factory = SplitMatchEngineFactory::new(fuzzy_engine_factory, delimiter);
|
||||
Rc::new(AndOrEngineFactory::new(split_factory))
|
||||
} else {
|
||||
Rc::new(AndOrEngineFactory::new(fuzzy_engine_factory))
|
||||
}
|
||||
};
|
||||
|
||||
Matcher::builder(engine_factory).case(options.case).build()
|
||||
|
|
|
|||
|
|
@ -247,6 +247,21 @@ pub struct SkimOptions {
|
|||
)]
|
||||
pub case: CaseMatching,
|
||||
|
||||
/// Enable split matching and set delimiter
|
||||
///
|
||||
/// Split matching runs the matcher in splits: `foo:bar` will match all items matching `foo`, then
|
||||
/// `:`, then `bar` if the delimiter is present, or match normally if not.
|
||||
#[cfg_attr(
|
||||
feature = "cli",
|
||||
arg(
|
||||
long,
|
||||
default_missing_value = ":",
|
||||
help_heading = "Search",
|
||||
num_args=0..
|
||||
)
|
||||
)]
|
||||
pub split_match: Option<char>,
|
||||
|
||||
// --- Interface ---
|
||||
/// Comma separated list of bindings
|
||||
///
|
||||
|
|
@ -1125,6 +1140,7 @@ pub struct SkimOptions {
|
|||
impl Default for SkimOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
split_match: None,
|
||||
no_strip_ansi: false,
|
||||
wrap_items: false,
|
||||
listen: None,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
source: tests/split_match.rs
|
||||
expression: h.buffer_view()
|
||||
---
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
"> foo:bar "
|
||||
" 1/3 0/0"
|
||||
"> foo:bar "
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
source: tests/split_match.rs
|
||||
expression: h.buffer_view()
|
||||
---
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" foo:qux "
|
||||
" baz:qux "
|
||||
"> foo:bar "
|
||||
" 3/3 0/0"
|
||||
"> "
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
source: tests/split_match.rs
|
||||
expression: h.buffer_view()
|
||||
---
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
"> foo/bar "
|
||||
" 1/2 0/0"
|
||||
"> foo/bar "
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
source: tests/split_match.rs
|
||||
expression: h.buffer_view()
|
||||
---
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" baz/qux "
|
||||
"> foo/bar "
|
||||
" 2/2 0/0"
|
||||
"> "
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
source: tests/split_match.rs
|
||||
expression: h.buffer_view()
|
||||
---
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" 0/2 0/0"
|
||||
"> foo:bar "
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
source: tests/split_match.rs
|
||||
expression: h.buffer_view()
|
||||
---
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" bazqux "
|
||||
"> foobar "
|
||||
" 2/2 0/0"
|
||||
"> "
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
source: tests/split_match.rs
|
||||
expression: h.buffer_view()
|
||||
---
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" foo:qux "
|
||||
"> foo:bar "
|
||||
" 2/2 0/0"
|
||||
"> foo: "
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
source: tests/split_match.rs
|
||||
expression: h.buffer_view()
|
||||
---
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" foo:qux "
|
||||
"> foo:bar "
|
||||
" 2/2 0/0"
|
||||
"> "
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
source: tests/split_match.rs
|
||||
expression: h.buffer_view()
|
||||
---
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" baz:bar "
|
||||
"> foo:bar "
|
||||
" 2/2 0/0"
|
||||
"> :bar "
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
source: tests/split_match.rs
|
||||
expression: h.buffer_view()
|
||||
---
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" baz:bar "
|
||||
"> foo:bar "
|
||||
" 2/2 0/0"
|
||||
"> "
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
source: tests/split_match.rs
|
||||
expression: h.buffer_view()
|
||||
---
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" a:bc:cd "
|
||||
"> a:b:c "
|
||||
" 2/3 0/0"
|
||||
"> a:b:c "
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
source: tests/split_match.rs
|
||||
expression: h.buffer_view()
|
||||
---
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" a:bc:cd "
|
||||
" x:y:z "
|
||||
"> a:b:c "
|
||||
" 3/3 0/0"
|
||||
"> "
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
source: tests/split_match.rs
|
||||
expression: h.buffer_view()
|
||||
---
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
"> foobar "
|
||||
" 1/2 0/0"
|
||||
"> foo "
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
source: tests/split_match.rs
|
||||
expression: h.buffer_view()
|
||||
---
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" bazqux "
|
||||
"> foobar "
|
||||
" 2/2 0/0"
|
||||
"> "
|
||||
28
tests/snapshots/split_match__insta_split_match_or-2.snap
Normal file
28
tests/snapshots/split_match__insta_split_match_or-2.snap
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
source: tests/split_match.rs
|
||||
expression: h.buffer_view()
|
||||
---
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" x:yz "
|
||||
"> a:bc "
|
||||
" 2/3 0/0"
|
||||
"> a:b | x:y "
|
||||
28
tests/snapshots/split_match__insta_split_match_or.snap
Normal file
28
tests/snapshots/split_match__insta_split_match_or.snap
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
source: tests/split_match.rs
|
||||
expression: h.buffer_view()
|
||||
---
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" z:ab "
|
||||
" x:yz "
|
||||
"> a:bc "
|
||||
" 3/3 0/0"
|
||||
"> "
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
source: tests/split_match.rs
|
||||
expression: h.buffer_view()
|
||||
---
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" fbaz:boo "
|
||||
" baz:foo "
|
||||
" foo:qux "
|
||||
"> foo:bar "
|
||||
" 4/5 0/0"
|
||||
"> foo "
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
source: tests/split_match.rs
|
||||
expression: h.buffer_view()
|
||||
---
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" fbaz:boo "
|
||||
" baz:foo "
|
||||
" foo:qux "
|
||||
" baz:qux "
|
||||
"> foo:bar "
|
||||
" 5/5 0/0"
|
||||
"> "
|
||||
65
tests/split_match.rs
Normal file
65
tests/split_match.rs
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
#[allow(dead_code)]
|
||||
#[macro_use]
|
||||
mod common;
|
||||
|
||||
// Test 1: Basic split match - query without delimiter matches before delimiter in item
|
||||
insta_test!(insta_split_match_query_before_delimiter, ["foo:bar", "baz:qux", "foo:qux", "baz:foo", "fbaz:boo"], &["--split-match", ":"], {
|
||||
@snap;
|
||||
@type "foo";
|
||||
@snap;
|
||||
});
|
||||
|
||||
// Test 2: Query with delimiter matches both parts
|
||||
insta_test!(insta_split_match_both_parts, ["foo:bar", "baz:qux", "foo:qux"], &["--split-match", ":"], {
|
||||
@snap;
|
||||
@type "foo:bar";
|
||||
@snap;
|
||||
});
|
||||
|
||||
// Test 3: Query with delimiter - empty before, match after
|
||||
insta_test!(insta_split_match_empty_before, ["foo:bar", "baz:bar"], &["--split-match", ":"], {
|
||||
@snap;
|
||||
@type ":bar";
|
||||
@snap;
|
||||
});
|
||||
|
||||
// Test 4: Query with delimiter - match before, empty after
|
||||
insta_test!(insta_split_match_empty_after, ["foo:bar", "foo:qux"], &["--split-match", ":"], {
|
||||
@snap;
|
||||
@type "foo:";
|
||||
@snap;
|
||||
});
|
||||
|
||||
// Test 5: Item without delimiter - query without delimiter matches whole item
|
||||
insta_test!(insta_split_match_no_delimiter_in_item, ["foobar", "bazqux"], &["--split-match", ":"], {
|
||||
@snap;
|
||||
@type "foo";
|
||||
@snap;
|
||||
});
|
||||
|
||||
// Test 6: Item without delimiter - query with delimiter doesn't match
|
||||
insta_test!(insta_split_match_delimiter_in_query_not_item, ["foobar", "bazqux"], &["--split-match", ":"], {
|
||||
@snap;
|
||||
@type "foo:bar";
|
||||
@snap;
|
||||
});
|
||||
|
||||
// Test 7: Multiple delimiters in item - only first one is used for splitting
|
||||
insta_test!(insta_split_match_multiple_delimiters_in_item, ["a:b:c", "x:y:z", "a:bc:cd"], &["--split-match", ":"], {
|
||||
@snap;
|
||||
@type "a:b:c";
|
||||
@snap;
|
||||
});
|
||||
|
||||
// Test 8: Custom delimiter (/)
|
||||
insta_test!(insta_split_match_custom_delimiter, ["foo/bar", "baz/qux"], &["--split-match", "/"], {
|
||||
@snap;
|
||||
@type "foo/bar";
|
||||
@snap;
|
||||
});
|
||||
|
||||
insta_test!(insta_split_match_or, ["a:bc", "x:yz", "z:ab"], &["--split-match", ":"], {
|
||||
@snap;
|
||||
@type "a:b | x:y";
|
||||
@snap;
|
||||
});
|
||||
Loading…
Reference in a new issue