Compare commits

...

51 Commits

Author SHA1 Message Date
Junegunn Choi
a0ef8987fb 0.31.0 2022-07-21 22:46:34 +09:00
Junegunn Choi
3af5b7f2ac Do not validate other options when --version is present
Close #2690
2022-07-21 22:36:52 +09:00
Junegunn Choi
7a7cfcacbe Lift unicode.IsGraphic constraint for pointer, marker, and ellipsis
Use at your own risk.

Close #2709
Close #2055
2022-07-21 22:30:01 +09:00
Junegunn Choi
52594355bf [shell] 'kill' completion will now require trigger sequence (**)
'kill **<tab>' instead of 'kill <tab>' just like any other completions.

Close #2716
Close #385
2022-07-21 22:21:11 +09:00
Junegunn Choi
0d06c28b19 Fix delimiter regex to properly support caret (^)
Fix #2861
2022-07-21 21:21:06 +09:00
Junegunn Choi
ccc4677252 [vim] fzf#exec: Shell-escape fzf binary path
Fix #2877
2022-07-20 14:29:52 +09:00
Junegunn Choi
821fc9feed Fix failing test case 2022-07-20 12:29:45 +09:00
Junegunn Choi
82b46726fc Add support for an alternative preview window layout
Close #2804
Close #2844

