feat: add min query length option (#806)

* feat: add min query length option

* chore: generate completions & manpage

* chore: fmt

---------

Co-authored-by: LoricAndre <loric.andre@pm.me>
Co-authored-by: Skim bot <skim-bot@skim-rs.github.io>
This commit is contained in:
LoricAndre 2025-06-22 00:25:55 +02:00 committed by GitHub
parent e66813518d
commit 71b82d0f58
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 105 additions and 3 deletions

28
AGENTS.md Normal file
View file

@ -0,0 +1,28 @@
# Skim Agent Guidelines
## Build/Test/Lint Commands
- Build: `cargo build [--release]`
- Run: `cargo run [--release]`
- Test (all): `cargo test`
- Test (single): `cargo test test_name` or `cargo test -- test_name`
- E2E tests: `cargo test -p e2e`
- Lint: `cargo clippy`
- Format: `cargo fmt` (check only: `cargo fmt --check`)
## Code Style
- Format with 120 char line width (defined in .rustfmt.toml)
- Use standard Rust naming conventions (snake_case for functions/variables, CamelCase for types)
- Organize imports by standard library, external crates, then internal modules
- Prefer Option/Result types for error handling over panicking
- Use proper error propagation with `?` operator
- Document public API with rustdoc comments
- Use meaningful type annotations, especially for public functions
- Follow the existing structure for new modules (see src/engine/ or src/model/)
- Implement relevant traits (SkimItem, etc.) for new types when needed
## Project Structure
- Core functionality in `skim/src/`
- UI toolkit in `skim-tuikit/`
- Common utilities in `skim-common/`
- End-to-end tests in `e2e/`
- Task automation in `xtask/`

View file

@ -49,6 +49,29 @@ fn opt_with_nth_preview() -> Result<()> {
Ok(())
}
#[test]
fn opt_min_query_length() -> Result<()> {
let (tmux, _) = setup("line1\nline2\nline3", &["--min-query-length", "3"])?;
// With empty query, no results should be shown
let lines = tmux.capture()?;
assert!(!lines.iter().any(|s| s.contains("line")));
// Type 'li' (2 chars), still no results should be shown
tmux.send_keys(&[Key('l'), Key('i')])?;
tmux.until(|l| l[0].starts_with("> li"))?;
let lines = tmux.capture()?;
assert!(!lines.iter().any(|s| s.contains("line")));
// Type 'n' (3rd char), now results should appear
tmux.send_keys(&[Key('n')])?;
tmux.until(|l| l[0].starts_with("> lin"))?;
let lines = tmux.capture()?;
assert!(lines.iter().any(|s| s.contains("line")));
Ok(())
}
#[test]
fn opt_with_nth_1() -> Result<()> {
let (tmux, _) = setup("f1,f2,f3,f4", &["--delimiter", ",", "--with-nth", "1"])?;

View file

@ -12,7 +12,7 @@ fn setup_tmux_mock(tmux: &TmuxController) -> Result<String> {
let dir = &tmux.tempdir;
let path = dir.path().join("tmux");
let mock_bin = Path::new(&path);
let mut writer = File::create_new(mock_bin)?;
let mut writer = File::create(mock_bin)?;
let outfile = dir.path().join("tmux-mock-cmd");
writer.write_fmt(format_args!(
"#!/bin/sh

View file

@ -4,7 +4,7 @@
.SH NAME
sk \- sk \- fuzzy finder in Rust
.SH SYNOPSIS
\fBsk\fR [\fB\-\-tac\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\-\-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\-\-ansi\fR] [\fB\-\-tabstop\fR] [\fB\-\-info\fR] [\fB\-\-no\-info\fR] [\fB\-\-inline\-info\fR] [\fB\-\-header\fR] [\fB\-\-header\-lines\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\-\-expect\fR] [\fB\-\-read0\fR] [\fB\-\-print0\fR] [\fB\-\-print\-query\fR] [\fB\-\-print\-cmd\fR] [\fB\-\-print\-score\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\-\-tmux\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\-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\-\-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\-\-ansi\fR] [\fB\-\-tabstop\fR] [\fB\-\-info\fR] [\fB\-\-no\-info\fR] [\fB\-\-inline\-info\fR] [\fB\-\-header\fR] [\fB\-\-header\-lines\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\-\-expect\fR] [\fB\-\-read0\fR] [\fB\-\-print0\fR] [\fB\-\-print\-query\fR] [\fB\-\-print\-cmd\fR] [\fB\-\-print\-score\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\-\-tmux\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR]
.SH DESCRIPTION
sk \- fuzzy finder in Rust
.PP
@ -71,6 +71,11 @@ Show results in reverse order
Often used in combination with \-\-no\-sort
.TP
\fB\-\-min\-query\-length\fR=\fIMIN_QUERY_LENGTH\fR
Minimum query length to start showing results
Only show results when the query is at least this many characters long
.TP
\fB\-\-no\-sort\fR
Do not sort the results

View file

@ -23,12 +23,16 @@ _sk() {
case "${cmd}" in
sk)
opts="-t -n -d -e -b -m -c -i -I -p -q -1 -0 -f -x -h -V --tac --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 --layout --reverse --height --no-height --min-height --margin --prompt --cmd-prompt --ansi --tabstop --info --no-info --inline-info --header --header-lines --history --history-size --cmd-history --cmd-history-size --preview --preview-window --query --cmd-query --expect --read0 --print0 --print-query --print-cmd --print-score --select-1 --exit-0 --sync --pre-select-n --pre-select-pat --pre-select-items --pre-select-file --filter --shell --tmux --extended --literal --cycle --hscroll-off --filepath-word --jump-labels --border --no-bold --pointer --marker --phony --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 --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 --layout --reverse --height --no-height --min-height --margin --prompt --cmd-prompt --ansi --tabstop --info --no-info --inline-info --header --header-lines --history --history-size --cmd-history --cmd-history-size --preview --preview-window --query --cmd-query --expect --read0 --print0 --print-query --print-cmd --print-score --select-1 --exit-0 --sync --pre-select-n --pre-select-pat --pre-select-items --pre-select-file --filter --shell --tmux --extended --literal --cycle --hscroll-off --filepath-word --jump-labels --border --no-bold --pointer --marker --phony --help --version"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
--min-query-length)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--tiebreak)
COMPREPLY=($(compgen -W "score -score begin -begin end -end length -length index -index" -- "${cur}"))
return 0

View file

@ -1,3 +1,4 @@
complete -c sk -l min-query-length -d 'Minimum query length to start showing results' -r
complete -c sk -s t -l tiebreak -d 'Comma-separated list of sort criteria to apply when the scores are tied' -r -f -a "score\t''
-score\t''
begin\t''

View file

@ -15,6 +15,7 @@ _sk() {
local context curcontext="$curcontext" state line
_arguments "${_arguments_options[@]}" : \
'--min-query-length=[Minimum query length to start showing results]:MIN_QUERY_LENGTH:_default' \
'*-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' \

View file

@ -94,6 +94,9 @@ pub struct Model {
no_clear_if_empty: bool,
theme: Arc<ColorTheme>,
// Minimum query length to show results
min_query_length: Option<usize>,
// timer thread for scheduled events
timer: Timer,
hb_timer_guard: Option<TimerGuard>,
@ -177,6 +180,7 @@ impl Model {
info: InfoDisplay::Default,
no_clear_if_empty: false,
theme,
min_query_length: options.min_query_length,
timer: Timer::new(),
hb_timer_guard: None,
@ -288,6 +292,15 @@ impl Model {
}
fn act_heart_beat(&mut self, env: &mut ModelEnv) {
// Check if query meets minimum length requirement
if let Some(min_length) = self.min_query_length {
if env.query.chars().count() < min_length {
// Clear selection if query is too short
self.selection.clear();
return;
}
}
// save the processed items
let matcher_stopped = self
.matcher_control
@ -406,6 +419,19 @@ impl Model {
if let Some(ctrl) = self.matcher_control.take() {
ctrl.kill();
}
// Check if query meets minimum length requirement
if let Some(min_length) = self.min_query_length {
if env.query.chars().count() < min_length {
// Clear selection if query is too short
self.selection.clear();
// Don't restart matcher if query is too short
self.item_pool.reset();
self.num_options = 0;
return;
}
}
env.clear_selection = ClearStrategy::Clear;
self.item_pool.reset();
self.num_options = 0;
@ -737,6 +763,14 @@ impl Model {
// send heart beat (so that heartbeat/refresh is triggered)
let _ = self.tx.send((Key::Null, Event::EvHeartBeat));
// Check if query meets minimum length requirement
if let Some(min_length) = self.min_query_length {
if query.chars().count() < min_length {
// Don't run matcher if query is too short
return;
}
}
let matcher = if self.use_regex {
&self.regex_matcher
} else {

View file

@ -88,6 +88,12 @@ pub struct SkimOptions {
#[arg(long, help_heading = "Search")]
pub tac: bool,
/// Minimum query length to start showing results
///
/// Only show results when the query is at least this many characters long
#[arg(long, help_heading = "Search")]
pub min_query_length: Option<usize>,
/// Do not sort the results
///
/// *Often used in combination with `--tac`*