mirror of
https://github.com/junegunn/fzf.git
synced 2025-07-26 09:42:02 -07:00
Compare commits
134 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
0476a65fca | ||
|
2cb2af115a | ||
|
789226ff6d | ||
|
805efc5bf1 | ||
|
cdcab26766 | ||
|
ec3acb1932 | ||
|
d30e37434e | ||
|
20d5b2e20e | ||
|
6c6be4ab1a | ||
|
d004eb1f7c | ||
|
3148b0f3e8 | ||
|
3fc0bd26a5 | ||
|
6c9025ff17 | ||
|
289997e373 | ||
|
db44cbdff0 | ||
|
da9179335c | ||
|
cdf641fa3e | ||
|
66dbee10f5 | ||
|
19e9b620ba | ||
|
e4e4700aff | ||
|
bb55045596 | ||
|
d7e51cdeb5 | ||
|
7f4964b366 | ||
|
a6957aba11 | ||
|
b5f94f961d | ||
|
e182d3db7a | ||
|
3e6e0528a6 | ||
|
ac508a1ce4 | ||
|
d7fc1e09b1 | ||
|
3b0c86e401 | ||
|
61d10d8ffa | ||
|
7d9548919e | ||
|
bee80a730f | ||
|
ac3e24c99c | ||
|
e7e852bdb3 | ||
|
2b7f168571 | ||
|
5b3da1d878 | ||
|
99f1bc0177 | ||
|
ed76f076dd | ||
|
4d357d1063 | ||
|
961ae1541c | ||
|
add1aec685 | ||
|
03d6ba7496 | ||
|
71e4d5cc51 | ||
|
215ab48222 | ||
|
0c64c68781 | ||
|
3ec035c68b | ||
|
20c7dcfbca | ||
|
c1b8780b9c | ||
|
64c61603e9 | ||
|
57c08d925f | ||
|
51623a5f6a | ||
|
ca3f6181d7 | ||
|
9c94f9c3d0 | ||
|
4a85843bcf | ||
|
d4d9b99879 | ||
|
6816b7d95b | ||
|
acdf265d7a | ||
|
19495eb9bb | ||
|
bacc8609ee | ||
|
99163f5afa | ||
|
0607227730 | ||
|
d938fdc496 | ||
|
dcb4c3d84a | ||
|
82ebcd9209 | ||
|
ff1687744d | ||
|
782c870fb2 | ||
|
71fad63829 | ||
|
d65c6101a8 | ||
|
3c40b1bd51 | ||
|
90a8800bb5 | ||
|
97f1dae2d1 | ||
|
e54ec05709 | ||
|
a24eb99679 | ||
|
ad113d00b7 | ||
|
7bd5884d12 | ||
|
c3505858a6 | ||
|
e76aa37fd4 | ||
|
1a32220ca9 | ||
|
4161403a1d | ||
|
53bcdc4294 | ||
|
30a8ef28cd | ||
|
855f90727a | ||
|
2191a44e36 | ||
|
952276dc2d | ||
|
2286edb329 | ||
|
a0f28583e7 | ||
|
8af0af3400 | ||
|
769e5cbb2d | ||
|
fc69308057 | ||
|
c6d620c99e | ||
|
f510a4def6 | ||
|
4ae3069c6f | ||
|
c0f27751d3 | ||
|
efbcd5a683 | ||
|
6a67712944 | ||
|
e8a690928d | ||
|
0eee95af57 | ||
|
a09c6e991a | ||
|
d06c5ab990 | ||
|
e0924d27b8 | ||
|
2775b771f2 | ||
|
cf7a3c6a0e | ||
|
230cc6acc3 | ||
|
626a23a585 | ||
|
74f196eebb | ||
|
cf2242aea3 | ||
|
8cb59e6fca | ||
|
5cce17e80a | ||
|
ee5302fb2d | ||
|
387c6ef664 | ||
|
581734c369 | ||
|
d90a969c00 | ||
|
2c8a96bb27 | ||
|
5da168db30 | ||
|
e215e2daf3 | ||
|
e28f5aa45b | ||
|
a2d0e8f233 | ||
|
303d04106a | ||
|
c423c496a1 | ||
|
4e85f72f0e | ||
|
dd0737aac0 | ||
|
f90985845d | ||
|
af4917dbb6 | ||
|
d9404fcce4 | ||
|
5c01fee5a9 | ||
|
9b27d68a37 | ||
|
b99d884e57 | ||
|
587df594b8 | ||
|
b896e0d314 | ||
|
559fb7ee45 | ||
|
62545cd983 | ||
|
c50812518e | ||
|
4cc5609d8b |
2
.github/workflows/linux.yml
vendored
2
.github/workflows/linux.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
run: sudo apt-get install --yes zsh fish tmux
|
||||
|
||||
- name: Install Ruby gems
|
||||
run: sudo gem install --no-document minitest:5.17.0 rubocop:1.43.0 rubocop-minitest:0.25.1 rubocop-performance:1.15.2
|
||||
run: sudo gem install --no-document minitest:5.25.1 rubocop:1.65.0 rubocop-minitest:0.35.1 rubocop-performance:1.21.1
|
||||
|
||||
- name: Rubocop
|
||||
run: rubocop --require rubocop-minitest --require rubocop-performance
|
||||
|
2
.github/workflows/typos.yml
vendored
2
.github/workflows/typos.yml
vendored
@@ -7,4 +7,4 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: crate-ci/typos@v1.23.1
|
||||
- uses: crate-ci/typos@v1.28.2
|
||||
|
@@ -1,4 +1,5 @@
|
||||
---
|
||||
version: 2
|
||||
project_name: fzf
|
||||
|
||||
before:
|
||||
@@ -6,60 +7,9 @@ before:
|
||||
- go mod download
|
||||
|
||||
builds:
|
||||
- id: fzf-macos
|
||||
binary: fzf
|
||||
goos:
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
|
||||
hooks:
|
||||
post: |
|
||||
sh -c '
|
||||
cat > /tmp/fzf-gon-amd64.hcl << EOF
|
||||
source = ["./dist/fzf-macos_darwin_amd64_v1/fzf"]
|
||||
bundle_id = "junegunn.fzf"
|
||||
sign {
|
||||
application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)"
|
||||
}
|
||||
zip {
|
||||
output_path = "./dist/fzf-{{ .Version }}-darwin_amd64.zip"
|
||||
}
|
||||
EOF
|
||||
gon /tmp/fzf-gon-amd64.hcl
|
||||
'
|
||||
|
||||
- id: fzf-macos-arm
|
||||
binary: fzf
|
||||
goos:
|
||||
- darwin
|
||||
goarch:
|
||||
- arm64
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
|
||||
hooks:
|
||||
post: |
|
||||
sh -c '
|
||||
cat > /tmp/fzf-gon-arm64.hcl << EOF
|
||||
source = ["./dist/fzf-macos-arm_darwin_arm64/fzf"]
|
||||
bundle_id = "junegunn.fzf"
|
||||
sign {
|
||||
application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)"
|
||||
}
|
||||
zip {
|
||||
output_path = "./dist/fzf-{{ .Version }}-darwin_arm64.zip"
|
||||
}
|
||||
EOF
|
||||
gon /tmp/fzf-gon-arm64.hcl
|
||||
'
|
||||
|
||||
- id: fzf
|
||||
goos:
|
||||
- darwin
|
||||
- linux
|
||||
- windows
|
||||
- freebsd
|
||||
@@ -89,6 +39,42 @@ builds:
|
||||
- goos: openbsd
|
||||
goarch: arm64
|
||||
|
||||
# .goreleaser.yaml
|
||||
notarize:
|
||||
macos:
|
||||
- # Whether this configuration is enabled or not.
|
||||
#
|
||||
# Default: false.
|
||||
# Templates: allowed.
|
||||
enabled: "{{ not .IsSnapshot }}"
|
||||
|
||||
# Before notarizing, we need to sign the binary.
|
||||
# This blocks defines the configuration for doing so.
|
||||
sign:
|
||||
# The .p12 certificate file path or its base64'd contents.
|
||||
certificate: "{{.Env.MACOS_SIGN_P12}}"
|
||||
|
||||
# The password to be used to open the certificate.
|
||||
password: "{{.Env.MACOS_SIGN_PASSWORD}}"
|
||||
|
||||
# Then, we notarize the binaries.
|
||||
notarize:
|
||||
# The issuer ID.
|
||||
# Its the UUID you see when creating the App Store Connect key.
|
||||
issuer_id: "{{.Env.MACOS_NOTARY_ISSUER_ID}}"
|
||||
|
||||
# Key ID.
|
||||
# You can see it in the list of App Store Connect Keys.
|
||||
# It will also be in the ApiKey filename.
|
||||
key_id: "{{.Env.MACOS_NOTARY_KEY_ID}}"
|
||||
|
||||
# The .p8 key file path or its base64'd contents.
|
||||
key: "{{.Env.MACOS_NOTARY_KEY}}"
|
||||
|
||||
# Whether to wait for the notarization to finish.
|
||||
# Not recommended, as it could take a really long time.
|
||||
wait: true
|
||||
|
||||
archives:
|
||||
- name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
|
||||
builds:
|
||||
@@ -100,18 +86,12 @@ archives:
|
||||
files:
|
||||
- non-existent*
|
||||
|
||||
checksum:
|
||||
extra_files:
|
||||
- glob: ./dist/fzf-*darwin*.zip
|
||||
|
||||
release:
|
||||
github:
|
||||
owner: junegunn
|
||||
name: fzf
|
||||
prerelease: auto
|
||||
name_template: '{{ .Version }}'
|
||||
extra_files:
|
||||
- glob: ./dist/fzf-*darwin*.zip
|
||||
|
||||
snapshot:
|
||||
name_template: "{{ .Version }}-devel"
|
||||
|
@@ -6,7 +6,7 @@ Lint/ShadowingOuterLocalVariable:
|
||||
Enabled: false
|
||||
Style/MethodCallWithArgsParentheses:
|
||||
Enabled: true
|
||||
IgnoredMethods:
|
||||
AllowedMethods:
|
||||
- assert
|
||||
- exit
|
||||
- paste
|
||||
@@ -15,7 +15,7 @@ Style/MethodCallWithArgsParentheses:
|
||||
- refute
|
||||
- require
|
||||
- send_keys
|
||||
IgnoredPatterns:
|
||||
AllowedPatterns:
|
||||
- ^assert_
|
||||
- ^refute_
|
||||
Style/NumericPredicate:
|
||||
|
111
CHANGELOG.md
111
CHANGELOG.md
@@ -1,6 +1,117 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.57.0
|
||||
------
|
||||
- You can now resize the preview window by dragging the border
|
||||
- Built-in walker improvements
|
||||
- `--walker-root` can take multiple directory arguments. e.g. `--walker-root include src lib`
|
||||
- `--walker-skip` can handle multi-component patterns. e.g. `--walker-skip target/build`
|
||||
- Removed long processing delay when displaying images in the preview window
|
||||
- `FZF_PREVIEW_*` environment variables are exported to all child processes (#4098)
|
||||
- Bug fixes in fish scripts
|
||||
|
||||
0.56.3
|
||||
------
|
||||
- Bug fixes in zsh scripts
|
||||
- fix(zsh): handle backtick trigger edge case (#4090)
|
||||
- revert(zsh): remove 'fc -RI' call in the history widget (#4093)
|
||||
- Thanks to @LangLangBart for the contributions
|
||||
|
||||
0.56.2
|
||||
------
|
||||
- Bug fixes
|
||||
- Fixed abnormal scrolling behavior when `--wrap` is set (#4083)
|
||||
- [zsh] Fixed warning message when `ksh_arrays` is set (#4084)
|
||||
|
||||
0.56.1
|
||||
------
|
||||
- Bug fixes and improvements
|
||||
- Fixed a race condition which would cause fzf to present stale results after `reload` (#4070)
|
||||
- `page-up` and `page-down` actions now work correctly with multi-line items (#4069)
|
||||
- `{n}` is allowed in `SCROLL` expression in `--preview-window` (#4079)
|
||||
- [zsh] Fixed regression in history loading with shared option (#4071)
|
||||
- [zsh] Better command extraction in zsh completion (#4082)
|
||||
- Thanks to @LangLangBart, @jaydee-coder, @alex-huff, and @vejkse for the contributions
|
||||
|
||||
0.56.0
|
||||
------
|
||||
- Added `--gap[=N]` option to display empty lines between items.
|
||||
- This can be useful to visually separate adjacent multi-line items.
|
||||
```sh
|
||||
# All bash functions, highlighted
|
||||
declare -f | perl -0777 -pe 's/^}\n/}\0/gm' |
|
||||
bat --plain --language bash --color always |
|
||||
fzf --read0 --ansi --reverse --multi --highlight-line --gap
|
||||
```
|
||||
- Or just to make the list easier to read. For single-line items, you probably want to set `--color gutter:-1` as well to hide the gutter.
|
||||
```sh
|
||||
fzf --info inline-right --gap --color gutter:-1
|
||||
```
|
||||
- Added `noinfo` option to `--preview-window` to hide the scroll indicator in the preview window
|
||||
- Bug fixes
|
||||
- Thanks to @LangLangBart, @akinomyoga, and @charlievieth for fixing the bugs
|
||||
|
||||
0.55.0
|
||||
------
|
||||
_Release highlights: https://junegunn.github.io/fzf/releases/0.55.0/_
|
||||
|
||||
- Added `exact-boundary-match` type to the search syntax. When a search term is single-quoted, fzf will search for the exact occurrences of the string with both ends at word boundaries.
|
||||
```sh
|
||||
fzf --query "'here'" << EOF
|
||||
come here
|
||||
not there
|
||||
EOF
|
||||
```
|
||||
- [bash] Fuzzy path completion is enabled for all commands
|
||||
- 1. If the default completion is not already set
|
||||
- 2. And if the current bash supports `complete -D` option
|
||||
- However, fuzzy completion for some commands can be "dynamically" disabled by the dynamic completion loader
|
||||
- See the comment in `__fzf_default_completion` function for more information
|
||||
- Comments are now allowed in `$FZF_DEFAULT_OPTS` and `$FZF_DEFAULT_OPTS_FILE`
|
||||
```sh
|
||||
export FZF_DEFAULT_OPTS='
|
||||
# Layout options
|
||||
--layout=reverse
|
||||
--info=inline-right # Show info on the right side of the prompt line
|
||||
# ...
|
||||
'
|
||||
```
|
||||
- Hyperlinks (OSC 8) are now supported in the preview window and in the main window
|
||||
```sh
|
||||
printf '<< \e]8;;http://github.com/junegunn/fzf\e\\Link to \e[32mfz\e[0mf\e]8;;\e\\ >>' | fzf --ansi
|
||||
|
||||
fzf --preview "printf '<< \e]8;;http://github.com/junegunn/fzf\e\\Link to \e[32mfz\e[0mf\e]8;;\e\\ >>'"
|
||||
```
|
||||
- The default `--ellipsis` is now `··` instead of `..`.
|
||||
- [vim] A spec can have `exit` callback that is called with the exit status of fzf
|
||||
- This can be used to clean up temporary resources or restore the original state when fzf is closed without a selection
|
||||
- Fixed `--tmux bottom` when the status line is not at the bottom
|
||||
- Fixed extra scroll offset in multi-line mode (`--read0` or `--wrap`)
|
||||
- Added fallback `ps` command for `kill` completion on Cygwin
|
||||
|
||||
0.54.3
|
||||
------
|
||||
- Fixed incompatibility of adaptive height specification and 'start:reload'
|
||||
```sh
|
||||
# A regression in 0.54.0 would cause this to fail
|
||||
fzf --height '~100%' --bind 'start:reload:seq 10'
|
||||
```
|
||||
- Environment variables are now available to `$FZF_DEFAULT_COMMAND`
|
||||
```sh
|
||||
FZF_DEFAULT_COMMAND='echo $FZF_QUERY' fzf --query foo
|
||||
```
|
||||
|
||||
0.54.2
|
||||
------
|
||||
- Fixed incorrect syntax highlighting of truncated multi-line entries
|
||||
- Updated GoReleaser to 2.1.0 to simplify notarization of macOS binaries
|
||||
- macOS archives will be in `tar.gz` format instead of `zip` format since we no longer notarize the zip files but binaries
|
||||
- (Windows) Reverted a mintty fix in 0.54.0
|
||||
- As a result, mouse may not work on mintty in fullscreen mode. However, fzf will correctly read non-ASCII input in fullscreen mode (`--no-height`).
|
||||
- fzf unfortunately cannot read non-ASCII input when not in fullscreen mode on Windows. So if you need to input non-ASCII characters, add `--no-height` to your `$FZF_DEFAULT_OPTS`.
|
||||
- Any help in fixing this issue will be appreciated (#3799, #3847).
|
||||
|
||||
0.54.1
|
||||
------
|
||||
- Updated [fastwalk](https://github.com/charlievieth/fastwalk) dependency for built-in directory walker
|
||||
|
@@ -8,5 +8,5 @@ RUN echo '. ~/.bashrc' >> ~/.bash_profile
|
||||
RUN rm -f /etc/bash.bashrc
|
||||
COPY . /fzf
|
||||
RUN cd /fzf && make install && ./install --all
|
||||
ENV LANG C.UTF-8
|
||||
CMD tmux new 'set -o pipefail; ruby /fzf/test/test_go.rb | tee out && touch ok' && cat out && [ -e ok ]
|
||||
ENV LANG=C.UTF-8
|
||||
CMD ["bash", "-ic", "tmux new 'set -o pipefail; ruby /fzf/test/test_go.rb | tee out && touch ok' && cat out && [ -e ok ]"]
|
||||
|
7
Makefile
7
Makefile
@@ -77,7 +77,6 @@ endif
|
||||
all: target/$(BINARY)
|
||||
|
||||
test: $(SOURCES)
|
||||
[ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1)
|
||||
SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" \
|
||||
github.com/junegunn/fzf/src \
|
||||
github.com/junegunn/fzf/src/algo \
|
||||
@@ -87,6 +86,10 @@ test: $(SOURCES)
|
||||
bench:
|
||||
cd src && SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" -run=Bench -bench=. -benchmem
|
||||
|
||||
lint: $(SOURCES) test/test_go.rb
|
||||
[ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1)
|
||||
rubocop --require rubocop-minitest --require rubocop-performance
|
||||
|
||||
install: bin/fzf
|
||||
|
||||
generate:
|
||||
@@ -184,4 +187,4 @@ update:
|
||||
$(GO) get -u
|
||||
$(GO) mod tidy
|
||||
|
||||
.PHONY: all generate build release test bench install clean docker docker-test update
|
||||
.PHONY: all generate build release test bench lint install clean docker docker-test update
|
||||
|
@@ -289,8 +289,9 @@ The following table summarizes the available options.
|
||||
| `source` | string | External command to generate input to fzf (e.g. `find .`) |
|
||||
| `source` | list | Vim list as input to fzf |
|
||||
| `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) |
|
||||
| `sink` | funcref | Reference to function to process each selected item |
|
||||
| `sink` | funcref | Function to be called with each selected item |
|
||||
| `sinklist` (or `sink*`) | funcref | Similar to `sink`, but takes the list of output lines at once |
|
||||
| `exit` | funcref | Function to be called with the exit status of fzf (e.g. 0, 1, 2, 130) |
|
||||
| `options` | string/list | Options to fzf |
|
||||
| `dir` | string | Working directory |
|
||||
| `up`/`down`/`left`/`right` | number/string | (Layout) Window position and size (e.g. `20`, `50%`) |
|
||||
|
24
bin/fzf-tmux
24
bin/fzf-tmux
@@ -19,6 +19,9 @@ term=""
|
||||
[[ -n "$LINES" ]] && lines=$LINES || lines=$(tput lines) || lines=$(tmux display-message -p "#{pane_height}")
|
||||
[[ -n "$COLUMNS" ]] && columns=$COLUMNS || columns=$(tput cols) || columns=$(tmux display-message -p "#{pane_width}")
|
||||
|
||||
tmux_version=$(tmux -V | sed 's/[^0-9.]//g')
|
||||
tmux_32=$(awk '{print ($1 >= 3.2)}' <<< "$tmux_version" 2> /dev/null || bc -l <<< "$tmux_version >= 3.2")
|
||||
|
||||
help() {
|
||||
>&2 echo 'usage: fzf-tmux [LAYOUT OPTIONS] [--] [FZF OPTIONS]
|
||||
|
||||
@@ -94,10 +97,18 @@ while [[ $# -gt 0 ]]; do
|
||||
opt="$opt ${arg:0:2}$size"
|
||||
elif [[ "$size" =~ %$ ]]; then
|
||||
size=${size:0:((${#size}-1))}
|
||||
if [[ -n "$swap" ]]; then
|
||||
opt="$opt -l $(( 100 - size ))%"
|
||||
if [[ $tmux_32 = 1 ]]; then
|
||||
if [[ -n "$swap" ]]; then
|
||||
opt="$opt -l $(( 100 - size ))%"
|
||||
else
|
||||
opt="$opt -l $size%"
|
||||
fi
|
||||
else
|
||||
opt="$opt -l $size%"
|
||||
if [[ -n "$swap" ]]; then
|
||||
opt="$opt -p $(( 100 - size ))"
|
||||
else
|
||||
opt="$opt -p $size"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
if [[ -n "$swap" ]]; then
|
||||
@@ -187,12 +198,11 @@ trap 'cleanup' EXIT
|
||||
|
||||
envs="export TERM=$TERM "
|
||||
if [[ "$opt" =~ "-E" ]]; then
|
||||
tmux_version=$(tmux -V | sed 's/[^0-9.]//g')
|
||||
if [[ $(awk '{print ($1 > 3.2)}' <<< "$tmux_version" 2> /dev/null || bc -l <<< "$tmux_version > 3.2") = 1 ]]; then
|
||||
if [[ $tmux_version = 3.2 ]]; then
|
||||
FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
|
||||
elif [[ $tmux_32 = 1 ]]; then
|
||||
FZF_DEFAULT_OPTS="--border $FZF_DEFAULT_OPTS"
|
||||
opt="-B $opt"
|
||||
elif [[ $tmux_version = 3.2 ]]; then
|
||||
FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
|
||||
else
|
||||
echo "fzf-tmux: tmux 3.2 or above is required for popup mode" >&2
|
||||
exit 2
|
||||
|
@@ -306,8 +306,9 @@ The following table summarizes the available options.
|
||||
`source` | string | External command to generate input to fzf (e.g. `find .` )
|
||||
`source` | list | Vim list as input to fzf
|
||||
`sink` | string | Vim command to handle the selected item (e.g. `e` , `tabe` )
|
||||
`sink` | funcref | Reference to function to process each selected item
|
||||
`sink` | funcref | Function to be called with each selected item
|
||||
`sinklist` (or `sink*` ) | funcref | Similar to `sink` , but takes the list of output lines at once
|
||||
`exit` | funcref | Function to be called with the exit status of fzf (e.g. 0, 1, 2, 130)
|
||||
`options` | string/list | Options to fzf
|
||||
`dir` | string | Working directory
|
||||
`up` / `down` / `left` / `right` | number/string | (Layout) Window position and size (e.g. `20` , `50%` )
|
||||
|
8
go.mod
8
go.mod
@@ -1,13 +1,13 @@
|
||||
module github.com/junegunn/fzf
|
||||
|
||||
require (
|
||||
github.com/charlievieth/fastwalk v1.0.8
|
||||
github.com/charlievieth/fastwalk v1.0.9
|
||||
github.com/gdamore/tcell/v2 v2.7.4
|
||||
github.com/junegunn/go-shellwords v0.0.0-20240813092932-a62c48c52e97
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/mattn/go-shellwords v1.0.12
|
||||
github.com/rivo/uniseg v0.4.7
|
||||
golang.org/x/sys v0.22.0
|
||||
golang.org/x/term v0.22.0
|
||||
golang.org/x/sys v0.28.0
|
||||
golang.org/x/term v0.27.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
16
go.sum
16
go.sum
@@ -1,17 +1,17 @@
|
||||
github.com/charlievieth/fastwalk v1.0.8 h1:uaoH6cAKSk73aK7aKXqs0+bL+J3Txzd3NGH8tRXgHko=
|
||||
github.com/charlievieth/fastwalk v1.0.8/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI=
|
||||
github.com/charlievieth/fastwalk v1.0.9 h1:Odb92AfoReO3oFBfDGT5J+nwgzQPF/gWAw6E6/lkor0=
|
||||
github.com/charlievieth/fastwalk v1.0.9/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI=
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU=
|
||||
github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg=
|
||||
github.com/junegunn/go-shellwords v0.0.0-20240813092932-a62c48c52e97 h1:rqzLixVo1c/GQW6px9j1xQmlvQIn+lf/V6M1UQ7IFzw=
|
||||
github.com/junegunn/go-shellwords v0.0.0-20240813092932-a62c48c52e97/go.mod h1:6EILKtGpo5t+KLb85LNZLAF6P9LKp78hJI80PXMcn3c=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
|
||||
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
@@ -36,14 +36,14 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
|
45
install
45
install
@@ -2,7 +2,7 @@
|
||||
|
||||
set -u
|
||||
|
||||
version=0.54.1
|
||||
version=0.57.0
|
||||
auto_completion=
|
||||
key_bindings=
|
||||
update_config=2
|
||||
@@ -168,8 +168,8 @@ archi=$(uname -sm)
|
||||
binary_available=1
|
||||
binary_error=""
|
||||
case "$archi" in
|
||||
Darwin\ arm64) download fzf-$version-darwin_arm64.zip ;;
|
||||
Darwin\ x86_64) download fzf-$version-darwin_amd64.zip ;;
|
||||
Darwin\ arm64) download fzf-$version-darwin_arm64.tar.gz ;;
|
||||
Darwin\ x86_64) download fzf-$version-darwin_amd64.tar.gz ;;
|
||||
Linux\ armv5*) download fzf-$version-linux_armv5.tar.gz ;;
|
||||
Linux\ armv6*) download fzf-$version-linux_armv6.tar.gz ;;
|
||||
Linux\ armv7*) download fzf-$version-linux_armv7.tar.gz ;;
|
||||
@@ -295,35 +295,44 @@ EOF
|
||||
fi
|
||||
|
||||
append_line() {
|
||||
set -e
|
||||
|
||||
local update line file pat lno
|
||||
local update line file pat lines
|
||||
update="$1"
|
||||
line="$2"
|
||||
file="$3"
|
||||
pat="${4:-}"
|
||||
lno=""
|
||||
lines=""
|
||||
|
||||
echo "Update $file:"
|
||||
echo " - $line"
|
||||
if [ -f "$file" ]; then
|
||||
if [ $# -lt 4 ]; then
|
||||
lno=$(\grep -nF "$line" "$file" | sed 's/:.*//' | tr '\n' ' ')
|
||||
lines=$(\grep -nF "$line" "$file")
|
||||
else
|
||||
lno=$(\grep -nF "$pat" "$file" | sed 's/:.*//' | tr '\n' ' ')
|
||||
lines=$(\grep -nF "$pat" "$file")
|
||||
fi
|
||||
fi
|
||||
if [ -n "$lno" ]; then
|
||||
echo " - Already exists: line #$lno"
|
||||
|
||||
if [ -n "$lines" ]; then
|
||||
echo " - Already exists:"
|
||||
sed 's/^/ Line /' <<< "$lines"
|
||||
|
||||
update=0
|
||||
if ! \grep -qv "^[0-9]*:[[:space:]]*#" <<< "$lines" ; then
|
||||
echo " - But they all seem to be commented"
|
||||
ask " - Continue modifying $file?"
|
||||
update=$?
|
||||
fi
|
||||
fi
|
||||
|
||||
set -e
|
||||
if [ "$update" -eq 1 ]; then
|
||||
[ -f "$file" ] && echo >> "$file"
|
||||
echo "$line" >> "$file"
|
||||
echo " + Added"
|
||||
else
|
||||
if [ $update -eq 1 ]; then
|
||||
[ -f "$file" ] && echo >> "$file"
|
||||
echo "$line" >> "$file"
|
||||
echo " + Added"
|
||||
else
|
||||
echo " ~ Skipped"
|
||||
fi
|
||||
echo " ~ Skipped"
|
||||
fi
|
||||
|
||||
echo
|
||||
set +e
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
$version="0.54.1"
|
||||
$version="0.57.0"
|
||||
|
||||
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
|
||||
|
2
main.go
2
main.go
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/junegunn/fzf/src/protector"
|
||||
)
|
||||
|
||||
var version = "0.54"
|
||||
var version = "0.57"
|
||||
var revision = "devel"
|
||||
|
||||
//go:embed shell/key-bindings.bash
|
||||
|
@@ -21,13 +21,13 @@ 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
|
||||
THE SOFTWARE.
|
||||
..
|
||||
.TH fzf\-tmux 1 "Jul 2024" "fzf 0.54.1" "fzf\-tmux - open fzf in tmux split pane"
|
||||
.TH fzf\-tmux 1 "Dec 2024" "fzf 0.57.0" "fzf\-tmux - open fzf in tmux split pane"
|
||||
|
||||
.SH NAME
|
||||
fzf\-tmux - open fzf in tmux split pane
|
||||
|
||||
.SH SYNOPSIS
|
||||
.B fzf\-tmux [LAYOUT OPTIONS] [\-\-] [FZF OPTIONS]
|
||||
.B fzf\-tmux [\fILAYOUT OPTIONS\fR] [\-\-] [\fIFZF OPTIONS\fR]
|
||||
|
||||
.SH DESCRIPTION
|
||||
fzf\-tmux is a wrapper script for fzf that opens fzf in a tmux split pane or in
|
||||
|
@@ -21,13 +21,13 @@ 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
|
||||
THE SOFTWARE.
|
||||
..
|
||||
.TH fzf 1 "Jul 2024" "fzf 0.54.1" "fzf - a command-line fuzzy finder"
|
||||
.TH fzf 1 "Dec 2024" "fzf 0.57.0" "fzf - a command-line fuzzy finder"
|
||||
|
||||
.SH NAME
|
||||
fzf - a command-line fuzzy finder
|
||||
|
||||
.SH SYNOPSIS
|
||||
fzf [options]
|
||||
fzf [\fIoptions\fR]
|
||||
|
||||
.SH DESCRIPTION
|
||||
fzf is an interactive filter program for any kind of list.
|
||||
@@ -208,6 +208,9 @@ Indicator for wrapped lines. The default is '↳ ' or '> ' depending on
|
||||
.B "\-\-no\-multi\-line"
|
||||
Disable multi-line display of items when using \fB\-\-read0\fR
|
||||
.TP
|
||||
.BI "\-\-gap" "[=N]"
|
||||
Render empty lines between each item
|
||||
.TP
|
||||
.B "\-\-keep\-right"
|
||||
Keep the right end of the line visible when it's too long. Effective only when
|
||||
the query string is empty.
|
||||
@@ -526,7 +529,7 @@ lines that follow.
|
||||
Print header before the prompt line
|
||||
.TP
|
||||
.BI "\-\-ellipsis=" "STR"
|
||||
Ellipsis to show when line is truncated (default: '..')
|
||||
Ellipsis to show when line is truncated (default: '··')
|
||||
.SS Display
|
||||
.TP
|
||||
.B "\-\-ansi"
|
||||
@@ -756,7 +759,7 @@ default value 0 (or \fBcenter\fR) will put the label at the center of the
|
||||
border line.
|
||||
|
||||
.TP
|
||||
.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)]"
|
||||
.BI "\-\-preview\-window=" "[POSITION][,SIZE[%]][,border\-BORDER_OPT][,[no]wrap][,[no]follow][,[no]cycle][,[no]info][,[no]hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]"
|
||||
|
||||
.RS
|
||||
.B POSITION: (default: right)
|
||||
@@ -790,6 +793,9 @@ e.g.
|
||||
|
||||
* Cyclic scrolling is enabled with \fBcycle\fR flag.
|
||||
|
||||
* To hide the scroll offset information on the top right corner, specify
|
||||
\fBnoinfo\fR.
|
||||
|
||||
* To change the style of the border of the preview window, specify one of
|
||||
the options for \fB\-\-border\fR with \fBborder\-\fR prefix.
|
||||
e.g. \fBborder\-rounded\fR (border with rounded edges, default),
|
||||
@@ -799,7 +805,7 @@ e.g. \fBborder\-rounded\fR (border with rounded edges, default),
|
||||
* \fB[:+SCROLL[OFFSETS][/DENOM]]\fR determines the initial scroll offset of the
|
||||
preview window.
|
||||
|
||||
- \fBSCROLL\fR can be either a numeric integer or a single-field index expression that refers to a numeric integer.
|
||||
- \fBSCROLL\fR can be either a numeric integer or a single-field index expression that refers to a numeric integer or {n} to refer to the zero-based ordinal index of the current item.
|
||||
|
||||
- The optional \fBOFFSETS\fR part is for adjusting the base offset. It should be given as a series of signed integers (\fB\-INTEGER\fR or \fB+INTEGER\fR).
|
||||
|
||||
@@ -999,8 +1005,8 @@ Determines the behavior of the built-in directory walker that is used when
|
||||
.br
|
||||
|
||||
.TP
|
||||
.B "\-\-walker\-root=DIR"
|
||||
The root directory from which to start the built-in directory walker.
|
||||
.B "\-\-walker\-root=DIR [...]"
|
||||
List of directory names to start the built-in directory walker.
|
||||
The default value is the current working directory.
|
||||
|
||||
.TP
|
||||
@@ -1116,9 +1122,6 @@ fzf exports the following environment variables to its child processes.
|
||||
.br
|
||||
.BR FZF_PORT " Port number when \-\-listen option is used"
|
||||
.br
|
||||
|
||||
The following variables are additionally exported to the preview commands.
|
||||
|
||||
.BR FZF_PREVIEW_TOP " Top position of the preview window"
|
||||
.br
|
||||
.BR FZF_PREVIEW_LEFT " Left position of the preview window"
|
||||
@@ -1146,6 +1149,22 @@ A term can be prefixed by \fB^\fR, or suffixed by \fB$\fR to become an
|
||||
anchored-match term. Then fzf will search for the lines that start with or end
|
||||
with the given string. An anchored-match term is also an exact-match term.
|
||||
|
||||
.SS Exact\-boundary\-match (quoted both ends)
|
||||
A single-quoted term is interpreted as an "exact\-boundary\-match". fzf will
|
||||
search for the exact occurrences of the string with both ends at the word
|
||||
boundaries. Unlike in regular expressions, this also sees an underscore as
|
||||
a word boundary. But the words around underscores are ranked lower and appear
|
||||
later in the result than the other words around the other types of word
|
||||
boundaries.
|
||||
|
||||
1. xxx foo xxx (highest score)
|
||||
.br
|
||||
2. xxx foo_xxx
|
||||
.br
|
||||
3. xxx_foo xxx
|
||||
.br
|
||||
4. xxx_foo_xxx (lowest score)
|
||||
|
||||
.SS Negation
|
||||
If a term is prefixed by \fB!\fR, fzf will exclude the lines that satisfy the
|
||||
term from the result. In this case, fzf performs exact match by default.
|
||||
|
@@ -198,6 +198,7 @@ function! s:compare_binary_versions(a, b)
|
||||
return s:compare_versions(s:get_version(a:a), s:get_version(a:b))
|
||||
endfunction
|
||||
|
||||
let s:min_version = '0.53.0'
|
||||
let s:checked = {}
|
||||
function! fzf#exec(...)
|
||||
if !exists('s:exec')
|
||||
@@ -225,7 +226,11 @@ function! fzf#exec(...)
|
||||
let s:exec = binaries[-1]
|
||||
endif
|
||||
|
||||
if a:0 && !has_key(s:checked, a:1)
|
||||
let min_version = s:min_version
|
||||
if a:0 && s:compare_versions(a:1, min_version) > 0
|
||||
let min_version = a:1
|
||||
endif
|
||||
if !has_key(s:checked, min_version)
|
||||
let fzf_version = s:get_version(s:exec)
|
||||
if empty(fzf_version)
|
||||
let message = printf('Failed to run "%s --version"', s:exec)
|
||||
@@ -233,17 +238,17 @@ function! fzf#exec(...)
|
||||
throw message
|
||||
end
|
||||
|
||||
if s:compare_versions(fzf_version, a:1) >= 0
|
||||
let s:checked[a:1] = 1
|
||||
if s:compare_versions(fzf_version, min_version) >= 0
|
||||
let s:checked[min_version] = 1
|
||||
return s:exec
|
||||
elseif a:0 < 2 && input(printf('You need fzf %s or above. Found: %s. Download binary? (y/n) ', a:1, fzf_version)) =~? '^y'
|
||||
elseif a:0 < 2 && input(printf('You need fzf %s or above. Found: %s. Download binary? (y/n) ', min_version, fzf_version)) =~? '^y'
|
||||
let s:versions = {}
|
||||
unlet s:exec
|
||||
redraw
|
||||
call fzf#install()
|
||||
return fzf#exec(a:1, 1)
|
||||
return fzf#exec(min_version, 1)
|
||||
else
|
||||
throw printf('You need to upgrade fzf (required: %s or above)', a:1)
|
||||
throw printf('You need to upgrade fzf (required: %s or above)', min_version)
|
||||
endif
|
||||
endif
|
||||
|
||||
@@ -665,21 +670,17 @@ else
|
||||
let s:launcher = function('s:xterm_launcher')
|
||||
endif
|
||||
|
||||
function! s:exit_handler(code, command, ...)
|
||||
if a:code == 130
|
||||
return 0
|
||||
elseif has('nvim') && a:code == 129
|
||||
" When deleting the terminal buffer while fzf is still running,
|
||||
" Nvim sends SIGHUP.
|
||||
return 0
|
||||
elseif a:code > 1
|
||||
function! s:exit_handler(dict, code, command, ...)
|
||||
if has_key(a:dict, 'exit')
|
||||
call a:dict.exit(a:code)
|
||||
endif
|
||||
if a:code == 2
|
||||
call s:error('Error running ' . a:command)
|
||||
if !empty(a:000)
|
||||
sleep
|
||||
endif
|
||||
return 0
|
||||
endif
|
||||
return 1
|
||||
return a:code
|
||||
endfunction
|
||||
|
||||
function! s:execute(dict, command, use_height, temps) abort
|
||||
@@ -731,7 +732,7 @@ function! s:execute(dict, command, use_height, temps) abort
|
||||
let exit_status = v:shell_error
|
||||
redraw!
|
||||
let lines = s:collect(a:temps)
|
||||
return s:exit_handler(exit_status, command) ? lines : []
|
||||
return s:exit_handler(a:dict, exit_status, command) < 2 ? lines : []
|
||||
endfunction
|
||||
|
||||
function! s:execute_tmux(dict, command, temps) abort
|
||||
@@ -746,7 +747,7 @@ function! s:execute_tmux(dict, command, temps) abort
|
||||
let exit_status = v:shell_error
|
||||
redraw!
|
||||
let lines = s:collect(a:temps)
|
||||
return s:exit_handler(exit_status, command) ? lines : []
|
||||
return s:exit_handler(a:dict, exit_status, command) < 2 ? lines : []
|
||||
endfunction
|
||||
|
||||
function! s:calc_size(max, val, dict)
|
||||
@@ -912,7 +913,7 @@ function! s:execute_term(dict, command, temps) abort
|
||||
endif
|
||||
|
||||
let lines = s:collect(self.temps)
|
||||
if !s:exit_handler(a:code, self.command, 1)
|
||||
if s:exit_handler(self.dict, a:code, self.command, 1) >= 2
|
||||
return
|
||||
endif
|
||||
|
||||
|
@@ -264,6 +264,7 @@ _fzf_handle_dynamic_completion() {
|
||||
# _completion_loader may not have updated completion for the command
|
||||
if [[ "$(complete -p "$orig_cmd" 2> /dev/null)" != "$orig_complete" ]]; then
|
||||
__fzf_orig_completion < <(complete -p "$orig_cmd" 2> /dev/null)
|
||||
__fzf_orig_completion_get_orig_func "$cmd" || ret=1
|
||||
|
||||
# Update orig_complete by _fzf_orig_completion entry
|
||||
[[ $orig_complete =~ ' -F '(_fzf_[^ ]+)' ' ]] &&
|
||||
@@ -289,7 +290,7 @@ __fzf_generic_path_completion() {
|
||||
fi
|
||||
COMPREPLY=()
|
||||
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
[[ $COMP_CWORD -ge 0 ]] && cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
|
||||
base=${cur:0:${#cur}-${#trigger}}
|
||||
eval "base=$base" 2> /dev/null || return
|
||||
@@ -376,7 +377,7 @@ _fzf_complete() {
|
||||
selected=$(
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse" "${FZF_COMPLETION_OPTS-} $str_arg") \
|
||||
FZF_DEFAULT_OPTS_FILE='' \
|
||||
__fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | $post | command tr '\n' ' ')
|
||||
__fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | eval "$post" | command tr '\n' ' ')
|
||||
selected=${selected% } # Strip trailing space not to repeat "-o nospace"
|
||||
if [[ -n "$selected" ]]; then
|
||||
COMPREPLY=("$selected")
|
||||
@@ -410,7 +411,8 @@ _fzf_complete_kill() {
|
||||
_fzf_proc_completion() {
|
||||
_fzf_complete -m --header-lines=1 --no-preview --wrap -- "$@" < <(
|
||||
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
|
||||
command ps -eo user,pid,ppid,time,args # For BusyBox
|
||||
command ps -eo user,pid,ppid,time,args 2> /dev/null || # For BusyBox
|
||||
command ps --everyone --full --windows # For cygwin
|
||||
)
|
||||
}
|
||||
|
||||
@@ -480,10 +482,36 @@ complete -o default -F _fzf_opts_completion fzf
|
||||
# fzf-tmux specific options (like `-w WIDTH`) are left as a future patch.
|
||||
complete -o default -F _fzf_opts_completion fzf-tmux
|
||||
|
||||
# Default path completion
|
||||
__fzf_default_completion() {
|
||||
__fzf_generic_path_completion _fzf_compgen_path "-m" "" "$@"
|
||||
|
||||
# Dynamic completion loader has updated the completion for the command
|
||||
if [[ $? -eq 124 ]]; then
|
||||
# We trigger _fzf_setup_completion so that fuzzy completion for the command
|
||||
# still works. However, loader can update the completion for multiple
|
||||
# commands at once, and fuzzy completion will no longer work for those
|
||||
# other commands. e.g. pytest -> py.test, pytest-2, pytest-3, etc
|
||||
_fzf_setup_completion path "$1"
|
||||
return 124
|
||||
fi
|
||||
}
|
||||
|
||||
# Set fuzzy path completion as the default completion for all commands.
|
||||
# We can't set up default completion,
|
||||
# 1. if it's already set up by another script
|
||||
# 2. or if the current version of bash doesn't support -D option
|
||||
complete | command grep -q __fzf_default_completion ||
|
||||
complete | command grep -- '-D$' | command grep -qv _comp_complete_load ||
|
||||
complete -D -F __fzf_default_completion -o default -o bashdefault 2> /dev/null
|
||||
|
||||
d_cmds="${FZF_COMPLETION_DIR_COMMANDS-cd pushd rmdir}"
|
||||
|
||||
# NOTE: $FZF_COMPLETION_PATH_COMMANDS and $FZF_COMPLETION_VAR_COMMANDS are
|
||||
# undocumented and subject to change in the future.
|
||||
#
|
||||
# NOTE: Although we have default completion, we still need to set up completion
|
||||
# for each command in case they already have completion set up by another script.
|
||||
a_cmds="${FZF_COMPLETION_PATH_COMMANDS-"
|
||||
awk bat cat code diff diff3
|
||||
emacs emacsclient ex file ftp g++ gcc gvim head hg hx java
|
||||
|
@@ -120,25 +120,18 @@ __fzf_comprun() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Extract the name of the command. e.g. foo=1 bar baz**<tab>
|
||||
# Extract the name of the command. e.g. ls; foo=1 ssh **<tab>
|
||||
__fzf_extract_command() {
|
||||
local token tokens
|
||||
tokens=(${(z)1})
|
||||
for token in $tokens; do
|
||||
token=${(Q)token}
|
||||
if [[ "$token" =~ [[:alnum:]] && ! "$token" =~ "=" ]]; then
|
||||
echo "$token"
|
||||
return
|
||||
fi
|
||||
done
|
||||
echo "${tokens[1]}"
|
||||
# Control completion with the "compstate" parameter, insert and list nothing
|
||||
compstate[insert]=
|
||||
compstate[list]=
|
||||
cmd_word="${(Q)words[1]}"
|
||||
}
|
||||
|
||||
__fzf_generic_path_completion() {
|
||||
local base lbuf cmd compgen fzf_opts suffix tail dir leftover matches
|
||||
local base lbuf compgen fzf_opts suffix tail dir leftover matches
|
||||
base=$1
|
||||
lbuf=$2
|
||||
cmd=$(__fzf_extract_command "$lbuf")
|
||||
compgen=$3
|
||||
fzf_opts=$4
|
||||
suffix=$5
|
||||
@@ -161,7 +154,7 @@ __fzf_generic_path_completion() {
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-}")
|
||||
unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
|
||||
if declare -f "$compgen" > /dev/null; then
|
||||
eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover"
|
||||
eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd_word" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover"
|
||||
else
|
||||
if [[ $compgen =~ dir ]]; then
|
||||
walker=dir,follow
|
||||
@@ -170,7 +163,7 @@ __fzf_generic_path_completion() {
|
||||
walker=file,dir,follow,hidden
|
||||
rest=${FZF_COMPLETION_PATH_OPTS-}
|
||||
fi
|
||||
__fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" --walker "$walker" --walker-root="$dir" ${(Q)${(Z+n+)rest}} < /dev/tty
|
||||
__fzf_comprun "$cmd_word" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" --walker "$walker" --walker-root="$dir" ${(Q)${(Z+n+)rest}} < /dev/tty
|
||||
fi | while read -r item; do
|
||||
item="${item%$suffix}$suffix"
|
||||
echo -n -E "${(q)item} "
|
||||
@@ -227,10 +220,9 @@ _fzf_complete() {
|
||||
rest=("$@")
|
||||
fi
|
||||
|
||||
local fifo lbuf cmd matches post
|
||||
local fifo lbuf matches post
|
||||
fifo="${TMPDIR:-/tmp}/fzf-complete-fifo-$$"
|
||||
lbuf=${rest[0]}
|
||||
cmd=$(__fzf_extract_command "$lbuf")
|
||||
post="${funcstack[1]}_post"
|
||||
type $post > /dev/null 2>&1 || post=cat
|
||||
|
||||
@@ -238,7 +230,7 @@ _fzf_complete() {
|
||||
matches=$(
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse" "${FZF_COMPLETION_OPTS-} $str_arg") \
|
||||
FZF_DEFAULT_OPTS_FILE='' \
|
||||
__fzf_comprun "$cmd" "${args[@]}" -q "${(Q)prefix}" < "$fifo" | $post | tr '\n' ' ')
|
||||
__fzf_comprun "$cmd_word" "${args[@]}" -q "${(Q)prefix}" < "$fifo" | $post | tr '\n' ' ')
|
||||
if [ -n "$matches" ]; then
|
||||
LBUFFER="$lbuf$matches"
|
||||
fi
|
||||
@@ -300,7 +292,8 @@ _fzf_complete_unalias() {
|
||||
_fzf_complete_kill() {
|
||||
_fzf_complete -m --header-lines=1 --no-preview --wrap -- "$@" < <(
|
||||
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
|
||||
command ps -eo user,pid,ppid,time,args # For BusyBox
|
||||
command ps -eo user,pid,ppid,time,args 2> /dev/null || # For BusyBox
|
||||
command ps --everyone --full --windows # For cygwin
|
||||
)
|
||||
}
|
||||
|
||||
@@ -309,7 +302,7 @@ _fzf_complete_kill_post() {
|
||||
}
|
||||
|
||||
fzf-completion() {
|
||||
local tokens cmd prefix trigger tail matches lbuf d_cmds
|
||||
local tokens prefix trigger tail matches lbuf d_cmds cursor_pos cmd_word
|
||||
setopt localoptions noshwordsplit noksh_arrays noposixbuiltins
|
||||
|
||||
# http://zsh.sourceforge.net/FAQ/zshfaq03.html
|
||||
@@ -320,11 +313,9 @@ fzf-completion() {
|
||||
return
|
||||
fi
|
||||
|
||||
cmd=$(__fzf_extract_command "$LBUFFER")
|
||||
|
||||
# Explicitly allow for empty trigger.
|
||||
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
||||
[ -z "$trigger" -a ${LBUFFER[-1]} = ' ' ] && tokens+=("")
|
||||
[[ -z $trigger && ${LBUFFER[-1]} == ' ' ]] && tokens+=("")
|
||||
|
||||
# When the trigger starts with ';', it becomes a separate token
|
||||
if [[ ${LBUFFER} = *"${tokens[-2]-}${tokens[-1]}" ]]; then
|
||||
@@ -339,16 +330,37 @@ fzf-completion() {
|
||||
if [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then
|
||||
d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS-cd pushd rmdir})
|
||||
|
||||
{
|
||||
cursor_pos=$CURSOR
|
||||
# Move the cursor before the trigger to preserve word array elements when
|
||||
# trigger chars like ';' or '`' would otherwise reset the 'words' array.
|
||||
CURSOR=$((cursor_pos - ${#trigger} - 1))
|
||||
# Check if at least one completion system (old or new) is active.
|
||||
# If at least one user-defined completion widget is detected, nothing will
|
||||
# be completed if neither the old nor the new completion system is enabled.
|
||||
# In such cases, the 'zsh/compctl' module is loaded as a fallback.
|
||||
if ! zmodload -F zsh/parameter p:functions 2>/dev/null || ! (( ${+functions[compdef]} )); then
|
||||
zmodload -F zsh/compctl 2>/dev/null
|
||||
fi
|
||||
# Create a completion widget to access the 'words' array (man zshcompwid)
|
||||
zle -C __fzf_extract_command .complete-word __fzf_extract_command
|
||||
zle __fzf_extract_command
|
||||
} always {
|
||||
CURSOR=$cursor_pos
|
||||
# Delete the completion widget
|
||||
zle -D __fzf_extract_command 2>/dev/null
|
||||
}
|
||||
|
||||
[ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}}
|
||||
if [[ $prefix = *'$('* ]] || [[ $prefix = *'<('* ]] || [[ $prefix = *'>('* ]] || [[ $prefix = *':='* ]] || [[ $prefix = *'`'* ]]; then
|
||||
return
|
||||
fi
|
||||
[ -n "${tokens[-1]}" ] && lbuf=${lbuf:0:-${#tokens[-1]}}
|
||||
|
||||
if eval "type _fzf_complete_${cmd} > /dev/null"; then
|
||||
prefix="$prefix" eval _fzf_complete_${cmd} ${(q)lbuf}
|
||||
if eval "noglob type _fzf_complete_${cmd_word} >/dev/null"; then
|
||||
prefix="$prefix" eval _fzf_complete_${cmd_word} ${(q)lbuf}
|
||||
zle reset-prompt
|
||||
elif [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then
|
||||
elif [ ${d_cmds[(i)$cmd_word]} -le ${#d_cmds} ]; then
|
||||
_fzf_dir_completion "$prefix" "$lbuf"
|
||||
else
|
||||
_fzf_path_completion "$prefix" "$lbuf"
|
||||
@@ -365,6 +377,7 @@ fzf-completion() {
|
||||
unset binding
|
||||
}
|
||||
|
||||
# Normal widget
|
||||
zle -N fzf-completion
|
||||
bindkey '^I' fzf-completion
|
||||
fi
|
||||
|
@@ -11,8 +11,6 @@
|
||||
# - $FZF_ALT_C_COMMAND
|
||||
# - $FZF_ALT_C_OPTS
|
||||
|
||||
status is-interactive; or exit 0
|
||||
|
||||
|
||||
# Key bindings
|
||||
# ------------
|
||||
@@ -23,7 +21,7 @@ function fzf_key_bindings
|
||||
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
||||
echo "--height $FZF_TMUX_HEIGHT --bind=ctrl-z:ignore" $argv[1]
|
||||
command cat "$FZF_DEFAULT_OPTS_FILE" 2> /dev/null
|
||||
test -r "$FZF_DEFAULT_OPTS_FILE"; and string collect -N -- <$FZF_DEFAULT_OPTS_FILE
|
||||
echo $FZF_DEFAULT_OPTS $argv[2]
|
||||
end
|
||||
|
||||
@@ -36,12 +34,12 @@ function fzf_key_bindings
|
||||
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
||||
begin
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path --walker-root='$dir'" "$FZF_CTRL_T_OPTS")
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path --walker-root=$dir" "$FZF_CTRL_T_OPTS")
|
||||
set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND"
|
||||
set -lx FZF_DEFAULT_OPTS_FILE ''
|
||||
eval (__fzfcmd)' -m --query "'$fzf_query'"' | while read -l r; set result $result $r; end
|
||||
eval (__fzfcmd) -m --query=$fzf_query | while read -l r; set -a result $r; end
|
||||
end
|
||||
if [ -z "$result" ]
|
||||
if test -z "$result"
|
||||
commandline -f repaint
|
||||
return
|
||||
else
|
||||
@@ -50,7 +48,7 @@ function fzf_key_bindings
|
||||
end
|
||||
for i in $result
|
||||
commandline -it -- $prefix
|
||||
commandline -it -- (string escape $i)
|
||||
commandline -it -- (string escape -- $i)
|
||||
commandline -it -- ' '
|
||||
end
|
||||
commandline -f repaint
|
||||
@@ -59,27 +57,27 @@ function fzf_key_bindings
|
||||
function fzf-history-widget -d "Show command history"
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
||||
begin
|
||||
set -l FISH_MAJOR (echo $version | cut -f1 -d.)
|
||||
set -l FISH_MINOR (echo $version | cut -f2 -d.)
|
||||
set -l FISH_MAJOR (string split '.' -- $version)[1]
|
||||
set -l FISH_MINOR (string split '.' -- $version)[2]
|
||||
|
||||
# merge history from other sessions before searching
|
||||
if test -z "$fish_private_mode"
|
||||
builtin history merge
|
||||
end
|
||||
test -z "$fish_private_mode"; and builtin history merge
|
||||
|
||||
# history's -z flag is needed for multi-line support.
|
||||
# history's -z flag was added in fish 2.4.0, so don't use it for versions
|
||||
# before 2.4.0.
|
||||
if [ "$FISH_MAJOR" -gt 2 -o \( "$FISH_MAJOR" -eq 2 -a "$FISH_MINOR" -ge 4 \) ];
|
||||
if type -P perl > /dev/null 2>&1
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '"\t"↳ ' --highlight-line $FZF_CTRL_R_OPTS +m")
|
||||
set -lx FZF_DEFAULT_OPTS_FILE ''
|
||||
builtin history -z --reverse | command perl -0 -pe 's/^/$.\t/g; s/\n/\n\t/gm' | eval (__fzfcmd) --tac --read0 --print0 -q '(commandline)' | command perl -pe 's/^\d*\t//' | read -lz result
|
||||
if test "$FISH_MAJOR" -gt 2 -o \( "$FISH_MAJOR" -eq 2 -a "$FISH_MINOR" -ge 4 \)
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '"\t"↳ ' --highlight-line $FZF_CTRL_R_OPTS +m")
|
||||
set -lx FZF_DEFAULT_OPTS_FILE ''
|
||||
if type -q perl
|
||||
builtin history -z --reverse | command perl -0 -pe 's/^/$.\t/g; s/\n/\n\t/gm' | eval (__fzfcmd) --tac --read0 --print0 -q '(commandline)' | string replace -r '^\d*\t' '' | read -lz result
|
||||
and commandline -- $result
|
||||
else
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "" "--scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '"\t"↳ ' --highlight-line $FZF_CTRL_R_OPTS +m")
|
||||
set -lx FZF_DEFAULT_OPTS_FILE ''
|
||||
builtin history -z | eval (__fzfcmd) --read0 --print0 -q '(commandline)' | read -lz result
|
||||
set -l line 0
|
||||
for i in (builtin history -z --reverse | string split0)
|
||||
set line (math $line + 1)
|
||||
string escape -n -- $line\t$i
|
||||
end | string join0 | string replace -a '\n' '\n\t' | string unescape -n | eval (__fzfcmd) --tac --read0 --print0 -q '(commandline)' | string replace -r '^\d*\t' '' | read -lz result
|
||||
and commandline -- $result
|
||||
end
|
||||
else
|
||||
@@ -98,12 +96,12 @@ function fzf_key_bindings
|
||||
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
||||
begin
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --walker=dir,follow,hidden --scheme=path --walker-root='$dir'" "$FZF_ALT_C_OPTS")
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --walker=dir,follow,hidden --scheme=path --walker-root=$dir" "$FZF_ALT_C_OPTS")
|
||||
set -lx FZF_DEFAULT_OPTS_FILE ''
|
||||
set -lx FZF_DEFAULT_COMMAND "$FZF_ALT_C_COMMAND"
|
||||
eval (__fzfcmd)' +m --query "'$fzf_query'"' | read -l result
|
||||
eval (__fzfcmd) +m --query=$fzf_query | read -l result
|
||||
|
||||
if [ -n "$result" ]
|
||||
if test -n "$result"
|
||||
cd -- $result
|
||||
|
||||
# Remove last token from commandline.
|
||||
@@ -118,9 +116,9 @@ function fzf_key_bindings
|
||||
function __fzfcmd
|
||||
test -n "$FZF_TMUX"; or set FZF_TMUX 0
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
||||
if [ -n "$FZF_TMUX_OPTS" ]
|
||||
if test -n "$FZF_TMUX_OPTS"
|
||||
echo "fzf-tmux $FZF_TMUX_OPTS -- "
|
||||
else if [ $FZF_TMUX -eq 1 ]
|
||||
else if test "$FZF_TMUX" = "1"
|
||||
echo "fzf-tmux -d$FZF_TMUX_HEIGHT -- "
|
||||
else
|
||||
echo "fzf"
|
||||
@@ -135,7 +133,7 @@ function fzf_key_bindings
|
||||
bind \ec fzf-cd-widget
|
||||
end
|
||||
|
||||
if bind -M insert > /dev/null 2>&1
|
||||
if bind -M insert &> /dev/null
|
||||
bind -M insert \cr fzf-history-widget
|
||||
if not set -q FZF_CTRL_T_COMMAND; or test -n "$FZF_CTRL_T_COMMAND"
|
||||
bind -M insert \ct fzf-file-widget
|
||||
@@ -152,40 +150,50 @@ function fzf_key_bindings
|
||||
set -l prefix (string match -r -- '^-[^\s=]+=' $commandline)
|
||||
set commandline (string replace -- "$prefix" '' $commandline)
|
||||
|
||||
# escape special characters, except for the $ sign of valid variable names,
|
||||
# so that after eval, the original string is returned, but with the
|
||||
# variable names replaced by their values.
|
||||
set commandline (string escape -n -- $commandline)
|
||||
set commandline (string replace -r -a '\x5c\$(?=[\w])' '\$' -- $commandline)
|
||||
|
||||
# eval is used to do shell expansion on paths
|
||||
eval set commandline $commandline
|
||||
|
||||
if [ -z $commandline ]
|
||||
# Combine multiple consecutive slashes into one
|
||||
set commandline (string replace -r -a '/+' '/' -- $commandline)
|
||||
|
||||
if test -z "$commandline"
|
||||
# Default to current directory with no --query
|
||||
set dir '.'
|
||||
set fzf_query ''
|
||||
else
|
||||
set dir (__fzf_get_dir $commandline)
|
||||
|
||||
if [ "$dir" = "." -a (string sub -l 1 -- $commandline) != '.' ]
|
||||
# BUG: on combined expressions, if a left argument is a single `!`, the
|
||||
# builtin test command of fish will treat it as the ! operator. To
|
||||
# overcome this, have the variable parts on the right.
|
||||
if test "." = "$dir" -a "." != (string sub -l 1 -- $commandline)
|
||||
# if $dir is "." but commandline is not a relative path, this means no file path found
|
||||
set fzf_query $commandline
|
||||
else
|
||||
# Also remove trailing slash after dir, to "split" input properly
|
||||
set fzf_query (string replace -r "^$dir/?" -- '' "$commandline")
|
||||
set fzf_query (string replace -r "^$dir/?" '' -- $commandline)
|
||||
end
|
||||
end
|
||||
|
||||
echo $dir
|
||||
echo $fzf_query
|
||||
echo (string escape -- $dir)
|
||||
echo (string escape -- $fzf_query)
|
||||
echo $prefix
|
||||
end
|
||||
|
||||
function __fzf_get_dir -d 'Find the longest existing filepath from input string'
|
||||
set dir $argv
|
||||
|
||||
# Strip all trailing slashes. Ignore if $dir is root dir (/)
|
||||
if [ (string length -- $dir) -gt 1 ]
|
||||
set dir (string replace -r '/*$' -- '' $dir)
|
||||
end
|
||||
# Strip trailing slash, unless $dir is root dir (/)
|
||||
set dir (string replace -r '(?<!^)/$' '' -- $dir)
|
||||
|
||||
# Iteratively check if dir exists and strip tail end of path
|
||||
while [ ! -d "$dir" ]
|
||||
while test ! -d "$dir"
|
||||
# If path is absolute, this can keep going until ends up at /
|
||||
# If path is relative, this can keep going until entire input is consumed, dirname returns "."
|
||||
set dir (dirname -- "$dir")
|
||||
|
@@ -108,9 +108,10 @@ fi
|
||||
fzf-history-widget() {
|
||||
local selected
|
||||
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases noglob nobash_rematch 2> /dev/null
|
||||
# Ensure the associative history array, which maps event numbers to the full
|
||||
# history lines, is loaded, and that Perl is installed for multi-line output.
|
||||
if zmodload -F zsh/parameter p:history 2>/dev/null && (( ${#commands[perl]} )); then
|
||||
# Ensure the module is loaded if not already, and the required features, such
|
||||
# as the associative 'history' array, which maps event numbers to full history
|
||||
# lines, are set. Also, make sure Perl is installed for multi-line output.
|
||||
if zmodload -F zsh/parameter p:{commands,history} 2>/dev/null && (( ${+commands[perl]} )); then
|
||||
selected="$(printf '%s\t%s\000' "${(kv)history[@]}" |
|
||||
perl -0 -ne 'if (!$seen{(/^\s*[0-9]+\**\t(.*)/s, $1)}++) { s/\n/\n\t/g; print; }' |
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '\t↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m --read0") \
|
||||
|
@@ -798,6 +798,14 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.C
|
||||
// The solution is much cheaper since there is only one possible alignment of
|
||||
// the pattern.
|
||||
func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||
return exactMatchNaive(caseSensitive, normalize, forward, false, text, pattern, withPos, slab)
|
||||
}
|
||||
|
||||
func ExactMatchBoundary(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||
return exactMatchNaive(caseSensitive, normalize, forward, true, text, pattern, withPos, slab)
|
||||
}
|
||||
|
||||
func exactMatchNaive(caseSensitive bool, normalize bool, forward bool, boundaryCheck bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||
if len(pattern) == 0 {
|
||||
return Result{0, 0, 0}, nil
|
||||
}
|
||||
@@ -832,10 +840,22 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *uti
|
||||
}
|
||||
pidx_ := indexAt(pidx, lenPattern, forward)
|
||||
pchar := pattern[pidx_]
|
||||
if pchar == char {
|
||||
ok := pchar == char
|
||||
if ok {
|
||||
if pidx_ == 0 {
|
||||
bonus = bonusAt(text, index_)
|
||||
}
|
||||
if boundaryCheck {
|
||||
ok = bonus >= bonusBoundary
|
||||
if ok && pidx_ == 0 {
|
||||
ok = index_ == 0 || charClassOf(text.Get(index_-1)) <= charDelimiter
|
||||
}
|
||||
if ok && pidx_ == len(pattern)-1 {
|
||||
ok = index_ == lenRunes-1 || charClassOf(text.Get(index_+1)) <= charDelimiter
|
||||
}
|
||||
}
|
||||
}
|
||||
if ok {
|
||||
pidx++
|
||||
if pidx == lenPattern {
|
||||
if bonus > bestBonus {
|
||||
@@ -861,7 +881,23 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *uti
|
||||
sidx = lenRunes - (bestPos + 1)
|
||||
eidx = lenRunes - (bestPos - lenPattern + 1)
|
||||
}
|
||||
score, _ := calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false)
|
||||
var score int
|
||||
if boundaryCheck {
|
||||
// Underscore boundaries should be ranked lower than the other types of boundaries
|
||||
score = int(bonus)
|
||||
deduct := int(bonus-bonusBoundary) + 1
|
||||
if sidx > 0 && text.Get(sidx-1) == '_' {
|
||||
score -= deduct + 1
|
||||
deduct = 1
|
||||
}
|
||||
if eidx < lenRunes && text.Get(eidx) == '_' {
|
||||
score -= deduct
|
||||
}
|
||||
// Add base score so that this can compete with other match types e.g. 'foo' | bar
|
||||
score += scoreMatch*lenPattern + int(bonusBoundaryWhite)*(lenPattern+1)
|
||||
} else {
|
||||
score, _ = calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false)
|
||||
}
|
||||
return Result{sidx, eidx, score}, nil
|
||||
}
|
||||
return Result{-1, -1, 0}, nil
|
||||
|
38
src/ansi.go
38
src/ansi.go
@@ -1,6 +1,7 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
@@ -13,22 +14,28 @@ type ansiOffset struct {
|
||||
color ansiState
|
||||
}
|
||||
|
||||
type url struct {
|
||||
uri string
|
||||
params string
|
||||
}
|
||||
|
||||
type ansiState struct {
|
||||
fg tui.Color
|
||||
bg tui.Color
|
||||
attr tui.Attr
|
||||
lbg tui.Color
|
||||
url *url
|
||||
}
|
||||
|
||||
func (s *ansiState) colored() bool {
|
||||
return s.fg != -1 || s.bg != -1 || s.attr > 0 || s.lbg >= 0
|
||||
return s.fg != -1 || s.bg != -1 || s.attr > 0 || s.lbg >= 0 || s.url != nil
|
||||
}
|
||||
|
||||
func (s *ansiState) equals(t *ansiState) bool {
|
||||
if t == nil {
|
||||
return !s.colored()
|
||||
}
|
||||
return s.fg == t.fg && s.bg == t.bg && s.attr == t.attr && s.lbg == t.lbg
|
||||
return s.fg == t.fg && s.bg == t.bg && s.attr == t.attr && s.lbg == t.lbg && s.url == t.url
|
||||
}
|
||||
|
||||
func (s *ansiState) ToString() string {
|
||||
@@ -60,7 +67,11 @@ func (s *ansiState) ToString() string {
|
||||
}
|
||||
ret += toAnsiString(s.fg, 30) + toAnsiString(s.bg, 40)
|
||||
|
||||
return "\x1b[" + strings.TrimSuffix(ret, ";") + "m"
|
||||
ret = "\x1b[" + strings.TrimSuffix(ret, ";") + "m"
|
||||
if s.url != nil {
|
||||
ret = fmt.Sprintf("\x1b]8;%s;%s\x1b\\%s\x1b]8;;\x1b", s.url.params, s.url.uri, ret)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func toAnsiString(color tui.Color, offset int) string {
|
||||
@@ -98,10 +109,19 @@ func matchOperatingSystemCommand(s string) int {
|
||||
if s[i] == '\x07' {
|
||||
return i + 1
|
||||
}
|
||||
// `\x1b]8;PARAMS;URI\x1b\\TITLE\x1b]8;;\x1b`
|
||||
// ------
|
||||
if s[i] == '\x1b' && i < len(s)-1 && s[i+1] == '\\' {
|
||||
return i + 2
|
||||
}
|
||||
}
|
||||
|
||||
// `\x1b]8;PARAMS;URI\x1b\\TITLE\x1b]8;;\x1b`
|
||||
// ------------
|
||||
if i < len(s) && s[:i+1] == "\x1b]8;;\x1b" {
|
||||
return i + 1
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
@@ -328,13 +348,21 @@ func parseAnsiCode(s string, delimiter byte) (int, byte, string) {
|
||||
func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
||||
var state ansiState
|
||||
if prevState == nil {
|
||||
state = ansiState{-1, -1, 0, -1}
|
||||
state = ansiState{-1, -1, 0, -1, nil}
|
||||
} else {
|
||||
state = ansiState{prevState.fg, prevState.bg, prevState.attr, prevState.lbg}
|
||||
state = ansiState{prevState.fg, prevState.bg, prevState.attr, prevState.lbg, prevState.url}
|
||||
}
|
||||
if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
|
||||
if prevState != nil && strings.HasSuffix(ansiCode, "0K") {
|
||||
state.lbg = prevState.bg
|
||||
} else if ansiCode == "\x1b]8;;\x1b\\" { // End of a hyperlink
|
||||
state.url = nil
|
||||
} else if strings.HasPrefix(ansiCode, "\x1b]8;") && strings.HasSuffix(ansiCode, "\x1b\\") {
|
||||
if paramsEnd := strings.IndexRune(ansiCode[4:], ';'); paramsEnd >= 0 {
|
||||
params := ansiCode[4 : 4+paramsEnd]
|
||||
uri := ansiCode[5+paramsEnd : len(ansiCode)-2]
|
||||
state.url = &url{uri: uri, params: params}
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
@@ -58,7 +58,6 @@ const (
|
||||
const (
|
||||
EvtReadNew util.EventType = iota
|
||||
EvtReadFin
|
||||
EvtReadNone
|
||||
EvtSearchNew
|
||||
EvtSearchProgress
|
||||
EvtSearchFin
|
||||
|
48
src/core.go
48
src/core.go
@@ -146,8 +146,25 @@ func Run(opts *Options) (int, error) {
|
||||
// Process executor
|
||||
executor := util.NewExecutor(opts.WithShell)
|
||||
|
||||
// Terminal I/O
|
||||
var terminal *Terminal
|
||||
var err error
|
||||
var initialEnv []string
|
||||
initialReload := opts.extractReloadOnStart()
|
||||
if opts.Filter == nil {
|
||||
terminal, err = NewTerminal(opts, eventBox, executor)
|
||||
if err != nil {
|
||||
return ExitError, err
|
||||
}
|
||||
if len(initialReload) > 0 {
|
||||
var temps []string
|
||||
initialReload, temps = terminal.replacePlaceholderInInitialCommand(initialReload)
|
||||
initialEnv = terminal.environ()
|
||||
defer removeFiles(temps)
|
||||
}
|
||||
}
|
||||
|
||||
// Reader
|
||||
reloadOnStart := opts.reloadOnStart()
|
||||
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
|
||||
var reader *Reader
|
||||
if !streamingFilter {
|
||||
@@ -155,12 +172,9 @@ func Run(opts *Options) (int, error) {
|
||||
return chunkList.Push(data)
|
||||
}, eventBox, executor, opts.ReadZero, opts.Filter == nil)
|
||||
|
||||
if reloadOnStart {
|
||||
// reload or reload-sync action is bound to 'start' event, no need to start the reader
|
||||
eventBox.Set(EvtReadNone, nil)
|
||||
} else {
|
||||
go reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
|
||||
}
|
||||
readyChan := make(chan bool)
|
||||
go reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip, initialReload, initialEnv, readyChan)
|
||||
<-readyChan
|
||||
}
|
||||
|
||||
// Matcher
|
||||
@@ -212,7 +226,7 @@ func Run(opts *Options) (int, error) {
|
||||
}
|
||||
return false
|
||||
}, eventBox, executor, opts.ReadZero, false)
|
||||
reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
|
||||
reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip, initialReload, initialEnv, nil)
|
||||
} else {
|
||||
eventBox.Unwatch(EvtReadNew)
|
||||
eventBox.WaitFor(EvtReadFin)
|
||||
@@ -234,8 +248,7 @@ func Run(opts *Options) (int, error) {
|
||||
}
|
||||
|
||||
// Synchronous search
|
||||
sync := opts.Sync && !reloadOnStart
|
||||
if sync {
|
||||
if opts.Sync {
|
||||
eventBox.Unwatch(EvtReadNew)
|
||||
eventBox.WaitFor(EvtReadFin)
|
||||
}
|
||||
@@ -244,18 +257,14 @@ func Run(opts *Options) (int, error) {
|
||||
go matcher.Loop()
|
||||
defer matcher.Stop()
|
||||
|
||||
// Terminal I/O
|
||||
terminal, err := NewTerminal(opts, eventBox, executor)
|
||||
if err != nil {
|
||||
return ExitError, err
|
||||
}
|
||||
// Handling adaptive height
|
||||
maxFit := 0 // Maximum number of items that can fit on screen
|
||||
padHeight := 0
|
||||
heightUnknown := opts.Height.auto
|
||||
if heightUnknown {
|
||||
maxFit, padHeight = terminal.MaxFitAndPad()
|
||||
}
|
||||
deferred := opts.Select1 || opts.Exit0 || sync
|
||||
deferred := opts.Select1 || opts.Exit0 || opts.Sync
|
||||
go terminal.Loop()
|
||||
if !deferred && !heightUnknown {
|
||||
// Start right away
|
||||
@@ -292,7 +301,9 @@ func Run(opts *Options) (int, error) {
|
||||
itemIndex = 0
|
||||
inputRevision.bumpMajor()
|
||||
header = make([]string, 0, opts.HeaderLines)
|
||||
go reader.restart(command, environ)
|
||||
readyChan := make(chan bool)
|
||||
go reader.restart(command, environ, readyChan)
|
||||
<-readyChan
|
||||
}
|
||||
|
||||
exitCode := ExitOk
|
||||
@@ -322,9 +333,6 @@ func Run(opts *Options) (int, error) {
|
||||
err = quitSignal.err
|
||||
stop = true
|
||||
return
|
||||
case EvtReadNone:
|
||||
reading = false
|
||||
terminal.UpdateCount(0, false, nil)
|
||||
case EvtReadNew, EvtReadFin:
|
||||
if evt == EvtReadFin && nextCommand != nil {
|
||||
restart(*nextCommand, nextEnviron)
|
||||
|
@@ -102,7 +102,7 @@ func (m *Matcher) Loop() {
|
||||
if !cacheCleared {
|
||||
if count == prevCount {
|
||||
// Look up mergerCache
|
||||
if cached, found := m.mergerCache[patternString]; found {
|
||||
if cached, found := m.mergerCache[patternString]; found && cached.final == request.final {
|
||||
merger = cached
|
||||
}
|
||||
} else {
|
||||
|
151
src/options.go
151
src/options.go
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/junegunn/fzf/src/algo"
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
|
||||
"github.com/mattn/go-shellwords"
|
||||
"github.com/junegunn/go-shellwords"
|
||||
"github.com/rivo/uniseg"
|
||||
)
|
||||
|
||||
@@ -56,6 +56,7 @@ Usage: fzf [options]
|
||||
--wrap Enable line wrap
|
||||
--wrap-sign=STR Indicator for wrapped lines
|
||||
--no-multi-line Disable multi-line display of items when using --read0
|
||||
--gap[=N] Render empty lines between each item
|
||||
--keep-right Keep the right end of the line visible on overflow
|
||||
--scroll-off=LINES Number of screen lines to keep above or below when
|
||||
scrolling to the top or to the bottom (default: 0)
|
||||
@@ -103,7 +104,7 @@ Usage: fzf [options]
|
||||
--header=STR String to print as header
|
||||
--header-lines=N The first N lines of the input are treated as header
|
||||
--header-first Print header before the prompt line
|
||||
--ellipsis=STR Ellipsis to show when line is truncated (default: '..')
|
||||
--ellipsis=STR Ellipsis to show when line is truncated (default: '··')
|
||||
|
||||
Display
|
||||
--ansi Enable processing of ANSI color codes
|
||||
@@ -120,8 +121,8 @@ Usage: fzf [options]
|
||||
--preview=COMMAND Command to preview highlighted line ({})
|
||||
--preview-window=OPT Preview window layout (default: right:50%)
|
||||
[up|down|left|right][,SIZE[%]]
|
||||
[,[no]wrap][,[no]cycle][,[no]follow][,[no]hidden]
|
||||
[,border-BORDER_OPT]
|
||||
[,[no]wrap][,[no]cycle][,[no]follow][,[no]info]
|
||||
[,[no]hidden][,border-BORDER_OPT]
|
||||
[,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES]
|
||||
[,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]
|
||||
--preview-label=LABEL
|
||||
@@ -144,7 +145,7 @@ Usage: fzf [options]
|
||||
|
||||
Directory traversal (Only used when $FZF_DEFAULT_COMMAND is not set)
|
||||
--walker=OPTS [file][,dir][,follow][,hidden] (default: file,follow,hidden)
|
||||
--walker-root=DIR Root directory from which to start walker (default: .)
|
||||
--walker-root=DIR [...] List of directories to walk (default: .)
|
||||
--walker-skip=DIRS Comma-separated list of directory names to skip
|
||||
(default: .git,node_modules)
|
||||
|
||||
@@ -271,6 +272,7 @@ type previewOpts struct {
|
||||
wrap bool
|
||||
cycle bool
|
||||
follow bool
|
||||
info bool
|
||||
border tui.BorderShape
|
||||
headerLines int
|
||||
threshold int
|
||||
@@ -379,14 +381,46 @@ func (a previewOpts) aboveOrBelow() bool {
|
||||
return a.size.size > 0 && (a.position == posUp || a.position == posDown)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
type previewOptsCompare int
|
||||
|
||||
func (a previewOpts) sameContentLayout(b previewOpts) bool {
|
||||
return a.wrap == b.wrap && a.headerLines == b.headerLines
|
||||
const (
|
||||
previewOptsSame previewOptsCompare = iota
|
||||
previewOptsDifferentContentLayout
|
||||
previewOptsDifferentLayout
|
||||
)
|
||||
|
||||
func (o *previewOpts) compare(active *previewOpts, b *previewOpts) previewOptsCompare {
|
||||
a := o
|
||||
|
||||
sameThreshold := o.position == b.position && o.threshold == b.threshold
|
||||
// Alternative layout is being used
|
||||
if o.alternative == active {
|
||||
a = active
|
||||
|
||||
// If the other also has an alternative layout,
|
||||
if b.alternative != nil {
|
||||
// and if the same condition is the same, compare alt vs. alt.
|
||||
if sameThreshold {
|
||||
b = b.alternative
|
||||
} else {
|
||||
// If not, we pessimistically decide that the layouts may not be the same
|
||||
return previewOptsDifferentLayout
|
||||
}
|
||||
}
|
||||
} else if b.alternative != nil && !sameThreshold {
|
||||
// We may choose the other's alternative layout, so let's be conservative.
|
||||
return previewOptsDifferentLayout
|
||||
}
|
||||
|
||||
if !(a.size == b.size && a.position == b.position && a.border == b.border && a.hidden == b.hidden) {
|
||||
return previewOptsDifferentLayout
|
||||
}
|
||||
|
||||
if a.wrap == b.wrap && a.headerLines == b.headerLines && a.info == b.info && a.scroll == b.scroll {
|
||||
return previewOptsSame
|
||||
}
|
||||
|
||||
return previewOptsDifferentContentLayout
|
||||
}
|
||||
|
||||
func firstLine(s string) string {
|
||||
@@ -472,7 +506,8 @@ type Options struct {
|
||||
Header []string
|
||||
HeaderLines int
|
||||
HeaderFirst bool
|
||||
Ellipsis string
|
||||
Gap int
|
||||
Ellipsis *string
|
||||
Scrollbar *string
|
||||
Margin [4]sizeSpec
|
||||
Padding [4]sizeSpec
|
||||
@@ -487,7 +522,7 @@ type Options struct {
|
||||
Unsafe bool
|
||||
ClearOnExit bool
|
||||
WalkerOpts walkerOpts
|
||||
WalkerRoot string
|
||||
WalkerRoot []string
|
||||
WalkerSkip []string
|
||||
Version bool
|
||||
Help bool
|
||||
@@ -508,7 +543,7 @@ func filterNonEmpty(input []string) []string {
|
||||
}
|
||||
|
||||
func defaultPreviewOpts(command string) previewOpts {
|
||||
return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, tui.DefaultBorderShape, 0, 0, nil}
|
||||
return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, true, tui.DefaultBorderShape, 0, 0, nil}
|
||||
}
|
||||
|
||||
func defaultOptions() *Options {
|
||||
@@ -578,7 +613,8 @@ func defaultOptions() *Options {
|
||||
Header: make([]string, 0),
|
||||
HeaderLines: 0,
|
||||
HeaderFirst: false,
|
||||
Ellipsis: "..",
|
||||
Gap: 0,
|
||||
Ellipsis: nil,
|
||||
Scrollbar: nil,
|
||||
Margin: defaultMargin(),
|
||||
Padding: defaultMargin(),
|
||||
@@ -590,7 +626,7 @@ func defaultOptions() *Options {
|
||||
Unsafe: false,
|
||||
ClearOnExit: true,
|
||||
WalkerOpts: walkerOpts{file: true, hidden: true, follow: true},
|
||||
WalkerRoot: ".",
|
||||
WalkerRoot: []string{"."},
|
||||
WalkerSkip: []string{".git", "node_modules"},
|
||||
Help: false,
|
||||
Version: false}
|
||||
@@ -622,6 +658,28 @@ func optionalNextString(args []string, i *int) (bool, string) {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
func isDir(path string) bool {
|
||||
stat, err := os.Stat(path)
|
||||
return err == nil && stat.IsDir()
|
||||
}
|
||||
|
||||
func nextDirs(args []string, i *int) ([]string, error) {
|
||||
dirs := []string{}
|
||||
for *i < len(args)-1 {
|
||||
arg := args[*i+1]
|
||||
if isDir(arg) {
|
||||
dirs = append(dirs, arg)
|
||||
*i++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(dirs) == 0 {
|
||||
return nil, errors.New("no directory specified")
|
||||
}
|
||||
return dirs, nil
|
||||
}
|
||||
|
||||
func atoi(str string) (int, error) {
|
||||
num, err := strconv.Atoi(str)
|
||||
if err != nil {
|
||||
@@ -1722,7 +1780,7 @@ func parsePreviewWindowImpl(opts *previewOpts, input string) error {
|
||||
var err error
|
||||
tokenRegex := regexp.MustCompile(`[:,]*(<([1-9][0-9]*)\(([^)<]+)\)|[^,:]+)`)
|
||||
sizeRegex := regexp.MustCompile("^[0-9]+%?$")
|
||||
offsetRegex := regexp.MustCompile(`^(\+{-?[0-9]+})?([+-][0-9]+)*(-?/[1-9][0-9]*)?$`)
|
||||
offsetRegex := regexp.MustCompile(`^(\+{(-?[0-9]+|n)})?([+-][0-9]+)*(-?/[1-9][0-9]*)?$`)
|
||||
headerRegex := regexp.MustCompile("^~(0|[1-9][0-9]*)$")
|
||||
tokens := tokenRegex.FindAllStringSubmatch(input, -1)
|
||||
var alternative string
|
||||
@@ -1789,6 +1847,10 @@ func parsePreviewWindowImpl(opts *previewOpts, input string) error {
|
||||
opts.follow = true
|
||||
case "nofollow":
|
||||
opts.follow = false
|
||||
case "info":
|
||||
opts.info = true
|
||||
case "noinfo":
|
||||
opts.info = false
|
||||
default:
|
||||
if headerRegex.MatchString(token) {
|
||||
if opts.headerLines, err = atoi(token[1:]); err != nil {
|
||||
@@ -2338,10 +2400,19 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
||||
opts.HeaderFirst = true
|
||||
case "--no-header-first":
|
||||
opts.HeaderFirst = false
|
||||
case "--ellipsis":
|
||||
if opts.Ellipsis, err = nextString(allArgs, &i, "ellipsis string required"); err != nil {
|
||||
case "--gap":
|
||||
if opts.Gap, err = optionalNumeric(allArgs, &i, 1); err != nil {
|
||||
return err
|
||||
}
|
||||
case "--no-gap":
|
||||
opts.Gap = 0
|
||||
case "--ellipsis":
|
||||
str, err := nextString(allArgs, &i, "ellipsis string required")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
str = firstLine(str)
|
||||
opts.Ellipsis = &str
|
||||
case "--preview":
|
||||
if opts.Preview.command, err = nextString(allArgs, &i, "preview command required"); err != nil {
|
||||
return err
|
||||
@@ -2470,7 +2541,7 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
||||
return err
|
||||
}
|
||||
case "--walker-root":
|
||||
if opts.WalkerRoot, err = nextString(allArgs, &i, "directory required"); err != nil {
|
||||
if opts.WalkerRoot, err = nextDirs(allArgs, &i); err != nil {
|
||||
return err
|
||||
}
|
||||
case "--walker-skip":
|
||||
@@ -2622,8 +2693,13 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
||||
if opts.HeaderLines, err = atoi(value); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if match, value := optString(arg, "--gap="); match {
|
||||
if opts.Gap, err = atoi(value); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if match, value := optString(arg, "--ellipsis="); match {
|
||||
opts.Ellipsis = value
|
||||
str := firstLine(value)
|
||||
opts.Ellipsis = &str
|
||||
} else if match, value := optString(arg, "--preview="); match {
|
||||
opts.Preview.command = value
|
||||
} else if match, value := optString(arg, "--preview-window="); match {
|
||||
@@ -2663,7 +2739,11 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
||||
return err
|
||||
}
|
||||
} else if match, value := optString(arg, "--walker-root="); match {
|
||||
opts.WalkerRoot = value
|
||||
if !isDir(value) {
|
||||
return errors.New("not a directory: " + value)
|
||||
}
|
||||
dirs, _ := nextDirs(allArgs, &i)
|
||||
opts.WalkerRoot = append([]string{value}, dirs...)
|
||||
} else if match, value := optString(arg, "--walker-skip="); match {
|
||||
opts.WalkerSkip = filterNonEmpty(strings.Split(value, ","))
|
||||
} else if match, value := optString(arg, "--hscroll-off="); match {
|
||||
@@ -2915,6 +2995,12 @@ func postProcessOptions(opts *Options) error {
|
||||
return processScheme(opts)
|
||||
}
|
||||
|
||||
func parseShellWords(str string) ([]string, error) {
|
||||
parser := shellwords.NewParser()
|
||||
parser.ParseComment = true
|
||||
return parser.Parse(str)
|
||||
}
|
||||
|
||||
// ParseOptions parses command-line options
|
||||
func ParseOptions(useDefaults bool, args []string) (*Options, error) {
|
||||
opts := defaultOptions()
|
||||
@@ -2928,7 +3014,7 @@ func ParseOptions(useDefaults bool, args []string) (*Options, error) {
|
||||
return nil, errors.New("$FZF_DEFAULT_OPTS_FILE: " + err.Error())
|
||||
}
|
||||
|
||||
words, parseErr := shellwords.Parse(string(bytes))
|
||||
words, parseErr := parseShellWords(string(bytes))
|
||||
if parseErr != nil {
|
||||
return nil, errors.New(path + ": " + parseErr.Error())
|
||||
}
|
||||
@@ -2940,7 +3026,7 @@ func ParseOptions(useDefaults bool, args []string) (*Options, error) {
|
||||
}
|
||||
|
||||
// 2. Options from $FZF_DEFAULT_OPTS string
|
||||
words, parseErr := shellwords.Parse(os.Getenv("FZF_DEFAULT_OPTS"))
|
||||
words, parseErr := parseShellWords(os.Getenv("FZF_DEFAULT_OPTS"))
|
||||
if parseErr != nil {
|
||||
return nil, errors.New("$FZF_DEFAULT_OPTS: " + parseErr.Error())
|
||||
}
|
||||
@@ -2964,17 +3050,18 @@ func ParseOptions(useDefaults bool, args []string) (*Options, error) {
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func (opts *Options) reloadOnStart() bool {
|
||||
// Not compatible with --filter
|
||||
if opts.Filter != nil {
|
||||
return false
|
||||
}
|
||||
func (opts *Options) extractReloadOnStart() string {
|
||||
cmd := ""
|
||||
if actions, prs := opts.Keymap[tui.Start.AsEvent()]; prs {
|
||||
filtered := []*action{}
|
||||
for _, action := range actions {
|
||||
if action.t == actReload || action.t == actReloadSync {
|
||||
return true
|
||||
cmd = action.a
|
||||
} else {
|
||||
filtered = append(filtered, action)
|
||||
}
|
||||
}
|
||||
opts.Keymap[tui.Start.AsEvent()] = filtered
|
||||
}
|
||||
return false
|
||||
return cmd
|
||||
}
|
||||
|
@@ -23,6 +23,7 @@ type termType int
|
||||
const (
|
||||
termFuzzy termType = iota
|
||||
termExact
|
||||
termExactBoundary
|
||||
termPrefix
|
||||
termSuffix
|
||||
termEqual
|
||||
@@ -147,6 +148,7 @@ func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy boo
|
||||
ptr.procFun[termFuzzy] = fuzzyAlgo
|
||||
ptr.procFun[termEqual] = algo.EqualMatch
|
||||
ptr.procFun[termExact] = algo.ExactMatchNaive
|
||||
ptr.procFun[termExactBoundary] = algo.ExactMatchBoundary
|
||||
ptr.procFun[termPrefix] = algo.PrefixMatch
|
||||
ptr.procFun[termSuffix] = algo.SuffixMatch
|
||||
|
||||
@@ -193,7 +195,10 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet
|
||||
text = text[:len(text)-1]
|
||||
}
|
||||
|
||||
if strings.HasPrefix(text, "'") {
|
||||
if len(text) > 2 && strings.HasPrefix(text, "'") && strings.HasSuffix(text, "'") {
|
||||
typ = termExactBoundary
|
||||
text = text[1 : len(text)-1]
|
||||
} else if strings.HasPrefix(text, "'") {
|
||||
// Flip exactness
|
||||
if fuzzy && !inv {
|
||||
typ = termExact
|
||||
|
22
src/proxy.go
22
src/proxy.go
@@ -9,6 +9,7 @@ import (
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -32,7 +33,7 @@ func fifo(name string) (string, error) {
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func runProxy(commandPrefix string, cmdBuilder func(temp string) *exec.Cmd, opts *Options, withExports bool) (int, error) {
|
||||
func runProxy(commandPrefix string, cmdBuilder func(temp string, needBash bool) (*exec.Cmd, error), opts *Options, withExports bool) (int, error) {
|
||||
output, err := fifo("proxy-output")
|
||||
if err != nil {
|
||||
return ExitError, err
|
||||
@@ -92,17 +93,28 @@ func runProxy(commandPrefix string, cmdBuilder func(temp string) *exec.Cmd, opts
|
||||
// To ensure that the options are processed by a POSIX-compliant shell,
|
||||
// we need to write the command to a temporary file and execute it with sh.
|
||||
var exports []string
|
||||
needBash := false
|
||||
if withExports {
|
||||
exports = os.Environ()
|
||||
for idx, pairStr := range exports {
|
||||
validIdentifier := regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
|
||||
for _, pairStr := range os.Environ() {
|
||||
pair := strings.SplitN(pairStr, "=", 2)
|
||||
exports[idx] = fmt.Sprintf("export %s=%s", pair[0], escapeSingleQuote(pair[1]))
|
||||
if validIdentifier.MatchString(pair[0]) {
|
||||
exports = append(exports, fmt.Sprintf("export %s=%s", pair[0], escapeSingleQuote(pair[1])))
|
||||
} else if strings.HasPrefix(pair[0], "BASH_FUNC_") && strings.HasSuffix(pair[0], "%%") {
|
||||
name := pair[0][10 : len(pair[0])-2]
|
||||
exports = append(exports, name+pair[1])
|
||||
exports = append(exports, "export -f "+name)
|
||||
needBash = true
|
||||
}
|
||||
}
|
||||
}
|
||||
temp := WriteTemporaryFile(append(exports, command), "\n")
|
||||
defer os.Remove(temp)
|
||||
|
||||
cmd := cmdBuilder(temp)
|
||||
cmd, err := cmdBuilder(temp, needBash)
|
||||
if err != nil {
|
||||
return ExitError, err
|
||||
}
|
||||
cmd.Stderr = os.Stderr
|
||||
intChan := make(chan os.Signal, 1)
|
||||
defer close(intChan)
|
||||
|
@@ -9,7 +9,10 @@ import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func sh() (string, error) {
|
||||
func sh(bash bool) (string, error) {
|
||||
if bash {
|
||||
return "bash", nil
|
||||
}
|
||||
return "sh", nil
|
||||
}
|
||||
|
||||
|
@@ -13,12 +13,16 @@ import (
|
||||
|
||||
var shPath atomic.Value
|
||||
|
||||
func sh() (string, error) {
|
||||
func sh(bash bool) (string, error) {
|
||||
if cached := shPath.Load(); cached != nil {
|
||||
return cached.(string), nil
|
||||
}
|
||||
|
||||
cmd := exec.Command("cygpath", "-w", "/usr/bin/sh")
|
||||
name := "sh"
|
||||
if bash {
|
||||
name = "bash"
|
||||
}
|
||||
cmd := exec.Command("cygpath", "-w", "/usr/bin/"+name)
|
||||
bytes, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -31,7 +35,7 @@ func sh() (string, error) {
|
||||
|
||||
func mkfifo(path string, mode uint32) (string, error) {
|
||||
m := strconv.FormatUint(uint64(mode), 8)
|
||||
sh, err := sh()
|
||||
sh, err := sh(false)
|
||||
if err != nil {
|
||||
return path, err
|
||||
}
|
||||
@@ -43,7 +47,7 @@ func mkfifo(path string, mode uint32) (string, error) {
|
||||
}
|
||||
|
||||
func withOutputPipe(output string, task func(io.ReadCloser)) error {
|
||||
sh, err := sh()
|
||||
sh, err := sh(false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -62,7 +66,7 @@ func withOutputPipe(output string, task func(io.ReadCloser)) error {
|
||||
}
|
||||
|
||||
func withInputPipe(input string, task func(io.WriteCloser)) error {
|
||||
sh, err := sh()
|
||||
sh, err := sh(false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
125
src/reader.go
125
src/reader.go
@@ -6,8 +6,8 @@ import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -25,16 +25,26 @@ type Reader struct {
|
||||
event int32
|
||||
finChan chan bool
|
||||
mutex sync.Mutex
|
||||
exec *exec.Cmd
|
||||
execOut io.ReadCloser
|
||||
command *string
|
||||
killed bool
|
||||
termFunc func()
|
||||
command *string
|
||||
wait bool
|
||||
}
|
||||
|
||||
// NewReader returns new Reader object
|
||||
func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, executor *util.Executor, delimNil bool, wait bool) *Reader {
|
||||
return &Reader{pusher, executor, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, nil, false, wait}
|
||||
return &Reader{
|
||||
pusher,
|
||||
executor,
|
||||
eventBox,
|
||||
delimNil,
|
||||
int32(EvtReady),
|
||||
make(chan bool, 1),
|
||||
sync.Mutex{},
|
||||
false,
|
||||
func() { os.Stdin.Close() },
|
||||
nil,
|
||||
wait}
|
||||
}
|
||||
|
||||
func (r *Reader) startEventPoller() {
|
||||
@@ -80,19 +90,19 @@ func (r *Reader) fin(success bool) {
|
||||
func (r *Reader) terminate() {
|
||||
r.mutex.Lock()
|
||||
r.killed = true
|
||||
if r.exec != nil && r.exec.Process != nil {
|
||||
r.execOut.Close()
|
||||
util.KillCommand(r.exec)
|
||||
} else {
|
||||
os.Stdin.Close()
|
||||
if r.termFunc != nil {
|
||||
r.termFunc()
|
||||
r.termFunc = nil
|
||||
}
|
||||
r.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (r *Reader) restart(command commandSpec, environ []string) {
|
||||
func (r *Reader) restart(command commandSpec, environ []string, readyChan chan bool) {
|
||||
r.event = int32(EvtReady)
|
||||
r.startEventPoller()
|
||||
success := r.readFromCommand(command.command, environ)
|
||||
success := r.readFromCommand(command.command, environ, func() {
|
||||
readyChan <- true
|
||||
})
|
||||
r.fin(success)
|
||||
removeFiles(command.tempFiles)
|
||||
}
|
||||
@@ -111,20 +121,29 @@ func (r *Reader) readChannel(inputChan chan string) bool {
|
||||
}
|
||||
|
||||
// ReadSource reads data from the default command or from standard input
|
||||
func (r *Reader) ReadSource(inputChan chan string, root string, opts walkerOpts, ignores []string) {
|
||||
func (r *Reader) ReadSource(inputChan chan string, roots []string, opts walkerOpts, ignores []string, initCmd string, initEnv []string, readyChan chan bool) {
|
||||
r.startEventPoller()
|
||||
var success bool
|
||||
signalReady := func() {
|
||||
if readyChan != nil {
|
||||
readyChan <- true
|
||||
}
|
||||
}
|
||||
if inputChan != nil {
|
||||
signalReady()
|
||||
success = r.readChannel(inputChan)
|
||||
} else if len(initCmd) > 0 {
|
||||
success = r.readFromCommand(initCmd, initEnv, signalReady)
|
||||
} else if util.IsTty(os.Stdin) {
|
||||
cmd := os.Getenv("FZF_DEFAULT_COMMAND")
|
||||
if len(cmd) == 0 {
|
||||
success = r.readFiles(root, opts, ignores)
|
||||
signalReady()
|
||||
success = r.readFiles(roots, opts, ignores)
|
||||
} else {
|
||||
// We can't export FZF_* environment variables to the default command
|
||||
success = r.readFromCommand(cmd, nil)
|
||||
success = r.readFromCommand(cmd, initEnv, signalReady)
|
||||
}
|
||||
} else {
|
||||
signalReady()
|
||||
success = r.readFromStdin()
|
||||
}
|
||||
r.fin(success)
|
||||
@@ -247,14 +266,33 @@ func trimPath(path string) string {
|
||||
return byteString(bytes)
|
||||
}
|
||||
|
||||
func (r *Reader) readFiles(root string, opts walkerOpts, ignores []string) bool {
|
||||
r.killed = false
|
||||
func (r *Reader) readFiles(roots []string, opts walkerOpts, ignores []string) bool {
|
||||
conf := fastwalk.Config{
|
||||
Follow: opts.follow,
|
||||
// Use forward slashes when running a Windows binary under WSL or MSYS
|
||||
ToSlash: fastwalk.DefaultToSlash(),
|
||||
Sort: fastwalk.SortFilesFirst,
|
||||
}
|
||||
ignoresBase := []string{}
|
||||
ignoresFull := []string{}
|
||||
ignoresSuffix := []string{}
|
||||
sep := string(os.PathSeparator)
|
||||
for _, ignore := range ignores {
|
||||
if strings.ContainsRune(ignore, os.PathSeparator) {
|
||||
if strings.HasPrefix(ignore, sep) {
|
||||
ignoresSuffix = append(ignoresSuffix, ignore)
|
||||
} else {
|
||||
// 'foo/bar' should match match
|
||||
// * 'foo/bar'
|
||||
// * 'baz/foo/bar'
|
||||
// * but NOT 'bazfoo/bar'
|
||||
ignoresFull = append(ignoresFull, ignore)
|
||||
ignoresSuffix = append(ignoresSuffix, sep+ignore)
|
||||
}
|
||||
} else {
|
||||
ignoresBase = append(ignoresBase, ignore)
|
||||
}
|
||||
}
|
||||
fn := func(path string, de os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -264,14 +302,24 @@ func (r *Reader) readFiles(root string, opts walkerOpts, ignores []string) bool
|
||||
isDir := de.IsDir()
|
||||
if isDir || opts.follow && isSymlinkToDir(path, de) {
|
||||
base := filepath.Base(path)
|
||||
if !opts.hidden && base[0] == '.' {
|
||||
if !opts.hidden && base[0] == '.' && base != ".." {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
for _, ignore := range ignores {
|
||||
for _, ignore := range ignoresBase {
|
||||
if ignore == base {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
}
|
||||
for _, ignore := range ignoresFull {
|
||||
if ignore == path {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
}
|
||||
for _, ignore := range ignoresSuffix {
|
||||
if strings.HasSuffix(path, ignore) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((opts.file && !isDir) || (opts.dir && isDir)) && r.pusher(stringBytes(path)) {
|
||||
atomic.StoreInt32(&r.event, int32(EvtReadNew))
|
||||
@@ -284,34 +332,39 @@ func (r *Reader) readFiles(root string, opts walkerOpts, ignores []string) bool
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fastwalk.Walk(&conf, root, fn) == nil
|
||||
noerr := true
|
||||
for _, root := range roots {
|
||||
noerr = noerr && (fastwalk.Walk(&conf, root, fn) == nil)
|
||||
}
|
||||
return noerr
|
||||
}
|
||||
|
||||
func (r *Reader) readFromCommand(command string, environ []string) bool {
|
||||
func (r *Reader) readFromCommand(command string, environ []string, signalReady func()) bool {
|
||||
r.mutex.Lock()
|
||||
|
||||
r.killed = false
|
||||
r.termFunc = nil
|
||||
r.command = &command
|
||||
r.exec = r.executor.ExecCommand(command, true)
|
||||
exec := r.executor.ExecCommand(command, true)
|
||||
if environ != nil {
|
||||
r.exec.Env = environ
|
||||
exec.Env = environ
|
||||
}
|
||||
|
||||
var err error
|
||||
r.execOut, err = r.exec.StdoutPipe()
|
||||
if err != nil {
|
||||
r.exec = nil
|
||||
execOut, err := exec.StdoutPipe()
|
||||
if err != nil || exec.Start() != nil {
|
||||
signalReady()
|
||||
r.mutex.Unlock()
|
||||
return false
|
||||
}
|
||||
|
||||
err = r.exec.Start()
|
||||
if err != nil {
|
||||
r.exec = nil
|
||||
r.mutex.Unlock()
|
||||
return false
|
||||
// Function to call to terminate the running command
|
||||
r.termFunc = func() {
|
||||
execOut.Close()
|
||||
util.KillCommand(exec)
|
||||
}
|
||||
|
||||
signalReady()
|
||||
r.mutex.Unlock()
|
||||
r.feed(r.execOut)
|
||||
return r.exec.Wait() == nil
|
||||
|
||||
r.feed(execOut)
|
||||
return exec.Wait() == nil
|
||||
}
|
||||
|
@@ -23,8 +23,12 @@ func TestReadFromCommand(t *testing.T) {
|
||||
}
|
||||
|
||||
// Normal command
|
||||
reader.fin(reader.readFromCommand(`echo abc&&echo def`, nil))
|
||||
if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" {
|
||||
counter := 0
|
||||
ready := func() {
|
||||
counter++
|
||||
}
|
||||
reader.fin(reader.readFromCommand(`echo abc&&echo def`, nil, ready))
|
||||
if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" || counter != 1 {
|
||||
t.Errorf("%s", strs)
|
||||
}
|
||||
|
||||
@@ -48,9 +52,9 @@ func TestReadFromCommand(t *testing.T) {
|
||||
reader.startEventPoller()
|
||||
|
||||
// Failing command
|
||||
reader.fin(reader.readFromCommand(`no-such-command`, nil))
|
||||
reader.fin(reader.readFromCommand(`no-such-command`, nil, ready))
|
||||
strs = []string{}
|
||||
if len(strs) > 0 {
|
||||
if len(strs) > 0 || counter != 2 {
|
||||
t.Errorf("%s", strs)
|
||||
}
|
||||
|
||||
|
@@ -16,6 +16,7 @@ type colorOffset struct {
|
||||
offset [2]int32
|
||||
color tui.ColorPair
|
||||
match bool
|
||||
url *url
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
@@ -177,8 +178,11 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
|
||||
if curr != 0 && idx > start {
|
||||
if curr < 0 {
|
||||
color := colMatch
|
||||
var url *url
|
||||
if curr < -1 && theme.Colored {
|
||||
origColor := ansiToColorPair(itemColors[-curr-2], colMatch)
|
||||
ansi := itemColors[-curr-2]
|
||||
url = ansi.color.url
|
||||
origColor := ansiToColorPair(ansi, colMatch)
|
||||
// hl or hl+ only sets the foreground color, so colMatch is the
|
||||
// combination of either [hl and bg] or [hl+ and bg+].
|
||||
//
|
||||
@@ -194,13 +198,14 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
|
||||
}
|
||||
}
|
||||
colors = append(colors, colorOffset{
|
||||
offset: [2]int32{int32(start), int32(idx)}, color: color, match: true})
|
||||
offset: [2]int32{int32(start), int32(idx)}, color: color, match: true, url: url})
|
||||
} else {
|
||||
ansi := itemColors[curr-1]
|
||||
colors = append(colors, colorOffset{
|
||||
offset: [2]int32{int32(start), int32(idx)},
|
||||
color: ansiToColorPair(ansi, colBase),
|
||||
match: false})
|
||||
match: false,
|
||||
url: ansi.color.url})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -124,10 +124,10 @@ func TestColorOffset(t *testing.T) {
|
||||
item := Result{
|
||||
item: &Item{
|
||||
colors: &[]ansiOffset{
|
||||
{[2]int32{0, 20}, ansiState{1, 5, 0, -1}},
|
||||
{[2]int32{22, 27}, ansiState{2, 6, tui.Bold, -1}},
|
||||
{[2]int32{30, 32}, ansiState{3, 7, 0, -1}},
|
||||
{[2]int32{33, 40}, ansiState{4, 8, tui.Bold, -1}}}}}
|
||||
{[2]int32{0, 20}, ansiState{1, 5, 0, -1, nil}},
|
||||
{[2]int32{22, 27}, ansiState{2, 6, tui.Bold, -1, nil}},
|
||||
{[2]int32{30, 32}, ansiState{3, 7, 0, -1, nil}},
|
||||
{[2]int32{33, 40}, ansiState{4, 8, tui.Bold, -1, nil}}}}}
|
||||
|
||||
colBase := tui.NewColorPair(89, 189, tui.AttrUndefined)
|
||||
colMatch := tui.NewColorPair(99, 199, tui.AttrUndefined)
|
||||
|
672
src/terminal.go
672
src/terminal.go
File diff suppressed because it is too large
Load Diff
@@ -507,6 +507,34 @@ func TestParsePlaceholder(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractPassthroughs(t *testing.T) {
|
||||
for _, middle := range []string{
|
||||
"\x1bPtmux;\x1b\x1bbar\x1b\\",
|
||||
"\x1bPtmux;\x1b\x1bbar\x1bbar\x1b\\",
|
||||
"\x1b]1337;bar\x1b\\",
|
||||
"\x1b]1337;bar\x1bbar\x1b\\",
|
||||
"\x1b]1337;bar\a",
|
||||
"\x1b_Ga=T,f=32,s=1258,v=1295,c=74,r=35,m=1\x1b\\",
|
||||
"\x1b_Ga=T,f=32,s=1258,v=1295,c=74,r=35,m=1\x1b\\\r",
|
||||
"\x1b_Ga=T,f=32,s=1258,v=1295,c=74,r=35,m=1\x1bbar\x1b\\\r",
|
||||
"\x1b_Gm=1;AAAAAAAAA=\x1b\\",
|
||||
"\x1b_Gm=1;AAAAAAAAA=\x1b\\\r",
|
||||
"\x1b_Gm=1;\x1bAAAAAAAAA=\x1b\\\r",
|
||||
} {
|
||||
line := "foo" + middle + "baz"
|
||||
loc := findPassThrough(line)
|
||||
if loc == nil || line[0:loc[0]] != "foo" || line[loc[1]:] != "baz" {
|
||||
t.Error("failed to find passthrough")
|
||||
}
|
||||
garbage := "\x1bPtmux;\x1b]1337;\x1b_Ga=\x1b]1337;bar\x1b."
|
||||
line = strings.Repeat("foo"+middle+middle+"baz", 3) + garbage
|
||||
passthroughs, result := extractPassThroughs(line)
|
||||
if result != "foobazfoobazfoobaz"+garbage || len(passthroughs) != 6 {
|
||||
t.Error("failed to extract passthroughs")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* utilities section */
|
||||
|
||||
// Item represents one line in fzf UI. Usually it is relative path to files and folders.
|
||||
|
@@ -20,9 +20,5 @@ func notifyStop(p *os.Process) {
|
||||
if err == nil {
|
||||
pid = pgid * -1
|
||||
}
|
||||
unix.Kill(pid, syscall.SIGSTOP)
|
||||
}
|
||||
|
||||
func notifyOnCont(resizeChan chan<- os.Signal) {
|
||||
signal.Notify(resizeChan, syscall.SIGCONT)
|
||||
unix.Kill(pid, syscall.SIGTSTP)
|
||||
}
|
||||
|
@@ -13,7 +13,3 @@ func notifyOnResize(resizeChan chan<- os.Signal) {
|
||||
func notifyStop(p *os.Process) {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
func notifyOnCont(resizeChan chan<- os.Signal) {
|
||||
// NOOP
|
||||
}
|
||||
|
11
src/tmux.go
11
src/tmux.go
@@ -38,7 +38,7 @@ func runTmux(args []string, opts *Options) (int, error) {
|
||||
case posUp:
|
||||
tmuxArgs = append(tmuxArgs, "-xC", "-y0")
|
||||
case posDown:
|
||||
tmuxArgs = append(tmuxArgs, "-xC", "-yS")
|
||||
tmuxArgs = append(tmuxArgs, "-xC", "-y9999")
|
||||
case posLeft:
|
||||
tmuxArgs = append(tmuxArgs, "-x0", "-yC")
|
||||
case posRight:
|
||||
@@ -49,9 +49,12 @@ func runTmux(args []string, opts *Options) (int, error) {
|
||||
tmuxArgs = append(tmuxArgs, "-w"+opts.Tmux.width.String())
|
||||
tmuxArgs = append(tmuxArgs, "-h"+opts.Tmux.height.String())
|
||||
|
||||
return runProxy(argStr, func(temp string) *exec.Cmd {
|
||||
sh, _ := sh()
|
||||
return runProxy(argStr, func(temp string, needBash bool) (*exec.Cmd, error) {
|
||||
sh, err := sh(needBash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tmuxArgs = append(tmuxArgs, sh, temp)
|
||||
return exec.Command("tmux", tmuxArgs...)
|
||||
return exec.Command("tmux", tmuxArgs...), nil
|
||||
}, opts, true)
|
||||
}
|
||||
|
@@ -73,11 +73,15 @@ func (r *LightRenderer) csi(code string) string {
|
||||
|
||||
func (r *LightRenderer) flush() {
|
||||
if r.queued.Len() > 0 {
|
||||
fmt.Fprint(r.ttyout, "\x1b[?7l\x1b[?25l"+r.queued.String()+"\x1b[?25h\x1b[?7h")
|
||||
r.flushRaw("\x1b[?7l\x1b[?25l" + r.queued.String() + "\x1b[?25h\x1b[?7h")
|
||||
r.queued.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *LightRenderer) flushRaw(sequence string) {
|
||||
fmt.Fprint(r.ttyout, sequence)
|
||||
}
|
||||
|
||||
// Light renderer
|
||||
type LightRenderer struct {
|
||||
closed *util.AtomicBool
|
||||
@@ -655,11 +659,13 @@ func (r *LightRenderer) mouseSequence(sz *int) Event {
|
||||
}
|
||||
|
||||
func (r *LightRenderer) smcup() {
|
||||
r.csi("?1049h")
|
||||
r.flush()
|
||||
r.flushRaw("\x1b[?1049h")
|
||||
}
|
||||
|
||||
func (r *LightRenderer) rmcup() {
|
||||
r.csi("?1049l")
|
||||
r.flush()
|
||||
r.flushRaw("\x1b[?1049l")
|
||||
}
|
||||
|
||||
func (r *LightRenderer) Pause(clear bool) {
|
||||
@@ -929,9 +935,6 @@ func (w *LightWindow) Height() int {
|
||||
func (w *LightWindow) Refresh() {
|
||||
}
|
||||
|
||||
func (w *LightWindow) Close() {
|
||||
}
|
||||
|
||||
func (w *LightWindow) X() int {
|
||||
return w.posx
|
||||
}
|
||||
@@ -1030,13 +1033,13 @@ func cleanse(str string) string {
|
||||
func (w *LightWindow) CPrint(pair ColorPair, text string) {
|
||||
_, code := w.csiColor(pair.Fg(), pair.Bg(), pair.Attr())
|
||||
w.stderrInternal(cleanse(text), false, code)
|
||||
w.csi("m")
|
||||
w.csi("0m")
|
||||
}
|
||||
|
||||
func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) {
|
||||
hasColors, code := w.csiColor(fg, bg, attr)
|
||||
if hasColors {
|
||||
defer w.csi("m")
|
||||
defer w.csi("0m")
|
||||
}
|
||||
w.stderrInternal(cleanse(text), false, code)
|
||||
}
|
||||
@@ -1097,7 +1100,7 @@ func (w *LightWindow) fill(str string, resetCode string) FillReturn {
|
||||
}
|
||||
}
|
||||
}
|
||||
if w.posx+1 >= w.Width() {
|
||||
if w.posx >= w.Width() {
|
||||
if w.posy+1 >= w.height {
|
||||
return FillSuspend
|
||||
}
|
||||
@@ -1118,6 +1121,14 @@ func (w *LightWindow) setBg() string {
|
||||
return "\x1b[m"
|
||||
}
|
||||
|
||||
func (w *LightWindow) LinkBegin(uri string, params string) {
|
||||
w.renderer.queued.WriteString("\x1b]8;" + params + ";" + uri + "\x1b\\")
|
||||
}
|
||||
|
||||
func (w *LightWindow) LinkEnd() {
|
||||
w.renderer.queued.WriteString("\x1b]8;;\x1b\\")
|
||||
}
|
||||
|
||||
func (w *LightWindow) Fill(text string) FillReturn {
|
||||
w.Move(w.posy, w.posx)
|
||||
code := w.setBg()
|
||||
@@ -1133,7 +1144,7 @@ func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillRetu
|
||||
bg = w.bg
|
||||
}
|
||||
if hasColors, resetCode := w.csiColor(fg, bg, attr); hasColors {
|
||||
defer w.csi("m")
|
||||
defer w.csi("0m")
|
||||
return w.fill(text, resetCode)
|
||||
}
|
||||
return w.fill(text, w.setBg())
|
||||
|
@@ -4,6 +4,7 @@ package tui
|
||||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
@@ -49,6 +50,8 @@ type TcellWindow struct {
|
||||
lastY int
|
||||
moveCursor bool
|
||||
borderStyle BorderStyle
|
||||
uri *string
|
||||
params *string
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Top() int {
|
||||
@@ -552,10 +555,6 @@ func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int,
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Close() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
func fill(x, y, w, h int, n ColorPair, r rune) {
|
||||
for ly := 0; ly <= h; ly++ {
|
||||
for lx := 0; lx <= w; lx++ {
|
||||
@@ -601,6 +600,16 @@ func (w *TcellWindow) Print(text string) {
|
||||
w.printString(text, w.normal)
|
||||
}
|
||||
|
||||
func (w *TcellWindow) withUrl(style tcell.Style) tcell.Style {
|
||||
if w.uri != nil {
|
||||
style = style.Url(*w.uri)
|
||||
if md := regexp.MustCompile(`id=([^:]+)`).FindStringSubmatch(*w.params); len(md) > 1 {
|
||||
style = style.UrlId(md[1])
|
||||
}
|
||||
}
|
||||
return style
|
||||
}
|
||||
|
||||
func (w *TcellWindow) printString(text string, pair ColorPair) {
|
||||
lx := 0
|
||||
a := pair.Attr()
|
||||
@@ -615,6 +624,7 @@ func (w *TcellWindow) printString(text string, pair ColorPair) {
|
||||
Blink(a&Attr(tcell.AttrBlink) != 0).
|
||||
Dim(a&Attr(tcell.AttrDim) != 0)
|
||||
}
|
||||
style = w.withUrl(style)
|
||||
|
||||
gr := uniseg.NewGraphemes(text)
|
||||
for gr.Next() {
|
||||
@@ -665,6 +675,7 @@ func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
|
||||
Underline(a&Attr(tcell.AttrUnderline) != 0).
|
||||
StrikeThrough(a&Attr(tcell.AttrStrikeThrough) != 0).
|
||||
Italic(a&Attr(tcell.AttrItalic) != 0)
|
||||
style = w.withUrl(style)
|
||||
|
||||
gr := uniseg.NewGraphemes(text)
|
||||
Loop:
|
||||
@@ -716,6 +727,16 @@ func (w *TcellWindow) Fill(str string) FillReturn {
|
||||
return w.fillString(str, w.normal)
|
||||
}
|
||||
|
||||
func (w *TcellWindow) LinkBegin(uri string, params string) {
|
||||
w.uri = &uri
|
||||
w.params = ¶ms
|
||||
}
|
||||
|
||||
func (w *TcellWindow) LinkEnd() {
|
||||
w.uri = nil
|
||||
w.params = nil
|
||||
}
|
||||
|
||||
func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
|
||||
if fg == colDefault {
|
||||
fg = w.normal.Fg()
|
||||
|
@@ -387,6 +387,14 @@ func (s BorderShape) HasTop() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s BorderShape) HasBottom() bool {
|
||||
switch s {
|
||||
case BorderNone, BorderLeft, BorderRight, BorderTop, BorderVertical: // No bottom
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type BorderStyle struct {
|
||||
shape BorderShape
|
||||
top rune
|
||||
@@ -402,6 +410,18 @@ type BorderStyle struct {
|
||||
type BorderCharacter int
|
||||
|
||||
func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
||||
if shape == BorderNone {
|
||||
return BorderStyle{
|
||||
shape: shape,
|
||||
top: ' ',
|
||||
bottom: ' ',
|
||||
left: ' ',
|
||||
right: ' ',
|
||||
topLeft: ' ',
|
||||
topRight: ' ',
|
||||
bottomLeft: ' ',
|
||||
bottomRight: ' '}
|
||||
}
|
||||
if !unicode {
|
||||
return BorderStyle{
|
||||
shape: shape,
|
||||
@@ -498,19 +518,6 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
||||
}
|
||||
}
|
||||
|
||||
func MakeTransparentBorder() BorderStyle {
|
||||
return BorderStyle{
|
||||
shape: BorderRounded,
|
||||
top: ' ',
|
||||
bottom: ' ',
|
||||
left: ' ',
|
||||
right: ' ',
|
||||
topLeft: ' ',
|
||||
topRight: ' ',
|
||||
bottomLeft: ' ',
|
||||
bottomRight: ' '}
|
||||
}
|
||||
|
||||
type TermSize struct {
|
||||
Lines int
|
||||
Columns int
|
||||
@@ -552,7 +559,6 @@ type Window interface {
|
||||
DrawHBorder()
|
||||
Refresh()
|
||||
FinishFill()
|
||||
Close()
|
||||
|
||||
X() int
|
||||
Y() int
|
||||
@@ -564,6 +570,8 @@ type Window interface {
|
||||
CPrint(color ColorPair, text string)
|
||||
Fill(text string) FillReturn
|
||||
CFill(fg Color, bg Color, attr Attr, text string) FillReturn
|
||||
LinkBegin(uri string, params string)
|
||||
LinkEnd()
|
||||
Erase()
|
||||
EraseMaybe() bool
|
||||
}
|
||||
|
@@ -306,5 +306,5 @@ func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWi
|
||||
}
|
||||
}
|
||||
|
||||
return wrapped, false
|
||||
return wrapped, overflow
|
||||
}
|
||||
|
@@ -44,11 +44,6 @@ func needWinpty(opts *Options) bool {
|
||||
}
|
||||
|
||||
func runWinpty(args []string, opts *Options) (int, error) {
|
||||
sh, err := sh()
|
||||
if err != nil {
|
||||
return ExitError, err
|
||||
}
|
||||
|
||||
argStr := escapeSingleQuote(args[0])
|
||||
for _, arg := range args[1:] {
|
||||
argStr += " " + escapeSingleQuote(arg)
|
||||
@@ -56,20 +51,30 @@ func runWinpty(args []string, opts *Options) (int, error) {
|
||||
argStr += ` --no-winpty`
|
||||
|
||||
if isMintty345() {
|
||||
return runProxy(argStr, func(temp string) *exec.Cmd {
|
||||
return runProxy(argStr, func(temp string, needBash bool) (*exec.Cmd, error) {
|
||||
sh, err := sh(needBash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cmd := exec.Command(sh, temp)
|
||||
cmd.Env = append(os.Environ(), "MSYS=enable_pcon")
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd
|
||||
return cmd, nil
|
||||
}, opts, false)
|
||||
}
|
||||
|
||||
return runProxy(argStr, func(temp string) *exec.Cmd {
|
||||
return runProxy(argStr, func(temp string, needBash bool) (*exec.Cmd, error) {
|
||||
sh, err := sh(needBash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cmd := exec.Command(sh, "-c", fmt.Sprintf(`winpty < /dev/tty > /dev/tty -- sh %q`, temp))
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd
|
||||
return cmd, nil
|
||||
}, opts, false)
|
||||
}
|
||||
|
108
test/test_go.rb
108
test/test_go.rb
@@ -1443,7 +1443,7 @@ class TestGoFZF < TestBase
|
||||
[0, 3, 6].each do |off|
|
||||
tmux.prepare
|
||||
tmux.send_keys "#{FZF} --hscroll-off=#{off} -q 0 < #{tempname}", :Enter
|
||||
tmux.until { |lines| assert lines[-3]&.end_with?((0..off).to_a.join + '..') }
|
||||
tmux.until { |lines| assert lines[-3]&.end_with?((0..off).to_a.join + '··') }
|
||||
tmux.send_keys '9'
|
||||
tmux.until { |lines| assert lines[-3]&.end_with?('789') }
|
||||
tmux.send_keys :Enter
|
||||
@@ -2814,13 +2814,13 @@ class TestGoFZF < TestBase
|
||||
tmux.send_keys "seq 3 | fzf --height ~100% --border=vertical --preview 'seq {}' --preview-window left,5,border-right --padding 1 --exit-0 --header $'hello\\nworld' --header-lines=2", :Enter
|
||||
expected = <<~OUTPUT
|
||||
│
|
||||
│ 1 │ > 3
|
||||
│ 2 │ 2
|
||||
│ 3 │ 1
|
||||
│ │ hello
|
||||
│ │ world
|
||||
│ │ 1/1 ─
|
||||
│ │ >
|
||||
│ 1 │ > 3
|
||||
│ 2 │ 2
|
||||
│ 3 │ 1
|
||||
│ │ hello
|
||||
│ │ world
|
||||
│ │ 1/1 ─
|
||||
│ │ >
|
||||
│
|
||||
OUTPUT
|
||||
tmux.until { assert_block(expected, _1) }
|
||||
@@ -3073,6 +3073,21 @@ class TestGoFZF < TestBase
|
||||
tmux.until { |lines| assert_includes lines, '/1/1/' }
|
||||
end
|
||||
|
||||
def test_alternative_preview_window_opts
|
||||
tmux.send_keys "seq 10 | #{FZF} --preview-window '~5,2,+0,<100000(~0,+100,wrap,noinfo)' --preview 'seq 1000'", :Enter
|
||||
tmux.until { |lines| assert_equal 10, lines.item_count }
|
||||
tmux.until do |lines|
|
||||
assert_equal ['╭────╮', '│ 10 │', '│ 0 │', '│ 10 │', '│ 1 │'], lines.take(5).map(&:strip)
|
||||
end
|
||||
end
|
||||
|
||||
def test_preview_window_width_exception
|
||||
tmux.send_keys "seq 10 | #{FZF} --scrollbar --preview-window border-left --border --preview 'seq 1000'", :Enter
|
||||
tmux.until do |lines|
|
||||
assert lines[1]&.end_with?(' 1/1000││')
|
||||
end
|
||||
end
|
||||
|
||||
def test_become
|
||||
tmux.send_keys "seq 100 | #{FZF} --bind 'enter:become:seq {} | #{FZF}'", :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||
@@ -3366,6 +3381,66 @@ class TestGoFZF < TestBase
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| assert_includes lines[-3], 'bar' }
|
||||
end
|
||||
|
||||
def test_boundary_match
|
||||
# Underscore boundaries should be ranked lower
|
||||
{
|
||||
default: [' x '] + %w[/x/ [x] -x- -x_ _x- _x_],
|
||||
path: ['/x/', ' x '] + %w[[x] -x- -x_ _x- _x_],
|
||||
history: ['[x]', '-x-', ' x '] + %w[/x/ -x_ _x- _x_]
|
||||
}.each do |scheme, expected|
|
||||
result = `printf -- 'xxx\n-xx\nxx-\n_x_\n_x-\n-x_\n[x]\n-x-\n x \n/x/\n' | #{FZF} -f"'x'" --scheme=#{scheme}`.lines(chomp: true)
|
||||
assert_equal expected, result
|
||||
end
|
||||
end
|
||||
|
||||
def test_preview_window_noinfo
|
||||
# │ 1 ││
|
||||
tmux.send_keys %(#{FZF} --preview 'seq 1000' --preview-window top,noinfo --scrollbar --bind space:change-preview-window:info), :Enter
|
||||
tmux.until do |lines|
|
||||
assert lines[1]&.start_with?('│ 1')
|
||||
assert lines[1]&.end_with?(' ││')
|
||||
end
|
||||
tmux.send_keys :Space
|
||||
tmux.until do |lines|
|
||||
assert lines[1]&.start_with?('│ 1')
|
||||
assert lines[1]&.end_with?('1000││')
|
||||
end
|
||||
end
|
||||
|
||||
def test_gap
|
||||
tmux.send_keys %(seq 100 | #{FZF} --gap --border --reverse), :Enter
|
||||
block = <<~BLOCK
|
||||
╭─────────────────
|
||||
│ >
|
||||
│ 100/100 ──────
|
||||
│ > 1
|
||||
│
|
||||
│ 2
|
||||
│
|
||||
│ 3
|
||||
│
|
||||
│ 4
|
||||
BLOCK
|
||||
tmux.until { assert_block(block, _1) }
|
||||
end
|
||||
|
||||
def test_gap_2
|
||||
tmux.send_keys %(seq 100 | #{FZF} --gap=2 --border --reverse), :Enter
|
||||
block = <<~BLOCK
|
||||
╭─────────────────
|
||||
│ >
|
||||
│ 100/100 ──────
|
||||
│ > 1
|
||||
│
|
||||
│
|
||||
│ 2
|
||||
│
|
||||
│
|
||||
│ 3
|
||||
BLOCK
|
||||
tmux.until { assert_block(block, _1) }
|
||||
end
|
||||
end
|
||||
|
||||
module TestShell
|
||||
@@ -3677,6 +3752,23 @@ module CompletionTest
|
||||
tmux.until { |lines| assert_equal 'unset FZFFOOBAR', lines[-1] }
|
||||
end
|
||||
|
||||
def test_completion_in_command_sequence
|
||||
tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
|
||||
tmux.prepare
|
||||
|
||||
triggers = ['**', '~~', '++', 'ff', '/']
|
||||
triggers.concat(['&', '[', ';', '`']) if instance_of?(TestZsh)
|
||||
|
||||
triggers.each do |trigger|
|
||||
set_var('FZF_COMPLETION_TRIGGER', trigger)
|
||||
command = "echo foo; QUX=THUD unset FZFFOOBR#{trigger}"
|
||||
tmux.send_keys command.sub(/(;|`)$/, '\\\\\1'), :Tab
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_equal 'echo foo; QUX=THUD unset FZFFOOBAR', lines[-1] }
|
||||
end
|
||||
end
|
||||
|
||||
def test_file_completion_unicode
|
||||
FileUtils.mkdir_p('/tmp/fzf-test')
|
||||
tmux.paste "cd /tmp/fzf-test; echo test3 > $'fzf-unicode \\355\\205\\214\\354\\212\\244\\355\\212\\2701'; echo test4 > $'fzf-unicode \\355\\205\\214\\354\\212\\244\\355\\212\\2702'"
|
||||
|
Reference in New Issue
Block a user