m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-15 14:53:47 -05:00

Compare commits

...

47 Commits

Author SHA1 Message Date
Junegunn Choi
c0d407f7ce 0.60.2 2025-02-23 19:52:57 +09:00
Junegunn Choi
461115afde Add support for {n} in --with-nth and --accept-nth templates
Close #4275
2025-02-23 19:47:56 +09:00
junegunn
bae1965231 Deploying to master from @ junegunn/fzf@b89c77ec9a 🚀 2025-02-23 00:02:08 +00:00
Junegunn Choi
b89c77ec9a Mention that actions after accept or abort are ignored (#4271) 2025-02-22 22:19:16 +09:00
Junegunn Choi
1ca5f09d7b Explain the difference of template from a single field index expression
Close #4272
2025-02-22 22:14:49 +09:00
Junegunn Choi
d79902ae59 Fix 'jump' when pointer is empty
Fix #4270
2025-02-22 19:05:30 +09:00
phanium
77568e114f Don't trim last field when delimiter is regex (#4266) 2025-02-21 22:21:55 +09:00
Junegunn Choi
a24d274a3c 0.60.1 2025-02-20 21:42:56 +09:00
Junegunn Choi
dac81432d6 [zsh/key-bindings] don't unescape FZF_DEFAULT_OPTS (addendum: #4262) 2025-02-20 20:58:21 +09:00
Steve Williams
309b5081ef [zsh/completion] don't unescape FZF_DEFAULT_OPTS (#4262) 2025-02-20 20:55:23 +09:00
bitraid
91bc4f2671 [fish] Add comment about fish version compatibility 2025-02-20 08:30:30 +09:00
bitraid
4c9d37d919 [fish] Reorder functions
Move the helper functions to the top of the main function, and the main
function commands (bind command) to the bottom.
2025-02-20 08:30:30 +09:00
bitraid
7e9566f66a [fish] Refactor bind commands
Use single check for each default command variable.
2025-02-20 08:30:30 +09:00
bitraid
3f7e8a475d [fish] Refactor fzf-cd-widget
- Remove check/set of FZF_TMUX_HEIGHT variable. It is already done by
  __fzf_defaults.
- Remove unnecessary begin/end block.
- Simplify result variable check.
- Set the command line using a single call to commandline.
2025-02-20 08:30:30 +09:00
bitraid
1cf7c0f334 [fish] Refactor fzf-history-widget
- Remove check/set of FZF_TMUX_HEIGHT variable. It is already done by
  __fzf_defaults.
- Remove unnecessary begin/end block.
- Pass all fzf options (except query) through FZF_DEFAULT_OPTS variable.
2025-02-20 08:30:30 +09:00
bitraid
ff8ee9ee4e [fish] Refactor fzf-file-widget
- Remove check/set of FZF_TMUX_HEIGHT variable. It is already done by
  __fzf_defaults.
- Remove unnecessary begin/end block.
- Simplify result variable check.
- Insert file names using a single call to commandline.
2025-02-20 08:30:30 +09:00
bitraid
cbbd939a94 [fish] Refactor __fzf_parse_commandline, remove __fzf_get_dir
The __fzf_get_dir function was called only once, and was basically a
single command in a while loop.
2025-02-20 08:30:30 +09:00
bitraid
f232df2887 [fish] __fzfcmd: Don't set FZF_TMUX
The FZF_TMUX variable check has already been changed from numeric to
string, so there is no need to set it to 0 if it's empty or undefined.
2025-02-20 08:30:30 +09:00
bitraid
16bfb2c80c [fish] Refactor __fzf_defaults
Append all arguments after the first one, so that functions don't have
to pass all appending options as a single string. Also, output
everything as a single string (an array of one item).
2025-02-20 08:30:30 +09:00
Junegunn Choi
0ba066123e Fix case where preview window is not scrollable (#4258)
When the last rendered line was wrapped, fzf would incorrectly determine
the scrollability of the window.
2025-02-20 08:22:43 +09:00
Junegunn Choi
81c51c26cc [man] Describe what 'smart-case' mode is
Close #4256
2025-02-20 08:02:04 +09:00
Junegunn Choi
6fa8295ac5 walker: Append path separator to directories
Close #4255
2025-02-18 22:03:59 +09:00
Junegunn Choi
f975b40236 Fix {q} in preview window affected by 'search' action 2025-02-18 10:08:47 +09:00
Alexei Șerșun
01d9d9c8c8 Normalize char before pattern lookup (#4252)
There is an edge-case in FuzzyMatchV1 during backward scan, related to
normalization: if string is initially denormalized (e.g. Unicode symbol),
backward scan will proceed further to the next char; however, when the
score is computed, the string is normalized first, then scanned based on
the pattern. This leads to accessing pattern index increment, which
itself leads to out-of-bound index access, resulting in a panic.

To illustrate the process, here's the sequence of operations when search
is perfored:

1. during backward scan by "minim" pattern

```
xxxxx Minímal example
      ^^^^^^^^^^^^
      ||||||||||||
      miniiiiiiiim <- compute score for this substring
```
2. during compute score by "minim" pattern
```
      Minímal exam
      minimal exam <- normalize chars before computing the score
      ^^^^^^
      ||||||
      minim <- at this point the pattern is already fully scanned and index
              is out-of-the-bound
```

In this commit the char is normalized during backward scan, to detect
properly the boundaries for the pattern.
2025-02-17 20:50:15 +09:00
Junegunn Choi
1eafc4e5d9 Ignore NULL byte before CSI 6N response
Close #2455
2025-02-16 21:18:01 +09:00
junegunn
38e4020aa8 Deploying to master from @ junegunn/fzf@ac32fbb3b2 🚀 2025-02-16 00:02:15 +00:00
Junegunn Choi
ac32fbb3b2 Avoid printing items in an extremely narrow screen 2025-02-13 22:12:25 +09:00
Junegunn Choi
7d26eca5cc Truncate wrap sign in the list section if necessary 2025-02-13 21:50:53 +09:00
Junegunn Choi
3347d61591 0.60.0 2025-02-13 00:54:21 +09:00
Junegunn Choi
9abf2c8c9c Allow suffix match on --nth with custom --delimiter
When --nth is used with a custom --delimiter, the last delimiter was
included in the search scope, forcing you to write the delimiter in
a suffix-match query. This commit removes the last delimiter from the
search scope.

  # No need to write 'bar,$'
  echo foo,bar,baz | fzf --delimiter , --nth 2 --filter 'bar$'

This can be seen as a breaking change, but I'm gonna say it's a bug fix.

Fix #3983
2025-02-12 20:53:32 +09:00
Junegunn Choi
84e2262ad6 Make --accept-nth and --with-nth support templates 2025-02-12 20:15:04 +09:00
Junegunn Choi
378137d34a Simplify code 2025-02-11 23:43:43 +09:00
Junegunn Choi
66ca16f836 Truncate wrap signs in extremely narrow preview window 2025-02-11 23:41:54 +09:00
bitraid
282884ad83 [fish] Unescape query from commandline (#4236)
More natural processing of the query taken from command line, by
unquoting/unescaping the token. Unescaped open quotes are removed.
Because of how `string unescape` works, if both single and double quotes
are present, with the outer quotes open, only the outer quotes are
removed.

Examples:
`'foo bar'`, `"foo bar"`, `foo\ bar` becomes `foo bar`
`"foobar`, `'foobar`, `foo"bar`, `foo'bar` becomes `foobar`
`'"foo"'`, `'"foo"` becomes `"foo"`
`"'foo'"`, `"'foo'` becomes `'foo'`
`"'foo` becomes `'foo`
`'"foo` becomes `"foo`
2025-02-11 23:19:40 +09:00
dependabot[bot]
7877ac42f0 Bump golang.org/x/term from 0.28.0 to 0.29.0 (#4234)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.28.0 to 0.29.0.
- [Commits](https://github.com/golang/term/compare/v0.28.0...v0.29.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-11 00:41:55 +09:00
Junegunn Choi
19ef8891e3 Print --wrap-sign in preview window
Close #4233
2025-02-11 00:01:50 +09:00
Coko
bfea9e53a6 fzf-preview.sh: Use kitten icat on ghostty (#4232)
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2025-02-09 20:02:05 +09:00
Junegunn Choi
a2420026ab Rename actions: exclude and exclude-multi
https://github.com/junegunn/fzf/pull/4231#issuecomment-2646067669
2025-02-09 13:52:20 +09:00
Junegunn Choi
1be1991299 Add exclude-current action
https://github.com/junegunn/fzf/pull/4231#issuecomment-2646063208
2025-02-09 13:37:22 +09:00
Junegunn Choi
67dd7e1923 Add 'exclude' action for excluding current/selected items from the result (#4231)
Close #4185
2025-02-09 13:22:33 +09:00
Junegunn Choi
2b584586ed Add --accept-nth option to transform the output
This option can be used to replace a sed or awk in the post-processing step.

  ps -ef | fzf --multi --header-lines 1 | awk '{print $2}'
  ps -ef | fzf --multi --header-lines 1 --accept-nth 2

This may not be a very "Unix-y" thing to do, so I've always felt that fzf
shouldn't have such an option, but I've finally changed my mind because:

* fzf can be configured with a custom delimiter that is a fixed string
  or a regular expression.
* In such cases, you'd need to repeat the delimiter again in the
  post-processing step.
* Also, tools like awk or sed may interpret a regular expression
  differently, causing mismatches.

You can still use sed, cut, or awk if you prefer.

Close #3987
Close #1323
2025-02-09 11:53:35 +09:00
Eric Chen
a1994ff0ab Update README.md (#4225) 2025-02-09 09:19:15 +09:00
junegunn
ca0e858871 Deploying to master from @ junegunn/fzf@06c6615507 🚀 2025-02-09 00:02:24 +00:00
bitraid
06c6615507 [fish] Fix for directories with special characters (#4230)
Using CTRL-T or ALT-C when the current command line token contained a
directory with special characters, the script would fail to detect it.
For exampe, an existing directory named `it\'s\ a\ test`, instead of
using it as walker-root, it would use it as the query.
2025-02-08 22:18:05 +09:00
Junegunn Choi
818d0be436 Fix change-header-label+change-header
Fix #4227
2025-02-07 20:57:09 +09:00
Junegunn Choi
fcd2baa945 Fix scrolling performance when --wrap is enabled
Fix #4221
2025-02-06 22:30:39 +09:00
Junegunn Choi
62e0a2824a Fix nth highlighting
Fix #4222
2025-02-06 19:57:39 +09:00
34 changed files with 874 additions and 299 deletions

View File

@@ -1,6 +1,55 @@
CHANGELOG CHANGELOG
========= =========
0.60.2
------
- Template for `--with-nth` and `--accept-nth` now supports `{n}` which evaluates to the zero-based ordinal index of the item
- Fixed a regression that caused the last field in the "nth" expression to be trimmed when a regular expression delimiter is used
- Thanks to @phanen for the fix
- Fixed 'jump' action when the pointer is an empty string
0.60.1
------
- Bug fixes and minor improvements
- Built-in walker now prints directory entries with a trailing slash
- Fixed a bug causing unexpected behavior with [fzf-tab](https://github.com/Aloxaf/fzf-tab). Please upgrade if you use it.
- Thanks to @alexeisersun, @bitraid, @Lompik, and @fsc0 for the contributions
0.60.0
------
_Release highlights: https://junegunn.github.io/fzf/releases/0.60.0/_
- Added `--accept-nth` for choosing output fields
```sh
ps -ef | fzf --multi --header-lines 1 | awk '{print $2}'
# Becomes
ps -ef | fzf --multi --header-lines 1 --accept-nth 2
git branch | fzf | cut -c3-
# Can be rewritten as
git branch | fzf --accept-nth -1
```
- `--accept-nth` and `--with-nth` now support a template that includes multiple field index expressions in curly braces
```sh
echo foo,bar,baz | fzf --delimiter , --accept-nth '{1}, {3}, {2}'
# foo, baz, bar
echo foo,bar,baz | fzf --delimiter , --with-nth '{1},{3},{2},{1..2}'
# foo,baz,bar,foo,bar
```
- Added `exclude` and `exclude-multi` actions for dynamically excluding items
```sh
seq 100 | fzf --bind 'ctrl-x:exclude'
# 'exclude-multi' will exclude the selected items or the current item
seq 100 | fzf --multi --bind 'ctrl-x:exclude-multi'
```
- Preview window now prints wrap indicator when wrapping is enabled
```sh
seq 100 | xargs | fzf --wrap --preview 'echo {}' --preview-window wrap
```
- Bug fixes and improvements
0.59.0 0.59.0
------ ------
_Release highlights: https://junegunn.github.io/fzf/releases/0.59.0/_ _Release highlights: https://junegunn.github.io/fzf/releases/0.59.0/_

File diff suppressed because one or more lines are too long

View File

@@ -57,15 +57,15 @@ elif ! [[ $KITTY_WINDOW_ID ]] && (( FZF_PREVIEW_TOP + FZF_PREVIEW_LINES == $(stt
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1)) dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))
fi fi
# 1. Use kitty icat on kitty terminal # 1. Use icat (from Kitty) if kitten is installed
if [[ $KITTY_WINDOW_ID ]]; then if [[ $KITTY_WINDOW_ID ]] || [[ $GHOSTTY_RESOURCES_DIR ]] && command -v kitten > /dev/null; then
# 1. 'memory' is the fastest option but if you want the image to be scrollable, # 1. 'memory' is the fastest option but if you want the image to be scrollable,
# you have to use 'stream'. # you have to use 'stream'.
# #
# 2. The last line of the output is the ANSI reset code without newline. # 2. The last line of the output is the ANSI reset code without newline.
# This confuses fzf and makes it render scroll offset indicator. # This confuses fzf and makes it render scroll offset indicator.
# So we remove the last line and append the reset code to its previous line. # So we remove the last line and append the reset code to its previous line.
kitty icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed '$d' | sed $'$s/$/\e[m/' kitten icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed '$d' | sed $'$s/$/\e[m/'
# 2. Use chafa with Sixel output # 2. Use chafa with Sixel output
elif command -v chafa > /dev/null; then elif command -v chafa > /dev/null; then

4
go.mod
View File

@@ -6,8 +6,8 @@ require (
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741 github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741
github.com/mattn/go-isatty v0.0.20 github.com/mattn/go-isatty v0.0.20
github.com/rivo/uniseg v0.4.7 github.com/rivo/uniseg v0.4.7
golang.org/x/sys v0.29.0 golang.org/x/sys v0.30.0
golang.org/x/term v0.28.0 golang.org/x/term v0.29.0
) )
require ( require (

6
go.sum
View File

@@ -54,8 +54,9 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -64,8 +65,9 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=

View File

@@ -2,7 +2,7 @@
set -u set -u
version=0.59.0 version=0.60.2
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2

View File

@@ -1,4 +1,4 @@
$version="0.59.0" $version="0.60.2"
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition $fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition

View File

@@ -11,7 +11,7 @@ import (
"github.com/junegunn/fzf/src/protector" "github.com/junegunn/fzf/src/protector"
) )
var version = "0.59" var version = "0.60"
var revision = "devel" var revision = "devel"
//go:embed shell/key-bindings.bash //go:embed shell/key-bindings.bash

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf\-tmux 1 "Feb 2025" "fzf 0.59.0" "fzf\-tmux - open fzf in tmux split pane" .TH fzf\-tmux 1 "Feb 2025" "fzf 0.60.2" "fzf\-tmux - open fzf in tmux split pane"
.SH NAME .SH NAME
fzf\-tmux - open fzf in tmux split pane fzf\-tmux - open fzf in tmux split pane

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "Feb 2025" "fzf 0.59.0" "fzf - a command-line fuzzy finder" .TH fzf 1 "Feb 2025" "fzf 0.60.2" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -56,7 +56,9 @@ Case-insensitive match (default: smart-case match)
Case-sensitive match Case-sensitive match
.TP .TP
.B "\-\-smart\-case" .B "\-\-smart\-case"
Smart-case match (default) Smart-case match (default). In this mode, the search is case-insensitive by
default, but it becomes case-sensitive if the query contains any uppercase
letters.
.TP .TP
.B "\-\-literal" .B "\-\-literal"
Do not normalize latin script letters for matching. Do not normalize latin script letters for matching.
@@ -117,8 +119,38 @@ transformed lines (unlike in \fB\-\-preview\fR where fields are extracted from
the original lines) because fzf doesn't allow searching against the hidden the original lines) because fzf doesn't allow searching against the hidden
fields. fields.
.TP .TP
.BI "\-\-with\-nth=" "N[,..]" .BI "\-\-with\-nth=" "N[,..] or TEMPLATE"
Transform the presentation of each line using field index expressions Transform the presentation of each line using the field index expressions.
For advanced transformation, you can provide a template containing field index
expressions in curly braces. When you use a template, the trailing delimiter is
stripped from each expression, giving you more control over the output.
\fB{n}\fR in template evaluates to the zero-based ordinal index of the line.
.RS
e.g.
# Single expression: drop the first field
echo foo bar baz | fzf --with-nth 2..
# Use template to rearrange fields
echo foo,bar,baz | fzf --delimiter , --with-nth '{n},{1},{3},{2},{1..2}'
.RE
.TP
.BI "\-\-accept\-nth=" "N[,..] or TEMPLATE"
Define which fields to print on accept. The last delimiter is stripped from the
output. For advanced transformation, you can provide a template containing
field index expressions in curly braces. When you use a template, the trailing
delimiter is stripped from each expression, giving you more control over the
output. \fB{n}\fR in template evaluates to the zero-based ordinal index of the
line.
.RS
e.g.
# Single expression
echo foo bar baz | fzf --accept-nth 2
# Template
echo foo bar baz | fzf --accept-nth 'Index: {n}, 1st: {1}, 2nd: {2}, 3rd: {3}'
.RE
.TP .TP
.B "+s, \-\-no\-sort" .B "+s, \-\-no\-sort"
Do not sort the result Do not sort the result
@@ -1597,6 +1629,8 @@ A key or an event can be bound to one or more of the following actions.
\fBdown\fR \fIctrl\-j ctrl\-n down\fR \fBdown\fR \fIctrl\-j ctrl\-n down\fR
\fBenable\-search\fR (enable search functionality) \fBenable\-search\fR (enable search functionality)
\fBend\-of\-line\fR \fIctrl\-e end\fR \fBend\-of\-line\fR \fIctrl\-e end\fR
\fBexclude\fR (exclude the current item from the result)
\fBexclude\-multi\fR (exclude the selected items or the current item from the result)
\fBexecute(...)\fR (see below for the details) \fBexecute(...)\fR (see below for the details)
\fBexecute\-silent(...)\fR (see below for the details) \fBexecute\-silent(...)\fR (see below for the details)
\fBfirst\fR (move to the first match; same as \fBpos(1)\fR) \fBfirst\fR (move to the first match; same as \fBpos(1)\fR)
@@ -1690,6 +1724,9 @@ e.g.
\fBfzf \-\-multi \-\-bind 'ctrl\-a:select\-all+accept'\fR \fBfzf \-\-multi \-\-bind 'ctrl\-a:select\-all+accept'\fR
\fBfzf \-\-multi \-\-bind 'ctrl\-a:select\-all' \-\-bind 'ctrl\-a:+accept'\fR \fBfzf \-\-multi \-\-bind 'ctrl\-a:select\-all' \-\-bind 'ctrl\-a:+accept'\fR
Any action after a terminal action that exits fzf, such as \fBaccept\fR or
\fBabort\fR, is ignored.
.SS ACTION ARGUMENT .SS ACTION ARGUMENT
An action denoted with \fB(...)\fR suffix takes an argument. An action denoted with \fB(...)\fR suffix takes an argument.

View File

@@ -99,9 +99,9 @@ if [[ -o interactive ]]; then
__fzf_defaults() { __fzf_defaults() {
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS # $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS # $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
echo "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1" echo -E "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
echo "${FZF_DEFAULT_OPTS-} $2" echo -E "${FZF_DEFAULT_OPTS-} $2"
} }
__fzf_comprun() { __fzf_comprun() {

View File

@@ -14,102 +14,24 @@
# Key bindings # Key bindings
# ------------ # ------------
# For compatibility with fish versions down to 3.1.2, the script does not use:
# - The -f/--function switch of command: set
# - The process substitution syntax: $(cmd)
# - Ranges that omit start/end indexes: $var[$start..] $var[..$end] $var[..]
function fzf_key_bindings function fzf_key_bindings
function __fzf_defaults function __fzf_defaults
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS # $argv[1]: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS # $argv[2..]: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40% test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
echo "--height $FZF_TMUX_HEIGHT --min-height 20+ --bind=ctrl-z:ignore" $argv[1] string join ' ' -- \
test -r "$FZF_DEFAULT_OPTS_FILE"; and string collect -N -- <$FZF_DEFAULT_OPTS_FILE "--height $FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" $argv[1] \
echo $FZF_DEFAULT_OPTS $argv[2] (test -r "$FZF_DEFAULT_OPTS_FILE"; and string join -- ' ' <$FZF_DEFAULT_OPTS_FILE) \
end $FZF_DEFAULT_OPTS $argv[2..-1]
# Store current token in $dir as root for the 'find' command
function fzf-file-widget -d "List files and folders"
set -l commandline (__fzf_parse_commandline)
set -lx dir $commandline[1]
set -l fzf_query $commandline[2]
set -l prefix $commandline[3]
set -l result
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
begin
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path --walker-root=$dir" "$FZF_CTRL_T_OPTS")
set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND"
set -lx FZF_DEFAULT_OPTS_FILE ''
set result (eval (__fzfcmd) -m --query=$fzf_query)
end
if test -z "$result"
commandline -f repaint
return
else
# Remove last token from commandline.
commandline -t ""
end
for i in $result
commandline -it -- $prefix
commandline -it -- (string escape -- $i)
commandline -it -- ' '
end
commandline -f repaint
end
function fzf-history-widget -d "Show command history"
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
begin
# merge history from other sessions before searching
test -z "$fish_private_mode"; and builtin history merge
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '"\t"↳ ' --highlight-line +m $FZF_CTRL_R_OPTS")
set -lx FZF_DEFAULT_OPTS_FILE ''
set -lx FZF_DEFAULT_COMMAND
set -a -- FZF_DEFAULT_OPTS --with-shell=(status fish-path)\\ -c
if type -q perl
set -a FZF_DEFAULT_OPTS '--tac'
set FZF_DEFAULT_COMMAND 'builtin history -z --reverse | command perl -0 -pe \'s/^/$.\t/g; s/\n/\n\t/gm\''
else
set FZF_DEFAULT_COMMAND \
'set -l h (builtin history -z --reverse | string split0);' \
'for i in (seq (count $h) -1 1);' \
'string join0 -- $i\t(string replace -a -- \n \n\t $h[$i] | string collect);' \
'end'
end
set -l result (eval $FZF_DEFAULT_COMMAND \| (__fzfcmd) --read0 --print0 -q (commandline | string escape) "--bind=enter:become:'string replace -a -- \n\t \n {2..} | string collect'")
and commandline -- $result
end
commandline -f repaint
end
function fzf-cd-widget -d "Change directory"
set -l commandline (__fzf_parse_commandline)
set -lx dir $commandline[1]
set -l fzf_query $commandline[2]
set -l prefix $commandline[3]
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
begin
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --walker=dir,follow,hidden --scheme=path --walker-root=$dir" "$FZF_ALT_C_OPTS")
set -lx FZF_DEFAULT_OPTS_FILE ''
set -lx FZF_DEFAULT_COMMAND "$FZF_ALT_C_COMMAND"
set -l result (eval (__fzfcmd) +m --query=$fzf_query)
if test -n "$result"
cd -- $result
# Remove last token from commandline.
commandline -t ""
commandline -it -- $prefix
end
end
commandline -f repaint
end end
function __fzfcmd function __fzfcmd
test -n "$FZF_TMUX"; or set FZF_TMUX 0 test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
if test -n "$FZF_TMUX_OPTS" if test -n "$FZF_TMUX_OPTS"
echo "fzf-tmux $FZF_TMUX_OPTS -- " echo "fzf-tmux $FZF_TMUX_OPTS -- "
else if test "$FZF_TMUX" = "1" else if test "$FZF_TMUX" = "1"
@@ -119,56 +41,42 @@ function fzf_key_bindings
end end
end end
bind \cr fzf-history-widget
if not set -q FZF_CTRL_T_COMMAND; or test -n "$FZF_CTRL_T_COMMAND"
bind \ct fzf-file-widget
end
if not set -q FZF_ALT_C_COMMAND; or test -n "$FZF_ALT_C_COMMAND"
bind \ec fzf-cd-widget
end
bind -M insert \cr fzf-history-widget
if not set -q FZF_CTRL_T_COMMAND; or test -n "$FZF_CTRL_T_COMMAND"
bind -M insert \ct fzf-file-widget
end
if not set -q FZF_ALT_C_COMMAND; or test -n "$FZF_ALT_C_COMMAND"
bind -M insert \ec fzf-cd-widget
end
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix' function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
set -l commandline (commandline -t) set -l dir '.'
set -l query
set -l commandline (commandline -t | string unescape -n)
# strip -option= from token if present # Strip -option= from token if present
set -l prefix (string match -r -- '^-[^\s=]+=' $commandline) set -l prefix (string match -r -- '^-[^\s=]+=' $commandline)
set commandline (string replace -- "$prefix" '' $commandline) set commandline (string replace -- "$prefix" '' $commandline)
# Enable home directory expansion of leading ~/ # Enable home directory expansion of leading ~/
set commandline (string replace -r -- '^~/' '\$HOME/' $commandline) set commandline (string replace -r -- '^~/' '\$HOME/' $commandline)
# escape special characters, except for the $ sign of valid variable names, # Escape special characters, except for the $ sign of valid variable names,
# so that after eval, the original string is returned, but with the # so that the original string with expanded variables is returned after eval.
# variable names replaced by their values.
set commandline (string escape -n -- $commandline) set commandline (string escape -n -- $commandline)
set commandline (string replace -r -a -- '\x5c\$(?=[\w])' '\$' $commandline) set commandline (string replace -r -a -- '\\\\\$(?=[\w])' '\$' $commandline)
# eval is used to do shell expansion on paths # eval is used to do shell expansion on paths
eval set commandline $commandline eval set commandline $commandline
# Combine multiple consecutive slashes into one # Combine multiple consecutive slashes into one.
set commandline (string replace -r -a -- '/+' '/' $commandline) set commandline (string replace -r -a -- '/+' '/' $commandline)
if test -z "$commandline" if test -n "$commandline"
# Default to current directory with no --query # Strip trailing slash, unless $dir is root dir (/)
set dir '.' set dir (string replace -r -- '(?<!^)/$' '' $commandline)
set fzf_query ''
else
set dir (__fzf_get_dir $commandline)
# BUG: on combined expressions, if a left argument is a single `!`, the # Set $dir to the longest existing filepath
# builtin test command of fish will treat it as the ! operator. To while not test -d "$dir"
# overcome this, have the variable parts on the right. # If path is absolute, this can keep going until ends up at /
if test "." = "$dir" -a "./" != (string sub -l 2 -- $commandline) # If path is relative, this can keep going until entire input is consumed, dirname returns "."
# if $dir is "." but commandline is not a relative path, this means no file path found set dir (dirname -- $dir)
end
if test "$dir" = '.'; and test (string sub -l 2 -- $commandline) != './'
# If $dir is "." but commandline is not a relative path, this means no file path found
set fzf_query $commandline set fzf_query $commandline
else else
# Also remove trailing slash after dir, to "split" input properly # Also remove trailing slash after dir, to "split" input properly
@@ -176,25 +84,98 @@ function fzf_key_bindings
end end
end end
echo (string escape -- $dir) string escape -n -- "$dir" "$fzf_query" "$prefix"
echo (string escape -- $fzf_query)
echo $prefix
end end
function __fzf_get_dir -d 'Find the longest existing filepath from input string' # Store current token in $dir as root for the 'find' command
set dir $argv function fzf-file-widget -d "List files and folders"
set -l commandline (__fzf_parse_commandline)
set -lx dir $commandline[1]
set -l fzf_query $commandline[2]
set -l prefix $commandline[3]
# Strip trailing slash, unless $dir is root dir (/) set -lx FZF_DEFAULT_OPTS (__fzf_defaults \
set dir (string replace -r -- '(?<!^)/$' '' $dir) "--reverse --walker=file,dir,follow,hidden --scheme=path --walker-root=$dir" \
"$FZF_CTRL_T_OPTS --multi")
# Iteratively check if dir exists and strip tail end of path set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND"
while test ! -d "$dir" set -lx FZF_DEFAULT_OPTS_FILE
# If path is absolute, this can keep going until ends up at /
# If path is relative, this can keep going until entire input is consumed, dirname returns "." if set -l result (eval (__fzfcmd) --query=$fzf_query)
set dir (dirname -- "$dir") # Remove last token from commandline.
commandline -t ''
for i in $result
commandline -it -- $prefix(string escape -- $i)' '
end
end end
echo $dir commandline -f repaint
end
function fzf-history-widget -d "Show command history"
set -l fzf_query (commandline | string escape)
set -lx FZF_DEFAULT_OPTS (__fzf_defaults '' \
'--nth=2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign="\t↳ "' \
"--highlight-line --no-multi $FZF_CTRL_R_OPTS --read0 --print0" \
"--bind='enter:become:string replace -a -- \n\t \n {2..} | string collect'" \
'--with-shell='(status fish-path)\\ -c)
set -lx FZF_DEFAULT_OPTS_FILE
set -lx FZF_DEFAULT_COMMAND
if type -q perl
set -a FZF_DEFAULT_OPTS '--tac'
set FZF_DEFAULT_COMMAND 'builtin history -z --reverse | command perl -0 -pe \'s/^/$.\t/g; s/\n/\n\t/gm\''
else
set FZF_DEFAULT_COMMAND \
'set -l h (builtin history -z --reverse | string split0);' \
'for i in (seq (count $h) -1 1);' \
'string join0 -- $i\t(string replace -a -- \n \n\t $h[$i] | string collect);' \
'end'
end
# Merge history from other sessions before searching
test -z "$fish_private_mode"; and builtin history merge
set -l result (eval $FZF_DEFAULT_COMMAND \| (__fzfcmd) --query=$fzf_query)
and commandline -- $result
commandline -f repaint
end
function fzf-cd-widget -d "Change directory"
set -l commandline (__fzf_parse_commandline)
set -lx dir $commandline[1]
set -l fzf_query $commandline[2]
set -l prefix $commandline[3]
set -lx FZF_DEFAULT_OPTS (__fzf_defaults \
"--reverse --walker=dir,follow,hidden --scheme=path --walker-root=$dir" \
"$FZF_ALT_C_OPTS --no-multi")
set -lx FZF_DEFAULT_OPTS_FILE
set -lx FZF_DEFAULT_COMMAND "$FZF_ALT_C_COMMAND"
if set -l result (eval (__fzfcmd) --query=$fzf_query)
cd -- $result
commandline -rt -- $prefix
end
commandline -f repaint
end
bind \cr fzf-history-widget
bind -M insert \cr fzf-history-widget
if not set -q FZF_CTRL_T_COMMAND; or test -n "$FZF_CTRL_T_COMMAND"
bind \ct fzf-file-widget
bind -M insert \ct fzf-file-widget
end
if not set -q FZF_ALT_C_COMMAND; or test -n "$FZF_ALT_C_COMMAND"
bind \ec fzf-cd-widget
bind -M insert \ec fzf-cd-widget
end end
end end

View File

@@ -41,9 +41,9 @@ if [[ -o interactive ]]; then
__fzf_defaults() { __fzf_defaults() {
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS # $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS # $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
echo "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1" echo -E "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
echo "${FZF_DEFAULT_OPTS-} $2" echo -E "${FZF_DEFAULT_OPTS-} $2"
} }
# CTRL-T - Paste the selected file path(s) into the command line # CTRL-T - Paste the selected file path(s) into the command line

View File

@@ -138,11 +138,13 @@ func _() {
_ = x[actShowHeader-127] _ = x[actShowHeader-127]
_ = x[actHideHeader-128] _ = x[actHideHeader-128]
_ = x[actBell-129] _ = x[actBell-129]
_ = x[actExclude-130]
_ = x[actExcludeMulti-131]
} }
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeListLabelactChangeInputLabelactChangeHeaderactChangeHeaderLabelactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactChangeNthactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformListLabelactTransformInputLabelactTransformHeaderactTransformHeaderLabelactTransformNthactTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactSearchactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBell" const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeListLabelactChangeInputLabelactChangeHeaderactChangeHeaderLabelactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactChangeNthactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformListLabelactTransformInputLabelactTransformHeaderactTransformHeaderLabelactTransformNthactTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactSearchactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMulti"
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 245, 264, 279, 299, 313, 334, 349, 363, 375, 389, 402, 419, 427, 440, 456, 468, 476, 490, 504, 515, 526, 544, 561, 568, 587, 599, 613, 622, 637, 649, 662, 673, 684, 696, 710, 731, 746, 759, 777, 793, 808, 822, 834, 846, 863, 870, 875, 884, 895, 906, 919, 934, 945, 958, 973, 980, 993, 1006, 1023, 1038, 1051, 1065, 1079, 1095, 1115, 1127, 1150, 1171, 1193, 1211, 1234, 1249, 1273, 1291, 1308, 1326, 1335, 1345, 1361, 1383, 1396, 1412, 1424, 1438, 1454, 1472, 1492, 1514, 1528, 1543, 1551, 1557, 1571, 1586, 1596, 1612, 1627, 1637, 1645, 1652, 1661, 1674, 1690, 1705, 1714, 1725, 1734, 1743, 1756, 1765, 1778, 1791, 1798} var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 245, 264, 279, 299, 313, 334, 349, 363, 375, 389, 402, 419, 427, 440, 456, 468, 476, 490, 504, 515, 526, 544, 561, 568, 587, 599, 613, 622, 637, 649, 662, 673, 684, 696, 710, 731, 746, 759, 777, 793, 808, 822, 834, 846, 863, 870, 875, 884, 895, 906, 919, 934, 945, 958, 973, 980, 993, 1006, 1023, 1038, 1051, 1065, 1079, 1095, 1115, 1127, 1150, 1171, 1193, 1211, 1234, 1249, 1273, 1291, 1308, 1326, 1335, 1345, 1361, 1383, 1396, 1412, 1424, 1438, 1454, 1472, 1492, 1514, 1528, 1543, 1551, 1557, 1571, 1586, 1596, 1612, 1627, 1637, 1645, 1652, 1661, 1674, 1690, 1705, 1714, 1725, 1734, 1743, 1756, 1765, 1778, 1791, 1798, 1808, 1823}
func (i actionType) String() string { func (i actionType) String() string {
if i < 0 || i >= actionType(len(_actionType_index)-1) { if i < 0 || i >= actionType(len(_actionType_index)-1) {

View File

@@ -767,6 +767,9 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.C
char = unicode.To(unicode.LowerCase, char) char = unicode.To(unicode.LowerCase, char)
} }
} }
if normalize {
char = normalizeRune(char)
}
pidx_ := indexAt(pidx, lenPattern, forward) pidx_ := indexAt(pidx, lenPattern, forward)
pchar := pattern[pidx_] pchar := pattern[pidx_]

View File

@@ -200,3 +200,12 @@ func TestLongString(t *testing.T) {
bytes[math.MaxUint16] = 'z' bytes[math.MaxUint16] = 'z'
assertMatch(t, FuzzyMatchV2, true, true, string(bytes), "zx", math.MaxUint16, math.MaxUint16+2, scoreMatch*2+bonusConsecutive) assertMatch(t, FuzzyMatchV2, true, true, string(bytes), "zx", math.MaxUint16, math.MaxUint16+2, scoreMatch*2+bonusConsecutive)
} }
func TestLongStringWithNormalize(t *testing.T) {
bytes := make([]byte, 30000)
for i := range bytes {
bytes[i] = 'x'
}
unicodeString := string(bytes) + " Minímal example"
assertMatch2(t, FuzzyMatchV1, false, true, false, unicodeString, "minim", 30001, 30006, 140)
}

View File

@@ -96,7 +96,7 @@ func Run(opts *Options) (int, error) {
var chunkList *ChunkList var chunkList *ChunkList
var itemIndex int32 var itemIndex int32
header := make([]string, 0, opts.HeaderLines) header := make([]string, 0, opts.HeaderLines)
if len(opts.WithNth) == 0 { if opts.WithNth == nil {
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool { chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
if len(header) < opts.HeaderLines { if len(header) < opts.HeaderLines {
header = append(header, byteString(data)) header = append(header, byteString(data))
@@ -109,6 +109,7 @@ func Run(opts *Options) (int, error) {
return true return true
}) })
} else { } else {
nthTransformer := opts.WithNth(opts.Delimiter)
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool { chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
tokens := Tokenize(byteString(data), opts.Delimiter) tokens := Tokenize(byteString(data), opts.Delimiter)
if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 { if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 {
@@ -127,15 +128,13 @@ func Run(opts *Options) (int, error) {
} }
} }
} }
trans := Transform(tokens, opts.WithNth) transformed := nthTransformer(tokens, itemIndex)
transformed := joinTokens(trans)
if len(header) < opts.HeaderLines { if len(header) < opts.HeaderLines {
header = append(header, transformed) header = append(header, transformed)
eventBox.Set(EvtHeader, header) eventBox.Set(EvtHeader, header)
return false return false
} }
item.text, item.colors = ansiProcessor(stringBytes(transformed)) item.text, item.colors = ansiProcessor(stringBytes(transformed))
item.text.TrimTrailingWhitespaces()
item.text.Index = itemIndex item.text.Index = itemIndex
item.origText = &data item.origText = &data
itemIndex++ itemIndex++
@@ -195,15 +194,30 @@ func Run(opts *Options) (int, error) {
} }
nth := opts.Nth nth := opts.Nth
nthRevision := 0
patternCache := make(map[string]*Pattern)
patternBuilder := func(runes []rune) *Pattern {
return BuildPattern(cache, patternCache,
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
opts.Filter == nil, nth, opts.Delimiter, nthRevision, runes)
}
inputRevision := revision{} inputRevision := revision{}
snapshotRevision := revision{} snapshotRevision := revision{}
patternCache := make(map[string]*Pattern)
denyMutex := sync.Mutex{}
denylist := make(map[int32]struct{})
clearDenylist := func() {
denyMutex.Lock()
if len(denylist) > 0 {
patternCache = make(map[string]*Pattern)
}
denylist = make(map[int32]struct{})
denyMutex.Unlock()
}
patternBuilder := func(runes []rune) *Pattern {
denyMutex.Lock()
denylistCopy := make(map[int32]struct{})
for k, v := range denylist {
denylistCopy[k] = v
}
denyMutex.Unlock()
return BuildPattern(cache, patternCache,
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
opts.Filter == nil, nth, opts.Delimiter, inputRevision, runes, denylistCopy)
}
matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision) matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision)
// Filtering mode // Filtering mode
@@ -302,6 +316,9 @@ func Run(opts *Options) (int, error) {
var snapshot []*Chunk var snapshot []*Chunk
var count int var count int
restart := func(command commandSpec, environ []string) { restart := func(command commandSpec, environ []string) {
if !useSnapshot {
clearDenylist()
}
reading = true reading = true
chunkList.Clear() chunkList.Clear()
itemIndex = 0 itemIndex = 0
@@ -348,7 +365,8 @@ func Run(opts *Options) (int, error) {
} else { } else {
reading = reading && evt == EvtReadNew reading = reading && evt == EvtReadNew
} }
if useSnapshot && evt == EvtReadFin { if useSnapshot && evt == EvtReadFin { // reload-sync
clearDenylist()
useSnapshot = false useSnapshot = false
} }
if !useSnapshot { if !useSnapshot {
@@ -379,10 +397,21 @@ func Run(opts *Options) (int, error) {
command = val.command command = val.command
environ = val.environ environ = val.environ
changed = val.changed changed = val.changed
bump := false
if len(val.denylist) > 0 && val.revision.compatible(inputRevision) {
denyMutex.Lock()
for _, itemIndex := range val.denylist {
denylist[itemIndex] = struct{}{}
}
denyMutex.Unlock()
bump = true
}
if val.nth != nil { if val.nth != nil {
// Change nth and clear caches // Change nth and clear caches
nth = *val.nth nth = *val.nth
nthRevision++ bump = true
}
if bump {
patternCache = make(map[string]*Pattern) patternCache = make(map[string]*Pattern)
cache.Clear() cache.Clear()
inputRevision.bumpMinor() inputRevision.bumpMinor()

View File

@@ -9,7 +9,7 @@ import (
type transformed struct { type transformed struct {
// Because nth can be changed dynamically by change-nth action, we need to // Because nth can be changed dynamically by change-nth action, we need to
// keep the revision number at the time of transformation. // keep the revision number at the time of transformation.
revision int revision revision
tokens []Token tokens []Token
} }

View File

@@ -41,6 +41,7 @@ Usage: fzf [options]
integer or a range expression ([BEGIN]..[END]). integer or a range expression ([BEGIN]..[END]).
--with-nth=N[,..] Transform the presentation of each line using --with-nth=N[,..] Transform the presentation of each line using
field index expressions field index expressions
--accept-nth=N[,..] Define which fields to print on accept
-d, --delimiter=STR Field delimiter regex (default: AWK-style) -d, --delimiter=STR Field delimiter regex (default: AWK-style)
+s, --no-sort Do not sort the result +s, --no-sort Do not sort the result
--literal Do not normalize latin script letters --literal Do not normalize latin script letters
@@ -543,7 +544,8 @@ type Options struct {
Case Case Case Case
Normalize bool Normalize bool
Nth []Range Nth []Range
WithNth []Range WithNth func(Delimiter) func([]Token, int32) string
AcceptNth func(Delimiter) func([]Token, int32) string
Delimiter Delimiter Delimiter Delimiter
Sort int Sort int
Track trackOption Track trackOption
@@ -665,7 +667,6 @@ func defaultOptions() *Options {
Case: CaseSmart, Case: CaseSmart,
Normalize: true, Normalize: true,
Nth: make([]Range, 0), Nth: make([]Range, 0),
WithNth: make([]Range, 0),
Delimiter: Delimiter{}, Delimiter: Delimiter{},
Sort: 1000, Sort: 1000,
Track: trackDisabled, Track: trackDisabled,
@@ -768,6 +769,70 @@ func splitNth(str string) ([]Range, error) {
return ranges, nil return ranges, nil
} }
func nthTransformer(str string) (func(Delimiter) func([]Token, int32) string, error) {
// ^[0-9,-.]+$"
if match, _ := regexp.MatchString("^[0-9,-.]+$", str); match {
nth, err := splitNth(str)
if err != nil {
return nil, err
}
return func(Delimiter) func([]Token, int32) string {
return func(tokens []Token, index int32) string {
return JoinTokens(Transform(tokens, nth))
}
}, nil
}
// {...} {...} ...
placeholder := regexp.MustCompile("{[0-9,-.]+}|{n}")
indexes := placeholder.FindAllStringIndex(str, -1)
if indexes == nil {
return nil, errors.New("template should include at least 1 placeholder: " + str)
}
type NthParts struct {
str string
index bool
nth []Range
}
parts := make([]NthParts, len(indexes))
idx := 0
for _, index := range indexes {
if idx < index[0] {
parts = append(parts, NthParts{str: str[idx:index[0]]})
}
expr := str[index[0]+1 : index[1]-1]
if expr == "n" {
parts = append(parts, NthParts{index: true})
} else if nth, err := splitNth(expr); err == nil {
parts = append(parts, NthParts{nth: nth})
}
idx = index[1]
}
if idx < len(str) {
parts = append(parts, NthParts{str: str[idx:]})
}
return func(delimiter Delimiter) func([]Token, int32) string {
return func(tokens []Token, index int32) string {
str := ""
for _, holder := range parts {
if holder.nth != nil {
str += StripLastDelimiter(JoinTokens(Transform(tokens, holder.nth)), delimiter)
} else if holder.index {
if index >= 0 {
str += strconv.Itoa(int(index))
}
} else {
str += holder.str
}
}
return str
}
}, nil
}
func delimiterRegexp(str string) Delimiter { func delimiterRegexp(str string) Delimiter {
// Special handling of \t // Special handling of \t
str = strings.ReplaceAll(str, "\\t", "\t") str = strings.ReplaceAll(str, "\\t", "\t")
@@ -1600,6 +1665,10 @@ func parseActionList(masked string, original string, prevActions []*action, putA
} }
case "bell": case "bell":
appendAction(actBell) appendAction(actBell)
case "exclude":
appendAction(actExclude)
case "exclude-multi":
appendAction(actExcludeMulti)
default: default:
t := isExecuteAction(specLower) t := isExecuteAction(specLower)
if t == actIgnore { if t == actIgnore {
@@ -2380,7 +2449,15 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
if err != nil { if err != nil {
return err return err
} }
if opts.WithNth, err = splitNth(str); err != nil { if opts.WithNth, err = nthTransformer(str); err != nil {
return err
}
case "--accept-nth":
str, err := nextString("nth expression required")
if err != nil {
return err
}
if opts.AcceptNth, err = nthTransformer(str); err != nil {
return err return err
} }
case "-s", "--sort": case "-s", "--sort":

View File

@@ -60,9 +60,10 @@ type Pattern struct {
cacheKey string cacheKey string
delimiter Delimiter delimiter Delimiter
nth []Range nth []Range
revision int revision revision
procFun map[termType]algo.Algo procFun map[termType]algo.Algo
cache *ChunkCache cache *ChunkCache
denylist map[int32]struct{}
} }
var _splitRegex *regexp.Regexp var _splitRegex *regexp.Regexp
@@ -73,7 +74,7 @@ func init() {
// BuildPattern builds Pattern object from the given arguments // BuildPattern builds Pattern object from the given arguments
func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool, func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, revision int, runes []rune) *Pattern { withPos bool, cacheable bool, nth []Range, delimiter Delimiter, revision revision, runes []rune, denylist map[int32]struct{}) *Pattern {
var asString string var asString string
if extended { if extended {
@@ -144,6 +145,7 @@ func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy boo
revision: revision, revision: revision,
delimiter: delimiter, delimiter: delimiter,
cache: cache, cache: cache,
denylist: denylist,
procFun: make(map[termType]algo.Algo)} procFun: make(map[termType]algo.Algo)}
ptr.cacheKey = ptr.buildCacheKey() ptr.cacheKey = ptr.buildCacheKey()
@@ -243,6 +245,9 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet
// IsEmpty returns true if the pattern is effectively empty // IsEmpty returns true if the pattern is effectively empty
func (p *Pattern) IsEmpty() bool { func (p *Pattern) IsEmpty() bool {
if len(p.denylist) > 0 {
return false
}
if !p.extended { if !p.extended {
return len(p.text) == 0 return len(p.text) == 0
} }
@@ -296,14 +301,38 @@ func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result {
func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result { func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result {
matches := []Result{} matches := []Result{}
if len(p.denylist) == 0 {
// Huge code duplication for minimizing unnecessary map lookups
if space == nil {
for idx := 0; idx < chunk.count; idx++ {
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil {
matches = append(matches, *match)
}
}
} else {
for _, result := range space {
if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match != nil {
matches = append(matches, *match)
}
}
}
return matches
}
if space == nil { if space == nil {
for idx := 0; idx < chunk.count; idx++ { for idx := 0; idx < chunk.count; idx++ {
if _, prs := p.denylist[chunk.items[idx].Index()]; prs {
continue
}
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil { if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil {
matches = append(matches, *match) matches = append(matches, *match)
} }
} }
} else { } else {
for _, result := range space { for _, result := range space {
if _, prs := p.denylist[result.item.Index()]; prs {
continue
}
if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match != nil { if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match != nil {
matches = append(matches, *match) matches = append(matches, *match)
} }
@@ -403,6 +432,13 @@ func (p *Pattern) transformInput(item *Item) []Token {
tokens := Tokenize(item.text.ToString(), p.delimiter) tokens := Tokenize(item.text.ToString(), p.delimiter)
ret := Transform(tokens, p.nth) ret := Transform(tokens, p.nth)
// Strip the last delimiter to allow suffix match
if len(ret) > 0 && !p.delimiter.IsAwk() {
chars := ret[len(ret)-1].text
stripped := StripLastDelimiter(chars.ToString(), p.delimiter)
newChars := util.ToChars(stringBytes(stripped))
ret[len(ret)-1].text = &newChars
}
item.transformed = &transformed{p.revision, ret} item.transformed = &transformed{p.revision, ret}
return ret return ret
} }

View File

@@ -68,7 +68,7 @@ func buildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern { withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
return BuildPattern(NewChunkCache(), make(map[string]*Pattern), return BuildPattern(NewChunkCache(), make(map[string]*Pattern),
fuzzy, fuzzyAlgo, extended, caseMode, normalize, forward, fuzzy, fuzzyAlgo, extended, caseMode, normalize, forward,
withPos, cacheable, nth, delimiter, 0, runes) withPos, cacheable, nth, delimiter, revision{}, runes, nil)
} }
func TestExact(t *testing.T) { func TestExact(t *testing.T) {

View File

@@ -320,6 +320,7 @@ func (r *Reader) readFiles(roots []string, opts walkerOpts, ignores []string) bo
return filepath.SkipDir return filepath.SkipDir
} }
} }
path += sep
} }
if ((opts.file && !isDir) || (opts.dir && isDir)) && r.pusher(stringBytes(path)) { if ((opts.file && !isDir) || (opts.dir && isDir)) && r.pusher(stringBytes(path)) {
atomic.StoreInt32(&r.event, int32(EvtReadNew)) atomic.StoreInt32(&r.event, int32(EvtReadNew))

View File

@@ -305,6 +305,7 @@ type Terminal struct {
nthAttr tui.Attr nthAttr tui.Attr
nth []Range nth []Range
nthCurrent []Range nthCurrent []Range
acceptNth func([]Token, int32) string
tabstop int tabstop int
margin [4]sizeSpec margin [4]sizeSpec
padding [4]sizeSpec padding [4]sizeSpec
@@ -390,6 +391,12 @@ type Terminal struct {
clickHeaderLine int clickHeaderLine int
clickHeaderColumn int clickHeaderColumn int
proxyScript string proxyScript string
numLinesCache map[int32]numLinesCacheValue
}
type numLinesCacheValue struct {
atMost int
numLines int
} }
type selectedItem struct { type selectedItem struct {
@@ -577,6 +584,8 @@ const (
actShowHeader actShowHeader
actHideHeader actHideHeader
actBell actBell
actExclude
actExcludeMulti
) )
func (a actionType) Name() string { func (a actionType) Name() string {
@@ -614,12 +623,14 @@ type placeholderFlags struct {
} }
type searchRequest struct { type searchRequest struct {
sort bool sort bool
sync bool sync bool
nth *[]Range nth *[]Range
command *commandSpec command *commandSpec
environ []string environ []string
changed bool changed bool
denylist []int32
revision revision
} }
type previewRequest struct { type previewRequest struct {
@@ -627,6 +638,7 @@ type previewRequest struct {
scrollOffset int scrollOffset int
list []*Item list []*Item
env []string env []string
query string
} }
type previewResult struct { type previewResult struct {
@@ -947,7 +959,11 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
initFunc: func() error { return renderer.Init() }, initFunc: func() error { return renderer.Init() },
executing: util.NewAtomicBool(false), executing: util.NewAtomicBool(false),
lastAction: actStart, lastAction: actStart,
lastFocus: minItem.Index()} lastFocus: minItem.Index(),
numLinesCache: make(map[int32]numLinesCacheValue)}
if opts.AcceptNth != nil {
t.acceptNth = opts.AcceptNth(t.delimiter)
}
// This should be called before accessing tui.Color* // This should be called before accessing tui.Color*
tui.InitTheme(opts.Theme, renderer.DefaultTheme(), opts.Black, opts.InputBorderShape.Visible(), opts.HeaderBorderShape.Visible()) tui.InitTheme(opts.Theme, renderer.DefaultTheme(), opts.Black, opts.InputBorderShape.Visible(), opts.HeaderBorderShape.Visible())
@@ -1318,6 +1334,10 @@ func (t *Terminal) wrapCols() int {
return util.Max(t.window.Width()-(t.pointerLen+t.markerLen+1), 1) return util.Max(t.window.Width()-(t.pointerLen+t.markerLen+1), 1)
} }
func (t *Terminal) clearNumLinesCache() {
t.numLinesCache = make(map[int32]numLinesCacheValue)
}
// Number of lines the item takes including the gap // Number of lines the item takes including the gap
func (t *Terminal) numItemLines(item *Item, atMost int) (int, bool) { func (t *Terminal) numItemLines(item *Item, atMost int) (int, bool) {
var numLines int var numLines int
@@ -1325,6 +1345,12 @@ func (t *Terminal) numItemLines(item *Item, atMost int) (int, bool) {
numLines = 1 + t.gap numLines = 1 + t.gap
return numLines, numLines > atMost return numLines, numLines > atMost
} }
if cached, prs := t.numLinesCache[item.Index()]; prs {
// Can we use this cache? Let's be conservative.
if cached.atMost >= atMost {
return cached.numLines, false
}
}
var overflow bool var overflow bool
if !t.wrap && t.multiLine { if !t.wrap && t.multiLine {
numLines, overflow = item.text.NumLines(atMost) numLines, overflow = item.text.NumLines(atMost)
@@ -1334,6 +1360,9 @@ func (t *Terminal) numItemLines(item *Item, atMost int) (int, bool) {
numLines = len(lines) numLines = len(lines)
} }
numLines += t.gap numLines += t.gap
if !overflow {
t.numLinesCache[item.Index()] = numLinesCacheValue{atMost, numLines}
}
return numLines, overflow || numLines > atMost return numLines, overflow || numLines > atMost
} }
@@ -1461,6 +1490,7 @@ func (t *Terminal) UpdateList(merger *Merger) {
if !t.revision.compatible(newRevision) { if !t.revision.compatible(newRevision) {
// Reloaded: clear selection // Reloaded: clear selection
t.selected = make(map[int32]selectedItem) t.selected = make(map[int32]selectedItem)
t.clearNumLinesCache()
} else { } else {
// Trimmed by --tail: filter selection by index // Trimmed by --tail: filter selection by index
filtered := make(map[int32]selectedItem) filtered := make(map[int32]selectedItem)
@@ -1540,16 +1570,26 @@ func (t *Terminal) output() bool {
for _, s := range t.printQueue { for _, s := range t.printQueue {
t.printer(s) t.printer(s)
} }
transform := func(item *Item) string {
return item.AsString(t.ansi)
}
if t.acceptNth != nil {
transform = func(item *Item) string {
tokens := Tokenize(item.AsString(t.ansi), t.delimiter)
transformed := t.acceptNth(tokens, item.Index())
return StripLastDelimiter(transformed, t.delimiter)
}
}
found := len(t.selected) > 0 found := len(t.selected) > 0
if !found { if !found {
current := t.currentItem() current := t.currentItem()
if current != nil { if current != nil {
t.printer(current.AsString(t.ansi)) t.printer(transform(current))
found = true found = true
} }
} else { } else {
for _, sel := range t.sortSelected() { for _, sel := range t.sortSelected() {
t.printer(sel.item.AsString(t.ansi)) t.printer(transform(sel.item))
} }
} }
return found return found
@@ -1712,6 +1752,7 @@ func (t *Terminal) hasHeaderLinesWindow() bool {
} }
func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) { func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
t.clearNumLinesCache()
t.forcePreview = forcePreview t.forcePreview = forcePreview
screenWidth, screenHeight, marginInt, paddingInt := t.adjustMarginAndPadding() screenWidth, screenHeight, marginInt, paddingInt := t.adjustMarginAndPadding()
width := screenWidth - marginInt[1] - marginInt[3] width := screenWidth - marginInt[1] - marginInt[3]
@@ -1900,6 +1941,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
pwidth -= 1 pwidth -= 1
} }
t.pwindow = t.tui.NewWindow(y, x, pwidth, pheight, tui.WindowPreview, noBorder, true) t.pwindow = t.tui.NewWindow(y, x, pwidth, pheight, tui.WindowPreview, noBorder, true)
t.pwindow.SetWrapSign(t.wrapSign, t.wrapSignWidth)
if !hadPreviewWindow { if !hadPreviewWindow {
t.pwindow.Erase() t.pwindow.Erase()
} }
@@ -2713,11 +2755,15 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
item := result.item item := result.item
_, selected := t.selected[item.Index()] _, selected := t.selected[item.Index()]
label := "" label := ""
extraWidth := 0
if t.jumping != jumpDisabled { if t.jumping != jumpDisabled {
if index < len(t.jumpLabels) { if index < len(t.jumpLabels) {
// Striped // Striped
current = index%2 == 0 current = index%2 == 0
label = t.jumpLabels[index:index+1] + strings.Repeat(" ", t.pointerLen-1) label = t.jumpLabels[index:index+1] + strings.Repeat(" ", util.Max(0, t.pointerLen-1))
if t.pointerLen == 0 {
extraWidth = 1
}
} }
} else if current { } else if current {
label = t.pointer label = t.pointer
@@ -2746,6 +2792,7 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1) maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1)
postTask := func(lineNum int, width int, wrapped bool, forceRedraw bool) { postTask := func(lineNum int, width int, wrapped bool, forceRedraw bool) {
width += extraWidth
if (current || selected) && t.highlightLine { if (current || selected) && t.highlightLine {
color := tui.ColSelected color := tui.ColSelected
if current { if current {
@@ -2932,7 +2979,7 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
} }
if !wholeCovered && t.nthAttr > 0 { if !wholeCovered && t.nthAttr > 0 {
var tokens []Token var tokens []Token
if item.transformed != nil { if item.transformed != nil && item.transformed.revision == t.merger.revision {
tokens = item.transformed.tokens tokens = item.transformed.tokens
} else { } else {
tokens = Transform(Tokenize(item.text.ToString(), t.delimiter), t.nthCurrent) tokens = Transform(Tokenize(item.text.ToString(), t.delimiter), t.nthCurrent)
@@ -3058,8 +3105,15 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
maxWidth := t.window.Width() - (indentSize + 1) maxWidth := t.window.Width() - (indentSize + 1)
wasWrapped := false wasWrapped := false
if wrapped { if wrapped {
maxWidth -= t.wrapSignWidth wrapSign := t.wrapSign
t.window.CPrint(colBase.WithAttr(tui.Dim), t.wrapSign) if maxWidth < t.wrapSignWidth {
runes, _ := util.Truncate(wrapSign, maxWidth)
wrapSign = string(runes)
maxWidth = 0
} else {
maxWidth -= t.wrapSignWidth
}
t.window.CPrint(colBase.WithAttr(tui.Dim), wrapSign)
wrapped = false wrapped = false
wasWrapped = true wasWrapped = true
} }
@@ -3124,7 +3178,9 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
displayWidth = t.displayWidthWithLimit(line, 0, displayWidth) displayWidth = t.displayWidthWithLimit(line, 0, displayWidth)
} }
t.printColoredString(t.window, line, offsets, colBase) if maxWidth > 0 {
t.printColoredString(t.window, line, offsets, colBase)
}
if postTask != nil { if postTask != nil {
postTask(actualLineNum, displayWidth, wasWrapped, forceRedraw) postTask(actualLineNum, displayWidth, wasWrapped, forceRedraw)
} else { } else {
@@ -3333,8 +3389,10 @@ func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unc
wiped := false wiped := false
image := false image := false
wireframe := false wireframe := false
var index int
var line string
Loop: Loop:
for _, line := range lines { for index, line = range lines {
var lbg tui.Color = -1 var lbg tui.Color = -1
if ansi != nil { if ansi != nil {
ansi.lbg = -1 ansi.lbg = -1
@@ -3477,6 +3535,7 @@ Loop:
} }
lineNo++ lineNo++
} }
t.previewer.scrollable = t.previewer.scrollable || index < len(lines)-1
t.previewed.image = image t.previewed.image = image
t.previewed.wireframe = wireframe t.previewed.wireframe = wireframe
} }
@@ -3825,7 +3884,7 @@ func replacePlaceholder(params replacePlaceholderParams) (string, []string) {
elems, prefixLength := awkTokenizer(params.query) elems, prefixLength := awkTokenizer(params.query)
tokens := withPrefixLengths(elems, prefixLength) tokens := withPrefixLengths(elems, prefixLength)
trans := Transform(tokens, nth) trans := Transform(tokens, nth)
result := joinTokens(trans) result := JoinTokens(trans)
if !flags.preserveSpace { if !flags.preserveSpace {
result = strings.TrimSpace(result) result = strings.TrimSpace(result)
} }
@@ -3875,7 +3934,7 @@ func replacePlaceholder(params replacePlaceholderParams) (string, []string) {
replace = func(item *Item) string { replace = func(item *Item) string {
tokens := Tokenize(item.AsString(params.stripAnsi), params.delimiter) tokens := Tokenize(item.AsString(params.stripAnsi), params.delimiter)
trans := Transform(tokens, ranges) trans := Transform(tokens, ranges)
str := joinTokens(trans) str := JoinTokens(trans)
// trim the last delimiter // trim the last delimiter
if params.delimiter.str != nil { if params.delimiter.str != nil {
@@ -4335,6 +4394,7 @@ func (t *Terminal) Loop() error {
var items []*Item var items []*Item
var commandTemplate string var commandTemplate string
var env []string var env []string
var query string
initialOffset := 0 initialOffset := 0
t.previewBox.Wait(func(events *util.Events) { t.previewBox.Wait(func(events *util.Events) {
for req, value := range *events { for req, value := range *events {
@@ -4348,6 +4408,7 @@ func (t *Terminal) Loop() error {
initialOffset = request.scrollOffset initialOffset = request.scrollOffset
items = request.list items = request.list
env = request.env env = request.env
query = request.query
} }
} }
events.Clear() events.Clear()
@@ -4361,8 +4422,7 @@ func (t *Terminal) Loop() error {
version++ version++
// We don't display preview window if no match // We don't display preview window if no match
if items[0] != nil { if items[0] != nil {
_, query := t.Input() command, tempFiles := t.replacePlaceholder(commandTemplate, false, query, items)
command, tempFiles := t.replacePlaceholder(commandTemplate, false, string(query), items)
cmd := t.executor.ExecCommand(command, true) cmd := t.executor.ExecCommand(command, true)
cmd.Env = env cmd.Env = env
@@ -4490,7 +4550,7 @@ func (t *Terminal) Loop() error {
if len(command) > 0 && t.canPreview() { if len(command) > 0 && t.canPreview() {
_, list := t.buildPlusList(command, false) _, list := t.buildPlusList(command, false)
t.cancelPreview() t.cancelPreview()
t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.evaluateScrollOffset(), list, t.environForPreview()}) t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.evaluateScrollOffset(), list, t.environForPreview(), string(t.input)})
} }
} }
@@ -4719,6 +4779,7 @@ func (t *Terminal) Loop() error {
changed := false changed := false
beof := false beof := false
queryChanged := false queryChanged := false
denylist := []int32{}
// Special handling of --sync. Activate the interface on the second tick. // Special handling of --sync. Activate the interface on the second tick.
if loopIndex == 1 && t.deferActivation() { if loopIndex == 1 && t.deferActivation() {
@@ -4875,6 +4936,27 @@ func (t *Terminal) Loop() error {
} }
case actBell: case actBell:
t.tui.Bell() t.tui.Bell()
case actExcludeMulti:
if len(t.selected) > 0 {
for _, item := range t.sortSelected() {
denylist = append(denylist, item.item.Index())
}
// Clear selected items
t.selected = make(map[int32]selectedItem)
t.version++
} else {
item := t.currentItem()
if item != nil {
denylist = append(denylist, item.Index())
}
}
changed = true
case actExclude:
if item := t.currentItem(); item != nil {
denylist = append(denylist, item.Index())
t.deselectItem(item)
changed = true
}
case actExecute, actExecuteSilent: case actExecute, actExecuteSilent:
t.executeCommand(a.a, false, a.t == actExecuteSilent, false, false, "") t.executeCommand(a.a, false, a.t == actExecuteSilent, false, false, "")
case actExecuteMulti: case actExecuteMulti:
@@ -4900,7 +4982,7 @@ func (t *Terminal) Loop() error {
if valid { if valid {
t.cancelPreview() t.cancelPreview()
t.previewBox.Set(reqPreviewEnqueue, t.previewBox.Set(reqPreviewEnqueue,
previewRequest{t.previewOpts.command, t.evaluateScrollOffset(), list, t.environForPreview()}) previewRequest{t.previewOpts.command, t.evaluateScrollOffset(), list, t.environForPreview(), string(t.input)})
} }
} else { } else {
// Discard the preview content so that it won't accidentally appear // Discard the preview content so that it won't accidentally appear
@@ -5021,34 +5103,52 @@ func (t *Terminal) Loop() error {
} else { } else {
req(reqHeader) req(reqHeader)
} }
case actChangeHeaderLabel: case actChangeHeaderLabel, actTransformHeaderLabel:
t.headerLabelOpts.label = a.a label := a.a
if t.headerBorder != nil { if a.t == actTransformHeaderLabel {
t.headerLabel, t.headerLabelLen = t.ansiLabelPrinter(a.a, &tui.ColHeaderLabel, false) label = t.captureLine(a.a)
req(reqRedrawHeaderLabel)
} }
case actChangeInputLabel: t.headerLabelOpts.label = label
t.inputLabelOpts.label = a.a t.headerLabel, t.headerLabelLen = t.ansiLabelPrinter(label, &tui.ColHeaderLabel, false)
req(reqRedrawHeaderLabel)
case actChangeInputLabel, actTransformInputLabel:
label := a.a
if a.t == actTransformInputLabel {
label = t.captureLine(a.a)
}
t.inputLabelOpts.label = label
if t.inputBorder != nil { if t.inputBorder != nil {
t.inputLabel, t.inputLabelLen = t.ansiLabelPrinter(a.a, &tui.ColInputLabel, false) t.inputLabel, t.inputLabelLen = t.ansiLabelPrinter(label, &tui.ColInputLabel, false)
req(reqRedrawInputLabel) req(reqRedrawInputLabel)
} }
case actChangeListLabel: case actChangeListLabel, actTransformListLabel:
t.listLabelOpts.label = a.a label := a.a
if a.t == actTransformListLabel {
label = t.captureLine(a.a)
}
t.listLabelOpts.label = label
if t.wborder != nil { if t.wborder != nil {
t.listLabel, t.listLabelLen = t.ansiLabelPrinter(a.a, &tui.ColListLabel, false) t.listLabel, t.listLabelLen = t.ansiLabelPrinter(label, &tui.ColListLabel, false)
req(reqRedrawListLabel) req(reqRedrawListLabel)
} }
case actChangeBorderLabel: case actChangeBorderLabel, actTransformBorderLabel:
t.borderLabelOpts.label = a.a label := a.a
if a.t == actTransformBorderLabel {
label = t.captureLine(a.a)
}
t.borderLabelOpts.label = label
if t.border != nil { if t.border != nil {
t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(a.a, &tui.ColBorderLabel, false) t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(label, &tui.ColBorderLabel, false)
req(reqRedrawBorderLabel) req(reqRedrawBorderLabel)
} }
case actChangePreviewLabel: case actChangePreviewLabel, actTransformPreviewLabel:
t.previewLabelOpts.label = a.a label := a.a
if a.t == actTransformPreviewLabel {
label = t.captureLine(a.a)
}
t.previewLabelOpts.label = label
if t.pborder != nil { if t.pborder != nil {
t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(a.a, &tui.ColPreviewLabel, false) t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(label, &tui.ColPreviewLabel, false)
req(reqRedrawPreviewLabel) req(reqRedrawPreviewLabel)
} }
case actTransform: case actTransform:
@@ -5056,41 +5156,6 @@ func (t *Terminal) Loop() error {
if actions, err := parseSingleActionList(strings.Trim(body, "\r\n")); err == nil { if actions, err := parseSingleActionList(strings.Trim(body, "\r\n")); err == nil {
return doActions(actions) return doActions(actions)
} }
case actTransformHeaderLabel:
label := t.captureLine(a.a)
t.headerLabelOpts.label = label
if t.headerBorder != nil {
t.headerLabel, t.headerLabelLen = t.ansiLabelPrinter(label, &tui.ColHeaderLabel, false)
req(reqRedrawHeaderLabel)
}
case actTransformInputLabel:
label := t.captureLine(a.a)
t.inputLabelOpts.label = label
if t.inputBorder != nil {
t.inputLabel, t.inputLabelLen = t.ansiLabelPrinter(label, &tui.ColInputLabel, false)
req(reqRedrawInputLabel)
}
case actTransformListLabel:
label := t.captureLine(a.a)
t.listLabelOpts.label = label
if t.wborder != nil {
t.listLabel, t.listLabelLen = t.ansiLabelPrinter(label, &tui.ColListLabel, false)
req(reqRedrawListLabel)
}
case actTransformBorderLabel:
label := t.captureLine(a.a)
t.borderLabelOpts.label = label
if t.border != nil {
t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(label, &tui.ColBorderLabel, false)
req(reqRedrawBorderLabel)
}
case actTransformPreviewLabel:
label := t.captureLine(a.a)
t.previewLabelOpts.label = label
if t.pborder != nil {
t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(label, &tui.ColPreviewLabel, false)
req(reqRedrawPreviewLabel)
}
case actChangePrompt: case actChangePrompt:
t.promptString = a.a t.promptString = a.a
t.prompt, t.promptLen = t.parsePrompt(a.a) t.prompt, t.promptLen = t.parsePrompt(a.a)
@@ -5464,9 +5529,11 @@ func (t *Terminal) Loop() error {
req(reqList, reqInfo, reqPrompt, reqHeader) req(reqList, reqInfo, reqPrompt, reqHeader)
case actToggleWrap: case actToggleWrap:
t.wrap = !t.wrap t.wrap = !t.wrap
t.clearNumLinesCache()
req(reqList, reqHeader) req(reqList, reqHeader)
case actToggleMultiLine: case actToggleMultiLine:
t.multiLine = !t.multiLine t.multiLine = !t.multiLine
t.clearNumLinesCache()
req(reqList) req(reqList)
case actToggleHscroll: case actToggleHscroll:
// Force re-rendering of the list // Force re-rendering of the list
@@ -5999,7 +6066,7 @@ func (t *Terminal) Loop() error {
reload := changed || newCommand != nil reload := changed || newCommand != nil
var reloadRequest *searchRequest var reloadRequest *searchRequest
if reload { if reload {
reloadRequest = &searchRequest{sort: t.sort, sync: reloadSync, nth: newNth, command: newCommand, environ: t.environ(), changed: changed} reloadRequest = &searchRequest{sort: t.sort, sync: reloadSync, nth: newNth, command: newCommand, environ: t.environ(), changed: changed, denylist: denylist, revision: t.merger.Revision()}
} }
t.mutex.Unlock() // Must be unlocked before touching reqBox t.mutex.Unlock() // Must be unlocked before touching reqBox

View File

@@ -6,6 +6,7 @@ import (
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"unicode"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
@@ -77,6 +78,11 @@ type Delimiter struct {
str *string str *string
} }
// IsAwk returns true if the delimiter is an AWK-style delimiter
func (d Delimiter) IsAwk() bool {
return d.regex == nil && d.str == nil
}
// String returns the string representation of a Delimiter. // String returns the string representation of a Delimiter.
func (d Delimiter) String() string { func (d Delimiter) String() string {
return fmt.Sprintf("Delimiter{regex: %v, str: &%q}", d.regex, *d.str) return fmt.Sprintf("Delimiter{regex: %v, str: &%q}", d.regex, *d.str)
@@ -211,7 +217,24 @@ func Tokenize(text string, delimiter Delimiter) []Token {
return withPrefixLengths(tokens, 0) return withPrefixLengths(tokens, 0)
} }
func joinTokens(tokens []Token) string { // StripLastDelimiter removes the trailing delimiter and whitespaces
func StripLastDelimiter(str string, delimiter Delimiter) string {
if delimiter.str != nil {
str = strings.TrimSuffix(str, *delimiter.str)
} else if delimiter.regex != nil {
locs := delimiter.regex.FindAllStringIndex(str, -1)
if len(locs) > 0 {
lastLoc := locs[len(locs)-1]
if lastLoc[1] == len(str) {
str = str[:lastLoc[0]]
}
}
}
return strings.TrimRightFunc(str, unicode.IsSpace)
}
// JoinTokens concatenates the tokens into a single string
func JoinTokens(tokens []Token) string {
var output bytes.Buffer var output bytes.Buffer
for _, token := range tokens { for _, token := range tokens {
output.WriteString(token.text.ToString()) output.WriteString(token.text.ToString())
@@ -229,7 +252,7 @@ func Transform(tokens []Token, withNth []Range) []Token {
if r.begin == r.end { if r.begin == r.end {
idx := r.begin idx := r.begin
if idx == rangeEllipsis { if idx == rangeEllipsis {
chars := util.ToChars(stringBytes(joinTokens(tokens))) chars := util.ToChars(stringBytes(JoinTokens(tokens)))
parts = append(parts, &chars) parts = append(parts, &chars)
} else { } else {
if idx < 0 { if idx < 0 {

View File

@@ -85,14 +85,14 @@ func TestTransform(t *testing.T) {
{ {
ranges, _ := splitNth("1,2,3") ranges, _ := splitNth("1,2,3")
tx := Transform(tokens, ranges) tx := Transform(tokens, ranges)
if joinTokens(tx) != "abc: def: ghi: " { if JoinTokens(tx) != "abc: def: ghi: " {
t.Errorf("%s", tx) t.Errorf("%s", tx)
} }
} }
{ {
ranges, _ := splitNth("1..2,3,2..,1") ranges, _ := splitNth("1..2,3,2..,1")
tx := Transform(tokens, ranges) tx := Transform(tokens, ranges)
if string(joinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " || if string(JoinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " ||
len(tx) != 4 || len(tx) != 4 ||
tx[0].text.ToString() != "abc: def: " || tx[0].prefixLength != 2 || tx[0].text.ToString() != "abc: def: " || tx[0].prefixLength != 2 ||
tx[1].text.ToString() != "ghi: " || tx[1].prefixLength != 14 || tx[1].text.ToString() != "ghi: " || tx[1].prefixLength != 14 ||
@@ -107,7 +107,7 @@ func TestTransform(t *testing.T) {
{ {
ranges, _ := splitNth("1..2,3,2..,1") ranges, _ := splitNth("1..2,3,2..,1")
tx := Transform(tokens, ranges) tx := Transform(tokens, ranges)
if joinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" || if JoinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" ||
len(tx) != 4 || len(tx) != 4 ||
tx[0].text.ToString() != " abc: def:" || tx[0].prefixLength != 0 || tx[0].text.ToString() != " abc: def:" || tx[0].prefixLength != 0 ||
tx[1].text.ToString() != " ghi:" || tx[1].prefixLength != 12 || tx[1].text.ToString() != " ghi:" || tx[1].prefixLength != 12 ||

View File

@@ -29,7 +29,7 @@ const (
const consoleDevice string = "/dev/tty" const consoleDevice string = "/dev/tty"
var offsetRegexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R") var offsetRegexp = regexp.MustCompile("(.*?)\x00?\x1b\\[([0-9]+);([0-9]+)R")
var offsetRegexpBegin = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R") var offsetRegexpBegin = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R")
func (r *LightRenderer) Bell() { func (r *LightRenderer) Bell() {
@@ -44,8 +44,9 @@ func (r *LightRenderer) stderr(str string) {
r.stderrInternal(str, true, "") r.stderrInternal(str, true, "")
} }
const CR string = "\x1b[2m" const DIM string = "\x1b[2m"
const LF string = "\x1b[2m␊" const CR string = DIM + "␍"
const LF string = DIM + "␊"
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool, resetCode string) { func (r *LightRenderer) stderrInternal(str string, allowNLCR bool, resetCode string) {
bytes := []byte(str) bytes := []byte(str)
@@ -127,19 +128,21 @@ type LightRenderer struct {
} }
type LightWindow struct { type LightWindow struct {
renderer *LightRenderer renderer *LightRenderer
colored bool colored bool
windowType WindowType windowType WindowType
border BorderStyle border BorderStyle
top int top int
left int left int
width int width int
height int height int
posx int posx int
posy int posy int
tabstop int tabstop int
fg Color fg Color
bg Color bg Color
wrapSign string
wrapSignWidth int
} }
func NewLightRenderer(ttyin *os.File, theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) (Renderer, error) { func NewLightRenderer(ttyin *os.File, theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) (Renderer, error) {
@@ -1105,11 +1108,12 @@ type wrappedLine struct {
displayWidth int displayWidth int
} }
func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLine { func wrapLine(input string, prefixLength int, initialMax int, tabstop int, wrapSignWidth int) []wrappedLine {
lines := []wrappedLine{} lines := []wrappedLine{}
width := 0 width := 0
line := "" line := ""
gr := uniseg.NewGraphemes(input) gr := uniseg.NewGraphemes(input)
max := initialMax
for gr.Next() { for gr.Next() {
rs := gr.Runes() rs := gr.Runes()
str := string(rs) str := string(rs)
@@ -1131,6 +1135,7 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
line = str line = str
prefixLength = 0 prefixLength = 0
width = w width = w
max = initialMax - wrapSignWidth
} }
} }
lines = append(lines, wrappedLine{string(line), width}) lines = append(lines, wrappedLine{string(line), width})
@@ -1140,7 +1145,7 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
func (w *LightWindow) fill(str string, resetCode string) FillReturn { func (w *LightWindow) fill(str string, resetCode string) FillReturn {
allLines := strings.Split(str, "\n") allLines := strings.Split(str, "\n")
for i, line := range allLines { for i, line := range allLines {
lines := wrapLine(line, w.posx, w.width, w.tabstop) lines := wrapLine(line, w.posx, w.width, w.tabstop, w.wrapSignWidth)
for j, wl := range lines { for j, wl := range lines {
w.stderrInternal(wl.text, false, resetCode) w.stderrInternal(wl.text, false, resetCode)
w.posx += wl.displayWidth w.posx += wl.displayWidth
@@ -1153,6 +1158,18 @@ func (w *LightWindow) fill(str string, resetCode string) FillReturn {
w.MoveAndClear(w.posy, w.posx) w.MoveAndClear(w.posy, w.posx)
w.Move(w.posy+1, 0) w.Move(w.posy+1, 0)
w.renderer.stderr(resetCode) w.renderer.stderr(resetCode)
if len(lines) > 1 {
sign := w.wrapSign
width := w.wrapSignWidth
if width > w.width {
runes, truncatedWidth := util.Truncate(w.wrapSign, w.width)
sign = string(runes)
width = truncatedWidth
}
w.stderrInternal(DIM+sign, false, resetCode)
w.renderer.stderr(resetCode)
w.Move(w.posy, width)
}
} }
} }
} }
@@ -1226,6 +1243,11 @@ func (w *LightWindow) EraseMaybe() bool {
return false return false
} }
func (w *LightWindow) SetWrapSign(sign string, width int) {
w.wrapSign = sign
w.wrapSignWidth = width
}
func (r *LightRenderer) HideCursor() { func (r *LightRenderer) HideCursor() {
r.showCursor = false r.showCursor = false
r.csi("?25l") r.csi("?25l")

View File

@@ -39,20 +39,22 @@ func (p ColorPair) style() tcell.Style {
type Attr int32 type Attr int32
type TcellWindow struct { type TcellWindow struct {
color bool color bool
windowType WindowType windowType WindowType
top int top int
left int left int
width int width int
height int height int
normal ColorPair normal ColorPair
lastX int lastX int
lastY int lastY int
moveCursor bool moveCursor bool
borderStyle BorderStyle borderStyle BorderStyle
uri *string uri *string
params *string params *string
showCursor bool showCursor bool
wrapSign string
wrapSignWidth int
} }
func (w *TcellWindow) Top() int { func (w *TcellWindow) Top() int {
@@ -629,6 +631,11 @@ func (w *TcellWindow) EraseMaybe() bool {
return true return true
} }
func (w *TcellWindow) SetWrapSign(sign string, width int) {
w.wrapSign = sign
w.wrapSignWidth = width
}
func (w *TcellWindow) EncloseX(x int) bool { func (w *TcellWindow) EncloseX(x int) bool {
return x >= w.left && x < (w.left+w.width) return x >= w.left && x < (w.left+w.width)
} }
@@ -757,11 +764,26 @@ Loop:
// word wrap: // word wrap:
xPos := w.left + w.lastX + lx xPos := w.left + w.lastX + lx
if xPos >= (w.left + w.width) { if xPos >= w.left+w.width {
w.lastY++ w.lastY++
if w.lastY >= w.height {
return FillSuspend
}
w.lastX = 0 w.lastX = 0
lx = 0 lx = 0
xPos = w.left xPos = w.left
sign := w.wrapSign
if w.wrapSignWidth > w.width {
runes, _ := util.Truncate(sign, w.width)
sign = string(runes)
}
wgr := uniseg.NewGraphemes(sign)
for wgr.Next() {
rs := wgr.Runes()
_screen.SetContent(w.left+lx, w.top+w.lastY, rs[0], rs[1:], style.Dim(true))
lx += uniseg.StringWidth(string(rs))
}
xPos = w.left + lx
} }
yPos := w.top + w.lastY yPos := w.top + w.lastY

View File

@@ -659,6 +659,8 @@ type Window interface {
LinkEnd() LinkEnd()
Erase() Erase()
EraseMaybe() bool EraseMaybe() bool
SetWrapSign(string, int)
} }
type FullscreenRenderer struct { type FullscreenRenderer struct {

View File

@@ -184,9 +184,25 @@ func (chars *Chars) TrailingWhitespaces() int {
return whitespaces return whitespaces
} }
func (chars *Chars) TrimTrailingWhitespaces() { func (chars *Chars) TrimSuffix(runes []rune) {
whitespaces := chars.TrailingWhitespaces() lastIdx := len(chars.slice)
chars.slice = chars.slice[0 : len(chars.slice)-whitespaces] firstIdx := lastIdx - len(runes)
if firstIdx < 0 {
return
}
for i := firstIdx; i < lastIdx; i++ {
char := chars.Get(i)
if char != runes[i-firstIdx] {
return
}
}
chars.slice = chars.slice[0:firstIdx]
}
func (chars *Chars) SliceRight(last int) {
chars.slice = chars.slice[:last]
} }
func (chars *Chars) ToString() string { func (chars *Chars) ToString() string {

View File

@@ -827,6 +827,24 @@ class TestCore < TestInteractive
tmux.until { |lines| assert(lines.any? { it.include?('jump cancelled at 3') }) } tmux.until { |lines| assert(lines.any? { it.include?('jump cancelled at 3') }) }
end end
def test_jump_no_pointer
tmux.send_keys "seq 100 | #{FZF} --pointer= --jump-labels 12345 --bind ctrl-j:jump", :Enter
tmux.until { |lines| assert_equal 100, lines.match_count }
tmux.send_keys 'C-j'
tmux.until { |lines| assert_equal '5 5', lines[-7] }
tmux.send_keys 'C-c'
tmux.until { |lines| assert_equal ' 5', lines[-7] }
end
def test_jump_no_pointer_no_marker
tmux.send_keys "seq 100 | #{FZF} --pointer= --marker= --jump-labels 12345 --bind ctrl-j:jump", :Enter
tmux.until { |lines| assert_equal 100, lines.match_count }
tmux.send_keys 'C-j'
tmux.until { |lines| assert_equal '55', lines[-7] }
tmux.send_keys 'C-c'
tmux.until { |lines| assert_equal '5', lines[-7] }
end
def test_pointer def test_pointer
tmux.send_keys "seq 10 | #{fzf("--pointer '>>'")}", :Enter tmux.send_keys "seq 10 | #{fzf("--pointer '>>'")}", :Enter
# Assert that specified pointer is displayed # Assert that specified pointer is displayed
@@ -1665,4 +1683,129 @@ class TestCore < TestInteractive
assert_equal '', File.read(tempname).chomp assert_equal '', File.read(tempname).chomp
end end
end end
def test_exclude_multi
tmux.send_keys %(seq 1000 | #{FZF} --multi --bind 'a:exclude-multi,b:reload(seq 1000),c:reload-sync(seq 1000)'), :Enter
tmux.until do |lines|
assert_equal 1000, lines.match_count
assert_includes lines, '> 1'
end
tmux.send_keys :a
tmux.until do |lines|
assert_includes lines, '> 2'
assert_equal 999, lines.match_count
end
tmux.send_keys :Up, :BTab, :BTab, :BTab, :a
tmux.until do |lines|
assert_equal 996, lines.match_count
assert_includes lines, '> 9'
end
tmux.send_keys :b
tmux.until do |lines|
assert_equal 1000, lines.match_count
assert_includes lines, '> 5'
end
tmux.send_keys :Tab, :Tab, :Tab, :a
tmux.until do |lines|
assert_equal 997, lines.match_count
assert_includes lines, '> 2'
end
tmux.send_keys :c
tmux.until do |lines|
assert_equal 1000, lines.match_count
assert_includes lines, '> 2'
end
# TODO: We should also check the behavior of 'exclude' during reloads
end
def test_exclude
tmux.send_keys %(seq 1000 | #{FZF} --multi --bind 'a:exclude,b:reload(seq 1000),c:reload-sync(seq 1000)'), :Enter
tmux.until do |lines|
assert_equal 1000, lines.match_count
assert_includes lines, '> 1'
end
tmux.send_keys :a
tmux.until do |lines|
assert_includes lines, '> 2'
assert_equal 999, lines.match_count
end
tmux.send_keys :Up, :BTab, :BTab, :BTab, :a
tmux.until do |lines|
assert_equal 998, lines.match_count
assert_equal 3, lines.select_count
assert_includes lines, '> 7'
end
tmux.send_keys :b
tmux.until do |lines|
assert_equal 1000, lines.match_count
assert_equal 0, lines.select_count
assert_includes lines, '> 5'
end
tmux.send_keys :Tab, :Tab, :Tab, :a
tmux.until do |lines|
assert_equal 999, lines.match_count
assert_equal 3, lines.select_count
assert_includes lines, '>>3'
end
tmux.send_keys :a
tmux.until do |lines|
assert_equal 998, lines.match_count
assert_equal 2, lines.select_count
assert_includes lines, '>>4'
end
tmux.send_keys :c
tmux.until do |lines|
assert_equal 1000, lines.match_count
assert_includes lines, '> 2'
end
# TODO: We should also check the behavior of 'exclude' during reloads
end
def test_accept_nth
tmux.send_keys %((echo "foo bar baz"; echo "bar baz foo") | #{FZF} --multi --accept-nth 2,2 --sync --bind start:select-all+accept > #{tempname}), :Enter
wait do
assert_path_exists tempname
assert_equal ['bar bar', 'baz baz'], File.readlines(tempname, chomp: true)
end
end
def test_accept_nth_string_delimiter
tmux.send_keys %(echo "foo ,bar,baz" | #{FZF} -d, --accept-nth 2,2,1,3,1 --sync --bind start:accept > #{tempname}), :Enter
wait do
assert_path_exists tempname
# Last delimiter and the whitespaces are removed
assert_equal ['bar,bar,foo ,bazfoo'], File.readlines(tempname, chomp: true)
end
end
def test_accept_nth_regex_delimiter
tmux.send_keys %(echo "foo :,:bar,baz" | #{FZF} --delimiter='[:,]+' --accept-nth 2,2,1,3,1 --sync --bind start:accept > #{tempname}), :Enter
wait do
assert_path_exists tempname
# Last delimiter and the whitespaces are removed
assert_equal ['bar,bar,foo :,:bazfoo'], File.readlines(tempname, chomp: true)
end
end
def test_accept_nth_regex_delimiter_strip_last
tmux.send_keys %((echo "foo:,bar:,baz"; echo "foo:,bar:,baz:,qux:,") | #{FZF} --multi --delimiter='[:,]+' --accept-nth 2.. --sync --bind 'load:select-all+accept' > #{tempname}), :Enter
wait do
assert_path_exists tempname
# Last delimiter and the whitespaces are removed
assert_equal ['bar:,baz', 'bar:,baz:,qux'], File.readlines(tempname, chomp: true)
end
end
def test_accept_nth_template
tmux.send_keys %(echo "foo ,bar,baz" | #{FZF} -d, --accept-nth '[{n}] 1st: {1}, 3rd: {3}, 2nd: {2}' --sync --bind start:accept > #{tempname}), :Enter
wait do
assert_path_exists tempname
# Last delimiter and the whitespaces are removed
assert_equal ['[0] 1st: foo, 3rd: baz, 2nd: bar'], File.readlines(tempname, chomp: true)
end
end
end end

View File

@@ -52,6 +52,12 @@ class TestFilter < TestBase
`find . -print0 | #{FZF} --read0 -e -f "^#{lines.last}$"`.chomp `find . -print0 | #{FZF} --read0 -e -f "^#{lines.last}$"`.chomp
end end
def test_nth_suffix_match
assert_equal \
'foo,bar,baz',
`echo foo,bar,baz | #{FZF} -d, -f'bar$' -n2`.chomp
end
def test_with_nth_basic def test_with_nth_basic
writelines(['hello world ', 'byebye']) writelines(['hello world ', 'byebye'])
assert_equal \ assert_equal \
@@ -59,6 +65,13 @@ class TestFilter < TestBase
`#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 < #{tempname}`.chomp `#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 < #{tempname}`.chomp
end end
def test_with_nth_template
writelines(['hello world ', 'byebye'])
assert_equal \
'hello world ',
`#{FZF} -f"^he he.he." -x -n 2.. --with-nth '{2} {1}. {1}.' < #{tempname}`.chomp
end
def test_with_nth_ansi def test_with_nth_ansi
writelines(["\x1b[33mhello \x1b[34;1mworld\x1b[m ", 'byebye']) writelines(["\x1b[33mhello \x1b[34;1mworld\x1b[m ", 'byebye'])
assert_equal \ assert_equal \

View File

@@ -978,4 +978,17 @@ class TestLayout < TestInteractive
setup setup
end end
end end
def test_change_header_and_label_at_once
tmux.send_keys %(seq 10 | #{FZF} --border sharp --header-border sharp --header-label-pos 3 --bind 'focus:change-header(header)+change-header-label(label)'), :Enter
block = <<~BLOCK
label
header
10/10
>
BLOCK
tmux.until { assert_block(block, it) }
end
end end

View File

@@ -453,7 +453,7 @@ class TestPreview < TestInteractive
tmux.send_keys 'f' tmux.send_keys 'f'
tmux.until do |lines| tmux.until do |lines|
assert_equal '::', lines[0] assert_equal '::', lines[0]
assert_equal ' 3', lines[1] assert_equal ' 3', lines[1]
end end
end end
@@ -527,7 +527,7 @@ class TestPreview < TestInteractive
tmux.send_keys "seq 10 | #{FZF} --preview-border rounded --preview-window '~5,2,+0,<100000(~0,+100,wrap,noinfo)' --preview 'seq 1000'", :Enter tmux.send_keys "seq 10 | #{FZF} --preview-border rounded --preview-window '~5,2,+0,<100000(~0,+100,wrap,noinfo)' --preview 'seq 1000'", :Enter
tmux.until { |lines| assert_equal 10, lines.match_count } tmux.until { |lines| assert_equal 10, lines.match_count }
tmux.until do |lines| tmux.until do |lines|
assert_equal ['╭────╮', '│ 10 │', '│ 0 │', '│ 10 │', '│ 1 │'], lines.take(5).map(&:strip) assert_equal ['╭────╮', '│ 10 │', '│ ↳ 0│', '│ 10 │', '│ ↳ 1│'], lines.take(5).map(&:strip)
end end
end end
@@ -544,4 +544,18 @@ class TestPreview < TestInteractive
tmux.send_keys :Up tmux.send_keys :Up
tmux.until { |lines| assert_includes lines, '> 2' } tmux.until { |lines| assert_includes lines, '> 2' }
end end
def test_preview_query_should_not_be_affected_by_search
tmux.send_keys "seq 1 | #{FZF} --bind 'change:transform-search(echo {q:1})' --preview 'echo [{q}/{}]'", :Enter
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys '1'
tmux.until { |lines| assert lines.any_include?('[1/1]') }
tmux.send_keys :Space
tmux.until { |lines| assert lines.any_include?('[1 /1]') }
tmux.send_keys '2'
tmux.until do |lines|
assert lines.any_include?('[1 2/1]')
assert_equal 1, lines.match_count
end
end
end end

View File

@@ -73,7 +73,7 @@ module TestShell
tmux.prepare tmux.prepare
tmux.send_keys :Escape, :c tmux.send_keys :Escape, :c
lines = tmux.until { |lines| assert_operator lines.match_count, :>, 0 } lines = tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
expected = lines.reverse.find { |l| l.start_with?('> ') }[2..] expected = lines.reverse.find { |l| l.start_with?('> ') }[2..].chomp('/')
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.prepare tmux.prepare
tmux.send_keys :pwd, :Enter tmux.send_keys :pwd, :Enter
@@ -241,7 +241,7 @@ module CompletionTest
tmux.until do |lines| tmux.until do |lines|
assert_equal 1, lines.match_count assert_equal 1, lines.match_count
assert_includes lines, '> 55' assert_includes lines, '> 55'
assert_includes lines, '> /tmp/fzf-test/d55' assert_includes lines, '> /tmp/fzf-test/d55/'
end end
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'cd /tmp/fzf-test/d55/', lines[-1] } tmux.until(true) { |lines| assert_equal 'cd /tmp/fzf-test/d55/', lines[-1] }