mirror of
https://github.com/lotabout/skim.git
synced 2026-09-10 07:16:23 -04:00
feat: add --hide-nth to hide fields from display but keep them searchable (#1122)
* Add --hide-nth flag to hide fields while keeping them searchable Introduce a `--hide-nth <fieldspec>` option that takes the same comma-separated field index expressions as `--nth`/`--with-nth`. The listed fields are removed from the displayed line but remain part of the text used for matching, so a query can still match them. Characters in the hidden fields are ignored for match highlighting and horizontal scrolling. Implementation: - Resolve the fieldspec to byte ranges in the same coordinate space as the matching/display text and store them as `hidden_ranges` in DefaultSkimItem metadata, exposed via a new `SkimItem::hidden_ranges()` trait method. text()/output() keep the full text so hidden fields stay searchable and are preserved on output. - DefaultSkimItem::display() removes hidden characters and remaps match highlight positions into visible coordinates (project_visible_text / project_match_indices); this path takes precedence over ANSI styling. - ItemRenderer::render_item applies the same projection to derive the visible sub-line text and hscroll match range, so hidden characters are ignored for horizontal scrolling. Add unit tests for range normalization/projection and item behavior, plus insta snapshot tests covering display removal, searchability, and hscroll. Update ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BCunXeAFMYAFduTc8SNjSM * Preserve ANSI colors for surviving text under --hide-nth Previously the hidden-field rendering path was applied ahead of the ANSI display branch and rebuilt the line from the ANSI-stripped text, so combining --hide-nth with --ansi dropped the colors of the visible fields. Integrate hidden-field removal into the ANSI branch instead: after parsing the styled spans, drop the hidden characters while preserving each span's style (retain_visible_spans) and remap the match positions into the resulting visible coordinate space, then run the normal highlighting. The plain (non-ANSI) branch keeps its project-and-to_line handling. Surviving characters now keep their ANSI colors while hidden fields stay searchable. Add unit tests for ANSI color preservation and remapped highlighting, plus ANSI color-snapshot integration tests. Update ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BCunXeAFMYAFduTc8SNjSM * Set hidden fields via builder instead of DefaultSkimItem::new param Remove the `hidden_fields` parameter from `DefaultSkimItem::new` and set the hidden fields through a `hidden_fields(&[FieldRange], &Regex)` builder method instead. The builder resolves the fields against the item's own `text()` (the same coordinate space `new` would have used), so the result is identical while keeping `new`'s signature unchanged for its many existing call sites. The reader chains `.hidden_fields(&opt.hidden_fields, &opt.delimiter)` onto construction. Revert the extra `&[]` argument at the other call sites (selector, fuzz target, tests) and update the hide-nth tests to use the builder. Update ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BCunXeAFMYAFduTc8SNjSM * chore: generate files --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
55aa7dacd5
commit
dce26d622a
|
|
@ -102,7 +102,7 @@ skim/ ← workspace root
|
|||
│ ├── binds.rs ← KeyMap, parse_key, parse_action_chain
|
||||
│ ├── theme.rs ← ColorTheme, named palettes
|
||||
│ ├── thread_pool.rs ← ThreadPool + parallel_work_queue
|
||||
│ ├── field.rs ← field range parsing (--nth / --with-nth)
|
||||
│ ├── field.rs ← field range parsing (--nth / --with-nth / --hide-nth)
|
||||
│ ├── spinlock.rs ← lightweight SpinLock<T>
|
||||
│ ├── util.rs ← printf helper, misc utilities
|
||||
│ ├── popup/ ← tmux & zellij popup integration
|
||||
|
|
@ -428,7 +428,8 @@ Source (stdin bytes or child process stdout)
|
|||
│ assigns monotonic sequence numbers, sends to MPMC channel
|
||||
├─ Thread N: workers — receive chunks, validate UTF-8,
|
||||
│ create DefaultSkimItem::new(line, ansi, trans_fields, matching_fields, delimiter)
|
||||
│ (handles ANSI stripping, --nth / --with-nth transforms inline),
|
||||
│ .hidden_fields(hidden_fields, delimiter)
|
||||
│ (handles ANSI stripping, --nth / --with-nth / --hide-nth inline),
|
||||
│ send (seq, items) pairs
|
||||
├─ Thread 1: reorder — collects (seq, items), emits in order through SkimItemReceiver;
|
||||
│ drops tx_pipeline_done on exit (signals killer thread)
|
||||
|
|
@ -458,6 +459,24 @@ SkimItemReceiver channel
|
|||
|
||||
Fields `/0` bytes are stripped from `text` (used for display/matching) but preserved in `orig_text` (used for output).
|
||||
|
||||
**`--hide-nth`** is orthogonal to the matrix above and applied through the builder method
|
||||
`DefaultSkimItem::hidden_fields(hidden_fields, delimiter)` after construction (rather than a `new`
|
||||
parameter). The requested fields are resolved to byte ranges (in the same coordinate space as
|
||||
`text()` — the stripped text under `--ansi`, otherwise the `text` field) and stored as
|
||||
`hidden_ranges` in the item metadata, exposed via the `SkimItem::hidden_ranges()` trait method. The
|
||||
hidden fields **remain part of `text()`**, so they stay searchable and still participate in matching.
|
||||
They only affect rendering:
|
||||
|
||||
- `DefaultSkimItem::display()` removes the hidden characters and remaps the match highlight
|
||||
positions into the visible coordinate space (`project_visible_text` / `project_match_indices` in
|
||||
`src/helper/item.rs`). This is integrated into **both** display branches: the plain branch projects
|
||||
the text through `to_line`, and the ANSI branch drops the hidden characters from the already-parsed
|
||||
styled spans (`retain_visible_spans`) so surviving characters **keep their ANSI colors**, then runs
|
||||
the normal highlighting on the remapped visible-coordinate matches.
|
||||
- `ItemRenderer::render_item` applies the same projection to derive the visible sub-line text and the
|
||||
match range used for horizontal scrolling, so hidden characters are ignored for hscroll and never
|
||||
highlighted.
|
||||
|
||||
---
|
||||
|
||||
## The Matching Subsystem
|
||||
|
|
@ -1216,10 +1235,10 @@ The global allocator is `mimalloc` (v3), chosen for its low-latency multi-thread
|
|||
| `merge_worker_results` | `src/matcher.rs:28` | Merge k sorted runs → ProcessedItems |
|
||||
| `ItemPool::append` | `src/item.rs:469` | Add items, notify matcher |
|
||||
| `ItemPool::take` | `src/item.rs:502` | Take un-matched items for matcher |
|
||||
| `DefaultSkimItem::new` | `src/helper/item.rs:58` | ANSI strip, field transform, ranges, disable pattern |
|
||||
| `SkimItemReader::parallel_bufread` | `src/helper/item_reader.rs:263` | Unified parallel pipeline (all inputs) |
|
||||
| `spawn_io_reader` | `src/helper/item_reader.rs:354` | I/O reader thread: chunk reads + line splitting |
|
||||
| `spawn_reorder_thread` | `src/helper/item_reader.rs:458` | Reorder thread: ordered output + pipeline-done signal |
|
||||
| `DefaultSkimItem::new` | `src/helper/item.rs:64` | ANSI strip, field transform, matching ranges (hidden ranges set later via `hidden_fields` builder) |
|
||||
| `SkimItemReader::parallel_bufread` | `src/helper/item_reader.rs:287` | Unified parallel pipeline (all inputs) |
|
||||
| `spawn_io_reader` | `src/helper/item_reader.rs:378` | I/O reader thread: chunk reads + line splitting |
|
||||
| `spawn_reorder_thread` | `src/helper/item_reader.rs:483` | Reorder thread: ordered output + pipeline-done signal |
|
||||
| `Preview::spawn` | `src/tui/preview.rs:319` | Start image, PTY, or plain preview worker |
|
||||
| `Tui::new_with_height_and_backend` | `src/tui/backend.rs:77` | Terminal init + viewport sizing |
|
||||
| `Tui::enter` | `src/tui/backend.rs:126` | Enable raw mode + terminal setup |
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@
|
|||
gnuplot
|
||||
llvm
|
||||
cargo-bloat
|
||||
cargo-public-api
|
||||
];
|
||||
gungraun = with pkgs; [
|
||||
valgrind
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ sk \- Fuzzy Finder in rust!
|
|||
.ie \n(.g .ds Aq \(aq
|
||||
.el .ds Aq '
|
||||
.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\-\-typos\fR] [\fB\-\-no\-typos\fR] [\fB\-\-normalize\fR] [\fB\-\-split\-match\fR] [\fB\-\-last\-match\fR] [\fB\-\-scheme\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\-\-highlight\-line\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\-\-disable\-pattern\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\-\-border\-no\-collapse\fR] [\fB\-\-no\-border\fR] [\fB\-\-wrap\fR] [\fB\-\-multiline\fR] [\fB\-\-scrollbar\fR] [\fB\-\-no\-scrollbar\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\-\-image\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\-\-print\-current\fR] [\fB\-\-output\-format\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\-\-remote\fR] [\fB\-\-popup\fR] [\fB\-\-log\-level\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\-\-hide\-nth\fR] [\fB\-d\fR|\fB\-\-delimiter\fR] [\fB\-e\fR|\fB\-\-exact\fR] [\fB\-\-regex\fR] [\fB\-\-algo\fR] [\fB\-\-case\fR] [\fB\-\-typos\fR] [\fB\-\-no\-typos\fR] [\fB\-\-normalize\fR] [\fB\-\-split\-match\fR] [\fB\-\-last\-match\fR] [\fB\-\-scheme\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\-\-highlight\-line\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\-\-disable\-pattern\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\-\-border\-no\-collapse\fR] [\fB\-\-no\-border\fR] [\fB\-\-wrap\fR] [\fB\-\-multiline\fR] [\fB\-\-scrollbar\fR] [\fB\-\-no\-scrollbar\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\-\-image\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\-\-print\-current\fR] [\fB\-\-output\-format\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\-\-remote\fR] [\fB\-\-popup\fR] [\fB\-\-log\-level\fR] [\fB\-\-log\-file\fR] [\fB\-\-expect\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR]
|
||||
.ie \n(.g .ds Aq \(aq
|
||||
.el .ds Aq '
|
||||
.SH OPTIONS
|
||||
|
|
@ -94,6 +94,16 @@ Fields to be transformed
|
|||
|
||||
See nth for the details
|
||||
.TP
|
||||
\fB\-\-hide\-nth\fR \fI<HIDE_NTH>\fR [default: ]
|
||||
Fields to hide from display while keeping them searchable
|
||||
|
||||
Takes the same comma\-separated field index expressions as **nth**. The listed
|
||||
fields are removed from the displayed line but remain part of the text used for
|
||||
matching, so a query can still match them. Characters in the hidden fields are
|
||||
ignored for match highlighting and horizontal scrolling.
|
||||
|
||||
See **nth** for the field index expression syntax.
|
||||
.TP
|
||||
\fB\-d\fR, \fB\-\-delimiter\fR \fI<DELIMITER>\fR [default: [\\t\\n ]+]
|
||||
Delimiter between fields
|
||||
|
||||
|
|
|
|||
|
|
@ -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 --typos --no-typos --normalize --split-match --last-match --scheme --bind --multi --no-multi --no-mouse --cmd --interactive --color --highlight-line --no-hscroll --keep-right --skip-to-pattern --no-clear-if-empty --no-clear-start --no-clear --show-cmd-error --cycle --disabled --disable-pattern --layout --reverse --height --no-height --min-height --margin --prompt --cmd-prompt --selector --multi-selector --ansi --tabstop --ellipsis --info --no-info --inline-info --header --header-lines --border --border-no-collapse --no-border --wrap --multiline --scrollbar --no-scrollbar --history --history-size --cmd-history --cmd-history-size --preview --preview-window --image --query --cmd-query --read0 --print0 --print-query --print-cmd --print-score --print-header --print-current --output-format --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 --remote --popup --log-level --log-file --flags --extended --literal --hscroll-off --filepath-word --jump-labels --no-bold --phony --tail --style --no-color --padding --border-label --border-label-pos --wrap-sign --no-multi-line --raw --track --gap --gap-line --freeze-left --freeze-right --scroll-off --gutter --gutter-raw --marker-multi-line --list-border --list-label --list-label-pos --no-input --info-command --separator --no-separator --ghost --input-border --input-label --input-label-pos --preview-label --preview-label-pos --header-first --header-border --header-lines-border --footer --footer-border --footer-label --footer-label-pos --with-shell --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 --hide-nth --delimiter --exact --regex --algo --case --typos --no-typos --normalize --split-match --last-match --scheme --bind --multi --no-multi --no-mouse --cmd --interactive --color --highlight-line --no-hscroll --keep-right --skip-to-pattern --no-clear-if-empty --no-clear-start --no-clear --show-cmd-error --cycle --disabled --disable-pattern --layout --reverse --height --no-height --min-height --margin --prompt --cmd-prompt --selector --multi-selector --ansi --tabstop --ellipsis --info --no-info --inline-info --header --header-lines --border --border-no-collapse --no-border --wrap --multiline --scrollbar --no-scrollbar --history --history-size --cmd-history --cmd-history-size --preview --preview-window --image --query --cmd-query --read0 --print0 --print-query --print-cmd --print-score --print-header --print-current --output-format --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 --remote --popup --log-level --log-file --flags --extended --literal --hscroll-off --filepath-word --jump-labels --no-bold --phony --tail --style --no-color --padding --border-label --border-label-pos --wrap-sign --no-multi-line --raw --track --gap --gap-line --freeze-left --freeze-right --scroll-off --gutter --gutter-raw --marker-multi-line --list-border --list-label --list-label-pos --no-input --info-command --separator --no-separator --ghost --input-border --input-label --input-label-pos --preview-label --preview-label-pos --header-first --header-border --header-lines-border --footer --footer-border --footer-label --footer-label-pos --with-shell --expect --help --version"
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||
return 0
|
||||
|
|
@ -53,6 +53,10 @@ _sk() {
|
|||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--hide-nth)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--delimiter)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ pathname\t''
|
|||
-pathname\t''"
|
||||
complete -c sk -s n -l nth -d 'Fields to be matched' -r
|
||||
complete -c sk -l with-nth -d 'Fields to be transformed' -r
|
||||
complete -c sk -l hide-nth -d 'Fields to hide from display while keeping them searchable' -r
|
||||
complete -c sk -s d -l delimiter -d 'Delimiter between fields' -r
|
||||
complete -c sk -l algo -d 'Fuzzy matching algorithm' -r -f -a "arinae\t'Arinae: typo-resistant & natural algorithm, default'
|
||||
clangd\t'Clangd fuzzy matching algorithm'
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ module completions {
|
|||
--tiebreak(-t): string@"nu-complete sk tiebreak" # Comma-separated list of sort criteria to apply when the scores are tied.
|
||||
--nth(-n): string # Fields to be matched
|
||||
--with-nth: string # Fields to be transformed
|
||||
--hide-nth: string # Fields to hide from display while keeping them searchable
|
||||
--delimiter(-d): string # Delimiter between fields
|
||||
--exact(-e) # Run in exact mode
|
||||
--regex # Start in regex mode instead of fuzzy-match
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ _sk() {
|
|||
'*-n+[Fields to be matched]:NTH:_default' \
|
||||
'*--nth=[Fields to be matched]:NTH:_default' \
|
||||
'*--with-nth=[Fields to be transformed]:WITH_NTH:_default' \
|
||||
'*--hide-nth=[Fields to hide from display while keeping them searchable]:HIDE_NTH:_default' \
|
||||
'-d+[Delimiter between fields]:DELIMITER:_default' \
|
||||
'--delimiter=[Delimiter between fields]:DELIMITER:_default' \
|
||||
'--algo=[Fuzzy matching algorithm]:ALGORITHM:((arinae\:"Arinae\: typo-resistant & natural algorithm, default"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
//! Including the `DefaultSkimItem`
|
||||
use crate::field::{FieldRange, parse_matching_fields, parse_transform_fields};
|
||||
use crate::tui::util::merge_styles;
|
||||
use crate::{DisplayContext, SkimItem};
|
||||
use crate::{DisplayContext, Matches, SkimItem};
|
||||
use ansi_to_tui::IntoText;
|
||||
use ratatui::text::{Line, Span};
|
||||
use regex::Regex;
|
||||
|
|
@ -48,6 +48,12 @@ pub struct DefaultSkimItemMetadata {
|
|||
/// The ranges on which to perform matching
|
||||
matching_ranges: Option<Vec<(usize, usize)>>,
|
||||
|
||||
/// Byte ranges (in the display/matching text) of fields hidden via `--hide-nth`.
|
||||
/// Characters inside these ranges are removed from the rendered line and ignored
|
||||
/// for match highlighting and horizontal scrolling, but remain part of the text
|
||||
/// used for matching so they stay searchable.
|
||||
hidden_ranges: Option<Vec<(usize, usize)>>,
|
||||
|
||||
/// Whether the item should be disabled or not
|
||||
disabled: bool,
|
||||
}
|
||||
|
|
@ -162,6 +168,7 @@ impl DefaultSkimItem {
|
|||
stripped_text: stripped_text.map(std::string::String::into_boxed_str),
|
||||
ansi_info,
|
||||
matching_ranges,
|
||||
hidden_ranges: None,
|
||||
disabled: false,
|
||||
}))
|
||||
} else {
|
||||
|
|
@ -174,6 +181,32 @@ impl DefaultSkimItem {
|
|||
}
|
||||
}
|
||||
|
||||
/// Builder-style setter for the fields hidden from display (via `--hide-nth`).
|
||||
///
|
||||
/// The fields are resolved against the item's display/matching text — which is
|
||||
/// exactly what [`text()`](Self::text) returns (the ANSI-stripped text under
|
||||
/// `--ansi`, otherwise the raw text) — so this must be called after construction.
|
||||
/// The requested fields stay part of `text()` (and therefore searchable); they are
|
||||
/// only removed from the rendered line and ignored for highlighting and hscroll.
|
||||
///
|
||||
/// A no-op when `hidden_fields` is empty or resolves to no ranges.
|
||||
#[must_use]
|
||||
pub fn hidden_fields(mut self, hidden_fields: &[FieldRange], delimiter: &Regex) -> Self {
|
||||
if hidden_fields.is_empty() {
|
||||
return self;
|
||||
}
|
||||
// Resolve the ranges before touching `self.metadata`; the `text()` borrow must
|
||||
// end before the mutable borrow below.
|
||||
let ranges = {
|
||||
let text = self.text();
|
||||
normalize_ranges(&parse_matching_fields(delimiter, text.as_ref(), hidden_fields))
|
||||
};
|
||||
if !ranges.is_empty() {
|
||||
self.metadata.get_or_insert_default().hidden_ranges = Some(ranges);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn contains_ansi_escape(s: &str) -> bool {
|
||||
memchr::memchr(b'\x1b', s.as_bytes()).is_some()
|
||||
}
|
||||
|
|
@ -228,6 +261,16 @@ impl DefaultSkimItem {
|
|||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Getter for `hidden_ranges` stored in metadata
|
||||
#[must_use]
|
||||
pub fn hidden_ranges(&self) -> Option<&[(usize, usize)]> {
|
||||
if let Some(meta) = &self.metadata {
|
||||
meta.hidden_ranges.as_ref().map(|v| v.as_ref() as &[(usize, usize)])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DefaultSkimItem {
|
||||
|
|
@ -273,6 +316,10 @@ impl SkimItem for DefaultSkimItem {
|
|||
self.matching_ranges()
|
||||
}
|
||||
|
||||
fn hidden_ranges(&self) -> Option<&[(usize, usize)]> {
|
||||
self.hidden_ranges()
|
||||
}
|
||||
|
||||
// The display function handles ANSI stripping, field highlighting, and match
|
||||
// rendering in a single pass; splitting it would require duplicating context handling.
|
||||
#[allow(clippy::too_many_lines)]
|
||||
|
|
@ -289,9 +336,23 @@ impl SkimItem for DefaultSkimItem {
|
|||
// Extract all spans from the parsed text (should be a single line)
|
||||
let all_spans: Vec<Span> = parsed_text.lines.into_iter().flat_map(|line| line.spans).collect();
|
||||
|
||||
// When fields are hidden (--hide-nth), drop the hidden characters from the
|
||||
// parsed spans while preserving their ANSI styling, and remap the match
|
||||
// positions into the resulting visible coordinate space. The remaining
|
||||
// highlighting logic then runs unchanged on visible-coordinate CharIndices.
|
||||
let (all_spans, matches) = if let Some(hidden) = self.hidden_ranges() {
|
||||
let stripped = self.text();
|
||||
let (_, map) = project_visible_text(stripped.as_ref(), hidden);
|
||||
let visible_spans = retain_visible_spans(all_spans, &map);
|
||||
let indices = project_match_indices(stripped.as_ref(), &context.matches, &map);
|
||||
(visible_spans, Matches::CharIndices(indices))
|
||||
} else {
|
||||
(all_spans, context.matches.clone())
|
||||
};
|
||||
|
||||
// Now apply highlighting based on matched positions
|
||||
// We need to map match positions from stripped text to original text
|
||||
match context.matches {
|
||||
match matches {
|
||||
crate::Matches::CharIndices(ref indices) => {
|
||||
// Indices are already in stripped text coordinates (same as parsed ANSI text)
|
||||
// No need to remap since both matching and ANSI parsing strip the codes
|
||||
|
|
@ -446,6 +507,19 @@ impl SkimItem for DefaultSkimItem {
|
|||
}
|
||||
crate::Matches::None => Line::from(all_spans),
|
||||
}
|
||||
} else if let Some(hidden) = self.hidden_ranges() {
|
||||
// Non-ANSI hidden path: remove the hidden characters and remap the match
|
||||
// highlight positions into the visible coordinate space.
|
||||
let (visible, map) = project_visible_text(&self.text, hidden);
|
||||
let indices = project_match_indices(&self.text, &context.matches, &map);
|
||||
DisplayContext {
|
||||
score: context.score,
|
||||
matches: Matches::CharIndices(indices),
|
||||
container_width: context.container_width,
|
||||
base_style: context.base_style,
|
||||
matched_style: context.matched_style,
|
||||
}
|
||||
.to_line(Cow::Owned(visible))
|
||||
} else {
|
||||
// No ANSI mapping needed, use text as-is
|
||||
context.to_line(Cow::Borrowed(&self.text))
|
||||
|
|
@ -551,6 +625,110 @@ fn escape_ansi(raw: &str) -> String {
|
|||
unsafe { String::from_utf8_unchecked(raw.bytes().map(|b| if b == 27 { b'?' } else { b }).collect()) }
|
||||
}
|
||||
|
||||
/// Sort and merge a list of byte ranges into a canonical, non-overlapping form.
|
||||
///
|
||||
/// Empty ranges are dropped. Overlapping or touching ranges are merged so callers
|
||||
/// can iterate the result assuming disjoint, ascending ranges.
|
||||
#[must_use]
|
||||
pub(crate) fn normalize_ranges(ranges: &[(usize, usize)]) -> Vec<(usize, usize)> {
|
||||
let mut sorted: Vec<(usize, usize)> = ranges.iter().copied().filter(|(s, e)| e > s).collect();
|
||||
sorted.sort_unstable();
|
||||
|
||||
let mut merged: Vec<(usize, usize)> = Vec::with_capacity(sorted.len());
|
||||
for (start, end) in sorted {
|
||||
if let Some(last) = merged.last_mut()
|
||||
&& start <= last.1
|
||||
{
|
||||
last.1 = last.1.max(end);
|
||||
} else {
|
||||
merged.push((start, end));
|
||||
}
|
||||
}
|
||||
merged
|
||||
}
|
||||
|
||||
/// Remove the hidden byte ranges from `text`, returning the visible string and a
|
||||
/// map from each original char index to its `Some(visible char index)`, or `None`
|
||||
/// when that char falls inside a hidden range.
|
||||
///
|
||||
/// `hidden` must be normalized (see [`normalize_ranges`]): sorted, disjoint byte ranges.
|
||||
#[must_use]
|
||||
pub(crate) fn project_visible_text(text: &str, hidden: &[(usize, usize)]) -> (String, Vec<Option<usize>>) {
|
||||
let mut visible = String::with_capacity(text.len());
|
||||
let mut map = Vec::new();
|
||||
let mut vis_idx = 0usize;
|
||||
let mut hi = 0usize;
|
||||
|
||||
for (byte_pos, ch) in text.char_indices() {
|
||||
while hi < hidden.len() && byte_pos >= hidden[hi].1 {
|
||||
hi += 1;
|
||||
}
|
||||
let is_hidden = hi < hidden.len() && byte_pos >= hidden[hi].0 && byte_pos < hidden[hi].1;
|
||||
if is_hidden {
|
||||
map.push(None);
|
||||
} else {
|
||||
map.push(Some(vis_idx));
|
||||
visible.push(ch);
|
||||
vis_idx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
(visible, map)
|
||||
}
|
||||
|
||||
/// Drop hidden characters from already-parsed styled spans while preserving each
|
||||
/// span's style, keeping the ANSI colors of the surviving characters intact.
|
||||
///
|
||||
/// `map` is the per-char index map produced by [`project_visible_text`]; the spans
|
||||
/// are iterated in the same (stripped-text) char order the map is indexed by. Spans
|
||||
/// that become empty after filtering are dropped.
|
||||
#[must_use]
|
||||
pub(crate) fn retain_visible_spans(spans: Vec<Span<'_>>, map: &[Option<usize>]) -> Vec<Span<'static>> {
|
||||
let mut out = Vec::with_capacity(spans.len());
|
||||
let mut char_idx = 0usize;
|
||||
for span in spans {
|
||||
let mut content = String::new();
|
||||
for ch in span.content.chars() {
|
||||
if map.get(char_idx).copied().flatten().is_some() {
|
||||
content.push(ch);
|
||||
}
|
||||
char_idx += 1;
|
||||
}
|
||||
if !content.is_empty() {
|
||||
out.push(Span::styled(content, span.style));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Convert the matched character positions of `matches` (in full-text coordinates)
|
||||
/// into visible-text char indices, dropping any that fall inside hidden ranges.
|
||||
///
|
||||
/// `map` is the per-char index map produced by [`project_visible_text`]. The result
|
||||
/// is sorted ascending and deduplicated, ready to feed a `Matches::CharIndices`.
|
||||
#[must_use]
|
||||
pub(crate) fn project_match_indices(text: &str, matches: &Matches, map: &[Option<usize>]) -> Vec<usize> {
|
||||
let full_indices: Vec<usize> = match matches {
|
||||
Matches::CharIndices(indices) => indices.clone(),
|
||||
Matches::CharRange(start, end) => (*start..*end).collect(),
|
||||
Matches::ByteRange(start, end) => text
|
||||
.char_indices()
|
||||
.enumerate()
|
||||
.filter(|(_, (byte_pos, _))| *byte_pos >= *start && *byte_pos < *end)
|
||||
.map(|(char_idx, _)| char_idx)
|
||||
.collect(),
|
||||
Matches::None => Vec::new(),
|
||||
};
|
||||
|
||||
let mut visible: Vec<usize> = full_indices
|
||||
.into_iter()
|
||||
.filter_map(|ci| map.get(ci).copied().flatten())
|
||||
.collect();
|
||||
visible.sort_unstable();
|
||||
visible.dedup();
|
||||
visible
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "item_tests.rs"]
|
||||
mod test;
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ pub struct SkimItemReaderOption {
|
|||
use_ansi_color: bool,
|
||||
transform_fields: Vec<FieldRange>,
|
||||
matching_fields: Vec<FieldRange>,
|
||||
hidden_fields: Vec<FieldRange>,
|
||||
delimiter: Regex,
|
||||
line_ending: u8,
|
||||
show_error: bool,
|
||||
|
|
@ -44,6 +45,7 @@ impl Default for SkimItemReaderOption {
|
|||
use_ansi_color: false,
|
||||
transform_fields: Vec::new(),
|
||||
matching_fields: Vec::new(),
|
||||
hidden_fields: Vec::new(),
|
||||
delimiter: Regex::new(DELIMITER_STR).unwrap(),
|
||||
show_error: false,
|
||||
disable_pattern: None,
|
||||
|
|
@ -69,6 +71,11 @@ impl SkimItemReaderOption {
|
|||
.iter()
|
||||
.filter_map(|f| if f.is_empty() { None } else { FieldRange::from_str(f) })
|
||||
.collect(),
|
||||
hidden_fields: options
|
||||
.hide_nth
|
||||
.iter()
|
||||
.filter_map(|f| if f.is_empty() { None } else { FieldRange::from_str(f) })
|
||||
.collect(),
|
||||
delimiter: options.delimiter.clone(),
|
||||
show_error: options.show_cmd_error,
|
||||
disable_pattern: options.disable_pattern.clone(),
|
||||
|
|
@ -137,6 +144,23 @@ impl SkimItemReaderOption {
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets the fields to hide from display (while keeping them searchable)
|
||||
#[must_use]
|
||||
pub fn hide_nth<'a, T>(mut self, hide_nth: T) -> Self
|
||||
where
|
||||
T: Iterator<Item = &'a str>,
|
||||
{
|
||||
self.hidden_fields = hide_nth.filter_map(FieldRange::from_str).collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the hidden fields directly
|
||||
#[must_use]
|
||||
pub fn hidden_fields(mut self, hidden_fields: Vec<FieldRange>) -> Self {
|
||||
self.hidden_fields = hidden_fields;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enables reading null-terminated lines instead of newline-terminated
|
||||
#[must_use]
|
||||
pub fn read0(mut self, enable: bool) -> Self {
|
||||
|
|
@ -441,7 +465,8 @@ impl SkimItemReader {
|
|||
&opt.transform_fields,
|
||||
&opt.matching_fields,
|
||||
&opt.delimiter,
|
||||
);
|
||||
)
|
||||
.hidden_fields(&opt.hidden_fields, &opt.delimiter);
|
||||
if opt.disable_pattern.as_ref().is_some_and(|re| re.is_match(line)) {
|
||||
item.disable();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -487,3 +487,171 @@ fn test_display_ansi_item_with_no_matches() {
|
|||
assert!(text.contains("red"));
|
||||
assert!(text.contains("text"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_ranges_sorts_and_merges() {
|
||||
// Overlapping and touching ranges are merged; empty ranges dropped; result sorted.
|
||||
assert_eq!(normalize_ranges(&[(5, 8), (0, 3)]), vec![(0, 3), (5, 8)]);
|
||||
assert_eq!(normalize_ranges(&[(0, 4), (2, 6)]), vec![(0, 6)]);
|
||||
assert_eq!(normalize_ranges(&[(0, 3), (3, 6)]), vec![(0, 6)]);
|
||||
assert_eq!(normalize_ranges(&[(2, 2), (0, 1)]), vec![(0, 1)]);
|
||||
assert!(normalize_ranges(&[]).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_project_visible_text_removes_hidden_ranges() {
|
||||
// Hide bytes 6..10 ("RED ") from "apple RED 001".
|
||||
let (visible, map) = project_visible_text("apple RED 001", &[(6, 10)]);
|
||||
assert_eq!(visible, "apple 001");
|
||||
// Chars 0..6 ("apple ") map to themselves, 6..10 ("RED ") are hidden,
|
||||
// and the trailing "001" is shifted left by 4 positions.
|
||||
assert_eq!(map[0], Some(0)); // 'a'
|
||||
assert_eq!(map[5], Some(5)); // ' '
|
||||
assert_eq!(map[6], None); // 'R'
|
||||
assert_eq!(map[9], None); // ' '
|
||||
assert_eq!(map[10], Some(6)); // '0'
|
||||
assert_eq!(map[12], Some(8)); // '1'
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_project_match_indices_drops_hidden_and_remaps() {
|
||||
use crate::Matches;
|
||||
let (_visible, map) = project_visible_text("apple RED 001", &[(6, 10)]);
|
||||
// A match spanning both a visible char ('e' at 4) and hidden chars (7,8) keeps
|
||||
// only the visible one, remapped into visible coordinates (unchanged here).
|
||||
let indices = project_match_indices("apple RED 001", &Matches::CharIndices(vec![4, 7, 8]), &map);
|
||||
assert_eq!(indices, vec![4]);
|
||||
// A byte range covering "001" (bytes 10..13) maps to visible chars 6,7,8.
|
||||
let indices = project_match_indices("apple RED 001", &Matches::ByteRange(10, 13), &map);
|
||||
assert_eq!(indices, vec![6, 7, 8]);
|
||||
// A match entirely inside the hidden field yields nothing.
|
||||
let indices = project_match_indices("apple RED 001", &Matches::CharRange(6, 9), &map);
|
||||
assert!(indices.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hidden_ranges_keep_text_searchable() {
|
||||
use crate::field::FieldRange;
|
||||
use regex::Regex;
|
||||
let delimiter = Regex::new(r"\s+").unwrap();
|
||||
// Hide field 2 ("RED") but keep it searchable.
|
||||
let item = DefaultSkimItem::new("apple RED 001", false, &[], &[], &delimiter)
|
||||
.hidden_fields(&[FieldRange::Single(2)], &delimiter);
|
||||
// text() (used for matching) retains the hidden field, so it stays searchable.
|
||||
assert_eq!(item.text(), "apple RED 001");
|
||||
// hidden_ranges exposes the field's byte range (including its trailing delimiter).
|
||||
assert_eq!(item.hidden_ranges(), Some(&[(6, 10)][..]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hidden_field_removed_from_display() {
|
||||
use crate::field::FieldRange;
|
||||
use crate::{DisplayContext, Matches, SkimItem};
|
||||
use ratatui::style::Style;
|
||||
use regex::Regex;
|
||||
let delimiter = Regex::new(r"\s+").unwrap();
|
||||
let item = DefaultSkimItem::new("apple RED 001", false, &[], &[], &delimiter)
|
||||
.hidden_fields(&[FieldRange::Single(2)], &delimiter);
|
||||
let context = DisplayContext {
|
||||
score: 0,
|
||||
matches: Matches::None,
|
||||
container_width: 80,
|
||||
base_style: Style::default(),
|
||||
matched_style: Style::default(),
|
||||
};
|
||||
let line = item.display(context);
|
||||
let rendered: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
|
||||
// The hidden field is gone from what is displayed, but the rest remains.
|
||||
assert_eq!(rendered, "apple 001");
|
||||
assert!(!rendered.contains("RED"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hidden_field_match_not_highlighted() {
|
||||
use crate::field::FieldRange;
|
||||
use crate::{DisplayContext, Matches, SkimItem};
|
||||
use ratatui::style::{Color, Style};
|
||||
use regex::Regex;
|
||||
let delimiter = Regex::new(r"\s+").unwrap();
|
||||
let item = DefaultSkimItem::new("apple RED 001", false, &[], &[], &delimiter)
|
||||
.hidden_fields(&[FieldRange::Single(2)], &delimiter);
|
||||
// Simulate a match on the hidden "RED" (chars 6,7,8 in the full text).
|
||||
let context = DisplayContext {
|
||||
score: 0,
|
||||
matches: Matches::CharIndices(vec![6, 7, 8]),
|
||||
container_width: 80,
|
||||
base_style: Style::default(),
|
||||
matched_style: Style::default().bg(Color::Yellow),
|
||||
};
|
||||
let line = item.display(context);
|
||||
let rendered: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
|
||||
assert_eq!(rendered, "apple 001");
|
||||
// No span carries the highlight background, since the matched chars are hidden.
|
||||
assert!(line.spans.iter().all(|span| span.style.bg != Some(Color::Yellow)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hidden_field_preserves_ansi_colors() {
|
||||
use crate::field::FieldRange;
|
||||
use crate::{DisplayContext, Matches, SkimItem};
|
||||
use ratatui::style::{Color, Style};
|
||||
use regex::Regex;
|
||||
let delimiter = Regex::new(r"\s+").unwrap();
|
||||
// Two colored, space-separated fields: green "one" and red "two".
|
||||
let item = DefaultSkimItem::new(
|
||||
"\x1b[32mone\x1b[0m \x1b[31mtwo\x1b[0m",
|
||||
true, // ansi_enabled
|
||||
&[],
|
||||
&[],
|
||||
&delimiter,
|
||||
)
|
||||
.hidden_fields(&[FieldRange::Single(1)], &delimiter); // hide the first (green) field
|
||||
// The hidden field is still part of the matchable text.
|
||||
assert_eq!(item.text(), "one two");
|
||||
let context = DisplayContext {
|
||||
score: 0,
|
||||
matches: Matches::None,
|
||||
container_width: 80,
|
||||
base_style: Style::default(),
|
||||
matched_style: Style::default(),
|
||||
};
|
||||
let line = item.display(context);
|
||||
let rendered: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
|
||||
// "one " (field 1 plus its trailing delimiter) is gone; "two" remains.
|
||||
assert_eq!(rendered, "two");
|
||||
// The surviving field keeps its ANSI red foreground; the hidden green is gone.
|
||||
assert!(
|
||||
line.spans.iter().any(|span| span.style.fg == Some(Color::Red)),
|
||||
"surviving field should keep its ANSI red foreground"
|
||||
);
|
||||
assert!(
|
||||
line.spans.iter().all(|span| span.style.fg != Some(Color::Green)),
|
||||
"hidden field's ANSI green foreground should not appear"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hidden_field_ansi_highlight_remapped() {
|
||||
use crate::field::FieldRange;
|
||||
use crate::{DisplayContext, Matches, SkimItem};
|
||||
use ratatui::style::{Color, Style};
|
||||
use regex::Regex;
|
||||
let delimiter = Regex::new(r"\s+").unwrap();
|
||||
let item = DefaultSkimItem::new("\x1b[32mone\x1b[0m \x1b[31mtwo\x1b[0m", true, &[], &[], &delimiter)
|
||||
.hidden_fields(&[FieldRange::Single(1)], &delimiter); // hide green "one"
|
||||
// Match "two" — chars 4,5,6 in the full stripped text "one two".
|
||||
let context = DisplayContext {
|
||||
score: 0,
|
||||
matches: Matches::CharIndices(vec![4, 5, 6]),
|
||||
container_width: 80,
|
||||
base_style: Style::default(),
|
||||
matched_style: Style::default().bg(Color::Yellow),
|
||||
};
|
||||
let line = item.display(context);
|
||||
let rendered: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
|
||||
assert_eq!(rendered, "two");
|
||||
// The match on the visible field is highlighted (remapped to visible coords 0..3)
|
||||
// while its ANSI red foreground is preserved alongside the highlight background.
|
||||
assert!(line.spans.iter().any(|span| span.style.bg == Some(Color::Yellow)));
|
||||
assert!(line.spans.iter().any(|span| span.style.fg == Some(Color::Red)));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -200,6 +200,27 @@ pub struct SkimOptions {
|
|||
)]
|
||||
pub with_nth: Vec<String>,
|
||||
|
||||
/// Fields to hide from display while keeping them searchable
|
||||
///
|
||||
/// Takes the same comma-separated field index expressions as **nth**. The listed
|
||||
/// fields are removed from the displayed line but remain part of the text used for
|
||||
/// matching, so a query can still match them. Characters in the hidden fields are
|
||||
/// ignored for match highlighting and horizontal scrolling.
|
||||
///
|
||||
/// See **nth** for the field index expression syntax.
|
||||
#[cfg_attr(
|
||||
feature = "cli",
|
||||
arg(
|
||||
long,
|
||||
default_value = "",
|
||||
help_heading = "Search",
|
||||
verbatim_doc_comment,
|
||||
value_delimiter = ',',
|
||||
allow_hyphen_values = true,
|
||||
)
|
||||
)]
|
||||
pub hide_nth: Vec<String>,
|
||||
|
||||
/// Delimiter between fields
|
||||
///
|
||||
/// In regex format, defaults to AWK-style. Escape sequences like \x00, \t, \n are supported.
|
||||
|
|
@ -1115,6 +1136,7 @@ impl Default for SkimOptions {
|
|||
tiebreak: vec![RankCriteria::Score, RankCriteria::Begin, RankCriteria::End],
|
||||
nth: Default::default(),
|
||||
with_nth: Default::default(),
|
||||
hide_nth: Default::default(),
|
||||
delimiter: Regex::new(r"[\t\n ]+").unwrap(),
|
||||
exact: Default::default(),
|
||||
regex: Default::default(),
|
||||
|
|
|
|||
|
|
@ -74,6 +74,17 @@ pub trait SkimItem: AsAny + Send + Sync + 'static {
|
|||
None
|
||||
}
|
||||
|
||||
/// Byte ranges of `text()` that are hidden from display (via `--hide-nth`).
|
||||
///
|
||||
/// Characters inside these ranges are removed from the rendered line and ignored
|
||||
/// for match highlighting and horizontal scrolling, but stay part of `text()` so
|
||||
/// they remain searchable. Ranges are expressed as (`start_byte`, `end_byte`) and
|
||||
/// are expected to be sorted and non-overlapping. Returns `None` when nothing is
|
||||
/// hidden.
|
||||
fn hidden_ranges(&self) -> Option<&[(usize, usize)]> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns true if the item should be disabled
|
||||
/// Disabled items cannot be selected
|
||||
fn disabled(&self) -> bool {
|
||||
|
|
|
|||
|
|
@ -92,8 +92,29 @@ impl<'a> ItemRenderer<'a> {
|
|||
out: &mut Vec<ListItem<'static>>,
|
||||
) -> usize {
|
||||
let item_text = item.item.text();
|
||||
let sub_lines = self.split_sub_lines(item_text.as_ref());
|
||||
let (match_start_char, match_end_char) = Self::matched_range(item_text.as_ref(), item.matched_range.as_ref());
|
||||
// When fields are hidden (--hide-nth), project the display text and match
|
||||
// positions into the visible coordinate space so hidden characters are ignored
|
||||
// for sub-line splitting, highlighting, and horizontal scrolling. `display()`
|
||||
// performs the same projection, keeping the styled line consistent with these
|
||||
// positions.
|
||||
let (display_text, match_start_char, match_end_char): (std::borrow::Cow<'_, str>, usize, usize) =
|
||||
match item.item.hidden_ranges() {
|
||||
Some(hidden) if !hidden.is_empty() => {
|
||||
let (visible, map) = crate::helper::item::project_visible_text(item_text.as_ref(), hidden);
|
||||
let matches = Self::display_matches(item.matched_range.as_ref());
|
||||
let indices = crate::helper::item::project_match_indices(item_text.as_ref(), &matches, &map);
|
||||
let (start, end) = match (indices.first(), indices.last()) {
|
||||
(Some(first), Some(last)) => (*first, *last + 1),
|
||||
_ => (0, 0),
|
||||
};
|
||||
(std::borrow::Cow::Owned(visible), start, end)
|
||||
}
|
||||
_ => {
|
||||
let (start, end) = Self::matched_range(item_text.as_ref(), item.matched_range.as_ref());
|
||||
(item_text, start, end)
|
||||
}
|
||||
};
|
||||
let sub_lines = self.split_sub_lines(display_text.as_ref());
|
||||
|
||||
let mut added = 0usize;
|
||||
// Collect rows for this item into a temporary buffer so we can reverse
|
||||
|
|
|
|||
|
|
@ -47,3 +47,30 @@ insta_test!(test_prompt_ansi, ["a"], &["--prompt", "\x1b[1;34mprompt\x1b[0m noco
|
|||
@snap;
|
||||
@snap_color;
|
||||
});
|
||||
|
||||
// --ansi combined with --hide-nth: the hidden (red) middle field is removed from
|
||||
// the rendered line, while the surviving green/plain fields keep their ANSI colors.
|
||||
// The color snapshot confirms the green foreground survives and the red one is gone.
|
||||
insta_test!(
|
||||
test_ansi_hide_nth,
|
||||
@bytes b"\x1b[32mgreen\x1b[0m \x1b[31mred\x1b[0m plain\n",
|
||||
&["--ansi", "--delimiter", " ", "--hide-nth", "2"],
|
||||
{
|
||||
@snap;
|
||||
@snap_color;
|
||||
}
|
||||
);
|
||||
|
||||
// The hidden ANSI field stays searchable: matching its text ("red") still selects
|
||||
// the item even though the field is not shown, and no highlight leaks onto the
|
||||
// visible text.
|
||||
insta_test!(
|
||||
test_ansi_hide_nth_searchable,
|
||||
@bytes b"\x1b[32mgreen\x1b[0m \x1b[31mred\x1b[0m plain\n",
|
||||
&["--ansi", "--delimiter", " ", "--hide-nth", "2"],
|
||||
{
|
||||
@type "red";
|
||||
@snap;
|
||||
@snap_color;
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -87,6 +87,46 @@ insta_test!(opt_with_nth_range_desc, ["f1,f2,f3,f4"], &["--delimiter", ",", "--w
|
|||
@snap;
|
||||
});
|
||||
|
||||
insta_test!(opt_hide_nth_1, ["f1,f2,f3,f4"], &["--delimiter", ",", "--hide-nth", "1"], {
|
||||
@snap;
|
||||
});
|
||||
|
||||
insta_test!(opt_hide_nth_2, ["f1,f2,f3,f4"], &["--delimiter", ",", "--hide-nth", "2"], {
|
||||
@snap;
|
||||
});
|
||||
|
||||
insta_test!(opt_hide_nth_last, ["f1,f2,f3,f4"], &["--delimiter", ",", "--hide-nth=-1"], {
|
||||
@snap;
|
||||
});
|
||||
|
||||
insta_test!(opt_hide_nth_range, ["f1,f2,f3,f4"], &["--delimiter", ",", "--hide-nth", "2..3"], {
|
||||
@snap;
|
||||
});
|
||||
|
||||
insta_test!(opt_hide_nth_multi, ["f1,f2,f3,f4"], &["--delimiter", ",", "--hide-nth", "1,3"], {
|
||||
@snap;
|
||||
});
|
||||
|
||||
// A hidden field stays searchable: querying "f2" still matches the item even though
|
||||
// the field is not displayed, and the hidden characters carry no highlight.
|
||||
insta_test!(opt_hide_nth_still_searchable, ["f1,f2,f3,f4"], &["--delimiter", ",", "--hide-nth", "2"], {
|
||||
@snap;
|
||||
@type "f2";
|
||||
@snap;
|
||||
});
|
||||
|
||||
// A very wide hidden field is ignored for horizontal scrolling: the short visible
|
||||
// field renders in full with no scroll ellipsis, even though the raw line is 1000+
|
||||
// columns wide.
|
||||
insta_test!(
|
||||
opt_hide_nth_hscroll,
|
||||
[&format!("{} target", ["a"; 1000].join(""))],
|
||||
&["--delimiter", " ", "--hide-nth", "1"],
|
||||
{
|
||||
@snap;
|
||||
}
|
||||
);
|
||||
|
||||
insta_test!(opt_nth_1, ["f1,f2,f3,f4"], &["--delimiter", ",", "--nth", "1"], {
|
||||
@snap;
|
||||
@char '1';
|
||||
|
|
|
|||
29
tests/snapshots/ansi__ansi_hide_nth@001.snap
Normal file
29
tests/snapshots/ansi__ansi_hide_nth@001.snap
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
source: tests/ansi.rs
|
||||
description: "input: bytes b\"\\x1b[32mgreen\\x1b[0m \\x1b[31mred\\x1b[0m plain\\n\"\noptions: --ansi --delimiter --hide-nth 2"
|
||||
---
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
"> green plain "
|
||||
" 1/1 0/0"
|
||||
"> "
|
||||
cursor: (24, 3)
|
||||
11
tests/snapshots/ansi__ansi_hide_nth@color002.snap
Normal file
11
tests/snapshots/ansi__ansi_hide_nth@color002.snap
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
source: tests/ansi.rs
|
||||
description: "input: bytes b\"\\x1b[32mgreen\\x1b[0m \\x1b[31mred\\x1b[0m plain\\n\"\noptions: --ansi --delimiter --hide-nth 2"
|
||||
---
|
||||
(21, 0..1) ">" fg=Indexed(161)
|
||||
(21, 1..2) " " fg=Indexed(168)
|
||||
(21, 2..7) "green" fg=Green bg=Indexed(236)
|
||||
(21, 7..13) " plain" bg=Indexed(236)
|
||||
(22, 0..5) " 1/1" fg=Indexed(144)
|
||||
(22, 77..80) "0/0" fg=Indexed(144)
|
||||
(23, 0..2) "> " fg=Indexed(110)
|
||||
29
tests/snapshots/ansi__ansi_hide_nth_searchable@001.snap
Normal file
29
tests/snapshots/ansi__ansi_hide_nth_searchable@001.snap
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
source: tests/ansi.rs
|
||||
description: "input: bytes b\"\\x1b[32mgreen\\x1b[0m \\x1b[31mred\\x1b[0m plain\\n\"\noptions: --ansi --delimiter --hide-nth 2\nafter:\n @type \"red\""
|
||||
---
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
"> green plain "
|
||||
" 1/1 0/0"
|
||||
"> red "
|
||||
cursor: (24, 6)
|
||||
11
tests/snapshots/ansi__ansi_hide_nth_searchable@color002.snap
Normal file
11
tests/snapshots/ansi__ansi_hide_nth_searchable@color002.snap
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
source: tests/ansi.rs
|
||||
description: "input: bytes b\"\\x1b[32mgreen\\x1b[0m \\x1b[31mred\\x1b[0m plain\\n\"\noptions: --ansi --delimiter --hide-nth 2"
|
||||
---
|
||||
(21, 0..1) ">" fg=Indexed(161)
|
||||
(21, 1..2) " " fg=Indexed(168)
|
||||
(21, 2..7) "green" fg=Green bg=Indexed(236)
|
||||
(21, 7..13) " plain" bg=Indexed(236)
|
||||
(22, 0..5) " 1/1" fg=Indexed(144)
|
||||
(22, 77..80) "0/0" fg=Indexed(144)
|
||||
(23, 0..2) "> " fg=Indexed(110)
|
||||
29
tests/snapshots/options__opt_hide_nth_1@001.snap
Normal file
29
tests/snapshots/options__opt_hide_nth_1@001.snap
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
source: tests/options.rs
|
||||
description: "input: items [\"f1,f2,f3,f4\"]\noptions: --delimiter , --hide-nth 1"
|
||||
---
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
"> f2,f3,f4 "
|
||||
" 1/1 0/0"
|
||||
"> "
|
||||
cursor: (24, 3)
|
||||
29
tests/snapshots/options__opt_hide_nth_2@001.snap
Normal file
29
tests/snapshots/options__opt_hide_nth_2@001.snap
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
source: tests/options.rs
|
||||
description: "input: items [\"f1,f2,f3,f4\"]\noptions: --delimiter , --hide-nth 2"
|
||||
---
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
"> f1,f3,f4 "
|
||||
" 1/1 0/0"
|
||||
"> "
|
||||
cursor: (24, 3)
|
||||
29
tests/snapshots/options__opt_hide_nth_hscroll@001.snap
Normal file
29
tests/snapshots/options__opt_hide_nth_hscroll@001.snap
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
source: tests/options.rs
|
||||
description: "input: items [&format!(\"{} target\", [\"a\"; 1000].join(\"\"))]\noptions: --delimiter --hide-nth 1"
|
||||
---
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
"> target "
|
||||
" 1/1 0/0"
|
||||
"> "
|
||||
cursor: (24, 3)
|
||||
29
tests/snapshots/options__opt_hide_nth_last@001.snap
Normal file
29
tests/snapshots/options__opt_hide_nth_last@001.snap
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
source: tests/options.rs
|
||||
description: "input: items [\"f1,f2,f3,f4\"]\noptions: --delimiter , --hide-nth=-1"
|
||||
---
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
"> f1,f2,f3, "
|
||||
" 1/1 0/0"
|
||||
"> "
|
||||
cursor: (24, 3)
|
||||
29
tests/snapshots/options__opt_hide_nth_multi@001.snap
Normal file
29
tests/snapshots/options__opt_hide_nth_multi@001.snap
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
source: tests/options.rs
|
||||
description: "input: items [\"f1,f2,f3,f4\"]\noptions: --delimiter , --hide-nth 1,3"
|
||||
---
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
"> f2,f4 "
|
||||
" 1/1 0/0"
|
||||
"> "
|
||||
cursor: (24, 3)
|
||||
29
tests/snapshots/options__opt_hide_nth_range@001.snap
Normal file
29
tests/snapshots/options__opt_hide_nth_range@001.snap
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
source: tests/options.rs
|
||||
description: "input: items [\"f1,f2,f3,f4\"]\noptions: --delimiter , --hide-nth 2..3"
|
||||
---
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
"> f1,f4 "
|
||||
" 1/1 0/0"
|
||||
"> "
|
||||
cursor: (24, 3)
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
source: tests/options.rs
|
||||
description: "input: items [\"f1,f2,f3,f4\"]\noptions: --delimiter , --hide-nth 2"
|
||||
---
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
"> f1,f3,f4 "
|
||||
" 1/1 0/0"
|
||||
"> "
|
||||
cursor: (24, 3)
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
source: tests/options.rs
|
||||
description: "input: items [\"f1,f2,f3,f4\"]\noptions: --delimiter , --hide-nth 2\nafter:\n @type \"f2\""
|
||||
---
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
"> f1,f3,f4 "
|
||||
" 1/1 0/0"
|
||||
"> f2 "
|
||||
cursor: (24, 5)
|
||||
Loading…
Reference in a new issue