lotabout.skim/shell/completion.zsh
LoricAndre c65274441a
feat: add Arinae algorithm (#990)
* feat: initial work on skim v3

wip

* wip: SW

* chore: refactor SkimV3 to make it more maintainable

* chore: remove SIMD batch scores

* fix: fix Skim V3 tests

* feat: small optimizations

* feat: bigger optimizations

* chore: generate completions & manpage

* chore: remove unused wide dependency

* chore: update deps

* chore: generate completions & manpage

* fix: make sure all subsequences pass in non-typos mode

* chore: trade some performance against more precision with typos

* feat: gain the performance back using unchecked accesses

* chore: remove failing tests

* feat: use banding across whole upper triangle

* chore: remove useless DEAD_COL checks

* feat: make sure we match everything `frizbee` does while enforcing first char

* feat: minor optimizations

* feat: more minor optimizations

* chore: tweak parameters to find a good balance between performance and accuracy

* chore: accept snap

* chore: penalize consecutive typos

* chore: revert consecutive typos penalization as it seems useless in practice

* wip: optimizations

* feat: multiple optimizations

* perf(skim_v3): use 2-row rolling buffer for score-only DP path

When compute_indices=false (fuzzy_match), the full (n+1)×mcols matrix
was allocated and populated even though traceback was never performed.
Introduce score_only_dp() which maintains only two rows at a time,
reducing memory from O(n×m) to O(m) and improving cache utilization
for long choice strings.

* perf(skim_v3): add early termination when DP rows are all-zero

Track consecutive rows where no cell has a positive score. After 2
consecutive dead rows, return None immediately: gap penalties can only
decrease existing scores, so no downstream row can produce a positive
result. Applied to both score_only_dp and full_dp.

* perf(skim_v3): add range_dp for fuzzy_match_range, avoiding full index vec

fuzzy_match_range previously called fuzzy_indices (full traceback collecting
every matched index) just to extract the first and last. Introduce range_dp
which performs the same full-matrix DP but during traceback only records the
begin and end positions, avoiding the Vec allocation and index collection.
Add range_consistent_with_indices test to verify correctness.

* perf(skim_v3): remove redundant is_subsequence scan in exact mode

In non-typo mode, is_subsequence was called before compute_banding, but
compute_banding -> compute_first_match_cols already validates the same
subsequence property (returning None if any pattern char is absent).
Remove the redundant O(m) scan and delete the now-unused is_subsequence
function. Typo mode retains cheap_typo_prefilter as its guard.

* perf(skim_v3): avoid clone in traceback by using mem::take on thread-local buffer

Previously full_dp returned indices via indices_ref.to_vec() which copies
all n index values into a new allocation. Replace with std::mem::take which
moves ownership of the populated Vec out of the thread-local without copying,
trading the reuse-across-calls benefit for zero-copy return per call.

* perf(skim_v3): tighten typo-mode upper band bound in typo_vband_row

Previously the upper column bound in typo mode was always m (the full
choice length), even for early rows where the diagonal sits far from the
right edge. Compute hi = (j + bandwidth).min(m) symmetrically with the
existing lower bound, skipping cells that cannot contribute to a valid
alignment and reducing work for short patterns on long strings.

* perf(skim_v3): use memchr SIMD for first-char search in prefilter and banding

Add memchr as a direct dependency and implement Atom::find_first_in with
a u8-specialization that calls memchr() for case-sensitive search and a
two-call min-of-two approach for case-insensitive. Use this in:
- cheap_typo_prefilter: first-character existence check
- find_first_char: typo-mode banding anchor computation
This replaces scalar byte-by-byte loops with SIMD-vectorized searches for
ASCII inputs, the common case.

* revert(skim_v3): restore m upper bound in typo_vband_row

The tightened hi = (j + bandwidth).min(m) bound incorrectly rejected valid
typo-mode alignments where the optimal path takes many LEFT (gap) steps
past the bandwidth boundary. The snapshot test confirms 5 fewer matches vs
the expected 37. Revert to hi = m; the affine gap penalty alone prevents
poor alignments from winning.

* perf(skim_v3): add ASCII fast path to char::eq_ignore_case

Replace the to_lowercase() iterator comparison with eq_ignore_ascii_case()
for the common case where both chars are ASCII. This avoids creating two
ToLowercase iterators per comparison in the non-ASCII DP path, using a
single bitwise comparison instead.

* perf(skim_v3): replace RefCell with UnsafeCell (TLCell) in thread-locals

ThreadLocal<RefCell<T>> incurs a runtime borrow-check on every access.
Since ThreadLocal already guarantees per-thread isolation and we never
re-enter the same thread-local within a single call stack, the RefCell
check is redundant.

Replace with TLCell<T>, a Send newtype over UnsafeCell<T>, and a tl_get_mut
helper that returns &mut T directly. Document the safety invariant at each
call site. Also remove the now-unused SWMatrix::zero constructor.

* fix(skim_v3): fix precompute_bonuses reserve logic

The previous reserve(cho.len().saturating_sub(buf.len())) computed the
needed additional capacity relative to the current length, which could
be wrong if buf.len() was stale (e.g. after a set_len call on a longer
buffer). Replace with clear() + reserve(cho.len()) for a correct and
clear-intent O(1) reset followed by a single exact reservation.

* guard: return None for pat.len() > MAX_PAT_LEN in exact mode

Patterns longer than MAX_PAT_LEN (16) used the stack-allocated
[usize; MAX_PAT_LEN] banding arrays with out-of-bounds indices,
causing undefined behaviour in the exact (non-typo) DP path.

Add an early return of None in compute_first_match_cols and
compute_last_match_cols so callers gracefully skip overlong patterns
rather than reading past the end of a fixed-size array.  Typo mode
is unaffected: its dummy arrays are never indexed by the pattern
length.

* perf: re-encode Dir::None=0 so CELL_ZERO is all-zero bytes

Previously Dir::None=3 made Cell::new(0,Dir::None) encode as
0x00030000, preventing bulk-zeroing with write_bytes(0).

Re-assign discriminants to None=0, Diag=1, Up=2, Left=3 so that
CELL_ZERO is now all-zero.  Update:
- Dir discriminants in the enum
- Cell::is_diag() (checks tag==1 instead of 0)
- compute_cell branchless arithmetic (base is Left=3, subtract 2 for
  Diag wins, 1 for Up wins; None=0 so no OR needed)
- score_only_dp: replace init loop with write_bytes(0)
- full_dp / range_dp: replace row-0 init loop with write_bytes(0)

* perf: 128-bit ASCII bitset for cheap_typo_prefilter tail scan

Add Atom::count_tail_present with a u8 specialisation that builds a
two-u64 presence bitset from the choice in a single O(m) pass, making
each subsequent pattern-char lookup O(1) instead of O(m).

The char (non-ASCII) path delegates to count_tail_present_ordered, the
same ordered linear scan that was previously inlined in the function.
The change is observationally equivalent: the prefilter remains a
lenient superset of the old check (unordered vs. ordered presence),
and the snapshot test count is unchanged.

* perf: early exit in count_tail_present_ordered when match is impossible

Add a hopeless-state check at the top of each iteration: if matched
plus remaining pattern chars cannot reach min_needed, bail out
immediately rather than completing the full scan.

This prunes the non-ASCII (char) ordered-scan fallback inside
cheap_typo_prefilter when the pattern is long and many chars are
missing from the choice.

* cleanup: remove unused constants SEPARATOR_MASK_LO/HI and FIRST_CHAR_BONUS_MULTIPLIER

All three were suppressed with #[allow(dead_code)] and are not
referenced by any live code.  SEPARATOR_TABLE is the active lookup;
the mask constants were documentation remnants.

* refactor: replace unsafe transmute in Cell::dir() and compute_cell with safe match

Both usages converted a u8 (guaranteed 0..=3) to Dir via transmute.
Replace with an exhaustive match on the 2-bit tag value — no unsafe
required, and the compiler generates the same conditional-move
sequence.

* perf: Atom::is_sep() trait method avoids u8→char→u32 in separator check

Add is_sep() to the Atom trait with a u8 specialisation that indexes
SEPARATOR_TABLE directly with self as usize, skipping the into::<char>
conversion required by the generic default.

Remove the now-unnecessary is_separator free function; callers use
prev.is_sep() instead.

* refactor: precompute_bonuses rewritten as safe iterator chain

Replace the unsafe raw-pointer write loop with a safe iterator that
starts with START_OF_STRING_BONUS and maps windows-of-2 to the
separator/camelCase bonus formula.  buf.extend() dispatches through
ExactSizeIterator, so no extra allocation occurs.

The safe form exposes the element-independent structure to the
compiler, enabling auto-vectorisation on release builds.

* refactor: extract match_slices_range; simplify run_range

Add match_slices_range<C: Atom> that mirrors match_slices but calls
range_dp instead of dispatch_dp.  run_range now delegates the ASCII
path to match_slices_range and keeps only the non-ASCII char-buf
setup inline, eliminating the duplicated prefilter + bonus +
range_dp block.

* mem: SWMatrix::resize shrinks when buffer is 4× over-allocated

After a one-off large input, the full-DP matrix buffer could hold
significantly more memory than typical inputs require.  Add a
shrink-or-cap heuristic: if the current capacity exceeds 4× the
needed size, truncate and shrink_to(2×needed) to release excess
memory without thrashing on stable-sized inputs.

* Revert "mem: SWMatrix::resize shrinks when buffer is 4× over-allocated"

This reverts commit 9c8571ebe8.

* Revert "refactor: replace unsafe transmute in Cell::dir() and compute_cell with safe match"

This reverts commit 8805fa14ce.

* Revert "perf: Atom::is_sep() trait method avoids u8→char→u32 in separator check"

This reverts commit 175f26af81.

* Revert "perf: early exit in count_tail_present_ordered when match is impossible"

This reverts commit 29721558f0.

* Revert "perf: 128-bit ASCII bitset for cheap_typo_prefilter tail scan"

This reverts commit d79947fcb5.

* Revert "refactor: extract match_slices_range; simplify run_range"

This reverts commit 0fb7f05513.

* Revert "perf(skim_v3): replace RefCell with UnsafeCell (TLCell) in thread-locals"

This reverts commit 0806683251.

* Revert "perf(skim_v3): add ASCII fast path to char::eq_ignore_case"

This reverts commit 069710ad7c.

* Revert "revert(skim_v3): restore m upper bound in typo_vband_row"

This reverts commit 90ffc46633.

* Revert "perf(skim_v3): tighten typo-mode upper band bound in typo_vband_row"

This reverts commit f38ca3a10d.

* Revert "perf(skim_v3): avoid clone in traceback by using mem::take on thread-local buffer"

This reverts commit ffa9a21167.

* Revert "perf(skim_v3): add early termination when DP rows are all-zero"

This reverts commit 073195be58.

* Revert "perf(skim_v3): use 2-row rolling buffer for score-only DP path"

This reverts commit 3acacaad74.

* fix: reverse only order of frizbee indices

* chore: rename & refactor into multiple files

* chore: optimizations to the main flow

* fix: correct banding in non-typo path

* chore: generate completions & manpage

* docs: add algorithms section to the README [skip ci]

* fix(ari): correctly bound vband low

* chore(ari): specific pre-separator bonuses

* fix(ari): boost consec a bit more to beat start/sep

* chore: generate completions & manpage

* feat: run matcher over chunks

* chore: adjust penalties to keep typos under subsequences

* chore: accept snapshot

* fix: replace greedy ordered prefilter with looser unordered

* chore: finish up rename

* chore: review

---------

Co-authored-by: Skim bot <skim-bot@skim-rs.github.io>
2026-03-01 18:50:28 +01:00

201 lines
9.9 KiB
Bash

#compdef sk
autoload -U is-at-least
_sk() {
typeset -A opt_args
typeset -a _arguments_options
local ret=1
if is-at-least 5.2; then
_arguments_options=(-s -S -C)
else
_arguments_options=(-s -C)
fi
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' \
'*--nth=[Fields to be matched]:NTH:_default' \
'*--with-nth=[Fields to be transformed]:WITH_NTH:_default' \
'-d+[Delimiter between fields]:DELIMITER:_default' \
'--delimiter=[Delimiter between fields]:DELIMITER:_default' \
'--algo=[Fuzzy matching algorithm]:ALGORITHM:((skim_v1\:"Original skim fuzzy matching algorithm (v1)"
skim_v2\:"Improved skim fuzzy matching algorithm (v2, default)"
clangd\:"Clangd fuzzy matching algorithm"
fzy\:"Fzy matching algorithm (https\://github.com/jhawthorn/fzy)"
frizbee\:"Frizbee matching algorithm, typo resistant"
arinae\:"Arinae\: typo-resistant & natural algorithm"))' \
'--case=[Case sensitivity]:CASE:((respect\:"Case-sensitive matching"
ignore\:"Case-insensitive matching"
smart\:"Smart case\: case-insensitive unless query contains uppercase"))' \
'--typos=[Enable typo-tolerant matching]::TYPOS:_default' \
'--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' \
'--cmd=[Command to invoke dynamically in interactive mode]:CMD:_default' \
'-I+[Replace replstr with the selected item in commands]:REPLSTR:_default' \
'--color=[Set color theme]:COLOR:_default' \
'--skip-to-pattern=[Show the matched pattern at the line start]:SKIP_TO_PATTERN:_default' \
'--layout=[Set layout]:LAYOUT:((default\:"Display from the bottom of the screen"
reverse\:"Display from the top of the screen"
reverse-list\:"Display from the top of the screen, prompt at the bottom"))' \
'--height=[Height of skim'\''s window]:HEIGHT:_default' \
'--min-height=[Minimum height of skim'\''s window]:MIN_HEIGHT:_default' \
'--margin=[Screen margin]:MARGIN:_default' \
'-p+[Set prompt]:PROMPT:_default' \
'--prompt=[Set prompt]:PROMPT:_default' \
'--cmd-prompt=[Set prompt in command mode]:CMD_PROMPT:_default' \
'--selector=[Set selected item icon]:SELECTOR_ICON:_default' \
'--multi-selector=[Set selected item icon]:MULTI_SELECT_ICON:_default' \
'--tabstop=[Number of spaces that make up a tab]:TABSTOP:_default' \
'--ellipsis=[The characters used to display truncated lines]:ELLIPSIS:_default' \
'--info=[Set matching result count display position]:INFO:(default inline hidden)' \
'--header=[Set header, displayed next to the info]:HEADER:_default' \
'--header-lines=[Number of lines of the input treated as header]:HEADER_LINES:_default' \
'--border=[Draw borders around the UI components]::BORDER:(plain rounded double thick light-double-dashed heavy-double-dashed light-triple-dashed heavy-triple-dashed light-quadruple-dashed heavy-quadruple-dashed quadrant-inside quadrant-outside)' \
'--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_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' \
'-q+[Initial query]:QUERY:_default' \
'--query=[Initial query]:QUERY:_default' \
'--cmd-query=[Initial query in interactive mode]:CMD_QUERY:_default' \
'--output-format=[Set the output format If set, overrides all print_ options Will be expanded the same way as preview or commands]:OUTPUT_FORMAT:_default' \
'--pre-select-n=[Pre-select the first n items in multi-selection mode]:PRE_SELECT_N:_default' \
'--pre-select-pat=[Pre-select the matched items in multi-selection mode]:PRE_SELECT_PAT:_default' \
'--pre-select-items=[Pre-select the items separated by newline character]:PRE_SELECT_ITEMS:_default' \
'--pre-select-file=[Pre-select the items read from this file]:PRE_SELECT_FILE:_default' \
'-f+[Query for filter mode]:FILTER:_default' \
'--filter=[Query for filter mode]:FILTER:_default' \
'--shell=[Generate shell completion script]:SHELL:((bash\:"Bourne Again SHell"
elvish\:"Elvish shell"
fish\:"Friendly Interactive SHell"
nushell\:"Nushell (nu)"
power-shell\:"PowerShell"
zsh\:"Zsh"))' \
'--listen=[Run an IPC socket with optional name (defaults to sk)]::LISTEN:_default' \
'--remote=[Send commands to an IPC socket with optional name (defaults to sk)]::REMOTE:_default' \
'--tmux=[Run in a tmux popup]::TMUX:_default' \
'--log-file=[Pipe log output to a file]:LOG_FILE:_default' \
'*--flags=[Feature flags]:FLAGS:((no-preview-pty\:"Disable preview PTY on linux"
show-score\:"Display the item'\''s match score before its value in the item list (for matcher debugging)"))' \
'--hscroll-off=[]:HSCROLL_OFF:_default' \
'--jump-labels=[]:JUMP_LABELS:_default' \
'--scheme=[]:SCHEME:_default' \
'--tail=[]:TAIL:_default' \
'--style=[]:STYLE:_default' \
'--padding=[]:PADDING:_default' \
'--border-label=[]:BORDER_LABEL:_default' \
'--border-label-pos=[]:BORDER_LABEL_POS:_default' \
'--wrap-sign=[]:WRAP_SIGN:_default' \
'--gap=[]:GAP:_default' \
'--gap-line=[]:GAP_LINE:_default' \
'--freeze-left=[]:FREEZE_LEFT:_default' \
'--freeze-right=[]:FREEZE_RIGHT:_default' \
'--scroll-off=[]:SCROLL_OFF:_default' \
'--gutter=[]:GUTTER:_default' \
'--gutter-raw=[]:GUTTER_RAW:_default' \
'--marker-multi-line=[]:MARKER_MULTI_LINE:_default' \
'--scrollbar=[]:SCROLLBAR:_default' \
'--list-border=[]:LIST_BORDER:_default' \
'--list-label=[]:LIST_LABEL:_default' \
'--list-label-pos=[]:LIST_LABEL_POS:_default' \
'--info-command=[]:INFO_COMMAND:_default' \
'--separator=[]:SEPARATOR:_default' \
'--ghost=[]:GHOST:_default' \
'--input-border=[]:INPUT_BORDER:_default' \
'--input-label=[]:INPUT_LABEL:_default' \
'--input-label-pos=[]:INPUT_LABEL_POS:_default' \
'--preview-label=[]:PREVIEW_LABEL:_default' \
'--preview-label-pos=[]:PREVIEW_LABEL_POS:_default' \
'--header-border=[]:HEADER_BORDER:_default' \
'--header-lines-border=[]:HEADER_LINES_BORDER:_default' \
'--footer=[]:FOOTER:_default' \
'--footer-border=[]:FOOTER_BORDER:_default' \
'--footer-label=[]:FOOTER_LABEL:_default' \
'--footer-label-pos=[]:FOOTER_LABEL_POS:_default' \
'--with-shell=[]:WITH_SHELL:_default' \
'--expect=[Deprecated, kept for compatibility purposes. See accept() bind instead]:EXPECT:_default' \
'--tac[Show results in reverse order]' \
'--no-sort[Do not sort the results]' \
'-e[Run in exact mode]' \
'--exact[Run in exact mode]' \
'--regex[Start in regex mode instead of fuzzy-match]' \
'(--typos --typos)--no-typos[Disable typo-resistant matching]' \
'--normalize[Normalize unicode characters]' \
'-m[Enable multiple selection]' \
'--multi[Enable multiple selection]' \
'(-m --multi)--no-multi[Disable multiple selection]' \
'--no-mouse[Disable mouse]' \
'-i[Start skim in interactive mode]' \
'--interactive[Start skim in interactive mode]' \
'--no-hscroll[Disable horizontal scroll]' \
'--keep-right[Keep the right end of the line visible on overflow]' \
'--no-clear-if-empty[Do not clear previous line if the command returns an empty result]' \
'--no-clear-start[Do not clear items on start]' \
'--no-clear[Do not clear screen on exit]' \
'--show-cmd-error[Show error message if command fails]' \
'--cycle[Cycle the results by wrapping around when scrolling]' \
'--disabled[Disable matching entirely]' \
'--reverse[Shorthand for reverse layout]' \
'--no-height[Disable height (force full screen)]' \
'--ansi[Parse ANSI color codes in input strings]' \
'--no-info[Alias for --info=hidden]' \
'--inline-info[Alias for --info=inline]' \
'--wrap[Wrap items in the item list]' \
'--read0[Read input delimited by ASCII NUL(\\0) characters]' \
'--print0[Print output delimited by ASCII NUL(\\0) characters]' \
'--print-query[Print the query as the first line]' \
'--print-cmd[Print the command as the first line (after print-query)]' \
'--print-score[Print the score after each item]' \
'--print-header[Print the header as the first line (after print-score)]' \
'--print-current[Print the current (highlighted) item as the first line (after print-header)]' \
'--no-strip-ansi[Print the ANSI codes, making the output exactly match the input even when --ansi is on]' \
'-1[Do not enter the TUI if the query passed in -q matches only one item and return it]' \
'--select-1[Do not enter the TUI if the query passed in -q matches only one item and return it]' \
'-0[Do not enter the TUI if the query passed in -q does not match any item]' \
'--exit-0[Do not enter the TUI if the query passed in -q does not match any item]' \
'--sync[Synchronous search for multi-staged filtering]' \
'--shell-bindings[Generate shell key bindings - only for bash, zsh and fish]' \
'--man[Generate man page and output it to stdout]' \
'-x[]' \
'--extended[]' \
'--literal[]' \
'--filepath-word[]' \
'--no-bold[]' \
'--phony[]' \
'--no-color[]' \
'--highlight-line[]' \
'--no-multi-line[]' \
'--raw[]' \
'--track[]' \
'--no-scrollbar[]' \
'--no-input[]' \
'--no-separator[]' \
'--header-first[]' \
'-h[Print help (see more with '\''--help'\'')]' \
'--help[Print help (see more with '\''--help'\'')]' \
'-V[Print version]' \
'--version[Print version]' \
&& ret=0
}
(( $+functions[_sk_commands] )) ||
_sk_commands() {
local commands; commands=()
_describe -t commands 'sk commands' commands "$@"
}
if [ "$funcstack[1]" = "_sk" ]; then
_sk "$@"
else
compdef _sk sk
fi