Related #2277
2022-07-20 12:08:54 +09:00
Jakub Jirutka
8df872a482 [zsh] Replace perl with awk (#2777)
Unlike awk, which is even defined in POSIX, perl is not pre-installed
on all *nix systems. This awk command is functionally equivalent to
the original perl command.
2022-07-20 11:53:34 +09:00
Jonathan Zacsh
c79c306adb [bash] Fix fzf-tmux to have fzf's completion (#2871) 2022-07-15 15:53:23 +09:00
Junegunn Choi
51fdaad002 [uninstall] Remove readlink to support relative symlinks of dotfiles
Close #2853
2022-07-01 16:32:32 +09:00
znley
885cd8ff04 [make] Add loongarch64 support (#2857) 2022-06-28 09:16:26 +09:00
Tanish Yadav
2707af403a [shell] Don't export PATH in ~/.fzf.{bash,zsh} (#2852)
There is no use exporting PATH when it is already exported. Moreover, it
causes things like `typeset -U path` in zsh to break if done before
sourcing "~/.fzf.zsh".
2022-06-24 16:53:51 +09:00
Junegunn Choi
2d227e5222 [man] Fix unescaped backslash in awk example
Close #2854
2022-06-23 15:45:36 +09:00
Junegunn Choi
70529878e2 Use SGR mouse mode for larger terminals
Fix #2840
2022-06-17 12:48:04 +09:00
Daniel Zhang
3b7a962dc6 [vim] Fix fzf#shellescape when shell=fish (#2828)
`shellescape()` behavior is different when `shell=fish`, so we should set `shell` before calling `shellescape()`, otherwise an unexpected result may occur (e.g. https://github.com/kevinhwang91/nvim-bqf/issues/56).

Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2022-05-25 09:50:10 +09:00
Jan Warchoł
6dcf5c3d7d [bash] Make complex commands slightly more friendly to work with (#2784)
- extract logical parts to separate variables (e.g. $opts)
- put options in $opts in similar order
- move +/-m into $opts (at the end, so they won't be overridden)
- split pipelines into multiple lines
- remove "echo" that seems to be redundant

All this should help with readability and also result in cleaner diffs
when changes are made.
2022-04-29 19:04:16 +09:00
Junegunn Choi
b089bb5e7b Fix scrollability of the preview window in certain cases
Fix #2683

This commit fixes the cases where fzf incorrectly determines the
scrollability of the preview window when `--preview-window-wrap` is set.

Wrapping of the preview content happens during the rendering phase, so
it's currently not possible to know how many lines are actually needed
to display the content beforehand. So `preview-bottom` still may not
move to the very bottom with wrapping enabled.
2022-04-28 11:46:24 +09:00
dependabot[bot]
a91a67668e Bump github/codeql-action from 2.1.6 to 2.1.8 (#2787)
* Bump github/codeql-action from 2.1.6 to 2.1.8

Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2.1.6 to 2.1.8.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](28eead2408...1ed1437484)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Delete incorrect comments

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2022-04-22 22:36:56 +09:00
dependabot[bot]
220a908118 Bump ruby/setup-ruby from 1.100.0 to 1.101.0 (#2795)
* Bump ruby/setup-ruby from 1.100.0 to 1.101.0

Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.100.0 to 1.101.0.
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Commits](bd94d6a504...ebaea52cb2)

---
updated-dependencies:
- dependency-name: ruby/setup-ruby
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Delete incorrect comments

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2022-04-22 22:35:51 +09:00
Ajeet D'Souza
54841248e7 [shell] ALT-C: Use builtin cd to avoid conflicts (#2799) 2022-04-22 21:58:32 +09:00
Junegunn Choi
a0b42e6538 Require Go 1.17 or above 2022-04-07 20:31:19 +09:00
Jan Warchoł
3312cf525d [bash] Allow passing args to __fzf_select__ via fzf-file-widget (#2783)
This makes it easier to make customizations, for example instead of

    bind -x '"\C-o\C-i": FZF_CTRL_T_COMMAND="fasd -Rl" FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS --tiebreak=index " fzf-file-widget'

it's enough to just

    bind -x '"\C-o\C-i": FZF_CTRL_T_COMMAND="fasd -Rl" fzf-file-widget --tiebreak=index'
2022-04-06 11:29:01 +09:00
Junegunn Choi
2093667548 0.30.0 2022-04-04 23:01:43 +09:00
Junegunn Choi
3c868d7961 ADVANCED.md: Add rebind example 2022-04-04 22:59:26 +09:00
dependabot[bot]
707f4f5816 Bump github/codeql-action from 1.1.5 to 2.1.6 (#2782)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 1.1.5 to 2.1.6.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](8834766498...28eead2408)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-04 22:44:10 +09:00
Junegunn Choi
b3ab6311c5 Hide cursor while rendering the screen
Fix #2781
Fix #2588
Fix #1805

Fix https://github.com/junegunn/fzf.vim/issues/1370
Fix https://github.com/junegunn/fzf.vim/issues/1060
2022-04-04 22:06:16 +09:00
Junegunn Choi
d56f605b63 Add rebind action for restoring bindings after unbind
Fix #2752
Close #2564
2022-04-04 21:54:22 +09:00
Junegunn Choi
f8b713f425 Remove redundant state update on reload
Related: 5209e95
2022-03-31 10:05:28 +09:00
Junegunn Choi
5209e95bc7 Make preview updated when reload and change-query are combined
Fix #2744
2022-03-29 22:27:03 +09:00
Junegunn Choi
ef67a45702 Add --ellipsis=.. option
Close #2432

Also see
- #1769
- https://github.com/junegunn/fzf/pull/1844#issuecomment-586663660
2022-03-29 21:35:36 +09:00
Junegunn Choi
b88eb72ac2 Modernize build tags 2022-03-29 21:23:45 +09:00
dependabot[bot]
32847f7254 Bump actions/setup-go from 2.2.0 to 3 (#2776)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 2.2.0 to 3.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](bfdd3570ce...f6164bd8c8)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-29 21:17:44 +09:00
dependabot[bot]
71df93b534 Bump ruby/setup-ruby from 1.62.0 to 1.100.0 (#2775)
Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.62.0 to 1.100.0.
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Commits](5aaa89ff0d...bd94d6a504)

---
updated-dependencies:
- dependency-name: ruby/setup-ruby
  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>
2022-03-29 21:17:29 +09:00
Naveen
bb028191f8 Set up dependabot for GitHub actions (#2764) 2022-03-29 21:08:41 +09:00
Naveen
19af8fc7d8 Pin actions to a full length commit SHA (#2765)
- Pinned actions by SHA https://github.com/ossf/scorecard/blob/main/docs/checks.md#pinned-dependencies
- Included permissions for the action. https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions

>Pin actions to a full length commit SHA

>Pinning an action to a full length commit SHA is currently the only way to use an action as an immutable release. Pinning to a particular SHA helps mitigate the risk of a bad actor adding a backdoor to the action's repository, as they would need to generate a SHA-1 collision for a valid Git object payload.

https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-third-party-actions

Also, dependabot supports upgrade based on SHA.

Signed-off-by: naveensrinivasan <172697+naveensrinivasan@users.noreply.github.com>s
2022-03-29 21:08:01 +09:00
Junegunn Choi
a06671b47f Increase TTY buffer limit
Kitty's shell intergration generates a long sequence of key presses in
certain cases. As long as the length of the sequence is finite, fzf can
process it.

Close #2748
2022-03-09 17:02:06 +09:00
Junegunn Choi
5f385d88e0 [zsh] Set up bindings for all three keymaps: emacs, vicmd, and viins
Fix #2694
2022-02-23 15:36:49 +09:00
Junegunn Choi
9cb7a364a3 [install] Remove code that might delete user fish script
Fix #2703
2022-01-05 21:48:20 +09:00
Junegunn Choi
f68cbc577d Add link to ADVANCED.md
Related #2701
2022-01-03 13:52:46 +09:00
Junegunn Choi
dc975e8974 0.29.0 2021-12-25 01:46:01 +09:00
Junegunn Choi
4311ade535 ADVANCED.md: Add change-preview-window example 2021-12-24 14:41:47 +09:00
Junegunn Choi
cd23401411 Fix rendering of the prompt line when overflow occurs with --info=inline
Fix #2692
2021-12-22 23:23:50 +09:00
Martin Jindra
176ee6910f Update Dockerfile (#2662)
`archlinux/base:latest`cannot be found
2021-12-09 11:05:12 +09:00
Junegunn Choi
13c8f3d3aa [vim] Handle writefile() failure gracefully
Fix #2676
2021-12-07 17:33:26 +09:00
Junegunn Choi
ce9af687bc Remove unused code 2021-12-05 21:17:38 +09:00
Junegunn Choi
43f0d0cacd change-preview-window to take multiple option sets separated by '|'
So you can "rotate" through the different options with a single binding.

  fzf --preview 'cat {}' \
      --bind 'ctrl-/:change-preview-window(70%|down,40%,border-horizontal|hidden|)'

Close #2376
2021-12-05 21:13:10 +09:00
Junegunn Choi
20b4e6953e Implement change-preview and change-preview-window actions
The new actions are named with 'change-' prefix to differentiate from
the pre-existing, one-off 'preview(...)' action.

Fix #2360
Fix #2505
Fix #2666

Related #2435
Related #2376
  - Can set up multiple bindings with different change-preview-window actions
  - Not possible to "rotate" through the options with a single binding
  - Enlarge or shrink not possible
2021-11-30 23:57:46 +09:00
Kai
7da287e3aa README.md: HTTP => HTTPS (#2673) 2021-11-28 22:28:32 +09:00
zsugabubus
205f885d69 [shell] Use cd -- (#2659)
Otherwise directories starting with '-' may treated as options.
2021-11-19 10:36:28 +09:00
Junegunn Choi
3715cd349d Add repology packaging status badge 2021-11-18 15:47:06 +09:00
50 changed files with 1042 additions and 549 deletions

View File

@@ -4,3 +4,7 @@ updates:
directory: "/" directory: "/"
schedule: schedule:
interval: "weekly" interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

View File

@@ -8,8 +8,15 @@ on:
branches: [ master ] branches: [ master ]
workflow_dispatch: workflow_dispatch:
permissions:
contents: read
jobs: jobs:
analyze: analyze:
permissions:
actions: read # for github/codeql-action/init to get workflow details
contents: read # for actions/checkout to fetch code
security-events: write # for github/codeql-action/autobuild to send a status report
name: Analyze name: Analyze
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -20,18 +27,18 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
with: with:
fetch-depth: 0 fetch-depth: 0
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v1 uses: github/codeql-action/init@1ed1437484560351c5be56cf73a48a279d116b78
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v1 uses: github/codeql-action/autobuild@1ed1437484560351c5be56cf73a48a279d116b78
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1 uses: github/codeql-action/analyze@1ed1437484560351c5be56cf73a48a279d116b78

View File

@@ -8,24 +8,24 @@ on:
branches: [ master ] branches: [ master ]
workflow_dispatch: workflow_dispatch:
permissions:
contents: read
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
go: [1.14, 1.16]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v2 uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2
with: with:
go-version: ${{ matrix.go }} go-version: 1.18
- name: Setup Ruby - name: Setup Ruby
uses: ruby/setup-ruby@v1.62.0 uses: ruby/setup-ruby@ebaea52cb20fea395b0904125276395e37183dac
with: with:
ruby-version: 3.0.0 ruby-version: 3.0.0

View File

@@ -8,24 +8,24 @@ on:
branches: [ master ] branches: [ master ]
workflow_dispatch: workflow_dispatch:
permissions:
contents: read
jobs: jobs:
build: build:
runs-on: macos-latest runs-on: macos-latest
strategy:
matrix:
go: [1.14, 1.16]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v2 uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2
with: with:
go-version: ${{ matrix.go }} go-version: 1.18
- name: Setup Ruby - name: Setup Ruby
uses: ruby/setup-ruby@v1.62.0 uses: ruby/setup-ruby@ebaea52cb20fea395b0904125276395e37183dac
with: with:
ruby-version: 3.0.0 ruby-version: 3.0.0

1
.tool-versions Normal file
View File

@@ -0,0 +1 @@
golang 1.18

View File

@@ -17,6 +17,7 @@ Advanced fzf examples
* [Using fzf as the secondary filter](#using-fzf-as-the-secondary-filter) * [Using fzf as the secondary filter](#using-fzf-as-the-secondary-filter)
* [Using fzf as interative Ripgrep launcher](#using-fzf-as-interative-ripgrep-launcher) * [Using fzf as interative Ripgrep launcher](#using-fzf-as-interative-ripgrep-launcher)
* [Switching to fzf-only search mode](#switching-to-fzf-only-search-mode) * [Switching to fzf-only search mode](#switching-to-fzf-only-search-mode)
* [Switching between Ripgrep mode and fzf mode](#switching-between-ripgrep-mode-and-fzf-mode)
* [Log tailing](#log-tailing) * [Log tailing](#log-tailing)
* [Key bindings for git objects](#key-bindings-for-git-objects) * [Key bindings for git objects](#key-bindings-for-git-objects)
* [Files listed in `git status`](#files-listed-in-git-status) * [Files listed in `git status`](#files-listed-in-git-status)
@@ -405,6 +406,40 @@ IFS=: read -ra selected < <(
- We reverted `--color` option for customizing how the matching chunks are - We reverted `--color` option for customizing how the matching chunks are
displayed in the second phase displayed in the second phase
### Switching between Ripgrep mode and fzf mode
*(Requires fzf 0.30.0 or above)*
fzf 0.30.0 added `rebind` action so we can "rebind" the bindings that were
previously "unbound" via `unbind`.
This is an improved version of the previous example that allows us to switch
between Ripgrep launcher mode and fzf-only filtering mode via CTRL-R and
CTRL-F.
```sh
#!/usr/bin/env bash
# Switch between Ripgrep launcher mode (CTRL-R) and fzf filtering mode (CTRL-F)
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="${*:-}"
IFS=: read -ra selected < <(
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \
fzf --ansi \
--color "hl:-1:underline,hl+:-1:underline:reverse" \
--disabled --query "$INITIAL_QUERY" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
--bind "ctrl-f:unbind(change,ctrl-f)+change-prompt(2. fzf> )+enable-search+clear-query+rebind(ctrl-r)" \
--bind "ctrl-r:unbind(ctrl-r)+change-prompt(1. ripgrep> )+disable-search+reload($RG_PREFIX {q} || true)+rebind(change,ctrl-f)" \
--prompt '1. Ripgrep> ' \
--delimiter : \
--header ' CTRL-R (Ripgrep mode) CTRL-F (fzf mode) ' \
--preview 'bat --color=always {1} --highlight-line {2}' \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3'
)
[ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}"
```
Log tailing Log tailing
----------- -----------
@@ -429,30 +464,34 @@ Admittedly, that was a silly example. Here's a practical one for browsing
Kubernetes pods. Kubernetes pods.
```bash ```bash
#!/usr/bin/env bash pods() {
FZF_DEFAULT_COMMAND="kubectl get pods --all-namespaces" \
read -ra tokens < <( fzf --info=inline --layout=reverse --header-lines=1 \
kubectl get pods --all-namespaces |
fzf --info=inline --layout=reverse --header-lines=1 --border \
--prompt "$(kubectl config current-context | sed 's/-context$//')> " \ --prompt "$(kubectl config current-context | sed 's/-context$//')> " \
--header $'Press CTRL-O to open log in editor\n\n' \ --header $' Enter (kubectl exec) CTRL-O (open log in editor) CTRL-R (reload) \n\n' \
--bind ctrl-/:toggle-preview \ --bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --namespace {1} {2}) > /dev/tty' \ --bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash > /dev/tty' \
--preview-window up,follow \ --bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2}) > /dev/tty' \
--preview 'kubectl logs --follow --tail=100000 --namespace {1} {2}' "$@" --bind 'ctrl-r:reload:$FZF_DEFAULT_COMMAND' \
) --preview-window up:follow \
[ ${#tokens} -gt 1 ] && --preview 'kubectl logs --follow --all-containers --tail=10000 --namespace {1} {2}' "$@"
kubectl exec -it --namespace "${tokens[0]}" "${tokens[1]}" -- bash }
``` ```
![image](https://user-images.githubusercontent.com/700826/113473547-1d7a4880-94a5-11eb-98ef-9aa6f0ed215a.png) ![image](https://user-images.githubusercontent.com/700826/113473547-1d7a4880-94a5-11eb-98ef-9aa6f0ed215a.png)
- The preview window will *"log tail"* the pod - The preview window will *"log tail"* the pod
- Holding on to a large amount of log will consume a lot of memory. So we - Holding on to a large amount of log will consume a lot of memory. So we
limited the initial log amount with `--tail=100000`. limited the initial log amount with `--tail=10000`.
- With `execute` binding, you can press CTRL-O to open the log in your editor - `execute` bindings allow you to run any command without leaving fzf
without leaving fzf - Press enter key on a pod to `kubectl exec` into it
- Select a pod (with an enter key) to `kubectl exec` into it - Press CTRL-O to open the log in your editor
- Press CTRL-R to reload the pod list
- Press CTRL-/ repeatedly to to rotate through a different sets of preview
window options
1. `80%,border-bottom`
1. `hidden`
1. Empty string after `|` translates to the default options from `--preview-window`
Key bindings for git objects Key bindings for git objects
---------------------------- ----------------------------

View File

@@ -6,7 +6,7 @@ Build instructions
### Prerequisites ### Prerequisites
- Go 1.13 or above - Go 1.17 or above
### Using Makefile ### Using Makefile

View File

@@ -1,6 +1,54 @@
CHANGELOG CHANGELOG
========= =========
0.31.0
------
- Added support for an alternative preview window layout that is activated
when the size of the preview window is smaller than a certain threshold.
```sh
# If the width of the preview window is smaller than 50 columns,
# it will be displayed above the search window.
fzf --preview 'cat {}' --preview-window 'right,50%,border-left,<50(up,30%,border-bottom)'
# Or you can just hide it like so
fzf --preview 'cat {}' --preview-window '<50(hidden)'
```
- fzf now uses SGR mouse mode to properly support mouse on larger terminals
- You can now use characters that do not satisfy `unicode.IsGraphic` constraint
for `--marker`, `--pointer`, and `--ellipsis`. Allows Nerd Fonts and stuff.
Use at your own risk.
- Bug fixes and improvements
- Shell extension
- `kill` completion now requires trigger sequence (`**`) for consistency
0.30.0
------
- Fixed cursor flickering over the screen by hiding it during rendering
- Added `--ellipsis` option. You can take advantage of it to make fzf
effectively search non-visible parts of the item.
```sh
# Search against hidden line numbers on the far right
nl /usr/share/dict/words |
awk '{printf "%s%1000s\n", $2, $1}' |
fzf --nth=-1 --no-hscroll --ellipsis='' |
awk '{print $2}'
```
- Added `rebind` action for restoring bindings after `unbind`
- Bug fixes and improvements
0.29.0
------
- Added `change-preview(...)` action to change the `--preview` command
- cf. `preview(...)` is a one-off action that doesn't change the default
preview command
- Added `change-preview-window(...)` action
- You can rotate through the different options separated by `|`
```sh
fzf --preview 'cat {}' --preview-window right:40% \
--bind 'ctrl-/:change-preview-window(right,70%|down,40%,border-top|hidden|)'
```
- Fixed rendering of the prompt line when overflow occurs with `--info=inline`
0.28.0 0.28.0
------ ------
- Added `--header-first` option to print header before the prompt line - Added `--header-first` option to print header before the prompt line

View File

@@ -1,4 +1,4 @@
FROM archlinux/base:latest FROM archlinux
RUN pacman -Sy && pacman --noconfirm -S awk git tmux zsh fish ruby procps go make gcc RUN pacman -Sy && pacman --noconfirm -S awk git tmux zsh fish ruby procps go make gcc
RUN gem install --no-document -v 5.14.2 minitest RUN gem install --no-document -v 5.14.2 minitest
RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc

View File

@@ -35,6 +35,7 @@ BINARYARM7 := fzf-$(GOOS)_arm7
BINARYARM8 := fzf-$(GOOS)_arm8 BINARYARM8 := fzf-$(GOOS)_arm8
BINARYPPC64LE := fzf-$(GOOS)_ppc64le BINARYPPC64LE := fzf-$(GOOS)_ppc64le
BINARYRISCV64 := fzf-$(GOOS)_riscv64 BINARYRISCV64 := fzf-$(GOOS)_riscv64
BINARYLOONG64 := fzf-$(GOOS)_loong64
# https://en.wikipedia.org/wiki/Uname # https://en.wikipedia.org/wiki/Uname
UNAME_M := $(shell uname -m) UNAME_M := $(shell uname -m)
@@ -62,6 +63,8 @@ else ifeq ($(UNAME_M),ppc64le)
BINARY := $(BINARYPPC64LE) BINARY := $(BINARYPPC64LE)
else ifeq ($(UNAME_M),riscv64) else ifeq ($(UNAME_M),riscv64)
BINARY := $(BINARYRISCV64) BINARY := $(BINARYRISCV64)
else ifeq ($(UNAME_M),loongarch64)
BINARY := $(BINARYLOONG64)
else else
$(error Build on $(UNAME_M) is not supported, yet.) $(error Build on $(UNAME_M) is not supported, yet.)
endif endif
@@ -148,6 +151,9 @@ target/$(BINARYPPC64LE): $(SOURCES)
target/$(BINARYRISCV64): $(SOURCES) target/$(BINARYRISCV64): $(SOURCES)
GOARCH=riscv64 $(GO) build $(BUILD_FLAGS) -o $@ GOARCH=riscv64 $(GO) build $(BUILD_FLAGS) -o $@
target/$(BINARYLOONG64): $(SOURCES)
GOARCH=loong64 $(GO) build $(BUILD_FLAGS) -o $@
bin/fzf: target/$(BINARY) | bin bin/fzf: target/$(BINARY) | bin
cp -f target/$(BINARY) bin/fzf cp -f target/$(BINARY) bin/fzf

View File

@@ -86,7 +86,7 @@ stuff.
### Using Homebrew ### Using Homebrew
You can use [Homebrew](http://brew.sh/) (on macOS or Linux) You can use [Homebrew](https://brew.sh/) (on macOS or Linux)
to install fzf. to install fzf.
```sh ```sh
@@ -131,6 +131,8 @@ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
> >
> Refer to the package documentation for more information. (e.g. `apt-cache show fzf`) > Refer to the package documentation for more information. (e.g. `apt-cache show fzf`)
[![Packaging status](https://repology.org/badge/vertical-allrepos/fzf.svg)](https://repology.org/project/fzf/versions)
### Windows ### Windows
Pre-built binaries for Windows can be downloaded [here][bin]. fzf is also Pre-built binaries for Windows can be downloaded [here][bin]. fzf is also
@@ -380,12 +382,11 @@ cd ~/github/fzf**<TAB>
#### Process IDs #### Process IDs
Fuzzy completion for PIDs is provided for kill command. In this case, Fuzzy completion for PIDs is provided for kill command.
there is no trigger sequence; just press the tab key after the kill command.
```sh ```sh
# Can select multiple processes with <TAB> or <Shift-TAB> keys # Can select multiple processes with <TAB> or <Shift-TAB> keys
kill -9 <TAB> kill -9 **<TAB>
``` ```
#### Host names #### Host names
@@ -590,6 +591,9 @@ If ripgrep doesn't find any matches, it will exit with a non-zero exit status,
and fzf will warn you about it. To suppress the warning message, we added and fzf will warn you about it. To suppress the warning message, we added
`|| true` to the command, so that it always exits with 0. `|| true` to the command, so that it always exits with 0.
See ["Using fzf as interative Ripgrep launcher"](https://github.com/junegunn/fzf/blob/master/ADVANCED.md#using-fzf-as-interative-ripgrep-launcher)
for a fuller example with preview window options.
### Preview window ### Preview window
When the `--preview` option is set, fzf automatically starts an external process When the `--preview` option is set, fzf automatically starts an external process

16
go.mod
View File

@@ -2,16 +2,20 @@ module github.com/junegunn/fzf
require ( require (
github.com/gdamore/tcell v1.4.0 github.com/gdamore/tcell v1.4.0
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.14 github.com/mattn/go-isatty v0.0.14
github.com/mattn/go-runewidth v0.0.13 github.com/mattn/go-runewidth v0.0.13
github.com/mattn/go-shellwords v1.0.12 github.com/mattn/go-shellwords v1.0.12
github.com/rivo/uniseg v0.2.0 github.com/rivo/uniseg v0.2.0
github.com/saracen/walker v0.1.2 github.com/saracen/walker v0.1.2
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6
golang.org/x/text v0.3.6 // indirect
) )
go 1.13 require (
github.com/gdamore/encoding v1.0.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/text v0.3.7 // indirect
)
go 1.17

13
go.sum
View File

@@ -20,12 +20,13 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 h1:EC6+IGYTjPpRfv9a2b/6Puw0W+hLtAhkV1tPsXhutqs= golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12 h1:QyVthZKMsyaQwBTJE04jdNN0Pp5Fn9Qga0mrgxyERQM=
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
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.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

View File

@@ -2,7 +2,7 @@
set -u set -u
version=0.28.0 version=0.31.0
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2
@@ -256,7 +256,7 @@ for shell in $shells; do
# Setup fzf # Setup fzf
# --------- # ---------
if [[ ! "\$PATH" == *$fzf_base_esc/bin* ]]; then if [[ ! "\$PATH" == *$fzf_base_esc/bin* ]]; then
export PATH="\${PATH:+\${PATH}:}$fzf_base/bin" PATH="\${PATH:+\${PATH}:}$fzf_base/bin"
fi fi
# Auto-completion # Auto-completion
@@ -280,11 +280,6 @@ EOF
[ $? -eq 0 ] && echo "OK" || echo "Failed" [ $? -eq 0 ] && echo "OK" || echo "Failed"
mkdir -p "${fish_dir}/functions" mkdir -p "${fish_dir}/functions"
if [ -e "${fish_dir}/functions/fzf.fish" ]; then
echo -n "Remove unnecessary ${fish_dir}/functions/fzf.fish ... "
rm -f "${fish_dir}/functions/fzf.fish" && echo "OK" || echo "Failed"
fi
fish_binding="${fish_dir}/functions/fzf_key_bindings.fish" fish_binding="${fish_dir}/functions/fzf_key_bindings.fish"
if [ $key_bindings -ne 0 ]; then if [ $key_bindings -ne 0 ]; then
echo -n "Symlink $fish_binding ... " echo -n "Symlink $fish_binding ... "

View File

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

View File

@@ -5,7 +5,7 @@ import (
"github.com/junegunn/fzf/src/protector" "github.com/junegunn/fzf/src/protector"
) )
var version string = "0.28" var version string = "0.31"
var revision string = "devel" var revision string = "devel"
func main() { func main() {

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 "Nov 2021" "fzf 0.28.0" "fzf-tmux - open fzf in tmux split pane" .TH fzf-tmux 1 "Jul 2022" "fzf 0.31.0" "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 "Nov 2021" "fzf 0.28.0" "fzf - a command-line fuzzy finder" .TH fzf 1 "Jul 2022" "fzf 0.31.0" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -302,6 +302,9 @@ lines that follow.
.TP .TP
.B "--header-first" .B "--header-first"
Print header before the prompt line Print header before the prompt line
.TP
.BI "--ellipsis=" "STR"
Ellipsis to show when line is truncated (default: '..')
.SS Display .SS Display
.TP .TP
.B "--ansi" .B "--ansi"
@@ -447,7 +450,7 @@ e.g.
\fB# Press CTRL-A to select 100K items and see the sum of all the numbers. \fB# Press CTRL-A to select 100K items and see the sum of all the numbers.
# This won't work properly without 'f' flag due to ARG_MAX limit. # This won't work properly without 'f' flag due to ARG_MAX limit.
seq 100000 | fzf --multi --bind ctrl-a:select-all \\ seq 100000 | fzf --multi --bind ctrl-a:select-all \\
--preview "awk '{sum+=\$1} END {print sum}' {+f}"\fR --preview "awk '{sum+=\\$1} END {print sum}' {+f}"\fR
Note that you can escape a placeholder pattern by prepending a backslash. Note that you can escape a placeholder pattern by prepending a backslash.
@@ -467,7 +470,7 @@ e.g.
done'\fR done'\fR
.RE .RE
.TP .TP
.BI "--preview-window=" "[POSITION][,SIZE[%]][,border-BORDER_OPT][,[no]wrap][,[no]follow][,[no]cycle][,[no]hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default]" .BI "--preview-window=" "[POSITION][,SIZE[%]][,border-BORDER_OPT][,[no]wrap][,[no]follow][,[no]cycle][,[no]hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]"
.RS .RS
.B POSITION: (default: right) .B POSITION: (default: right)
@@ -485,9 +488,9 @@ default until \fBtoggle-preview\fR action is triggered.
execute the command in the background. execute the command in the background.
* Long lines are truncated by default. Line wrap can be enabled with * Long lines are truncated by default. Line wrap can be enabled with
\fB:wrap\fR flag. \fBwrap\fR flag.
* Preview window will automatically scroll to the bottom when \fB:follow\fR * Preview window will automatically scroll to the bottom when \fBfollow\fR
flag is set, similarly to how \fBtail -f\fR works. flag is set, similarly to how \fBtail -f\fR works.
.RS .RS
@@ -499,7 +502,7 @@ e.g.
done'\fR done'\fR
.RE .RE
* Cyclic scrolling is enabled with \fB:cycle\fR flag. * Cyclic scrolling is enabled with \fBcycle\fR flag.
* To change the style of the border of the preview window, specify one of * To change the style of the border of the preview window, specify one of
the options for \fB--border\fR with \fBborder-\fR prefix. the options for \fB--border\fR with \fBborder-\fR prefix.
@@ -549,6 +552,15 @@ e.g.
fzf --preview 'bat --style=full --color=always {}' --preview-window '~3'\fR fzf --preview 'bat --style=full --color=always {}' --preview-window '~3'\fR
.RE .RE
* You can specify an alternative set of options that are used only when the size
of the preview window is below a certain threshold. Note that only one
alternative layout is allowed.
.RS
e.g.
\fBfzf --preview 'cat {}' --preview-window 'right,border-left,<30(up,30%,border-bottom)'\fR
.RE
.SS Scripting .SS Scripting
.TP .TP
.BI "-q, --query=" "STR" .BI "-q, --query=" "STR"
@@ -820,6 +832,8 @@ A key or an event can be bound to one or more of the following actions.
\fBbackward-word\fR \fIalt-b shift-left\fR \fBbackward-word\fR \fIalt-b shift-left\fR
\fBbeginning-of-line\fR \fIctrl-a home\fR \fBbeginning-of-line\fR \fIctrl-a home\fR
\fBcancel\fR (clear query string if not empty, abort fzf otherwise) \fBcancel\fR (clear query string if not empty, abort fzf otherwise)
\fBchange-preview(...)\fR (change \fB--preview\fR option)
\fBchange-preview-window(...)\fR (change \fB--preview-window\fR option; rotate through the multiple option sets separated by '|')
\fBchange-prompt(...)\fR (change prompt to the given string) \fBchange-prompt(...)\fR (change prompt to the given string)
\fBclear-screen\fR \fIctrl-l\fR \fBclear-screen\fR \fIctrl-l\fR
\fBclear-selection\fR (clear multi-selection) \fBclear-selection\fR (clear multi-selection)
@@ -862,6 +876,7 @@ A key or an event can be bound to one or more of the following actions.
\fBprint-query\fR (print query and exit) \fBprint-query\fR (print query and exit)
\fBput\fR (put the character to the prompt) \fBput\fR (put the character to the prompt)
\fBrefresh-preview\fR \fBrefresh-preview\fR
\fBrebind(...)\fR (rebind bindings after \fBunbind\fR)
\fBreload(...)\fR (see below for the details) \fBreload(...)\fR (see below for the details)
\fBreplace-query\fR (replace query string with the current selection) \fBreplace-query\fR (replace query string with the current selection)
\fBselect\fR \fBselect\fR
@@ -970,7 +985,6 @@ commands in addition to the default preview command given by \fB--preview\fR
option. option.
e.g. e.g.
# Default preview command with an extra preview binding # Default preview command with an extra preview binding
fzf --preview 'file {}' --bind '?:preview:cat {}' fzf --preview 'file {}' --bind '?:preview:cat {}'
@@ -981,6 +995,22 @@ e.g.
# Preview window hidden by default, it appears when you first hit '?' # Preview window hidden by default, it appears when you first hit '?'
fzf --bind '?:preview:cat {}' --preview-window hidden fzf --bind '?:preview:cat {}' --preview-window hidden
.SS CHANGE PREVIEW WINDOW ATTRIBUTES
\fBchange-preview-window\fR action can be used to change the properties of the
preview window. Unlike the \fB--preview-window\fR option, you can specify
multiple sets of options separated by '|' characters.
e.g.
# Rotate through the options using CTRL-/
fzf --preview 'cat {}' --bind 'ctrl-/:change-preview-window(right,70%|down,40%,border-horizontal|hidden|right)'
# The default properties given by `--preview-window` are inherited, so an empty string in the list is interpreted as the default
fzf --preview 'cat {}' --preview-window 'right,40%,border-left' --bind 'ctrl-/:change-preview-window(70%|down,border-top|hidden|)'
# This is equivalent to toggle-preview action
fzf --preview 'cat {}' --bind 'ctrl-/:change-preview-window(hidden|)'
.SH AUTHOR .SH AUTHOR
Junegunn Choi (\fIjunegunn.c@gmail.com\fR) Junegunn Choi (\fIjunegunn.c@gmail.com\fR)

View File

@@ -96,7 +96,12 @@ function! fzf#shellescape(arg, ...)
if shell =~# 'cmd.exe$' if shell =~# 'cmd.exe$'
return s:shellesc_cmd(a:arg) return s:shellesc_cmd(a:arg)
endif endif
try
let [shell, &shell] = [&shell, shell]
return s:fzf_call('shellescape', a:arg) return s:fzf_call('shellescape', a:arg)
finally
let [shell, &shell] = [&shell, shell]
endtry
endfunction endfunction
function! s:fzf_getcwd() function! s:fzf_getcwd()
@@ -159,7 +164,7 @@ function s:get_version(bin)
if has_key(s:versions, a:bin) if has_key(s:versions, a:bin)
return s:versions[a:bin] return s:versions[a:bin]
end end
let command = a:bin . ' --version --no-height' let command = fzf#shellescape(a:bin) . ' --version --no-height'
let output = systemlist(command) let output = systemlist(command)
if v:shell_error || empty(output) if v:shell_error || empty(output)
return '' return ''
@@ -444,6 +449,12 @@ function! s:use_sh()
return [shell, shellslash, shellcmdflag, shellxquote] return [shell, shellslash, shellcmdflag, shellxquote]
endfunction endfunction
function! s:writefile(...)
if call('writefile', a:000) == -1
throw 'Failed to write temporary file. Check if you can write to the path tempname() returns.'
endif
endfunction
function! fzf#run(...) abort function! fzf#run(...) abort
try try
let [shell, shellslash, shellcmdflag, shellxquote] = s:use_sh() let [shell, shellslash, shellcmdflag, shellxquote] = s:use_sh()
@@ -471,7 +482,7 @@ try
let source_command = source let source_command = source
elseif type == 3 elseif type == 3
let temps.input = s:fzf_tempname() let temps.input = s:fzf_tempname()
call writefile(source, temps.input) call s:writefile(source, temps.input)
let source_command = (s:is_win ? 'type ' : 'cat ').fzf#shellescape(temps.input) let source_command = (s:is_win ? 'type ' : 'cat ').fzf#shellescape(temps.input)
else else
throw 'Invalid source type' throw 'Invalid source type'
@@ -515,7 +526,7 @@ try
call s:callback(dict, lines) call s:callback(dict, lines)
return lines return lines
finally finally
if len(source_command) if exists('source_command') && len(source_command)
if len(prev_default_command) if len(prev_default_command)
let $FZF_DEFAULT_COMMAND = prev_default_command let $FZF_DEFAULT_COMMAND = prev_default_command
else else
@@ -660,7 +671,7 @@ function! s:execute(dict, command, use_height, temps) abort
endif endif
if s:is_win if s:is_win
let batchfile = s:fzf_tempname().'.bat' let batchfile = s:fzf_tempname().'.bat'
call writefile(s:wrap_cmds(command), batchfile) call s:writefile(s:wrap_cmds(command), batchfile)
let command = batchfile let command = batchfile
let a:temps.batchfile = batchfile let a:temps.batchfile = batchfile
if has('nvim') if has('nvim')
@@ -678,7 +689,7 @@ function! s:execute(dict, command, use_height, temps) abort
endif endif
elseif has('win32unix') && $TERM !=# 'cygwin' elseif has('win32unix') && $TERM !=# 'cygwin'
let shellscript = s:fzf_tempname() let shellscript = s:fzf_tempname()
call writefile([command], shellscript) call s:writefile([command], shellscript)
let command = 'cmd.exe /C '.fzf#shellescape('set "TERM=" & start /WAIT sh -c '.shellscript) let command = 'cmd.exe /C '.fzf#shellescape('set "TERM=" & start /WAIT sh -c '.shellscript)
let a:temps.shellscript = shellscript let a:temps.shellscript = shellscript
endif endif
@@ -877,7 +888,7 @@ function! s:execute_term(dict, command, temps) abort
call s:pushd(a:dict) call s:pushd(a:dict)
if s:is_win if s:is_win
let fzf.temps.batchfile = s:fzf_tempname().'.bat' let fzf.temps.batchfile = s:fzf_tempname().'.bat'
call writefile(s:wrap_cmds(a:command), fzf.temps.batchfile) call s:writefile(s:wrap_cmds(a:command), fzf.temps.batchfile)
let command = fzf.temps.batchfile let command = fzf.temps.batchfile
else else
let command = a:command let command = a:command

View File

@@ -260,14 +260,6 @@ _fzf_dir_completion() {
} }
_fzf_complete_kill() { _fzf_complete_kill() {
local trigger=${FZF_COMPLETION_TRIGGER-'**'}
local cur="${COMP_WORDS[COMP_CWORD]}"
if [[ -z "$cur" ]]; then
COMP_WORDS[$COMP_CWORD]=$trigger
elif [[ "$cur" != *"$trigger" ]]; then
return 1
fi
_fzf_proc_completion "$@" _fzf_proc_completion "$@"
} }
@@ -304,6 +296,10 @@ _fzf_alias_completion() {
# fzf options # fzf options
complete -o default -F _fzf_opts_completion fzf complete -o default -F _fzf_opts_completion fzf
# fzf-tmux is a thin fzf wrapper that has only a few more options than fzf
# itself. As a quick improvement we take fzf's completion. Adding the few extra
# fzf-tmux specific options (like `-w WIDTH`) are left as a future patch.
complete -o default -F _fzf_opts_completion fzf-tmux
d_cmds="${FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}" d_cmds="${FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}"
a_cmds=" a_cmds="
@@ -348,9 +344,6 @@ for cmd in $d_cmds; do
__fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o dirnames" __fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o dirnames"
done done
# Kill completion (supports empty completion trigger)
complete -F _fzf_complete_kill -o default -o bashdefault kill
unset cmd d_cmds a_cmds unset cmd d_cmds a_cmds
_fzf_setup_completion() { _fzf_setup_completion() {
@@ -373,9 +366,10 @@ _fzf_setup_completion() {
done done
} }
# Environment variables / Aliases / Hosts # Environment variables / Aliases / Hosts / Process
_fzf_setup_completion 'var' export unset _fzf_setup_completion 'var' export unset
_fzf_setup_completion 'alias' unalias _fzf_setup_completion 'alias' unalias
_fzf_setup_completion 'host' ssh telnet _fzf_setup_completion 'host' ssh telnet
_fzf_setup_completion 'proc' kill
fi fi

View File

@@ -285,12 +285,6 @@ fzf-completion() {
lbuf=$LBUFFER lbuf=$LBUFFER
tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))} tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))}
# Kill completion (do not require trigger sequence)
if [ "$cmd" = kill -a ${LBUFFER[-1]} = ' ' ]; then
tail=$trigger
tokens+=$trigger
lbuf="$lbuf$trigger"
fi
# Trigger sequence given # Trigger sequence given
if [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then if [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then

View File

@@ -14,14 +14,17 @@
# Key bindings # Key bindings
# ------------ # ------------
__fzf_select__() { __fzf_select__() {
local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ local cmd opts
cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
-o -type f -print \ -o -type f -print \
-o -type d -print \ -o -type d -print \
-o -type l -print 2> /dev/null | cut -b3-"}" -o -type l -print 2> /dev/null | cut -b3-"}"
eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" $(__fzfcmd) -m "$@" | while read -r item; do opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS -m"
printf '%q ' "$item" eval "$cmd" |
FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) "$@" |
while read -r item; do
printf '%q ' "$item" # escape special chars
done done
echo
} }
if [[ $- =~ i ]]; then if [[ $- =~ i ]]; then
@@ -32,24 +35,27 @@ __fzfcmd() {
} }
fzf-file-widget() { fzf-file-widget() {
local selected="$(__fzf_select__)" local selected="$(__fzf_select__ "$@")"
READLINE_LINE="${READLINE_LINE:0:$READLINE_POINT}$selected${READLINE_LINE:$READLINE_POINT}" READLINE_LINE="${READLINE_LINE:0:$READLINE_POINT}$selected${READLINE_LINE:$READLINE_POINT}"
READLINE_POINT=$(( READLINE_POINT + ${#selected} )) READLINE_POINT=$(( READLINE_POINT + ${#selected} ))
} }
__fzf_cd__() { __fzf_cd__() {
local cmd dir local cmd opts dir
cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
-o -type d -print 2> /dev/null | cut -b3-"}" -o -type d -print 2> /dev/null | cut -b3-"}"
dir=$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m) && printf 'cd %q' "$dir" opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS +m"
dir=$(eval "$cmd" | FZF_DEFAULT_OPTS="$opts" $(__fzfcmd)) && printf 'builtin cd -- %q' "$dir"
} }
__fzf_history__() { __fzf_history__() {
local output local output opts script
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m --read0"
script='BEGIN { getc; $/ = "\n\t"; $HISTCOUNT = $ENV{last_hist} + 1 } s/^[ *]//; print $HISTCOUNT - $. . "\t$_" if !$seen{$_}++'
output=$( output=$(
builtin fc -lnr -2147483648 | builtin fc -lnr -2147483648 |
last_hist=$(HISTTIMEFORMAT='' builtin history 1) perl -n -l0 -e 'BEGIN { getc; $/ = "\n\t"; $HISTCMD = $ENV{last_hist} + 1 } s/^[ *]//; print $HISTCMD - $. . "\t$_" if !$seen{$_}++' | last_hist=$(HISTTIMEFORMAT='' builtin history 1) perl -n -l0 -e "$script" |
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort,ctrl-z:ignore $FZF_CTRL_R_OPTS +m --read0" $(__fzfcmd) --query "$READLINE_LINE" FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) --query "$READLINE_LINE"
) || return ) || return
READLINE_LINE=${output#*$'\t'} READLINE_LINE=${output#*$'\t'}
if [[ -z "$READLINE_POINT" ]]; then if [[ -z "$READLINE_POINT" ]]; then

View File

@@ -87,7 +87,7 @@ function fzf_key_bindings
eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)' +m --query "'$fzf_query'"' | read -l result eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)' +m --query "'$fzf_query'"' | read -l result
if [ -n "$result" ] if [ -n "$result" ]
cd $result builtin cd -- $result
# Remove last token from commandline. # Remove last token from commandline.
commandline -t "" commandline -t ""

View File

@@ -66,7 +66,9 @@ fzf-file-widget() {
return $ret return $ret
} }
zle -N fzf-file-widget zle -N fzf-file-widget
bindkey '^T' fzf-file-widget bindkey -M emacs '^T' fzf-file-widget
bindkey -M vicmd '^T' fzf-file-widget
bindkey -M viins '^T' fzf-file-widget
# ALT-C - cd into the selected directory # ALT-C - cd into the selected directory
fzf-cd-widget() { fzf-cd-widget() {
@@ -79,7 +81,7 @@ fzf-cd-widget() {
return 0 return 0
fi fi
zle push-line # Clear buffer. Auto-restored on next prompt. zle push-line # Clear buffer. Auto-restored on next prompt.
BUFFER="cd ${(q)dir}" BUFFER="builtin cd -- ${(q)dir}"
zle accept-line zle accept-line
local ret=$? local ret=$?
unset dir # ensure this doesn't end up appearing in prompt expansion unset dir # ensure this doesn't end up appearing in prompt expansion
@@ -87,13 +89,15 @@ fzf-cd-widget() {
return $ret return $ret
} }
zle -N fzf-cd-widget zle -N fzf-cd-widget
bindkey '\ec' fzf-cd-widget bindkey -M emacs '\ec' fzf-cd-widget
bindkey -M vicmd '\ec' fzf-cd-widget
bindkey -M viins '\ec' fzf-cd-widget
# CTRL-R - Paste the selected command from history into the command line # CTRL-R - Paste the selected command from history into the command line
fzf-history-widget() { fzf-history-widget() {
local selected num local selected num
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null
selected=( $(fc -rl 1 | perl -ne 'print if !$seen{(/^\s*[0-9]+\**\s+(.*)/, $1)}++' | selected=( $(fc -rl 1 | awk '{ cmd=$0; sub(/^\s*[0-9]+\**\s+/, "", cmd); if (!seen[cmd]++) print $0 }' |
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort,ctrl-z:ignore $FZF_CTRL_R_OPTS --query=${(qqq)LBUFFER} +m" $(__fzfcmd)) ) FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort,ctrl-z:ignore $FZF_CTRL_R_OPTS --query=${(qqq)LBUFFER} +m" $(__fzfcmd)) )
local ret=$? local ret=$?
if [ -n "$selected" ]; then if [ -n "$selected" ]; then
@@ -106,7 +110,9 @@ fzf-history-widget() {
return $ret return $ret
} }
zle -N fzf-history-widget zle -N fzf-history-widget
bindkey '^R' fzf-history-widget bindkey -M emacs '^R' fzf-history-widget
bindkey -M vicmd '^R' fzf-history-widget
bindkey -M viins '^R' fzf-history-widget
} always { } always {
eval $__fzf_key_bindings_options eval $__fzf_key_bindings_options

View File

@@ -242,9 +242,11 @@ func Run(opts *Options, version string, revision string) {
for { for {
delay := true delay := true
ticks++ ticks++
input := func() []rune { input := func(reloaded bool) []rune {
paused, input := terminal.Input() paused, input := terminal.Input()
if !paused { if reloaded && paused {
query = []rune{}
} else if !paused {
query = input query = input
} }
return query return query
@@ -274,7 +276,8 @@ func Run(opts *Options, version string, revision string) {
opts.Sync = false opts.Sync = false
terminal.UpdateList(PassMerger(&snapshot, opts.Tac), false) terminal.UpdateList(PassMerger(&snapshot, opts.Tac), false)
} }
matcher.Reset(snapshot, input(), false, !reading, sort, clearCache()) reset := clearCache()
matcher.Reset(snapshot, input(reset), false, !reading, sort, reset)
case EvtSearchNew: case EvtSearchNew:
var command *string var command *string
@@ -293,7 +296,8 @@ func Run(opts *Options, version string, revision string) {
break break
} }
snapshot, _ := chunkList.Snapshot() snapshot, _ := chunkList.Snapshot()
matcher.Reset(snapshot, input(), true, !reading, sort, clearCache()) reset := clearCache()
matcher.Reset(snapshot, input(reset), true, !reading, sort, reset)
delay = false delay = false
case EvtSearchProgress: case EvtSearchProgress:

View File

@@ -70,6 +70,7 @@ const usage = `usage: fzf [options]
--header=STR String to print as header --header=STR String to print as header
--header-lines=N The first N lines of the input are treated as header --header-lines=N The first N lines of the input are treated as header
--header-first Print header before the prompt line --header-first Print header before the prompt line
--ellipsis=STR Ellipsis to show when line is truncated (default: '..')
Display Display
--ansi Enable processing of ANSI color codes --ansi Enable processing of ANSI color codes
@@ -88,7 +89,7 @@ const usage = `usage: fzf [options]
[,[no]wrap][,[no]cycle][,[no]follow][,[no]hidden] [,[no]wrap][,[no]cycle][,[no]follow][,[no]hidden]
[,border-BORDER_OPT] [,border-BORDER_OPT]
[,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES] [,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES]
[,default] [,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]
Scripting Scripting
-q, --query=STR Start the finder with the given query -q, --query=STR Start the finder with the given query
@@ -174,6 +175,18 @@ type previewOpts struct {
follow bool follow bool
border tui.BorderShape border tui.BorderShape
headerLines int headerLines int
threshold int
alternative *previewOpts
}
func (a previewOpts) sameLayout(b previewOpts) bool {
return a.size == b.size && a.position == b.position && a.border == b.border && a.hidden == b.hidden && a.threshold == b.threshold &&
(a.alternative != nil && b.alternative != nil && a.alternative.sameLayout(*b.alternative) ||
a.alternative == nil && b.alternative == nil)
}
func (a previewOpts) sameContentLayout(b previewOpts) bool {
return a.wrap == b.wrap && a.headerLines == b.headerLines
} }
// Options stores the values of command-line options // Options stores the values of command-line options
@@ -216,7 +229,7 @@ type Options struct {
Filter *string Filter *string
ToggleSort bool ToggleSort bool
Expect map[tui.Event]string Expect map[tui.Event]string
Keymap map[tui.Event][]action Keymap map[tui.Event][]*action
Preview previewOpts Preview previewOpts
PrintQuery bool PrintQuery bool
ReadZero bool ReadZero bool
@@ -227,6 +240,7 @@ type Options struct {
Header []string Header []string
HeaderLines int HeaderLines int
HeaderFirst bool HeaderFirst bool
Ellipsis string
Margin [4]sizeSpec Margin [4]sizeSpec
Padding [4]sizeSpec Padding [4]sizeSpec
BorderShape tui.BorderShape BorderShape tui.BorderShape
@@ -237,7 +251,7 @@ type Options struct {
} }
func defaultPreviewOpts(command string) previewOpts { func defaultPreviewOpts(command string) previewOpts {
return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, tui.BorderRounded, 0} return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, tui.BorderRounded, 0, 0, nil}
} }
func defaultOptions() *Options { func defaultOptions() *Options {
@@ -279,7 +293,7 @@ func defaultOptions() *Options {
Filter: nil, Filter: nil,
ToggleSort: false, ToggleSort: false,
Expect: make(map[tui.Event]string), Expect: make(map[tui.Event]string),
Keymap: make(map[tui.Event][]action), Keymap: make(map[tui.Event][]*action),
Preview: defaultPreviewOpts(""), Preview: defaultPreviewOpts(""),
PrintQuery: false, PrintQuery: false,
ReadZero: false, ReadZero: false,
@@ -290,6 +304,7 @@ func defaultOptions() *Options {
Header: make([]string, 0), Header: make([]string, 0),
HeaderLines: 0, HeaderLines: 0,
HeaderFirst: false, HeaderFirst: false,
Ellipsis: "..",
Margin: defaultMargin(), Margin: defaultMargin(),
Padding: defaultMargin(), Padding: defaultMargin(),
Unicode: true, Unicode: true,
@@ -787,10 +802,10 @@ func init() {
// Backreferences are not supported. // Backreferences are not supported.
// "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|') // "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
executeRegexp = regexp.MustCompile( executeRegexp = regexp.MustCompile(
`(?si)[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|unbind):.+|[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|unbind)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`) `(?si)[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|change-preview-window|change-preview|(?:re|un)bind):.+|[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|change-preview-window|change-preview|(?:re|un)bind)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`)
} }
func parseKeymap(keymap map[tui.Event][]action, str string) { func parseKeymap(keymap map[tui.Event][]*action, str string) {
masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string { masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string {
symbol := ":" symbol := ":"
if strings.HasPrefix(src, "+") { if strings.HasPrefix(src, "+") {
@@ -799,10 +814,16 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
prefix := symbol + "execute" prefix := symbol + "execute"
if strings.HasPrefix(src[1:], "reload") { if strings.HasPrefix(src[1:], "reload") {
prefix = symbol + "reload" prefix = symbol + "reload"
} else if strings.HasPrefix(src[1:], "change-preview-window") {
prefix = symbol + "change-preview-window"
} else if strings.HasPrefix(src[1:], "change-preview") {
prefix = symbol + "change-preview"
} else if strings.HasPrefix(src[1:], "preview") { } else if strings.HasPrefix(src[1:], "preview") {
prefix = symbol + "preview" prefix = symbol + "preview"
} else if strings.HasPrefix(src[1:], "unbind") { } else if strings.HasPrefix(src[1:], "unbind") {
prefix = symbol + "unbind" prefix = symbol + "unbind"
} else if strings.HasPrefix(src[1:], "rebind") {
prefix = symbol + "rebind"
} else if strings.HasPrefix(src[1:], "change-prompt") { } else if strings.HasPrefix(src[1:], "change-prompt") {
prefix = symbol + "change-prompt" prefix = symbol + "change-prompt"
} else if src[len(prefix)] == '-' { } else if src[len(prefix)] == '-' {
@@ -842,7 +863,7 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
idx2 := len(pair[0]) + 1 idx2 := len(pair[0]) + 1
specs := strings.Split(pair[1], "+") specs := strings.Split(pair[1], "+")
actions := make([]action, 0, len(specs)) actions := make([]*action, 0, len(specs))
appendAction := func(types ...actionType) { appendAction := func(types ...actionType) {
actions = append(actions, toActions(types...)...) actions = append(actions, toActions(types...)...)
} }
@@ -1002,10 +1023,16 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
offset = len("reload") offset = len("reload")
case actPreview: case actPreview:
offset = len("preview") offset = len("preview")
case actChangePreviewWindow:
offset = len("change-preview-window")
case actChangePreview:
offset = len("change-preview")
case actChangePrompt: case actChangePrompt:
offset = len("change-prompt") offset = len("change-prompt")
case actUnbind: case actUnbind:
offset = len("unbind") offset = len("unbind")
case actRebind:
offset = len("rebind")
case actExecuteSilent: case actExecuteSilent:
offset = len("execute-silent") offset = len("execute-silent")
case actExecuteMulti: case actExecuteMulti:
@@ -1017,17 +1044,22 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
if spec[offset] == ':' { if spec[offset] == ':' {
if specIndex == len(specs)-1 { if specIndex == len(specs)-1 {
actionArg = spec[offset+1:] actionArg = spec[offset+1:]
actions = append(actions, action{t: t, a: actionArg}) actions = append(actions, &action{t: t, a: actionArg})
} else { } else {
prevSpec = spec + "+" prevSpec = spec + "+"
continue continue
} }
} else { } else {
actionArg = spec[offset+1 : len(spec)-1] actionArg = spec[offset+1 : len(spec)-1]
actions = append(actions, action{t: t, a: actionArg}) actions = append(actions, &action{t: t, a: actionArg})
}
if t == actUnbind || t == actRebind {
parseKeyChords(actionArg, spec[0:offset]+" target required")
} else if t == actChangePreviewWindow {
opts := previewOpts{}
for _, arg := range strings.Split(actionArg, "|") {
parsePreviewWindow(&opts, arg)
} }
if t == actUnbind {
parseKeyChords(actionArg, "unbind target required")
} }
} }
} }
@@ -1051,8 +1083,14 @@ func isExecuteAction(str string) actionType {
return actReload return actReload
case "unbind": case "unbind":
return actUnbind return actUnbind
case "rebind":
return actRebind
case "preview": case "preview":
return actPreview return actPreview
case "change-preview-window":
return actChangePreviewWindow
case "change-preview":
return actChangePreview
case "change-prompt": case "change-prompt":
return actChangePrompt return actChangePrompt
case "execute": case "execute":
@@ -1065,7 +1103,7 @@ func isExecuteAction(str string) actionType {
return actIgnore return actIgnore
} }
func parseToggleSort(keymap map[tui.Event][]action, str string) { func parseToggleSort(keymap map[tui.Event][]*action, str string) {
keys := parseKeyChords(str, "key name required") keys := parseKeyChords(str, "key name required")
if len(keys) != 1 { if len(keys) != 1 {
errorExit("multiple keys specified") errorExit("multiple keys specified")
@@ -1135,12 +1173,19 @@ func parseInfoStyle(str string) infoStyle {
} }
func parsePreviewWindow(opts *previewOpts, input string) { func parsePreviewWindow(opts *previewOpts, input string) {
delimRegex := regexp.MustCompile("[:,]") // : for backward compatibility tokenRegex := regexp.MustCompile(`[:,]*(<([1-9][0-9]*)\(([^)<]+)\)|[^,:]+)`)
sizeRegex := regexp.MustCompile("^[0-9]+%?$") sizeRegex := regexp.MustCompile("^[0-9]+%?$")
offsetRegex := regexp.MustCompile(`^(\+{-?[0-9]+})?([+-][0-9]+)*(-?/[1-9][0-9]*)?$`) offsetRegex := regexp.MustCompile(`^(\+{-?[0-9]+})?([+-][0-9]+)*(-?/[1-9][0-9]*)?$`)
headerRegex := regexp.MustCompile("^~(0|[1-9][0-9]*)$") headerRegex := regexp.MustCompile("^~(0|[1-9][0-9]*)$")
tokens := delimRegex.Split(input, -1) tokens := tokenRegex.FindAllStringSubmatch(input, -1)
for _, token := range tokens { var alternative string
for _, match := range tokens {
if len(match[2]) > 0 {
opts.threshold = atoi(match[2])
alternative = match[3]
continue
}
token := match[1]
switch token { switch token {
case "": case "":
case "default": case "default":
@@ -1199,6 +1244,13 @@ func parsePreviewWindow(opts *previewOpts, input string) {
} }
} }
} }
if len(alternative) > 0 {
alternativeOpts := *opts
opts.alternative = &alternativeOpts
opts.alternative.hidden = false
opts.alternative.alternative = nil
parsePreviewWindow(opts.alternative, alternative)
}
} }
func parseMargin(opt string, margin string) [4]sizeSpec { func parseMargin(opt string, margin string) [4]sizeSpec {
@@ -1440,6 +1492,8 @@ func parseOptions(opts *Options, allArgs []string) {
opts.HeaderFirst = true opts.HeaderFirst = true
case "--no-header-first": case "--no-header-first":
opts.HeaderFirst = false opts.HeaderFirst = false
case "--ellipsis":
opts.Ellipsis = nextString(allArgs, &i, "ellipsis string required")
case "--preview": case "--preview":
opts.Preview.command = nextString(allArgs, &i, "preview command required") opts.Preview.command = nextString(allArgs, &i, "preview command required")
case "--no-preview": case "--no-preview":
@@ -1537,6 +1591,8 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Header = strLines(value) opts.Header = strLines(value)
} else if match, value := optString(arg, "--header-lines="); match { } else if match, value := optString(arg, "--header-lines="); match {
opts.HeaderLines = atoi(value) opts.HeaderLines = atoi(value)
} else if match, value := optString(arg, "--ellipsis="); match {
opts.Ellipsis = value
} else if match, value := optString(arg, "--preview="); match { } else if match, value := optString(arg, "--preview="); match {
opts.Preview.command = value opts.Preview.command = value
} else if match, value := optString(arg, "--preview-window="); match { } else if match, value := optString(arg, "--preview-window="); match {
@@ -1605,11 +1661,6 @@ func validateSign(sign string, signOptName string) error {
if sign == "" { if sign == "" {
return fmt.Errorf("%v cannot be empty", signOptName) return fmt.Errorf("%v cannot be empty", signOptName)
} }
for _, r := range sign {
if !unicode.IsGraphic(r) {
return fmt.Errorf("invalid character in %v", signOptName)
}
}
if runewidth.StringWidth(sign) > 2 { if runewidth.StringWidth(sign) > 2 {
return fmt.Errorf("%v display width should be up to 2", signOptName) return fmt.Errorf("%v display width should be up to 2", signOptName)
} }
@@ -1633,11 +1684,29 @@ func postProcessOptions(opts *Options) {
// Extend the default key map // Extend the default key map
keymap := defaultKeymap() keymap := defaultKeymap()
for key, actions := range opts.Keymap { for key, actions := range opts.Keymap {
var lastChangePreviewWindow *action
for _, act := range actions { for _, act := range actions {
if act.t == actToggleSort { switch act.t {
case actToggleSort:
// To display "+S"/"-S" on info line
opts.ToggleSort = true opts.ToggleSort = true
case actChangePreviewWindow:
lastChangePreviewWindow = act
} }
} }
// Re-organize actions so that we only keep the last change-preview-window
// and it comes first in the list.
// * change-preview-window(up,+10)+preview(sleep 3; cat {})+change-preview-window(up,+20)
// -> change-preview-window(up,+20)+preview(sleep 3; cat {})
if lastChangePreviewWindow != nil {
reordered := []*action{lastChangePreviewWindow}
for _, act := range actions {
if act.t != actChangePreviewWindow {
reordered = append(reordered, act)
}
}
actions = reordered
}
keymap[key] = actions keymap[key] = actions
} }
opts.Keymap = keymap opts.Keymap = keymap
@@ -1677,6 +1746,13 @@ func postProcessOptions(opts *Options) {
func ParseOptions() *Options { func ParseOptions() *Options {
opts := defaultOptions() opts := defaultOptions()
for _, arg := range os.Args[1:] {
if arg == "--version" {
opts.Version = true
return opts
}
}
// Options from Env var // Options from Env var
words, _ := shellwords.Parse(os.Getenv("FZF_DEFAULT_OPTS")) words, _ := shellwords.Parse(os.Getenv("FZF_DEFAULT_OPTS"))
if len(words) > 0 { if len(words) > 0 {

View File

@@ -65,6 +65,19 @@ func TestDelimiterRegexRegex(t *testing.T) {
} }
} }
func TestDelimiterRegexRegexCaret(t *testing.T) {
delim := delimiterRegexp(`(^\s*|\s+)`)
tokens := Tokenize("foo bar baz", delim)
if delim.str != nil ||
len(tokens) != 4 ||
tokens[0].text.ToString() != "" ||
tokens[1].text.ToString() != "foo " ||
tokens[2].text.ToString() != "bar " ||
tokens[3].text.ToString() != "baz" {
t.Errorf("%s %d", tokens, len(tokens))
}
}
func TestSplitNth(t *testing.T) { func TestSplitNth(t *testing.T) {
{ {
ranges := splitNth("..") ranges := splitNth("..")
@@ -440,8 +453,6 @@ func TestValidateSign(t *testing.T) {
{"😀", true}, {"😀", true},
{"", false}, {"", false},
{">>>", false}, {">>>", false},
{"\n", false},
{"\t", false},
} }
for _, testCase := range testCases { for _, testCase := range testCases {

View File

@@ -1,4 +1,4 @@
// +build !openbsd //go:build !openbsd
package protector package protector

View File

@@ -1,4 +1,4 @@
// +build openbsd //go:build openbsd
package protector package protector

View File

@@ -1,4 +1,4 @@
// +build !386,!amd64 //go:build !386 && !amd64
package fzf package fzf

View File

@@ -1,5 +1,3 @@
// +build !tcell
package fzf package fzf
import ( import (

View File

@@ -1,4 +1,4 @@
// +build 386 amd64 //go:build 386 || amd64
package fzf package fzf

View File

@@ -4,6 +4,7 @@ import (
"bufio" "bufio"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"math"
"os" "os"
"os/signal" "os/signal"
"regexp" "regexp"
@@ -49,7 +50,6 @@ var offsetComponentRegex *regexp.Regexp
var offsetTrimCharsRegex *regexp.Regexp var offsetTrimCharsRegex *regexp.Regexp
var activeTempFiles []string var activeTempFiles []string
const ellipsis string = ".."
const clearCode string = "\x1b[2J" const clearCode string = "\x1b[2J"
func init() { func init() {
@@ -135,7 +135,8 @@ type Terminal struct {
toggleSort bool toggleSort bool
delimiter Delimiter delimiter Delimiter
expect map[tui.Event]string expect map[tui.Event]string
keymap map[tui.Event][]action keymap map[tui.Event][]*action
keymapOrg map[tui.Event][]*action
pressed string pressed string
printQuery bool printQuery bool
history *History history *History
@@ -144,6 +145,7 @@ type Terminal struct {
headerLines int headerLines int
header []string header []string
header0 []string header0 []string
ellipsis string
ansi bool ansi bool
tabstop int tabstop int
margin [4]sizeSpec margin [4]sizeSpec
@@ -170,6 +172,7 @@ type Terminal struct {
selected map[int32]selectedItem selected map[int32]selectedItem
version int64 version int64
reqBox *util.EventBox reqBox *util.EventBox
initialPreviewOpts previewOpts
previewOpts previewOpts previewOpts previewOpts
previewer previewer previewer previewer
previewed previewed previewed previewed
@@ -216,6 +219,7 @@ const (
reqRefresh reqRefresh
reqReinit reqReinit
reqRedraw reqRedraw
reqFullRedraw
reqClose reqClose
reqPrintQuery reqPrintQuery
reqPreviewEnqueue reqPreviewEnqueue
@@ -286,6 +290,8 @@ const (
actTogglePreview actTogglePreview
actTogglePreviewWrap actTogglePreviewWrap
actPreview actPreview
actChangePreview
actChangePreviewWindow
actPreviewTop actPreviewTop
actPreviewBottom actPreviewBottom
actPreviewUp actPreviewUp
@@ -308,6 +314,7 @@ const (
actSelect actSelect
actDeselect actDeselect
actUnbind actUnbind
actRebind
) )
type placeholderFlags struct { type placeholderFlags struct {
@@ -326,6 +333,7 @@ type searchRequest struct {
type previewRequest struct { type previewRequest struct {
template string template string
pwindow tui.Window pwindow tui.Window
scrollOffset int
list []*Item list []*Item
} }
@@ -336,16 +344,16 @@ type previewResult struct {
spinner string spinner string
} }
func toActions(types ...actionType) []action { func toActions(types ...actionType) []*action {
actions := make([]action, len(types)) actions := make([]*action, len(types))
for idx, t := range types { for idx, t := range types {
actions[idx] = action{t: t, a: ""} actions[idx] = &action{t: t, a: ""}
} }
return actions return actions
} }
func defaultKeymap() map[tui.Event][]action { func defaultKeymap() map[tui.Event][]*action {
keymap := make(map[tui.Event][]action) keymap := make(map[tui.Event][]*action)
add := func(e tui.EventType, a actionType) { add := func(e tui.EventType, a actionType) {
keymap[e.AsEvent()] = toActions(a) keymap[e.AsEvent()] = toActions(a)
} }
@@ -416,7 +424,7 @@ func trimQuery(query string) []rune {
func hasPreviewAction(opts *Options) bool { func hasPreviewAction(opts *Options) bool {
for _, actions := range opts.Keymap { for _, actions := range opts.Keymap {
for _, action := range actions { for _, action := range actions {
if action.t == actPreview { if action.t == actPreview || action.t == actChangePreview {
return true return true
} }
} }
@@ -495,6 +503,10 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
wordRubout = fmt.Sprintf("%s[^%s]", sep, sep) wordRubout = fmt.Sprintf("%s[^%s]", sep, sep)
wordNext = fmt.Sprintf("[^%s]%s|(.$)", sep, sep) wordNext = fmt.Sprintf("[^%s]%s|(.$)", sep, sep)
} }
keymapCopy := make(map[tui.Event][]*action)
for key, action := range opts.Keymap {
keymapCopy[key] = action
}
t := Terminal{ t := Terminal{
initDelay: delay, initDelay: delay,
infoStyle: opts.InfoStyle, infoStyle: opts.InfoStyle,
@@ -520,6 +532,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
delimiter: opts.Delimiter, delimiter: opts.Delimiter,
expect: opts.Expect, expect: opts.Expect,
keymap: opts.Keymap, keymap: opts.Keymap,
keymapOrg: keymapCopy,
pressed: "", pressed: "",
printQuery: opts.PrintQuery, printQuery: opts.PrintQuery,
history: opts.History, history: opts.History,
@@ -535,6 +548,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
headerLines: opts.HeaderLines, headerLines: opts.HeaderLines,
header: header, header: header,
header0: header, header0: header,
ellipsis: opts.Ellipsis,
ansi: opts.Ansi, ansi: opts.Ansi,
tabstop: opts.Tabstop, tabstop: opts.Tabstop,
reading: true, reading: true,
@@ -547,6 +561,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
merger: EmptyMerger, merger: EmptyMerger,
selected: make(map[int32]selectedItem), selected: make(map[int32]selectedItem),
reqBox: util.NewEventBox(), reqBox: util.NewEventBox(),
initialPreviewOpts: opts.Preview,
previewOpts: opts.Preview, previewOpts: opts.Preview,
previewer: previewer{0, []string{}, 0, showPreviewWindow, false, true, false, ""}, previewer: previewer{0, []string{}, 0, showPreviewWindow, false, true, false, ""},
previewed: previewed{0, 0, 0, false}, previewed: previewed{0, 0, 0, false},
@@ -665,6 +680,7 @@ func (t *Terminal) UpdateList(merger *Merger, reset bool) {
t.merger = merger t.merger = merger
if reset { if reset {
t.selected = make(map[int32]selectedItem) t.selected = make(map[int32]selectedItem)
t.version++
} }
t.mutex.Unlock() t.mutex.Unlock()
t.reqBox.Set(reqInfo, nil) t.reqBox.Set(reqInfo, nil)
@@ -703,7 +719,7 @@ func (t *Terminal) sortSelected() []selectedItem {
} }
func (t *Terminal) displayWidth(runes []rune) int { func (t *Terminal) displayWidth(runes []rune) int {
width, _ := util.RunesWidth(runes, 0, t.tabstop, 0) width, _ := util.RunesWidth(runes, 0, t.tabstop, math.MaxInt32)
return width return width
} }
@@ -803,12 +819,15 @@ func (t *Terminal) resizeWindows() {
} }
if t.window != nil { if t.window != nil {
t.window.Close() t.window.Close()
t.window = nil
} }
if t.pborder != nil { if t.pborder != nil {
t.pborder.Close() t.pborder.Close()
t.pborder = nil
} }
if t.pwindow != nil { if t.pwindow != nil {
t.pwindow.Close() t.pwindow.Close()
t.pwindow = nil
} }
// Reset preview version so that full redraw occurs // Reset preview version so that full redraw occurs
t.previewed.version = 0 t.previewed.version = 0
@@ -853,19 +872,23 @@ func (t *Terminal) resizeWindows() {
width = screenWidth - marginInt[1] - marginInt[3] width = screenWidth - marginInt[1] - marginInt[3]
height = screenHeight - marginInt[0] - marginInt[2] height = screenHeight - marginInt[0] - marginInt[2]
// Set up preview window
noBorder := tui.MakeBorderStyle(tui.BorderNone, t.unicode) noBorder := tui.MakeBorderStyle(tui.BorderNone, t.unicode)
if previewVisible { if previewVisible {
var resizePreviewWindows func(previewOpts previewOpts)
resizePreviewWindows = func(previewOpts previewOpts) {
hasThreshold := previewOpts.threshold > 0 && previewOpts.alternative != nil
createPreviewWindow := func(y int, x int, w int, h int) { createPreviewWindow := func(y int, x int, w int, h int) {
pwidth := w pwidth := w
pheight := h pheight := h
var previewBorder tui.BorderStyle var previewBorder tui.BorderStyle
if t.previewOpts.border == tui.BorderNone { if previewOpts.border == tui.BorderNone {
previewBorder = tui.MakeTransparentBorder() previewBorder = tui.MakeTransparentBorder()
} else { } else {
previewBorder = tui.MakeBorderStyle(t.previewOpts.border, t.unicode) previewBorder = tui.MakeBorderStyle(previewOpts.border, t.unicode)
} }
t.pborder = t.tui.NewWindow(y, x, w, h, true, previewBorder) t.pborder = t.tui.NewWindow(y, x, w, h, true, previewBorder)
switch t.previewOpts.border { switch previewOpts.border {
case tui.BorderSharp, tui.BorderRounded: case tui.BorderSharp, tui.BorderRounded:
pwidth -= 4 pwidth -= 4
pheight -= 2 pheight -= 2
@@ -892,7 +915,7 @@ func (t *Terminal) resizeWindows() {
} }
verticalPad := 2 verticalPad := 2
minPreviewHeight := 3 minPreviewHeight := 3
switch t.previewOpts.border { switch previewOpts.border {
case tui.BorderNone, tui.BorderVertical, tui.BorderLeft, tui.BorderRight: case tui.BorderNone, tui.BorderVertical, tui.BorderLeft, tui.BorderRight:
verticalPad = 0 verticalPad = 0
minPreviewHeight = 1 minPreviewHeight = 1
@@ -900,29 +923,46 @@ func (t *Terminal) resizeWindows() {
verticalPad = 1 verticalPad = 1
minPreviewHeight = 2 minPreviewHeight = 2
} }
switch t.previewOpts.position { switch previewOpts.position {
case posUp: case posUp, posDown:
pheight := calculateSize(height, t.previewOpts.size, minHeight, minPreviewHeight, verticalPad) pheight := calculateSize(height, previewOpts.size, minHeight, minPreviewHeight, verticalPad)
if hasThreshold && pheight < previewOpts.threshold {
if !previewOpts.alternative.hidden {
resizePreviewWindows(*previewOpts.alternative)
}
return
}
if previewOpts.position == posUp {
t.window = t.tui.NewWindow( t.window = t.tui.NewWindow(
marginInt[0]+pheight, marginInt[3], width, height-pheight, false, noBorder) marginInt[0]+pheight, marginInt[3], width, height-pheight, false, noBorder)
createPreviewWindow(marginInt[0], marginInt[3], width, pheight) createPreviewWindow(marginInt[0], marginInt[3], width, pheight)
case posDown: } else {
pheight := calculateSize(height, t.previewOpts.size, minHeight, minPreviewHeight, verticalPad)
t.window = t.tui.NewWindow( t.window = t.tui.NewWindow(
marginInt[0], marginInt[3], width, height-pheight, false, noBorder) marginInt[0], marginInt[3], width, height-pheight, false, noBorder)
createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight) createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight)
case posLeft: }
pwidth := calculateSize(width, t.previewOpts.size, minWidth, 5, 4) case posLeft, posRight:
pwidth := calculateSize(width, previewOpts.size, minWidth, 5, 4)
if hasThreshold && pwidth < previewOpts.threshold {
if !previewOpts.alternative.hidden {
resizePreviewWindows(*previewOpts.alternative)
}
return
}
if previewOpts.position == posLeft {
t.window = t.tui.NewWindow( t.window = t.tui.NewWindow(
marginInt[0], marginInt[3]+pwidth, width-pwidth, height, false, noBorder) marginInt[0], marginInt[3]+pwidth, width-pwidth, height, false, noBorder)
createPreviewWindow(marginInt[0], marginInt[3], pwidth, height) createPreviewWindow(marginInt[0], marginInt[3], pwidth, height)
case posRight: } else {
pwidth := calculateSize(width, t.previewOpts.size, minWidth, 5, 4)
t.window = t.tui.NewWindow( t.window = t.tui.NewWindow(
marginInt[0], marginInt[3], width-pwidth, height, false, noBorder) marginInt[0], marginInt[3], width-pwidth, height, false, noBorder)
createPreviewWindow(marginInt[0], marginInt[3]+width-pwidth, pwidth, height) createPreviewWindow(marginInt[0], marginInt[3]+width-pwidth, pwidth, height)
} }
} else { }
}
resizePreviewWindows(t.previewOpts)
}
if t.window == nil {
t.window = t.tui.NewWindow( t.window = t.tui.NewWindow(
marginInt[0], marginInt[0],
marginInt[3], marginInt[3],
@@ -1191,7 +1231,7 @@ func (t *Terminal) printItem(result Result, line int, i int, current bool) {
func (t *Terminal) trimRight(runes []rune, width int) ([]rune, bool) { func (t *Terminal) trimRight(runes []rune, width int) ([]rune, bool) {
// We start from the beginning to handle tab characters // We start from the beginning to handle tab characters
width, overflowIdx := util.RunesWidth(runes, 0, t.tabstop, width) _, overflowIdx := util.RunesWidth(runes, 0, t.tabstop, width)
if overflowIdx >= 0 { if overflowIdx >= 0 {
return runes[:overflowIdx], true return runes[:overflowIdx], true
} }
@@ -1254,47 +1294,54 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
offsets := result.colorOffsets(charOffsets, t.theme, colBase, colMatch, current) offsets := result.colorOffsets(charOffsets, t.theme, colBase, colMatch, current)
maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1) maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1)
maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text)) ellipsis, ellipsisWidth := util.Truncate(t.ellipsis, maxWidth/2)
maxe = util.Constrain(maxe+util.Min(maxWidth/2-ellipsisWidth, t.hscrollOff), 0, len(text))
displayWidth := t.displayWidthWithLimit(text, 0, maxWidth) displayWidth := t.displayWidthWithLimit(text, 0, maxWidth)
if displayWidth > maxWidth { if displayWidth > maxWidth {
transformOffsets := func(diff int32) { transformOffsets := func(diff int32, rightTrim bool) {
for idx, offset := range offsets { for idx, offset := range offsets {
b, e := offset.offset[0], offset.offset[1] b, e := offset.offset[0], offset.offset[1]
b += 2 - diff el := int32(len(ellipsis))
e += 2 - diff b += el - diff
b = util.Max32(b, 2) e += el - diff
b = util.Max32(b, el)
if rightTrim {
e = util.Min32(e, int32(maxWidth-ellipsisWidth))
}
offsets[idx].offset[0] = b offsets[idx].offset[0] = b
offsets[idx].offset[1] = util.Max32(b, e) offsets[idx].offset[1] = util.Max32(b, e)
} }
} }
if t.hscroll { if t.hscroll {
if t.keepRight && pos == nil { if t.keepRight && pos == nil {
trimmed, diff := t.trimLeft(text, maxWidth-2) trimmed, diff := t.trimLeft(text, maxWidth-ellipsisWidth)
transformOffsets(diff) transformOffsets(diff, false)
text = append([]rune(ellipsis), trimmed...) text = append(ellipsis, trimmed...)
} else if !t.overflow(text[:maxe], maxWidth-2) { } else if !t.overflow(text[:maxe], maxWidth-ellipsisWidth) {
// Stri.. // Stri..
text, _ = t.trimRight(text, maxWidth-2) text, _ = t.trimRight(text, maxWidth-ellipsisWidth)
text = append(text, []rune(ellipsis)...) text = append(text, ellipsis...)
} else { } else {
// Stri.. // Stri..
if t.overflow(text[maxe:], 2) { rightTrim := false
text = append(text[:maxe], []rune(ellipsis)...) if t.overflow(text[maxe:], ellipsisWidth) {
text = append(text[:maxe], ellipsis...)
rightTrim = true
} }
// ..ri.. // ..ri..
var diff int32 var diff int32
text, diff = t.trimLeft(text, maxWidth-2) text, diff = t.trimLeft(text, maxWidth-ellipsisWidth)
// Transform offsets // Transform offsets
transformOffsets(diff) transformOffsets(diff, rightTrim)
text = append([]rune(ellipsis), text...) text = append(ellipsis, text...)
} }
} else { } else {
text, _ = t.trimRight(text, maxWidth-2) text, _ = t.trimRight(text, maxWidth-ellipsisWidth)
text = append(text, []rune(ellipsis)...) text = append(text, ellipsis...)
for idx, offset := range offsets { for idx, offset := range offsets {
offsets[idx].offset[0] = util.Min32(offset.offset[0], int32(maxWidth-2)) offsets[idx].offset[0] = util.Min32(offset.offset[0], int32(maxWidth-len(ellipsis)))
offsets[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth)) offsets[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth))
} }
} }
@@ -1394,6 +1441,7 @@ func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unc
line = strings.TrimSuffix(line, "\n") line = strings.TrimSuffix(line, "\n")
if lineNo >= height || t.pwindow.Y() == height-1 && t.pwindow.X() > 0 { if lineNo >= height || t.pwindow.Y() == height-1 && t.pwindow.X() > 0 {
t.previewed.filled = true t.previewed.filled = true
t.previewer.scrollable = true
break break
} else if lineNo >= 0 { } else if lineNo >= 0 {
var fillRet tui.FillReturn var fillRet tui.FillReturn
@@ -1642,9 +1690,14 @@ func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input str
template, t.ansi, t.delimiter, t.printsep, forcePlus, input, list) template, t.ansi, t.delimiter, t.printsep, forcePlus, input, list)
} }
func (t *Terminal) evaluateScrollOffset(list []*Item, height int) int { func (t *Terminal) evaluateScrollOffset() int {
if t.pwindow == nil {
return 0
}
// We only need the current item to calculate the scroll offset
offsetExpr := offsetTrimCharsRegex.ReplaceAllString( offsetExpr := offsetTrimCharsRegex.ReplaceAllString(
t.replacePlaceholder(t.previewOpts.scroll, false, "", list), "") t.replacePlaceholder(t.previewOpts.scroll, false, "", []*Item{t.currentItem(), nil}), "")
atoi := func(s string) int { atoi := func(s string) int {
n, e := strconv.Atoi(s) n, e := strconv.Atoi(s)
@@ -1655,20 +1708,21 @@ func (t *Terminal) evaluateScrollOffset(list []*Item, height int) int {
} }
base := -1 base := -1
height := util.Max(0, t.pwindow.Height()-t.previewOpts.headerLines)
for _, component := range offsetComponentRegex.FindAllString(offsetExpr, -1) { for _, component := range offsetComponentRegex.FindAllString(offsetExpr, -1) {
if strings.HasPrefix(component, "-/") { if strings.HasPrefix(component, "-/") {
component = component[1:] component = component[1:]
} }
if component[0] == '/' { if component[0] == '/' {
denom := atoi(component[1:]) denom := atoi(component[1:])
if denom == 0 { if denom != 0 {
return base base -= height / denom
} }
return base - height/denom break
} }
base += atoi(component) base += atoi(component)
} }
return base return util.Max(0, base)
} }
func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string { func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string {
@@ -1767,8 +1821,10 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr
}) })
} }
func (t *Terminal) redraw() { func (t *Terminal) redraw(clear bool) {
if clear {
t.tui.Clear() t.tui.Clear()
}
t.tui.Refresh() t.tui.Refresh()
t.printAll() t.printAll()
} }
@@ -1788,7 +1844,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
t.tui.Pause(true) t.tui.Pause(true)
cmd.Run() cmd.Run()
t.tui.Resume(true, false) t.tui.Resume(true, false)
t.redraw() t.redraw(true)
t.refresh() t.refresh()
} else { } else {
t.tui.Pause(false) t.tui.Pause(false)
@@ -1933,7 +1989,7 @@ func (t *Terminal) Loop() {
go func() { go func() {
for { for {
<-resizeChan <-resizeChan
t.reqBox.Set(reqRedraw, nil) t.reqBox.Set(reqFullRedraw, nil)
} }
}() }()
@@ -1972,12 +2028,14 @@ func (t *Terminal) Loop() {
var items []*Item var items []*Item
var commandTemplate string var commandTemplate string
var pwindow tui.Window var pwindow tui.Window
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 {
switch req { switch req {
case reqPreviewEnqueue: case reqPreviewEnqueue:
request := value.(previewRequest) request := value.(previewRequest)
commandTemplate = request.template commandTemplate = request.template
initialOffset = request.scrollOffset
items = request.list items = request.list
pwindow = request.pwindow pwindow = request.pwindow
} }
@@ -1989,11 +2047,9 @@ func (t *Terminal) Loop() {
if items[0] != nil { if items[0] != nil {
_, query := t.Input() _, query := t.Input()
command := t.replacePlaceholder(commandTemplate, false, string(query), items) command := t.replacePlaceholder(commandTemplate, false, string(query), items)
initialOffset := 0
cmd := util.ExecCommand(command, true) cmd := util.ExecCommand(command, true)
if pwindow != nil { if pwindow != nil {
height := pwindow.Height() height := pwindow.Height()
initialOffset = util.Max(0, t.evaluateScrollOffset(items, util.Max(0, height-t.previewOpts.headerLines)))
env := os.Environ() env := os.Environ()
lines := fmt.Sprintf("LINES=%d", height) lines := fmt.Sprintf("LINES=%d", height)
columns := fmt.Sprintf("COLUMNS=%d", pwindow.Width()) columns := fmt.Sprintf("COLUMNS=%d", pwindow.Width())
@@ -2128,7 +2184,7 @@ func (t *Terminal) Loop() {
if len(command) > 0 && t.isPreviewEnabled() { if len(command) > 0 && t.isPreviewEnabled() {
_, list := t.buildPlusList(command, false) _, list := t.buildPlusList(command, false)
t.cancelPreview() t.cancelPreview()
t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindow, list}) t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindow, t.evaluateScrollOffset(), list})
} }
} }
@@ -2183,9 +2239,11 @@ func (t *Terminal) Loop() {
t.suppress = false t.suppress = false
case reqReinit: case reqReinit:
t.tui.Resume(t.fullscreen, t.sigstop) t.tui.Resume(t.fullscreen, t.sigstop)
t.redraw() t.redraw(true)
case reqRedraw: case reqRedraw:
t.redraw() t.redraw(false)
case reqFullRedraw:
t.redraw(true)
case reqClose: case reqClose:
exit(func() int { exit(func() int {
if t.output() { if t.output() {
@@ -2253,13 +2311,15 @@ func (t *Terminal) Loop() {
} }
} }
} }
togglePreview := func(enabled bool) { togglePreview := func(enabled bool) bool {
if t.previewer.enabled != enabled { if t.previewer.enabled != enabled {
t.previewer.enabled = enabled t.previewer.enabled = enabled
t.tui.Clear() // We need to immediately update t.pwindow so we don't use reqRedraw
t.resizeWindows() t.resizeWindows()
req(reqPrompt, reqList, reqInfo, reqHeader) req(reqPrompt, reqList, reqInfo, reqHeader)
return true
} }
return false
} }
toggle := func() bool { toggle := func() bool {
current := t.currentItem() current := t.currentItem()
@@ -2296,12 +2356,12 @@ func (t *Terminal) Loop() {
} }
} }
actionsFor := func(eventType tui.EventType) []action { actionsFor := func(eventType tui.EventType) []*action {
return t.keymap[eventType.AsEvent()] return t.keymap[eventType.AsEvent()]
} }
var doAction func(action) bool var doAction func(*action) bool
doActions := func(actions []action) bool { doActions := func(actions []*action) bool {
for _, action := range actions { for _, action := range actions {
if !doAction(action) { if !doAction(action) {
return false return false
@@ -2309,7 +2369,7 @@ func (t *Terminal) Loop() {
} }
return true return true
} }
doAction = func(a action) bool { doAction = func(a *action) bool {
switch a.t { switch a.t {
case actIgnore: case actIgnore:
case actExecute, actExecuteSilent: case actExecute, actExecuteSilent:
@@ -2327,7 +2387,7 @@ func (t *Terminal) Loop() {
if valid { if valid {
t.cancelPreview() t.cancelPreview()
t.previewBox.Set(reqPreviewEnqueue, t.previewBox.Set(reqPreviewEnqueue,
previewRequest{t.previewOpts.command, t.pwindow, list}) previewRequest{t.previewOpts.command, t.pwindow, t.evaluateScrollOffset(), list})
} }
} }
} }
@@ -2489,14 +2549,14 @@ func (t *Terminal) Loop() {
} }
case actToggleIn: case actToggleIn:
if t.layout != layoutDefault { if t.layout != layoutDefault {
return doAction(action{t: actToggleUp}) return doAction(&action{t: actToggleUp})
} }
return doAction(action{t: actToggleDown}) return doAction(&action{t: actToggleDown})
case actToggleOut: case actToggleOut:
if t.layout != layoutDefault { if t.layout != layoutDefault {
return doAction(action{t: actToggleDown}) return doAction(&action{t: actToggleDown})
} }
return doAction(action{t: actToggleUp}) return doAction(&action{t: actToggleUp})
case actToggleDown: case actToggleDown:
if t.multi > 0 && t.merger.Length() > 0 && toggle() { if t.multi > 0 && t.merger.Length() > 0 && toggle() {
t.vmove(-1, true) t.vmove(-1, true)
@@ -2520,7 +2580,7 @@ func (t *Terminal) Loop() {
req(reqClose) req(reqClose)
} }
case actClearScreen: case actClearScreen:
req(reqRedraw) req(reqFullRedraw)
case actClearQuery: case actClearQuery:
t.input = []rune{} t.input = []rune{}
t.cx = 0 t.cx = 0
@@ -2700,13 +2760,58 @@ func (t *Terminal) Loop() {
command := t.replacePlaceholder(a.a, false, string(t.input), list) command := t.replacePlaceholder(a.a, false, string(t.input), list)
newCommand = &command newCommand = &command
t.reading = true t.reading = true
t.version++
} }
case actUnbind: case actUnbind:
keys := parseKeyChords(a.a, "PANIC") keys := parseKeyChords(a.a, "PANIC")
for key := range keys { for key := range keys {
delete(t.keymap, key) delete(t.keymap, key)
} }
case actRebind:
keys := parseKeyChords(a.a, "PANIC")
for key := range keys {
if originalAction, found := t.keymapOrg[key]; found {
t.keymap[key] = originalAction
}
}
case actChangePreview:
if t.previewOpts.command != a.a {
togglePreview(len(a.a) > 0)
t.previewOpts.command = a.a
refreshPreview(t.previewOpts.command)
}
case actChangePreviewWindow:
currentPreviewOpts := t.previewOpts
// Reset preview options and apply the additional options
t.previewOpts = t.initialPreviewOpts
// Split window options
tokens := strings.Split(a.a, "|")
parsePreviewWindow(&t.previewOpts, tokens[0])
if len(tokens) > 1 {
a.a = strings.Join(append(tokens[1:], tokens[0]), "|")
}
if t.previewOpts.hidden {
togglePreview(false)
} else {
// Full redraw
if !currentPreviewOpts.sameLayout(t.previewOpts) {
if togglePreview(true) {
refreshPreview(t.previewOpts.command)
} else {
req(reqRedraw)
}
} else if !currentPreviewOpts.sameContentLayout(t.previewOpts) {
t.previewed.version = 0
req(reqPreviewRefresh)
}
// Adjust scroll offset
if t.hasPreviewWindow() && currentPreviewOpts.scroll != t.previewOpts.scroll {
scrollPreviewTo(t.evaluateScrollOffset())
}
}
} }
return true return true
} }
@@ -2714,7 +2819,7 @@ func (t *Terminal) Loop() {
if t.jumping == jumpDisabled { if t.jumping == jumpDisabled {
actions := t.keymap[event.Comparable()] actions := t.keymap[event.Comparable()]
if len(actions) == 0 && event.Type == tui.Rune { if len(actions) == 0 && event.Type == tui.Rune {
doAction(action{t: actRune}) doAction(&action{t: actRune})
} else if !doActions(actions) { } else if !doActions(actions) {
continue continue
} }

View File

@@ -1,4 +1,4 @@
// +build !windows //go:build !windows
package fzf package fzf

View File

@@ -1,4 +1,4 @@
// +build windows //go:build windows
package fzf package fzf

View File

@@ -156,14 +156,14 @@ func Tokenize(text string, delimiter Delimiter) []Token {
// FIXME performance // FIXME performance
var tokens []string var tokens []string
if delimiter.regex != nil { if delimiter.regex != nil {
for len(text) > 0 { locs := delimiter.regex.FindAllStringIndex(text, -1)
loc := delimiter.regex.FindStringIndex(text) begin := 0
if len(loc) < 2 { for _, loc := range locs {
loc = []int{0, len(text)} tokens = append(tokens, text[begin:loc[1]])
begin = loc[1]
} }
last := util.Max(loc[1], 1) if begin < len(text) {
tokens = append(tokens, text[:last]) tokens = append(tokens, text[begin:])
text = text[last:]
} }
} }
return withPrefixLengths(tokens, 0) return withPrefixLengths(tokens, 0)

View File

@@ -1,6 +1,4 @@
// +build !ncurses //go:build !tcell && !windows
// +build !tcell
// +build !windows
package tui package tui

View File

@@ -23,7 +23,7 @@ const (
defaultEscDelay = 100 defaultEscDelay = 100
escPollInterval = 5 escPollInterval = 5
offsetPollTries = 10 offsetPollTries = 10
maxInputBuffer = 10 * 1024 maxInputBuffer = 1024 * 1024
) )
const consoleDevice string = "/dev/tty" const consoleDevice string = "/dev/tty"
@@ -60,7 +60,7 @@ func (r *LightRenderer) csi(code string) {
func (r *LightRenderer) flush() { func (r *LightRenderer) flush() {
if r.queued.Len() > 0 { if r.queued.Len() > 0 {
fmt.Fprint(os.Stderr, r.queued.String()) fmt.Fprint(os.Stderr, "\x1b[?25l"+r.queued.String()+"\x1b[?25h")
r.queued.Reset() r.queued.Reset()
} }
} }
@@ -176,6 +176,7 @@ func (r *LightRenderer) Init() {
if r.mouse { if r.mouse {
r.csi("?1000h") r.csi("?1000h")
r.csi("?1006h")
} }
r.csi(fmt.Sprintf("%dA", r.MaxY()-1)) r.csi(fmt.Sprintf("%dA", r.MaxY()-1))
r.csi("G") r.csi("G")
@@ -378,7 +379,7 @@ func (r *LightRenderer) escSequence(sz *int) Event {
return Event{Home, 0, nil} return Event{Home, 0, nil}
case 'F': case 'F':
return Event{End, 0, nil} return Event{End, 0, nil}
case 'M': case '<':
return r.mouseSequence(sz) return r.mouseSequence(sz)
case 'P': case 'P':
return Event{F1, 0, nil} return Event{F1, 0, nil}
@@ -519,19 +520,55 @@ func (r *LightRenderer) escSequence(sz *int) Event {
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }
// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
func (r *LightRenderer) mouseSequence(sz *int) Event { func (r *LightRenderer) mouseSequence(sz *int) Event {
if len(r.buffer) < 6 || !r.mouse { // "\e[<0;0;0M"
if len(r.buffer) < 9 || !r.mouse {
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }
*sz = 6
switch r.buffer[3] { rest := r.buffer[*sz:]
case 32, 34, 36, 40, 48, // mouse-down / shift / cmd / ctrl end := bytes.IndexAny(rest, "mM")
35, 39, 43, 51: // mouse-up / shift / cmd / ctrl if end == -1 {
mod := r.buffer[3] >= 36 return Event{Invalid, 0, nil}
left := r.buffer[3] == 32 }
down := r.buffer[3]%2 == 0
x := int(r.buffer[4] - 33) elems := strings.SplitN(string(rest[:end]), ";", 3)
y := int(r.buffer[5]-33) - r.yoffset if len(elems) != 3 {
return Event{Invalid, 0, nil}
}
t := atoi(elems[0], -1)
x := atoi(elems[1], -1) - 1
y := atoi(elems[2], -1) - 1
if t < 0 || x < 0 || y < 0 {
return Event{Invalid, 0, nil}
}
*sz += end + 1
down := rest[end] == 'M'
scroll := 0
if t >= 64 {
t -= 64
if t&0b1 == 1 {
scroll = -1
} else {
scroll = 1
}
}
// middle := t & 0b1
left := t&0b11 == 0
// shift := t & 0b100
// ctrl := t & 0b1000
mod := t&0b1100 > 0
if scroll != 0 {
return Event{Mouse, 0, &MouseEvent{y, x, scroll, false, false, false, mod}}
}
double := false double := false
if down { if down {
now := time.Now() now := time.Now()
@@ -549,17 +586,7 @@ func (r *LightRenderer) mouseSequence(sz *int) Event {
double = true double = true
} }
} }
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}} return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}}
case 96, 100, 104, 112, // scroll-up / shift / cmd / ctrl
97, 101, 105, 113: // scroll-down / shift / cmd / ctrl
mod := r.buffer[3] >= 100
s := 1 - int(r.buffer[3]%2)*2
x := int(r.buffer[4] - 33)
y := int(r.buffer[5]-33) - r.yoffset
return Event{Mouse, 0, &MouseEvent{y, x, s, false, false, false, mod}}
}
return Event{Invalid, 0, nil}
} }
func (r *LightRenderer) smcup() { func (r *LightRenderer) smcup() {
@@ -597,6 +624,7 @@ func (r *LightRenderer) Resume(clear bool, sigcont bool) {
// It's highly likely that the offset we obtained at the beginning is // It's highly likely that the offset we obtained at the beginning is
// no longer correct, so we simply disable mouse input. // no longer correct, so we simply disable mouse input.
r.csi("?1000l") r.csi("?1000l")
r.csi("?1006l")
r.mouse = false r.mouse = false
} }
} }
@@ -636,6 +664,7 @@ func (r *LightRenderer) Close() {
} }
if r.mouse { if r.mouse {
r.csi("?1000l") r.csi("?1000l")
r.csi("?1006l")
} }
r.flush() r.flush()
r.closePlatform() r.closePlatform()

View File

@@ -1,4 +1,4 @@
// +build !windows //go:build !windows
package tui package tui

View File

@@ -1,4 +1,4 @@
//+build windows //go:build windows
package tui package tui

View File

@@ -1,4 +1,4 @@
// +build tcell windows //go:build tcell || windows
package tui package tui

View File

@@ -1,4 +1,4 @@
// +build tcell windows //go:build tcell || windows
package tui package tui

View File

@@ -1,4 +1,4 @@
// +build !windows //go:build !windows
package tui package tui

View File

@@ -1,4 +1,4 @@
// +build windows //go:build windows
package tui package tui

View File

@@ -26,7 +26,7 @@ func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int
w = runewidth.StringWidth(s) + strings.Count(s, "\n") w = runewidth.StringWidth(s) + strings.Count(s, "\n")
} }
width += w width += w
if limit > 0 && width > limit { if width > limit {
return width, idx return width, idx
} }
idx += len(rs) idx += len(rs)
@@ -34,6 +34,23 @@ func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int
return width, -1 return width, -1
} }
// Truncate returns the truncated runes and its width
func Truncate(input string, limit int) ([]rune, int) {
runes := []rune{}
width := 0
gr := uniseg.NewGraphemes(input)
for gr.Next() {
rs := gr.Runes()
w := runewidth.StringWidth(string(rs))
if width+w > limit {
return runes, width
}
width += w
runes = append(runes, rs...)
}
return runes, width
}
// Max returns the largest integer // Max returns the largest integer
func Max(first int, second int) int { func Max(first int, second int) int {
if first >= second { if first >= second {

View File

@@ -38,3 +38,29 @@ func TestOnce(t *testing.T) {
t.Error("Expected: false") t.Error("Expected: false")
} }
} }
func TestRunesWidth(t *testing.T) {
for _, args := range [][]int{
{100, 5, -1},
{3, 4, 3},
{0, 1, 0},
} {
width, overflowIdx := RunesWidth([]rune("hello"), 0, 0, args[0])
if width != args[1] {
t.Errorf("Expected width: %d, actual: %d", args[1], width)
}
if overflowIdx != args[2] {
t.Errorf("Expected overflow index: %d, actual: %d", args[2], overflowIdx)
}
}
}
func TestTruncate(t *testing.T) {
truncated, width := Truncate("가나다라마", 7)
if string(truncated) != "가나다" {
t.Errorf("Expected: 가나다, actual: %s", string(truncated))
}
if width != 6 {
t.Errorf("Expected: 6, actual: %d", width)
}
}

View File

@@ -1,4 +1,4 @@
// +build !windows //go:build !windows
package util package util

View File

@@ -1,4 +1,4 @@
// +build windows //go:build windows
package util package util

View File

@@ -2043,8 +2043,8 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_equal(%w[1 2 3 4 5], top5[lines]) } tmux.until { |lines| assert_equal(%w[1 2 3 4 5], top5[lines]) }
end end
def test_unbind def test_unbind_rebind
tmux.send_keys "seq 100 | #{FZF} --bind 'c:clear-query,d:unbind(c,d)'", :Enter tmux.send_keys "seq 100 | #{FZF} --bind 'c:clear-query,d:unbind(c,d),e:rebind(c,d)'", :Enter
tmux.until { |lines| assert_equal 100, lines.item_count } tmux.until { |lines| assert_equal 100, lines.item_count }
tmux.send_keys 'ab' tmux.send_keys 'ab'
tmux.until { |lines| assert_equal '> ab', lines[-1] } tmux.until { |lines| assert_equal '> ab', lines[-1] }
@@ -2052,6 +2052,8 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_equal '>', lines[-1] } tmux.until { |lines| assert_equal '>', lines[-1] }
tmux.send_keys 'dabcd' tmux.send_keys 'dabcd'
tmux.until { |lines| assert_equal '> abcd', lines[-1] } tmux.until { |lines| assert_equal '> abcd', lines[-1] }
tmux.send_keys 'ecabddc'
tmux.until { |lines| assert_equal '> abdc', lines[-1] }
end end
def test_item_index_reset_on_reload def test_item_index_reset_on_reload
@@ -2077,6 +2079,15 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_includes lines[1], '4' } tmux.until { |lines| assert_includes lines[1], '4' }
end end
def test_reload_and_change_preview_should_update_preview
tmux.send_keys "seq 3 | #{FZF} --bind 'ctrl-t:reload(echo 4)+change-preview(echo {})'", :Enter
tmux.until { |lines| assert_equal 3, lines.item_count }
tmux.until { |lines| refute_includes lines[1], '1' }
tmux.send_keys 'C-t'
tmux.until { |lines| assert_equal 1, lines.item_count }
tmux.until { |lines| assert_includes lines[1], '4' }
end
def test_scroll_off def test_scroll_off
tmux.send_keys "seq 1000 | #{FZF} --scroll-off=3 --bind l:last", :Enter tmux.send_keys "seq 1000 | #{FZF} --scroll-off=3 --bind l:last", :Enter
tmux.until { |lines| assert_equal 1000, lines.item_count } tmux.until { |lines| assert_equal 1000, lines.item_count }
@@ -2142,6 +2153,78 @@ class TestGoFZF < TestBase
assert_equal expected.chomp, lines.take(6).join("\n") assert_equal expected.chomp, lines.take(6).join("\n")
end end
end end
def test_change_preview_window
tmux.send_keys "seq 1000 | #{FZF} --preview 'echo [[{}]]' --preview-window border-none --bind '" \
'a:change-preview(echo __{}__),' \
'b:change-preview-window(down)+change-preview(echo =={}==)+change-preview-window(up),' \
'c:change-preview(),d:change-preview-window(hidden),' \
"e:preview(printf ::%${FZF_PREVIEW_COLUMNS}s{})+change-preview-window(up),f:change-preview-window(up,wrap)'", :Enter
tmux.until { |lines| assert_equal 1000, lines.item_count }
tmux.until { |lines| assert_includes lines[0], '[[1]]' }
# change-preview action permanently changes the preview command set by --preview
tmux.send_keys 'a'
tmux.until { |lines| assert_includes lines[0], '__1__' }
tmux.send_keys :Up
tmux.until { |lines| assert_includes lines[0], '__2__' }
# When multiple change-preview-window actions are bound to a single key,
# the last one wins and the updated options are immediately applied to the new preview
tmux.send_keys 'b'
tmux.until { |lines| assert_equal '==2==', lines[0] }
tmux.send_keys :Up
tmux.until { |lines| assert_equal '==3==', lines[0] }
# change-preview with an empty preview command closes the preview window
tmux.send_keys 'c'
tmux.until { |lines| refute_includes lines[0], '==' }
# change-preview again to re-open the preview window
tmux.send_keys 'a'
tmux.until { |lines| assert_equal '__3__', lines[0] }
# Hide the preview window with hidden flag
tmux.send_keys 'd'
tmux.until { |lines| refute_includes lines[0], '__3__' }
# One-off preview
tmux.send_keys 'e'
tmux.until do |lines|
assert_equal '::', lines[0]
refute_includes lines[1], '3'
end
# Wrapped
tmux.send_keys 'f'
tmux.until do |lines|
assert_equal '::', lines[0]
assert_equal ' 3', lines[1]
end
end
def test_change_preview_window_rotate
tmux.send_keys "seq 100 | #{FZF} --preview-window left,border-none --preview 'echo hello' --bind '" \
"a:change-preview-window(right|down|up|hidden|)'", :Enter
3.times do
tmux.until { |lines| lines[0].start_with?('hello') }
tmux.send_keys 'a'
tmux.until { |lines| lines[0].end_with?('hello') }
tmux.send_keys 'a'
tmux.until { |lines| lines[-1].start_with?('hello') }
tmux.send_keys 'a'
tmux.until { |lines| assert_equal 'hello', lines[0] }
tmux.send_keys 'a'
tmux.until { |lines| refute_includes lines[0], 'hello' }
tmux.send_keys 'a'
end
end
def test_ellipsis
tmux.send_keys 'seq 1000 | tr "\n" , | fzf --ellipsis=SNIPSNIP -e -q500', :Enter
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.until { |lines| assert_match(/^> SNIPSNIP.*SNIPSNIP$/, lines[-3]) }
end
end end
module TestShell module TestShell
@@ -2395,7 +2478,7 @@ module CompletionTest
pid = lines[-1]&.split&.last pid = lines[-1]&.split&.last
tmux.prepare tmux.prepare
tmux.send_keys 'C-L' tmux.send_keys 'C-L'
tmux.send_keys 'kill ', :Tab tmux.send_keys 'kill **', :Tab
tmux.until { |lines| assert_operator lines.match_count, :>, 0 } tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys 'sleep12345' tmux.send_keys 'sleep12345'
tmux.until { |lines| assert lines.any_include?('sleep 12345') } tmux.until { |lines| assert lines.any_include?('sleep 12345') }

View File

@@ -51,13 +51,8 @@ remove() {
} }
remove_line() { remove_line() {
src=$(readlink "$1")
if [ $? -eq 0 ]; then
echo "Remove from $1 ($src):"
else
src=$1 src=$1
echo "Remove from $1:" echo "Remove from $1:"
fi
shift shift
line_no=1 line_no=1
@@ -75,8 +70,9 @@ remove_line() {
echo " - Line #$line_no: $content" echo " - Line #$line_no: $content"
[ "$content" = "$1" ] || ask " - Remove?" [ "$content" = "$1" ] || ask " - Remove?"
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
awk -v n=$line_no 'NR == n {next} {print}' "$src" > "$src.bak" && temp=$(mktemp)
mv "$src.bak" "$src" || break awk -v n=$line_no 'NR == n {next} {print}' "$src" > "$temp" &&
cat "$temp" > "$src" && rm -f "$temp" || break
echo " - Removed" echo " - Removed"
else else
echo " - Skipped" echo " - Skipped"