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:
LoricAndre 2026-01-21 18:35:30 +01:00 committed by GitHub
parent 1af0f643cb
commit 1ee19d3caa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 774 additions and 22 deletions

5
.config/insta.yaml Normal file
View file

@ -0,0 +1,5 @@
behavior:
output: minimal
test:
runner: nextest

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,28 @@
---
source: tests/split_match.rs
expression: h.buffer_view()
---
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
"> foo:bar "
" 1/3 0/0"
"> foo:bar "

View file

@ -0,0 +1,28 @@
---
source: tests/split_match.rs
expression: h.buffer_view()
---
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" foo:qux "
" baz:qux "
"> foo:bar "
" 3/3 0/0"
"> "

View file

@ -0,0 +1,28 @@
---
source: tests/split_match.rs
expression: h.buffer_view()
---
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
"> foo/bar "
" 1/2 0/0"
"> foo/bar "

View file

@ -0,0 +1,28 @@
---
source: tests/split_match.rs
expression: h.buffer_view()
---
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" baz/qux "
"> foo/bar "
" 2/2 0/0"
"> "

View file

@ -0,0 +1,28 @@
---
source: tests/split_match.rs
expression: h.buffer_view()
---
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" 0/2 0/0"
"> foo:bar "

View file

@ -0,0 +1,28 @@
---
source: tests/split_match.rs
expression: h.buffer_view()
---
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" bazqux "
"> foobar "
" 2/2 0/0"
"> "

View file

@ -0,0 +1,28 @@
---
source: tests/split_match.rs
expression: h.buffer_view()
---
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" foo:qux "
"> foo:bar "
" 2/2 0/0"
"> foo: "

View file

@ -0,0 +1,28 @@
---
source: tests/split_match.rs
expression: h.buffer_view()
---
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" foo:qux "
"> foo:bar "
" 2/2 0/0"
"> "

View file

@ -0,0 +1,28 @@
---
source: tests/split_match.rs
expression: h.buffer_view()
---
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" baz:bar "
"> foo:bar "
" 2/2 0/0"
"> :bar "

View file

@ -0,0 +1,28 @@
---
source: tests/split_match.rs
expression: h.buffer_view()
---
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" baz:bar "
"> foo:bar "
" 2/2 0/0"
"> "

View file

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

View file

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

View file

@ -0,0 +1,28 @@
---
source: tests/split_match.rs
expression: h.buffer_view()
---
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
"> foobar "
" 1/2 0/0"
"> foo "

View file

@ -0,0 +1,28 @@
---
source: tests/split_match.rs
expression: h.buffer_view()
---
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" bazqux "
"> foobar "
" 2/2 0/0"
"> "

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

View file

@ -0,0 +1,28 @@
---
source: tests/split_match.rs
expression: h.buffer_view()
---
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
" z:ab "
" x:yz "
"> a:bc "
" 3/3 0/0"
"> "

View file

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

View file

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