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

Compare commits

...

92 Commits
0.8.1 ... 0.8.5

Author SHA1 Message Date
Junegunn Choi
61ba8d5a11 Add a small delay when search is interrupted
Search is interrupted when the query string has changed. This frequently
happens when the user is actively typing in a query. This (rather
arbitrary) delay is introduced not to start the next search immediately,
which is likely to be interrupted as well. The result of it is that fzf
feels more responsive.
2014-06-15 18:35:47 +09:00
Junegunn Choi
4a3a5ee70d [vim] External terminal emulator for GVim 2014-06-15 12:15:39 +09:00
Junegunn Choi
f58a53a001 Fix mouse click on --reverse mode 2014-06-15 04:32:21 +09:00
Junegunn Choi
65c1b53275 [vim] Options to xterm command 2014-06-15 03:18:29 +09:00
Junegunn Choi
0b43f988c7 [vim] Enable fzf in GVim using xterm 2014-06-15 03:06:26 +09:00
Junegunn Choi
f8e357fa19 Extend --nth option to take ranges
As discussed in #55
2014-06-14 00:27:34 +09:00
Junegunn Choi
c3a4e4cd23 Implement CTRL-D 2014-06-12 23:43:09 +09:00
Junegunn Choi
9dac12cb32 Remove duplicate examples from README
As discussed in #54
2014-06-12 23:28:59 +09:00
Junegunn Choi
d76a3646b7 Update Vim example: Rename functions
See: ftp://ftp.vim.org/pub/vim/patches/7.4/7.4.260
2014-06-09 10:06:07 +09:00
Junegunn Choi
d7c734acd6 Ignore regex error inside trim call (#51) 2014-06-07 01:17:38 +09:00
Junegunn Choi
ed13fc8618 Fix fzf-history-widget (#48) 2014-05-29 11:12:48 +09:00
Junegunn Choi
edcd7c6aa6 Remove UTF-8 NFD conversion
We have iconv.
2014-05-29 01:08:44 +09:00
Junegunn Choi
b0fdd6db99 Merge pull request #47 from cskeeters/master
[zsh-keybinding] Remove tailing substitution
2014-05-29 00:20:08 +09:00
Chad Skeeters
edf27f47f2 removed tailing substitution causing all trailing space to be removed when extendedglob is set 2014-05-28 10:16:07 -05:00
Junegunn Choi
3b218b77eb Merge pull request #46 from takac/install-update
Fix fzf-history-widget to strip `*` from history lines when using tmux and fc
2014-05-26 16:59:56 +09:00
Tom Cammann
1e02471940 Update install
Update sed regex to strip "*" from history lines when using tmux and fc
e.g. "637* ls -a"
2014-05-26 08:56:47 +01:00
Junegunn Choi
1b9dadb3d3 Avoid unnecessary confirmation 2014-05-22 02:34:20 +09:00
Junegunn Choi
c3827dea10 Add linewise user confirmation 2014-05-22 02:26:59 +09:00
Junegunn Choi
6a1b916598 OK 2014-05-22 02:24:13 +09:00
Junegunn Choi
a2c7b001d5 Update version/date 2014-05-21 10:43:30 +09:00
Junegunn Choi
3c6e938bb1 Fix arrow keys on zsh widget
Fixes the problem reported by @elemakil. For some reason unknown,
sometimes the escape sequences of arrow keys are prefixed by 27-79
instead of the ordinary 27-91.
2014-05-21 10:22:17 +09:00
Junegunn Choi
5a0afc5fea Merge branch 'aboettger-master' 2014-05-21 01:16:49 +09:00
Junegunn Choi
f37be006c3 Update uninstall script 2014-05-21 01:16:42 +09:00
Andreas Böttger
459c332351 Some improvements 2014-05-20 17:05:02 +02:00
Andreas Böttger
153a87d84a uninstall script 2014-05-20 14:17:03 +02:00
Junegunn Choi
05da892cd2 On writing fzf-tmux combo 2014-05-18 11:01:30 +09:00
Junegunn Choi
f6b1a6278f Add --reverse option (top-to-bottom layout) 2014-05-17 22:07:18 +09:00
Junegunn Choi
db58182483 Customization of key bindings (#40) 2014-05-06 15:39:44 +09:00
Junegunn Choi
6e9f0882da Update README (missing link to fish key bindings file) 2014-05-04 12:56:43 +09:00
Junegunn Choi
7ed18579dc set -o vi is required for vi-mode bash key bindings (#39) 2014-05-04 12:52:33 +09:00
Junegunn Choi
f250fc8f86 Fix #41: [CTRL-T] long file paths causing wrapping artifacts 2014-05-04 11:28:34 +09:00
Junegunn Choi
6eea9603c2 Fix bug in install script 2014-05-04 00:49:29 +09:00
Junegunn Choi
20915529b7 Add link to examples wiki page 2014-05-02 23:38:36 +09:00
Junegunn Choi
b3efccca81 [fish] Remove temporary file after use 2014-05-02 16:35:36 +09:00
Junegunn Choi
809d465de5 Tip on using git ls-tree (#31) 2014-05-02 12:52:06 +09:00
Junegunn Choi
7d15071c63 Fish shell support - installer / key bindings (#33) 2014-05-02 11:27:32 +09:00
Junegunn Choi
89eb1575e7 Simpler check for curses 2014-05-02 04:51:35 +09:00
Junegunn Choi
5d6ed935a4 Update README: master.tar.gz 2014-04-25 19:45:07 +09:00
Junegunn Choi
0528435386 Update README 2014-04-25 19:16:33 +09:00
Junegunn Choi
fe22213b51 Remove gif link 2014-04-14 16:48:54 +09:00
Junegunn Choi
aab42eaaba Update Vim plugin example 2014-04-12 20:02:04 +09:00
Junegunn Choi
16031b0d54 [Vim] Allow vertical split of tmux window 2014-04-12 19:53:39 +09:00
Junegunn Choi
ded184daaf Typo 2014-04-12 19:52:25 +09:00
Junegunn Choi
ecf90bd25b Update README 2014-04-06 15:25:58 +09:00
Junegunn Choi
d82e38adc1 0.8.3 2014-04-05 12:56:10 +09:00
Junegunn Choi
af677e7e35 Vim plugin: do not enable tmux-integration if version < 1.7 2014-04-04 12:43:29 +09:00
Junegunn Choi
6ad38bdad3 Update example: suppress error message from fc on bash (#37)
`'fc' -l 1` generated an error message on bash
2014-04-04 10:26:38 +09:00
Junegunn Choi
8b80136a87 Merge pull request #37 from wellle/zsh-history
Feed all zsh history into fzf
2014-04-03 23:14:28 +09:00
Christian Wellenbrock
97de919152 Feed all zsh history into fzf 2014-04-03 16:11:51 +02:00
Junegunn Choi
0eafa725b9 Fix test code indentation 2014-04-03 14:53:47 +09:00
Junegunn Choi
fa212efe5f Fix ranking when multiple regions overlap
e.g.
  Match region #1: [-----------]
  Match region #2:       [---]
  Match region #3:         [------]
2014-04-03 14:51:01 +09:00
Junegunn Choi
a9056ce90c Add gif showing tmux integration 2014-04-03 01:29:21 +09:00
Junegunn Choi
16682a3f92 Update fe example as the exit status from -0 has changed (#36) 2014-04-03 01:20:22 +09:00
Junegunn Choi
02c01c81a0 Improve -0 and -1 as suggested in #36
- Make -0 and -1 work without -q
- Change exit status to 0 when exiting with -0
2014-04-03 01:06:40 +09:00
Junegunn Choi
22d3929ae3 Implement --select-1 and --exit-0 (#27, #36) 2014-04-02 21:41:57 +09:00
Junegunn Choi
ab9fbf1967 Allow --nth option to take multiple indexes (comma-separated) 2014-04-02 01:49:07 +09:00
Junegunn Choi
608ec2b806 set -o nonomatch for zsh (#34)
Avoid error message in an empty directory
2014-04-01 21:39:40 +09:00
Junegunn Choi
e5ae4f0ef6 Do not load interactive parts when not required (#34) 2014-04-01 20:55:26 +09:00
Junegunn Choi
67ba87d390 Avoid CTRL-T error when default shell != zsh (#34) 2014-04-01 20:49:54 +09:00
Junegunn Choi
77d45cb173 Avoid starting interactive bash (#34) 2014-04-01 20:48:15 +09:00
Junegunn Choi
d83febea46 Merge pull request #35 from junegunn/fix-tmux-on-linux
Fix #34: tmux integration on Linux
2014-04-01 17:58:19 +09:00
Junegunn Choi
546a315884 Fix #34: tmux integration on Linux 2014-04-01 08:55:16 +00:00
Junegunn Choi
af616457e3 Use -p option of split-window instead of manual calculation 2014-03-31 13:48:53 +09:00
Junegunn Choi
1a100a2919 No need for screenrow() 2014-03-31 13:36:58 +09:00
Junegunn Choi
a85bb93b69 Fix use of screenrow when tmux height is given in % 2014-03-31 13:22:52 +09:00
Junegunn Choi
057eda060c Installation on other shells 2014-03-31 10:15:56 +09:00
Junegunn Choi
48f9ee6763 Update install script 2014-03-31 01:01:23 +09:00
Junegunn Choi
52b74abb99 Merge pull request #32 from junegunn/nth
Add --nth and --delimiter option
2014-03-30 15:19:05 +09:00
Junegunn Choi
ec4b8a59fa Implement --nth and --delimiter option 2014-03-30 15:12:04 +09:00
Junegunn Choi
cf8dbf8047 Allow setting tmux split height in % 2014-03-28 17:15:45 +09:00
Junegunn Choi
995d380200 Merge pull request #30 from junegunn/keybinding-tmux-split
Make CTRL-T use tmux split when possible
2014-03-28 15:32:23 +09:00
Junegunn Choi
ae86cdf09a Make CTRL-T use tmux split when possible 2014-03-28 15:28:10 +09:00
Junegunn Choi
2b346659a0 Vim plugin: tmux integration 2014-03-28 00:58:07 +09:00
Junegunn Choi
49081711a9 Execute clear before fzf 2014-03-26 01:34:59 +09:00
Junegunn Choi
e7439ce193 Major update to Vim plugin 2014-03-25 19:55:52 +09:00
Junegunn Choi
b8e438b6be Prefer pre-existing function/alias in Vim plugin 2014-03-25 12:05:57 +09:00
Junegunn Choi
678e950b6d Use --reverse option in fco example (#29) 2014-03-20 10:38:53 +09:00
Junegunn Choi
9ea651f1cd Merge pull request #29 from wellle/fix/fco
Fix small typo in Readme
2014-03-20 10:33:47 +09:00
Christian Wellenbrock
bd98a08b89 Fix small typo in Readme 2014-03-20 00:07:47 +01:00
Junegunn Choi
f02bb4fdac Add fe command to examples section as suggested in #27 2014-03-20 01:57:57 +09:00
Junegunn Choi
0a8352a5cd Quote $1 in vimf example (#26) 2014-03-19 21:57:03 +09:00
Junegunn Choi
737423995d Merge pull request #28 from wellle/ignore-dsstore
Add .DS_Store to .gitignore
2014-03-19 21:19:28 +09:00
Christian Wellenbrock
2916bf7ee4 Add .DS_Store to .gitignore 2014-03-19 13:14:20 +01:00
Junegunn Choi
fa54c5d9b0 Merge pull request #26 from wellle/vimf-query
Add --query parameter to fzf invocation in vimf function
2014-03-19 21:09:52 +09:00
Christian Wellenbrock
693b6651b4 Add --query parameter to fzf invocation in vimf function 2014-03-19 12:30:42 +01:00
Junegunn Choi
5c71ecb267 Implement C-Y (yank) 2014-03-15 16:55:20 +09:00
Junegunn Choi
1ba50eba98 Fix gemspec
Reference:
16ead977fa
2014-03-14 18:25:55 +09:00
Junegunn Choi
2c8a256b13 Update README and install
- Unset multi-select option with +m
2014-03-14 17:53:23 +09:00
Junegunn Choi
f4c5aa03d7 Update README and install script
- Added examples: fbr and fco
- Always use local variables
2014-03-14 17:46:55 +09:00
Junegunn Choi
c6acb2a639 Update README 2014-03-13 15:28:01 +09:00
Junegunn Choi
2296013174 Add ALT-C keybinding for bash 2014-03-13 14:29:27 +09:00
Junegunn Choi
8a3e8c2d81 Install curses gem in user's home directory 2014-03-13 11:01:35 +09:00
10 changed files with 1283 additions and 411 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
pkg pkg
Gemfile.lock Gemfile.lock
.DS_Store

305
README.md
View File

@@ -24,11 +24,24 @@ git clone https://github.com/junegunn/fzf.git ~/.fzf
~/.fzf/install ~/.fzf/install
``` ```
In case you don't have git installed:
```sh
mkdir -p ~/.fzf
curl -L https://github.com/junegunn/fzf/archive/master.tar.gz |
tar xz --strip-components 1 -C ~/.fzf
~/.fzf/install
```
The script will setup: The script will setup:
- `fzf` executable - `fzf` function (bash, zsh, fish)
- Key bindings (`CTRL-T`, `CTRL-R`, etc.) - Key bindings (`CTRL-T`, `CTRL-R`, and `ALT-C`) (bash, zsh, fish)
- Fuzzy auto-completion for bash - Fuzzy auto-completion (bash)
If you don't use any of the aforementioned shells, you have to manually place
fzf executable in a directory included in `$PATH`. Key bindings and
auto-completion will not be available in that case.
### Install as Vim plugin ### Install as Vim plugin
@@ -47,20 +60,33 @@ Usage
``` ```
usage: fzf [options] usage: fzf [options]
Options Search
-m, --multi Enable multi-select
-x, --extended Extended-search mode -x, --extended Extended-search mode
-e, --extended-exact Extended-search mode (exact match) -e, --extended-exact Extended-search mode (exact match)
-q, --query=STR Initial query
-f, --filter=STR Filter mode. Do not start interactive finder.
-s, --sort=MAX Maximum number of matched items to sort (default: 1000)
+s, --no-sort Do not sort the result. Keep the sequence unchanged.
-i Case-insensitive match (default: smart-case match) -i Case-insensitive match (default: smart-case match)
+i Case-sensitive match +i Case-sensitive match
-n, --nth=N[,..] Comma-separated list of field index expressions
for limiting search scope. Each can be a non-zero
integer or a range expression ([BEGIN]..[END])
-d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style)
Search result
-s, --sort=MAX Maximum number of matched items to sort (default: 1000)
+s, --no-sort Do not sort the result. Keep the sequence unchanged.
Interface
-m, --multi Enable multi-select with tab/shift-tab
--no-mouse Disable mouse
+c, --no-color Disable colors +c, --no-color Disable colors
+2, --no-256 Disable 256-color +2, --no-256 Disable 256-color
--black Use black background --black Use black background
--no-mouse Disable mouse --reverse Reverse orientation
Scripting
-q, --query=STR Start the finder with the given query
-1, --select-1 Automatically select the only match
-0, --exit-0 Exit immediately when there's no match
-f, --filter=STR Filter mode. Do not start interactive finder.
Environment variables Environment variables
FZF_DEFAULT_COMMAND Default command to use when input is tty FZF_DEFAULT_COMMAND Default command to use when input is tty
@@ -89,7 +115,7 @@ If you want to preserve the exact sequence of the input, provide `--no-sort` (or
history | fzf +s history | fzf +s
``` ```
### Key binding ### Keys
Use CTRL-J and CTRL-K (or CTRL-N and CTRL-P) to change the selection, press Use CTRL-J and CTRL-K (or CTRL-N and CTRL-P) to change the selection, press
enter key to select the item. CTRL-C, CTRL-G, or ESC will terminate the finder. enter key to select the item. CTRL-C, CTRL-G, or ESC will terminate the finder.
@@ -98,7 +124,8 @@ The following readline key bindings should also work as expected.
- CTRL-A / CTRL-E - CTRL-A / CTRL-E
- CTRL-B / CTRL-F - CTRL-B / CTRL-F
- CTRL-W / CTRL-U - CTRL-H / CTRL-D
- CTRL-W / CTRL-U / CTRL-Y
- ALT-B / ALT-F - ALT-B / ALT-F
If you enable multi-select mode with `-m` option, you can select multiple items If you enable multi-select mode with `-m` option, you can select multiple items
@@ -131,24 +158,26 @@ Useful examples
--------------- ---------------
```sh ```sh
# vimf - Open selected file in Vim # fe [FUZZY PATTERN] - Open the selected file with the default editor
vimf() { # - Bypass fuzzy finder if there's only one match (--select-1)
FILE=$(fzf) && vim "$FILE" # - Exit if there's no match (--exit-0)
fe() {
local file
file=$(fzf --query="$1" --select-1 --exit-0)
[ -n "$file" ] && ${EDITOR:-vim} "$file"
} }
# fd - cd to selected directory # fd - cd to selected directory
fd() { fd() {
DIR=$(find ${1:-*} -path '*/\.*' -prune -o -type d -print 2> /dev/null | fzf) && cd "$DIR" local dir
} dir=$(find ${1:-*} -path '*/\.*' -prune \
-o -type d -print 2> /dev/null | fzf +m) &&
# fda - including hidden directories cd "$dir"
fda() {
DIR=$(find ${1:-.} -type d 2> /dev/null | fzf) && cd "$DIR"
} }
# fh - repeat history # fh - repeat history
fh() { fh() {
eval $(history | fzf +s | sed 's/ *[0-9]* *//') eval $(([ -n "$ZSH_NAME" ] && fc -l 1 || history) | fzf +s | sed 's/ *[0-9]* *//')
} }
# fkill - kill process # fkill - kill process
@@ -157,25 +186,30 @@ fkill() {
} }
``` ```
For more examples, see [the wiki
page](https://github.com/junegunn/fzf/wiki/examples).
Key bindings for command line Key bindings for command line
----------------------------- -----------------------------
The install script will setup the following key bindings. The install script will setup the following key bindings for bash, zsh, and
fish.
### bash
- `CTRL-T` - Paste the selected file path(s) into the command line
- `CTRL-R` - Paste the selected command from history into the command line
The source code can be found in `~/.fzf.bash`.
### zsh
- `CTRL-T` - Paste the selected file path(s) into the command line - `CTRL-T` - Paste the selected file path(s) into the command line
- `CTRL-R` - Paste the selected command from history into the command line - `CTRL-R` - Paste the selected command from history into the command line
- `ALT-C` - cd into the selected directory - `ALT-C` - cd into the selected directory
The source code can be found in `~/.fzf.zsh`. If you're on a tmux session, `CTRL-T` will launch fzf in a new split-window. You
may disable this tmux integration by setting `FZF_TMUX` to 0, or change the
height of the window with `FZF_TMUX_HEIGHT` (e.g. `20`, `50%`).
If you use vi mode on bash, you need to add `set -o vi` *before* `source
~/.fzf.bash` in your .bashrc, so that it correctly sets up key bindings for vi
mode.
If you want to customize the key bindings, consider editing the
installer-generated source code: `~/.fzf.bash`, `~/.fzf.zsh`, and
`~/.config/fish/functions/fzf_key_bindings.fish`.
Auto-completion Auto-completion
--------------- ---------------
@@ -253,7 +287,11 @@ TODO :smiley:
Usage as Vim plugin Usage as Vim plugin
------------------- -------------------
If you install fzf as a Vim plugin, `:FZF` command will be added. (Note: To use fzf in GVim, an external terminal emulator is required.)
### `:FZF[!]`
If you have set up fzf for Vim, `:FZF` command will be added.
```vim ```vim
" Look for files under current directory " Look for files under current directory
@@ -266,27 +304,101 @@ If you install fzf as a Vim plugin, `:FZF` command will be added.
:FZF --no-sort -m /tmp :FZF --no-sort -m /tmp
``` ```
You can override the source command which produces input to fzf. Note that the environment variables `FZF_DEFAULT_COMMAND` and `FZF_DEFAULT_OPTS`
also apply here.
If you're on a tmux session, `:FZF` will launch fzf in a new split-window whose
height can be adjusted with `g:fzf_tmux_height` (default: '40%'). However, the
bang version (`:FZF!`) will always start in fullscreen.
In GVim, you need an external terminal emulator to start fzf with. `xterm`
command is used by default, but you can customize it with `g:fzf_launcher`.
```vim ```vim
let g:fzf_source = 'find . -type f' " This is the default. %s is replaced with fzf command
let g:fzf_launcher = 'xterm -e bash -ic %s'
" Use urxvt instead
let g:fzf_launcher = 'urxvt -geometry 120x30 -e sh -c %s'
``` ```
And you can predefine default options to fzf command. ### `fzf#run([options])`
For more advanced uses, you can call `fzf#run()` function which returns the list
of the selected items.
`fzf#run()` may take an options-dictionary:
| Option name | Type | Description |
| --------------- | ------------- | ------------------------------------------------------------------ |
| `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 |
| `options` | string | Options to fzf |
| `dir` | string | Working directory |
| `tmux_width` | number/string | Use tmux vertical split with the given height (e.g. `20`, `50%`) |
| `tmux_height` | number/string | Use tmux horizontal split with the given height (e.g. `20`, `50%`) |
| `launcher` | string | External terminal emulator to start fzf with (Only used in GVim) |
#### Examples
If `sink` option is not given, `fzf#run` will simply return the list.
```vim ```vim
let g:fzf_options = '--no-color --extended' let items = fzf#run({ 'options': '-m +c', 'dir': '~', 'source': 'ls' })
``` ```
For more advanced uses, you can call `fzf#run` function as follows. But if `sink` is given as a string, the command will be executed for each
selected item.
```vim ```vim
:call fzf#run('tabedit', '-m +c') " Each selected item will be opened in a new tab
let items = fzf#run({ 'sink': 'tabe', 'options': '-m +c', 'dir': '~', 'source': 'ls' })
``` ```
Most of the time, you will prefer native Vim plugins with better integration We can also use a Vim list as the source as follows:
with Vim. The only reason one might consider using fzf in Vim is its speed. For
a very large list of files, fzf is significantly faster and it does not block. ```vim
" Choose a color scheme with fzf
nnoremap <silent> <Leader>C :call fzf#run({
\ 'source':
\ map(split(globpath(&rtp, "colors/*.vim"), "\n"),
\ "substitute(fnamemodify(v:val, ':t'), '\\..\\{-}$', '', '')"),
\ 'sink': 'colo',
\ 'options': '+m',
\ 'tmux_width': 20,
\ 'launcher': 'xterm -geometry 20x30 -e bash -ic %s'
\ })<CR>
```
`sink` option can be a function reference. The following example creates a
handy mapping that selects an open buffer.
```vim
" List of buffers
function! BufList()
redir => ls
silent ls
redir END
return split(ls, '\n')
endfunction
function! BufOpen(e)
execute 'buffer '. matchstr(a:e, '^[ 0-9]*')
endfunction
nnoremap <silent> <Leader><Enter> :call fzf#run({
\ 'source': reverse(BufList()),
\ 'sink': function('BufOpen'),
\ 'options': '+m',
\ 'tmux_height': '40%'
\ })<CR>
```
### Articles
- [fzf+vim+tmux](http://junegunn.kr/2014/04/fzf+vim+tmux)
Tips Tips
---- ----
@@ -320,6 +432,113 @@ This limit can be adjusted with `-s` option, or with the environment variable
export FZF_DEFAULT_OPTS="--sort 20000" export FZF_DEFAULT_OPTS="--sort 20000"
``` ```
### Respecting `.gitignore`, `.hgignore`, and `svn:ignore`
[ag](https://github.com/ggreer/the_silver_searcher) or
[pt](https://github.com/monochromegane/the_platinum_searcher) will do the
filtering:
```sh
# Feed the output of ag into fzf
ag -l -g "" | fzf
# Setting ag as the default source for fzf
export FZF_DEFAULT_COMMAND='ag -l -g ""'
# Now fzf (w/o pipe) will use ag instead of find
fzf
```
### `git ls-tree` for fast traversal
If you're running fzf in a large git repository, `git ls-tree` can boost up the
speed of the traversal.
```sh
# Copy the original fzf function to __fzf
declare -f __fzf > /dev/null ||
eval "$(echo "__fzf() {"; declare -f fzf | grep -v '^{' | tail -n +2)"
# Use git ls-tree when possible
fzf() {
if [ -n "$(git rev-parse HEAD 2> /dev/null)" ]; then
FZF_DEFAULT_COMMAND="git ls-tree -r --name-only HEAD" __fzf "$@"
else
__fzf "$@"
fi
}
```
### Using fzf with tmux splits
It isn't too hard to write your own fzf-tmux combo like the default
CTRL-T key binding. (Or is it?)
```sh
# This is a helper function that splits the current pane to start the given
# command ($1) and sends its output back to the original pane with any number of
# optional keys (shift; $*).
fzf_tmux_helper() {
[ -n "$TMUX_PANE" ] || return
local cmd=$1
shift
tmux split-window -p 40 \
"bash -c \"\$(tmux send-keys -t $TMUX_PANE \"\$(source ~/.fzf.bash; $cmd)\" $*)\""
}
# This is the function we are going to run in the split pane.
# - "find" to list the directories
# - "sed" will escape spaces in the paths.
# - "paste" will join the selected paths into a single line
fzf_tmux_dir() {
fzf_tmux_helper \
'find * -path "*/\.*" -prune -o -type d -print 2> /dev/null |
fzf --multi |
sed "s/ /\\\\ /g" |
paste -sd" " -' Space
}
# Bind CTRL-X-CTRL-D to fzf_tmux_dir
bind '"\C-x\C-d": "$(fzf_tmux_dir)\e\C-e"'
```
### Fish shell
It's [a known bug of fish](https://github.com/fish-shell/fish-shell/issues/1362)
that it doesn't allow reading from STDIN in command substitution, which means
simple `vim (fzf)` won't work as expected. The workaround is to store the result
of fzf to a temporary file.
```sh
function vimf
if fzf > $TMPDIR/fzf.result
vim (cat $TMPDIR/fzf.result)
end
end
function fe
set tmp $TMPDIR/fzf.result
fzf --query="$argv[1]" --select-1 --exit-0 > $tmp
if [ (cat $tmp | wc -l) -gt 0 ]
vim (cat $tmp)
end
end
```
### Windows
fzf works on [Cygwin](http://www.cygwin.com/) and
[MSYS2](http://sourceforge.net/projects/msys2/). You may need to use `--black`
option on MSYS2 to avoid rendering issues.
### Handling UTF-8 NFD paths on OSX
Use iconv to convert NFD paths to NFC:
```sh
find . | iconv -f utf-8-mac -t utf8//ignore | fzf
```
License License
------- -------

9
ext/mkrf_conf.rb Normal file
View File

@@ -0,0 +1,9 @@
require 'rubygems/dependency_installer'
if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.1.0')
Gem::DependencyInstaller.new.install 'curses', '~> 1.0'
end
File.open(File.expand_path('../Rakefile', __FILE__), 'w') do |f|
f.puts 'task :default'
end

444
fzf
View File

@@ -7,7 +7,7 @@
# / __/ / /_/ __/ # / __/ / /_/ __/
# /_/ /___/_/ Fuzzy finder for your shell # /_/ /___/_/ Fuzzy finder for your shell
# #
# Version: 0.8.1 (March 9, 2014) # Version: 0.8.5 (Jun 15, 2014)
# #
# Author: Junegunn Choi # Author: Junegunn Choi
# URL: https://github.com/junegunn/fzf # URL: https://github.com/junegunn/fzf
@@ -50,7 +50,8 @@ end
class FZF class FZF
C = Curses C = Curses
attr_reader :rxflag, :sort, :color, :black, :ansi256, :mouse, :multi, :query, :filter, :extended attr_reader :rxflag, :sort, :nth, :color, :black, :ansi256, :reverse,
:mouse, :multi, :query, :select1, :exit0, :filter, :extended
class AtomicVar class AtomicVar
def initialize value def initialize value
@@ -82,7 +83,12 @@ class FZF
@multi = false @multi = false
@mouse = true @mouse = true
@extended = nil @extended = nil
@select1 = false
@exit0 = false
@filter = nil @filter = nil
@nth = nil
@delim = nil
@reverse = false
argv = argv =
if opts = ENV['FZF_DEFAULT_OPTS'] if opts = ENV['FZF_DEFAULT_OPTS']
@@ -93,7 +99,7 @@ class FZF
end end
while o = argv.shift while o = argv.shift
case o case o
when '--version' then version when '--version' then FZF.version
when '-h', '--help' then usage 0 when '-h', '--help' then usage 0
when '-m', '--multi' then @multi = true when '-m', '--multi' then @multi = true
when '+m', '--no-multi' then @multi = false when '+m', '--no-multi' then @multi = false
@@ -109,7 +115,13 @@ class FZF
when '--no-black' then @black = false when '--no-black' then @black = false
when '--mouse' then @mouse = true when '--mouse' then @mouse = true
when '--no-mouse' then @mouse = false when '--no-mouse' then @mouse = false
when '--reverse' then @reverse = true
when '--no-reverse' then @reverse = false
when '+s', '--no-sort' then @sort = nil when '+s', '--no-sort' then @sort = nil
when '-1', '--select-1' then @select1 = true
when '+1', '--no-select-1' then @select1 = false
when '-0', '--exit-0' then @exit0 = true
when '+0', '--no-exit-0' then @exit0 = false
when '-q', '--query' when '-q', '--query'
usage 1, 'query string required' unless query = argv.shift usage 1, 'query string required' unless query = argv.shift
@query = AtomicVar.new query.dup @query = AtomicVar.new query.dup
@@ -120,6 +132,16 @@ class FZF
@filter = query @filter = query
when /^-f(.*)$/, /^--filter=(.*)$/ when /^-f(.*)$/, /^--filter=(.*)$/
@filter = $1 @filter = $1
when '-n', '--nth'
usage 1, 'field expression required' unless nth = argv.shift
@nth = parse_nth nth
when /^-n([0-9,-\.]+)$/, /^--nth=([0-9,-\.]+)$/
@nth = parse_nth $1
when '-d', '--delimiter'
usage 1, 'delimiter required' unless delim = argv.shift
@delim = FZF.build_delim_regex delim
when /^-d(.+)$/, /^--delimiter=(.+)$/
@delim = FZF.build_delim_regex $1
when '-s', '--sort' when '-s', '--sort'
usage 1, 'sort size required' unless sort = argv.shift usage 1, 'sort size required' unless sort = argv.shift
usage 1, 'invalid sort size' unless sort =~ /^[0-9]+$/ usage 1, 'invalid sort size' unless sort =~ /^[0-9]+$/
@@ -155,67 +177,141 @@ class FZF
end end
end end
def parse_nth nth
nth.split(',').map { |expr|
x = proc { usage 1, "invalid field expression: #{expr}" }
first, second = expr.split('..', 2)
x.call if !first.empty? && first.to_i == 0 ||
second && !second.empty? && (second.to_i == 0 || second.include?('.'))
first = first.empty? ? 1 : first.to_i
second = case second
when nil then first
when '' then -1
else second.to_i
end
Range.new(*[first, second].map { |e| e > 0 ? e - 1 : e })
}
end
def FZF.build_delim_regex delim
Regexp.compile(delim) rescue (delim = Regexp.escape(delim))
Regexp.compile "(?:.*?#{delim})|(?:.+?$)"
end
def start def start
if @filter if @filter
start_reader(false).join start_reader.join
filter_list @new filter_list @new
else else
@stdout = $stdout.clone start_reader
$stdout.reopen($stderr) emit(:key) { q = @query.get; [q, q.length] } unless empty = @query.empty?
if @select1 || @exit0
start_search do |loaded, matches|
len = empty ? @count.get : matches.length
if loaded
if @select1 && len == 1
puts empty ? matches.first : matches.first.first
exit 0
elsif @exit0 && len == 0
exit 0
end
end
start_reader true if loaded || len > 1
init_screen
start_renderer start_renderer
Thread.new { start_loop }
end
end
sleep
else
start_search start_search
start_renderer
start_loop start_loop
end end
end end
end
def filter_list list def filter_list list
matches = get_matcher.match(list, @filter, '', '') matches = matcher.match(list, @filter, '', '')
if @sort && matches.length <= @sort if @sort && matches.length <= @sort
matches = sort_by_rank(matches) matches = FZF.sort(matches)
end end
matches.each { |m| puts m.first } matches.each { |m| puts m.first }
end end
def get_matcher def matcher
@matcher ||=
if @extended if @extended
ExtendedFuzzyMatcher.new @rxflag, @extended ExtendedFuzzyMatcher.new @rxflag, @extended, @nth, @delim
else else
FuzzyMatcher.new @rxflag FuzzyMatcher.new @rxflag, @nth, @delim
end end
end end
class << self
def version def version
File.open(__FILE__, 'r') do |f| File.open(__FILE__, 'r') do |f|
f.each_line do |line| f.each_line do |line|
if line =~ /Version: (.*)/ if line =~ /Version: (.*)/
$stdout.puts "fzf " << $1 $stdout.puts 'fzf ' << $1
exit exit
end end
end end
end end
end end
def sort list
list.sort_by { |tuple| rank tuple }
end
def rank tuple
line, offsets = tuple
matchlen = 0
pe = 0
offsets.sort.each do |pair|
b, e = pair
b = pe if pe > b
pe = e if e > pe
matchlen += e - b if e > b
end
[matchlen, line.length, line]
end
end
def usage x, message = nil def usage x, message = nil
$stderr.puts message if message $stderr.puts message if message
$stderr.puts %[usage: fzf [options] $stderr.puts %[usage: fzf [options]
Options Search
-m, --multi Enable multi-select
-x, --extended Extended-search mode -x, --extended Extended-search mode
-e, --extended-exact Extended-search mode (exact match) -e, --extended-exact Extended-search mode (exact match)
-q, --query=STR Initial query
-f, --filter=STR Filter mode. Do not start interactive finder.
-s, --sort=MAX Maximum number of matched items to sort (default: 1000)
+s, --no-sort Do not sort the result. Keep the sequence unchanged.
-i Case-insensitive match (default: smart-case match) -i Case-insensitive match (default: smart-case match)
+i Case-sensitive match +i Case-sensitive match
-n, --nth=N[,..] Comma-separated list of field index expressions
for limiting search scope. Each can be a non-zero
integer or a range expression ([BEGIN]..[END])
-d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style)
Search result
-s, --sort=MAX Maximum number of matched items to sort (default: 1000)
+s, --no-sort Do not sort the result. Keep the sequence unchanged.
Interface
-m, --multi Enable multi-select with tab/shift-tab
--no-mouse Disable mouse
+c, --no-color Disable colors +c, --no-color Disable colors
+2, --no-256 Disable 256-color +2, --no-256 Disable 256-color
--black Use black background --black Use black background
--no-mouse Disable mouse --reverse Reverse orientation
Scripting
-q, --query=STR Start the finder with the given query
-1, --select-1 Automatically select the only match
-0, --exit-0 Exit immediately when there's no match
-f, --filter=STR Filter mode. Do not start interactive finder.
Environment variables Environment variables
FZF_DEFAULT_COMMAND Default command to use when input is tty FZF_DEFAULT_COMMAND Default command to use when input is tty
@@ -223,121 +319,6 @@ class FZF
exit x exit x
end end
case RUBY_PLATFORM
when /darwin/
module UConv
CHOSUNG = 0x1100
JUNGSUNG = 0x1161
JONGSUNG = 0x11A7
CHOSUNGS = 19
JUNGSUNGS = 21
JONGSUNGS = 28
JJCOUNT = JUNGSUNGS * JONGSUNGS
NFC_BEGIN = 0xAC00
NFC_END = NFC_BEGIN + CHOSUNGS * JUNGSUNGS * JONGSUNGS
def self.nfd str
str.split(//).map do |c|
cp = c.ord
if cp >= NFC_BEGIN && cp < NFC_END
chr = ''
idx = cp - NFC_BEGIN
cho = CHOSUNG + idx / JJCOUNT
jung = JUNGSUNG + (idx % JJCOUNT) / JONGSUNGS
jong = JONGSUNG + idx % JONGSUNGS
chr << cho << jung
chr << jong if jong != JONGSUNG
chr
else
c
end
end
end
def self.to_nfc arr
[NFC_BEGIN + arr[0] * JJCOUNT +
(arr[1] || 0) * JONGSUNGS +
(arr[2] || 0)].pack('U*')
end
if String.method_defined?(:each_char)
def self.split str
str.each_char.to_a
end
else
def self.split str
str.split('')
end
end
def self.nfc str, offsets = []
ret = ''
omap = []
pend = []
split(str).each_with_index do |c, idx|
cp =
begin
c.ord
rescue Exception
next
end
omap << ret.length
unless pend.empty?
if cp >= JUNGSUNG && cp < JUNGSUNG + JUNGSUNGS
pend << cp - JUNGSUNG
next
elsif cp >= JONGSUNG && cp < JONGSUNG + JONGSUNGS
pend << cp - JONGSUNG
next
else
omap[-1] = omap[-1] + 1
ret << to_nfc(pend)
pend.clear
end
end
if cp >= CHOSUNG && cp < CHOSUNG + CHOSUNGS
pend << cp - CHOSUNG
else
ret << c
end
end
ret << to_nfc(pend) unless pend.empty?
return [ret,
offsets.map { |pair|
b, e = pair
[omap[b] || 0, omap[e] || ((omap.last || 0) + 1)] }]
end
end
def convert_item item
UConv.nfc(*item)
end
class Matcher
def query_chars q
UConv.nfd(q)
end
def sanitize q
UConv.nfd(q).join
end
end
else
def convert_item item
item
end
class Matcher
def query_chars q
q.split(//)
end
def sanitize q
q
end
end
end
def emit event def emit event
@mtx.synchronize do @mtx.synchronize do
@events[event] = yield @events[event] = yield
@@ -346,7 +327,11 @@ class FZF
end end
def max_items; C.lines - 2; end def max_items; C.lines - 2; end
def cursor_y; C.lines - 1; end
def cursor_y offset = 0
@reverse ? (offset) : (C.lines - 1 - offset)
end
def cprint str, col def cprint str, col
C.attron(col) do C.attron(col) do
addstr_safe str addstr_safe str
@@ -366,7 +351,7 @@ class FZF
end end
def print_info msg = nil def print_info msg = nil
C.setpos cursor_y - 1, 0 C.setpos cursor_y(1), 0
C.clrtoeol C.clrtoeol
prefix = prefix =
if spinner = @spinner.first if spinner = @spinner.first
@@ -456,21 +441,6 @@ class FZF
C.attroff color(:chosen, true) if chosen C.attroff color(:chosen, true) if chosen
end end
def sort_by_rank list
list.sort_by { |tuple|
line, offsets = tuple
matchlen = 0
pe = nil
offsets.sort.each do |pair|
b, e = pair
b = pe if pe && pe > b
pe = e
matchlen += e - b
end
[matchlen, line.length, line]
}
end
AFTER_1_9 = RUBY_VERSION.split('.').map { |e| e.rjust(3, '0') }.join >= '001009' AFTER_1_9 = RUBY_VERSION.split('.').map { |e| e.rjust(3, '0') }.join >= '001009'
if AFTER_1_9 if AFTER_1_9
@@ -483,7 +453,7 @@ class FZF
width = width str width = width str
diff = 0 diff = 0
while width > len while width > len
width -= (left ? str[0, 1] : str[-1, 1]) =~ @@wrx ? 2 : 1 width -= ((left ? str[0, 1] : str[-1, 1]) =~ @@wrx ? 2 : 1) rescue 1
str = left ? str[1..-1] : str[0...-1] str = left ? str[1..-1] : str[0...-1]
diff += 1 diff += 1
end end
@@ -517,6 +487,9 @@ class FZF
end end
def init_screen def init_screen
@stdout = $stdout.clone
$stdout.reopen($stderr)
C.init_screen C.init_screen
C.mousemask C::ALL_MOUSE_EVENTS if @mouse C.mousemask C::ALL_MOUSE_EVENTS if @mouse
C.start_color C.start_color
@@ -574,7 +547,7 @@ class FZF
C.refresh C.refresh
end end
def start_reader curses def start_reader
stream = stream =
if @source.tty? if @source.tty?
if default_command = ENV['FZF_DEFAULT_COMMAND'] if default_command = ENV['FZF_DEFAULT_COMMAND']
@@ -593,13 +566,12 @@ class FZF
emit(:new) { @new << line.chomp } emit(:new) { @new << line.chomp }
end end
emit(:loaded) { true } emit(:loaded) { true }
@spinner.clear if curses @spinner.clear if @spinner
end end
end end
def start_search def start_search &callback
matcher = get_matcher Thread.new do
searcher = Thread.new {
lists = [] lists = []
events = {} events = {}
fcache = {} fcache = {}
@@ -638,7 +610,7 @@ class FZF
progress = 0 progress = 0
started_at = Time.now started_at = Time.now
if new_search && !lists.empty? if updated = new_search && !lists.empty?
q, cx = events.delete(:key) || [q, 0] q, cx = events.delete(:key) || [q, 0]
empty = matcher.empty?(q) empty = matcher.empty?(q)
unless matches = fcache[q] unless matches = fcache[q]
@@ -657,10 +629,13 @@ class FZF
found.concat(q.empty? ? list : found.concat(q.empty? ? list :
matcher.match(list, q, q[0, cx], q[cx..-1])) matcher.match(list, q, q[0, cx], q[cx..-1]))
end end
next if skip if skip
sleep 0.1
next
end
matches = @sort ? found : found.reverse matches = @sort ? found : found.reverse
if !empty && @sort && matches.length <= @sort if !empty && @sort && matches.length <= @sort
matches = sort_by_rank(matches) matches = FZF.sort(matches)
end end
fcache[q] = matches fcache[q] = matches
end end
@@ -669,6 +644,10 @@ class FZF
@matches.set matches @matches.set matches
end#new_search end#new_search
callback = nil if callback &&
(updated || events[:loaded]) &&
callback.call(events[:loaded], matches)
# This small delay reduces the number of partial lists # This small delay reduces the number of partial lists
sleep((delay = [20, delay + 5].min) * 0.01) unless user_input sleep((delay = [20, delay + 5].min) * 0.01) unless user_input
@@ -677,7 +656,7 @@ class FZF
rescue Exception => e rescue Exception => e
@main.raise e @main.raise e
end end
} end
end end
def pick def pick
@@ -693,7 +672,7 @@ class FZF
# Wipe # Wipe
if items.length < @plcount if items.length < @plcount
@plcount.downto(items.length) do |idx| @plcount.downto(items.length) do |idx|
C.setpos cursor_y - idx - 2, 0 C.setpos cursor_y(idx + 2), 0
C.clrtoeol C.clrtoeol
end end
end end
@@ -708,10 +687,10 @@ class FZF
} }
items.each_with_index do |item, idx| items.each_with_index do |item, idx|
next unless wipe || cleanse.include?(idx) next unless wipe || cleanse.include?(idx)
row = cursor_y - idx - 2 row = cursor_y(idx + 2)
chosen = idx == vcursor chosen = idx == vcursor
selected = @selects.include?([*item][0]) selected = @selects.include?([*item][0])
line, offsets = convert_item item line, offsets = item
tokens = format line, maxc, offsets tokens = format line, maxc, offsets
print_item row, tokens, chosen, selected print_item row, tokens, chosen, selected
end end
@@ -721,6 +700,8 @@ class FZF
end end
def start_renderer def start_renderer
init_screen
Thread.new do Thread.new do
begin begin
while blk = @queue.shift while blk = @queue.shift
@@ -828,7 +809,7 @@ class FZF
ord = ord =
case read_nb(1, :esc) case read_nb(1, :esc)
when 91 when 91, 79
case read_nb(1, nil) case read_nb(1, nil)
when 68 then ctrl(:b) when 68 then ctrl(:b)
when 67 then ctrl(:f) when 67 then ctrl(:f)
@@ -906,26 +887,40 @@ class FZF
begin begin
input = @query.get.dup input = @query.get.dup
cursor = input.length cursor = input.length
yanked = ''
mouse_event = MouseEvent.new
backword = proc { backword = proc {
cursor = (input[0, cursor].rindex(/\s\S/) || -1) + 1 cursor = (input[0, cursor].rindex(/\s\S/) || -1) + 1
} }
actions = { actions = {
:esc => proc { exit 1 }, :esc => proc { exit 1 },
ctrl(:d) => proc { exit 1 if input.empty? }, ctrl(:d) => proc {
if input.empty?
exit 1
elsif cursor < input.length
input = input[0...cursor] + input[(cursor + 1)..-1]
end
},
ctrl(:m) => proc { ctrl(:m) => proc {
got = pick got = pick
exit 0 exit 0
}, },
ctrl(:u) => proc { input = input[cursor..-1]; cursor = 0 }, ctrl(:u) => proc {
yanked = input[0...cursor] if cursor > 0
input = input[cursor..-1]
cursor = 0
},
ctrl(:a) => proc { cursor = 0; nil }, ctrl(:a) => proc { cursor = 0; nil },
ctrl(:e) => proc { cursor = input.length; nil }, ctrl(:e) => proc { cursor = input.length; nil },
ctrl(:j) => proc { vselect { |v| v - 1 } }, ctrl(:j) => proc { vselect { |v| v - (@reverse ? -1 : 1) } },
ctrl(:k) => proc { vselect { |v| v + 1 } }, ctrl(:k) => proc { vselect { |v| v + (@reverse ? -1 : 1) } },
ctrl(:w) => proc { ctrl(:w) => proc {
pcursor = cursor pcursor = cursor
backword.call backword.call
yanked = input[cursor...pcursor] if pcursor > cursor
input = input[0...cursor] + input[pcursor..-1] input = input[0...cursor] + input[pcursor..-1]
}, },
ctrl(:y) => proc { actions[:default].call yanked },
ctrl(:h) => proc { input[cursor -= 1] = '' if cursor > 0 }, ctrl(:h) => proc { input[cursor -= 1] = '' if cursor > 0 },
ctrl(:i) => proc { |o| ctrl(:i) => proc { |o|
if @multi && sel = pick if @multi && sel = pick
@@ -938,7 +933,7 @@ class FZF
when :stab then 1 when :stab then 1
when :sclick then 0 when :sclick then 0
else -1 else -1
end } end * (@reverse ? -1 : 1) }
end end
}, },
ctrl(:b) => proc { cursor = [0, cursor - 1].max; nil }, ctrl(:b) => proc { cursor = [0, cursor - 1].max; nil },
@@ -952,21 +947,7 @@ class FZF
cursor += (input[cursor..-1].index(/(\S\s)|(.$)/) || -1) + 1 cursor += (input[cursor..-1].index(/(\S\s)|(.$)/) || -1) + 1
nil nil
}, },
} :default => proc { |val|
actions[ctrl(:p)] = actions[ctrl(:k)]
actions[ctrl(:n)] = actions[ctrl(:j)]
actions[:stab] = actions[ctrl(:i)]
actions[127] = actions[ctrl(:h)]
actions[ctrl(:q)] = actions[ctrl(:g)] = actions[ctrl(:c)] = actions[:esc]
emit(:key) { [@query.get, cursor] } unless @query.empty?
mouse = MouseEvent.new
while true
@cursor_x.set cursor
render { print_input }
if key = get_input(actions)
upd = actions.fetch(key, proc { |val|
case val case val
when String when String
input.insert cursor, val input.insert cursor, val
@@ -976,6 +957,7 @@ class FZF
case event case event
when :click, :release when :click, :release
x, y, shift = val.values_at :x, :y, :shift x, y, shift = val.values_at :x, :y, :shift
y = @reverse ? (C.lines - 1 - y) : y
if y == cursor_y if y == cursor_y
cursor = [0, [input.length, x - 2].min].max cursor = [0, [input.length, x - 2].min].max
elsif x > 1 && y <= max_items elsif x > 1 && y <= max_items
@@ -985,9 +967,9 @@ class FZF
when :click when :click
vselect { |_| tv } vselect { |_| tv }
actions[ctrl(:i)].call(:sclick) if shift actions[ctrl(:i)].call(:sclick) if shift
mouse.v = tv mouse_event.v = tv
when :release when :release
if !shift && mouse.double?(tv) if !shift && mouse_event.double?(tv)
actions[ctrl(:m)].call actions[ctrl(:m)].call
end end
end end
@@ -998,7 +980,20 @@ class FZF
actions[ctrl(diff > 0 ? :j : :k)].call actions[ctrl(diff > 0 ? :j : :k)].call
end end
end end
}).call(key) }
}
actions[ctrl(:p)] = actions[ctrl(:k)]
actions[ctrl(:n)] = actions[ctrl(:j)]
actions[:stab] = actions[ctrl(:i)]
actions[127] = actions[ctrl(:h)]
actions[ctrl(:q)] = actions[ctrl(:g)] = actions[ctrl(:c)] = actions[:esc]
while true
@cursor_x.set cursor
render { print_input }
if key = get_input(actions)
upd = actions.fetch(key, actions[:default]).call(key)
# Dispatch key event # Dispatch key event
emit(:key) { [@query.set(input.dup), cursor] } if upd emit(:key) { [@query.set(input.dup), cursor] } if upd
@@ -1018,10 +1013,59 @@ class FZF
end end
end end
class Matcher
class MatchData
def initialize n
@n = n
end
def offset _
@n
end
end
def initialize nth, delim
@nth = nth
@delim = delim
@tokens_cache = {}
end
def tokenize str
@tokens_cache[str] ||=
unless @delim
# AWK default
prefix_length = str[/^\s+/].length rescue 0
[prefix_length, (str.strip.scan(/\S+\s*/) rescue [])]
else
prefix_length = 0
[prefix_length, (str.scan(@delim) rescue [])]
end
end
def do_match str, pat
if @nth
prefix_length, tokens = tokenize str
@nth.each do |n|
if (range = tokens[n]) && (token = range.join) &&
(md = token.sub(/\s+$/, '').match(pat) rescue nil)
prefix_length += (tokens[0...(n.begin)] || []).join.length
offset = md.offset(0).map { |o| o + prefix_length }
return MatchData.new(offset)
end
end
nil
else
str.match(pat) rescue nil
end
end
end
class FuzzyMatcher < Matcher class FuzzyMatcher < Matcher
attr_reader :caches, :rxflag attr_reader :caches, :rxflag
def initialize rxflag def initialize rxflag, nth = nil, delim = nil
super nth, delim
@caches = Hash.new { |h, k| h[k] = {} } @caches = Hash.new { |h, k| h[k] = {} }
@regexp = {} @regexp = {}
@rxflag = rxflag @rxflag = rxflag
@@ -1038,7 +1082,7 @@ class FZF
def fuzzy_regex q def fuzzy_regex q
@regexp[q] ||= begin @regexp[q] ||= begin
q = q.downcase if @rxflag == Regexp::IGNORECASE q = q.downcase if @rxflag == Regexp::IGNORECASE
Regexp.new(query_chars(q).inject('') { |sum, e| Regexp.new(q.split(//).inject('') { |sum, e|
e = Regexp.escape e e = Regexp.escape e
sum << (e.length > 1 ? "(?:#{e}).*?" : # FIXME: not equivalent sum << (e.length > 1 ? "(?:#{e}).*?" : # FIXME: not equivalent
"#{e}[^#{e}]*?") "#{e}[^#{e}]*?")
@@ -1065,15 +1109,15 @@ class FZF
cache[q] ||= (partial_cache ? cache[q] ||= (partial_cache ?
partial_cache.map { |e| e.first } : list).map { |line| partial_cache.map { |e| e.first } : list).map { |line|
# Ignore errors: e.g. invalid byte sequence in UTF-8 # Ignore errors: e.g. invalid byte sequence in UTF-8
md = line.match(regexp) rescue nil md = do_match(line, regexp)
md && [line, [md.offset(0)]] md && [line, [md.offset(0)]]
}.compact }.compact
end end
end end
class ExtendedFuzzyMatcher < FuzzyMatcher class ExtendedFuzzyMatcher < FuzzyMatcher
def initialize rxflag, mode = :fuzzy def initialize rxflag, mode = :fuzzy, nth = nil, delim = nil
super rxflag super rxflag, nth, delim
@regexps = {} @regexps = {}
@mode = mode @mode = mode
end end
@@ -1096,7 +1140,7 @@ class FZF
when '' when ''
nil nil
when /^\^(.*)\$$/ when /^\^(.*)\$$/
Regexp.new('^' << sanitize(Regexp.escape($1)) << '$', rxflag_for(w)) Regexp.new('^' << Regexp.escape($1) << '$', rxflag_for(w))
when /^'/ when /^'/
if @mode == :fuzzy && w.length > 1 if @mode == :fuzzy && w.length > 1
exact_regex w[1..-1] exact_regex w[1..-1]
@@ -1105,10 +1149,10 @@ class FZF
end end
when /^\^/ when /^\^/
w.length > 1 ? w.length > 1 ?
Regexp.new('^' << sanitize(Regexp.escape(w[1..-1])), rxflag_for(w)) : nil Regexp.new('^' << Regexp.escape(w[1..-1]), rxflag_for(w)) : nil
when /\$$/ when /\$$/
w.length > 1 ? w.length > 1 ?
Regexp.new(sanitize(Regexp.escape(w[0..-2])) << '$', rxflag_for(w)) : nil Regexp.new(Regexp.escape(w[0..-2]) << '$', rxflag_for(w)) : nil
else else
@mode == :fuzzy ? fuzzy_regex(w) : exact_regex(w) @mode == :fuzzy ? fuzzy_regex(w) : exact_regex(w)
end, invert ] end, invert ]
@@ -1116,7 +1160,7 @@ class FZF
end end
def exact_regex w def exact_regex w
Regexp.new(sanitize(Regexp.escape(w)), rxflag_for(w)) Regexp.new(Regexp.escape(w), rxflag_for(w))
end end
def match list, q, prefix, suffix def match list, q, prefix, suffix
@@ -1135,7 +1179,7 @@ class FZF
offsets = [] offsets = []
regexps.all? { |pair| regexps.all? { |pair|
regexp, invert = pair regexp, invert = pair
md = line.match(regexp) rescue nil md = do_match(line, regexp)
if md && !invert if md && !invert
offsets << md.offset(0) offsets << md.offset(0)
elsif !md && invert elsif !md && invert

View File

@@ -1,7 +1,7 @@
# coding: utf-8 # coding: utf-8
Gem::Specification.new do |spec| Gem::Specification.new do |spec|
spec.name = 'fzf' spec.name = 'fzf'
spec.version = '0.8.1' spec.version = '0.8.4'
spec.authors = ['Junegunn Choi'] spec.authors = ['Junegunn Choi']
spec.email = ['junegunn.c@gmail.com'] spec.email = ['junegunn.c@gmail.com']
spec.description = %q{Fuzzy finder for your shell} spec.description = %q{Fuzzy finder for your shell}
@@ -13,5 +13,5 @@ Gem::Specification.new do |spec|
spec.files = %w[fzf.gemspec] spec.files = %w[fzf.gemspec]
spec.executables = 'fzf' spec.executables = 'fzf'
spec.add_runtime_dependency 'curses', '~> 1.0.0' spec.extensions += ['ext/mkrf_conf.rb']
end end

230
install
View File

@@ -12,10 +12,9 @@ if [ $? -ne 0 ]; then
fi fi
# System ruby is preferred # System ruby is preferred
curses_check="begin; require 'curses'; rescue Exception; exit 1; end"
system_ruby=/usr/bin/ruby system_ruby=/usr/bin/ruby
if [ -x $system_ruby -a $system_ruby != "$ruby" ]; then if [ -x $system_ruby -a $system_ruby != "$ruby" ]; then
$system_ruby --disable-gems -e "$curses_check" 2> /dev/null $system_ruby --disable-gems -rcurses -e0 2> /dev/null
[ $? -eq 0 ] && ruby=$system_ruby [ $? -eq 0 ] && ruby=$system_ruby
fi fi
@@ -23,16 +22,19 @@ echo "OK ($ruby)"
# Curses-support # Curses-support
echo -n "Checking Curses support ... " echo -n "Checking Curses support ... "
"$ruby" -e "$curses_check" "$ruby" -rcurses -e0 2> /dev/null
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
echo "OK" echo "OK"
else else
echo "Not found" echo "Not found"
echo "Installing 'curses' gem ... " echo "Installing 'curses' gem ... "
/usr/bin/env gem install curses -v 1.0.0 /usr/bin/env gem install curses -v 1.0.0 --user-install
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo
echo "Failed to install 'curses' gem." echo "Failed to install 'curses' gem."
echo "Try installing it as root: sudo gem install curses" if [[ $(uname -r) =~ 'ARCH' ]]; then
echo "Make sure that base-devel package group is installed."
fi
exit 1 exit 1
fi fi
fi fi
@@ -42,7 +44,7 @@ echo -n "Checking Ruby version ... "
"$ruby" -e 'exit RUBY_VERSION >= "1.9"' "$ruby" -e 'exit RUBY_VERSION >= "1.9"'
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
echo ">= 1.9" echo ">= 1.9"
"$ruby" --disable-gems -e "$curses_check" "$ruby" --disable-gems -rcurses -e0 2> /dev/null
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
fzf_cmd="$ruby --disable-gems $fzf_base/fzf" fzf_cmd="$ruby --disable-gems $fzf_base/fzf"
else else
@@ -70,7 +72,7 @@ for shell in bash zsh; do
echo -n "Generate ~/.fzf.$shell ... " echo -n "Generate ~/.fzf.$shell ... "
src=~/.fzf.${shell} src=~/.fzf.${shell}
fzf_completion="source $fzf_base/fzf-completion.${shell}" fzf_completion="[[ \$- =~ i ]] && source $fzf_base/fzf-completion.${shell}"
if [ $auto_completion -ne 0 ]; then if [ $auto_completion -ne 0 ]; then
fzf_completion="# $fzf_completion" fzf_completion="# $fzf_completion"
fi fi
@@ -92,11 +94,9 @@ EOF
if [ $key_bindings -eq 0 ]; then if [ $key_bindings -eq 0 ]; then
if [ $shell = bash ]; then if [ $shell = bash ]; then
cat >> $src << "EOF" cat >> $src << "EOFZF"
# Key bindings # Key bindings
# ------------ # ------------
if [[ $- =~ i ]]; then
__fsel() { __fsel() {
find * -path '*/\.*' -prune \ find * -path '*/\.*' -prune \
-o -type f -print \ -o -type f -print \
@@ -107,54 +107,107 @@ __fsel() {
echo echo
} }
if [[ $- =~ i ]]; then
__fsel_tmux() {
local height
height=${FZF_TMUX_HEIGHT:-40%}
if [[ $height =~ %$ ]]; then
height="-p ${height%\%}"
else
height="-l $height"
fi
tmux split-window $height "bash -c 'source ~/.fzf.bash; tmux send-keys -t $TMUX_PANE \"\$(__fsel)\"'"
}
__fcd() {
local dir
dir=$(find ${1:-*} -path '*/\.*' -prune -o -type d -print 2> /dev/null | fzf +m) && printf 'cd %q' "$dir"
}
__use_tmux=0
[ -n "$TMUX_PANE" -a ${FZF_TMUX:-1} -ne 0 -a ${LINES:-40} -gt 15 ] && __use_tmux=1
if [ -z "$(set -o | grep '^vi.*on')" ]; then if [ -z "$(set -o | grep '^vi.*on')" ]; then
# Required to refresh the prompt after fzf # Required to refresh the prompt after fzf
bind '"\er": redraw-current-line' bind '"\er": redraw-current-line'
# CTRL-T - Paste the selected file path into the command line # CTRL-T - Paste the selected file path into the command line
bind '"\C-t": " \C-u \C-a\C-k$(__fsel)\e\C-e\C-y\C-a\C-y\ey\C-h\C-e\er"' if [ $__use_tmux -eq 1 ]; then
bind '"\C-t": " \C-u \C-a\C-k$(__fsel_tmux)\e\C-e\C-y\C-a\C-d\C-y\ey\C-h"'
else
bind '"\C-t": " \C-u \C-a\C-k$(__fsel)\e\C-e\C-y\C-a\C-y\ey\C-h\C-e\er \C-h"'
fi
# CTRL-R - Paste the selected command from history into the command line # CTRL-R - Paste the selected command from history into the command line
bind '"\C-r": " \C-e\C-u$(HISTTIMEFORMAT= history | fzf +s | sed \"s/ *[0-9]* *//\")\e\C-e\er"' bind '"\C-r": " \C-e\C-u$(HISTTIMEFORMAT= history | fzf +s +m -n..,1,2.. | sed \"s/ *[0-9]* *//\")\e\C-e\er"'
# ALT-C - cd into the selected directory
bind '"\ec": " \C-e\C-u$(__fcd)\e\C-e\er\C-m"'
else else
bind '"\C-x\C-e": shell-expand-line' bind '"\C-x\C-e": shell-expand-line'
bind '"\C-x\C-r": redraw-current-line' bind '"\C-x\C-r": redraw-current-line'
# CTRL-T - Paste the selected file path into the command line # CTRL-T - Paste the selected file path into the command line
# - FIXME: Selected items are attached to the end regardless of cursor position # - FIXME: Selected items are attached to the end regardless of cursor position
bind '"\C-t": "\eddi$(__fsel)\C-x\C-e\e0P$a \C-x\C-r"' if [ $__use_tmux -eq 1 ]; then
bind '"\C-t": "\e$a \eddi$(__fsel_tmux)\C-x\C-e\e0P$xa"'
else
bind '"\C-t": "\e$a \eddi$(__fsel)\C-x\C-e\e0Px$a \C-x\C-r\exa "'
fi
# CTRL-R - Paste the selected command from history into the command line # CTRL-R - Paste the selected command from history into the command line
bind '"\C-r": "\eddi$(HISTTIMEFORMAT= history | fzf +s | sed \"s/ *[0-9]* *//\")\C-x\C-e\e$a\C-x\C-r"' bind '"\C-r": "\eddi$(HISTTIMEFORMAT= history | fzf +s +m -n..,1,2.. | sed \"s/ *[0-9]* *//\")\C-x\C-e\e$a\C-x\C-r"'
# ALT-C - cd into the selected directory
bind '"\ec": "\eddi$(__fcd)\C-x\C-e\C-x\C-r\C-m"'
fi fi
unset __use_tmux
fi fi
EOF EOFZF
else else
cat >> $src << "EOF" cat >> $src << "EOFZF"
# Key bindings # Key bindings
# ------------ # ------------
# CTRL-T - Paste the selected file path(s) into the command line # CTRL-T - Paste the selected file path(s) into the command line
fzf-file-widget() { __fsel() {
local FILES set -o nonomatch
local IFS="
"
FILES=($(
find * -path '*/\.*' -prune \ find * -path '*/\.*' -prune \
-o -type f -print \ -o -type f -print \
-o -type d -print \ -o -type d -print \
-o -type l -print 2> /dev/null | fzf -m)) -o -type l -print 2> /dev/null | fzf -m | while read item; do
unset IFS printf '%q ' "$item"
FILES=$FILES:q done
LBUFFER="${LBUFFER%% #} $FILES" echo
zle redisplay
} }
if [[ $- =~ i ]]; then
if [ -n "$TMUX_PANE" -a ${FZF_TMUX:-1} -ne 0 -a ${LINES:-40} -gt 15 ]; then
fzf-file-widget() {
local height
height=${FZF_TMUX_HEIGHT:-40%}
if [[ $height =~ %$ ]]; then
height="-p ${height%\%}"
else
height="-l $height"
fi
tmux split-window $height "zsh -c 'source ~/.fzf.zsh; tmux send-keys -t $TMUX_PANE \"\$(__fsel)\"'"
}
else
fzf-file-widget() {
LBUFFER="${LBUFFER}$(__fsel)"
zle redisplay
}
fi
zle -N fzf-file-widget zle -N fzf-file-widget
bindkey '^T' fzf-file-widget bindkey '^T' fzf-file-widget
# ALT-C - cd into the selected directory # ALT-C - cd into the selected directory
fzf-cd-widget() { fzf-cd-widget() {
cd "${$(find * -path '*/\.*' -prune \ cd "${$(set -o nonomatch; find * -path '*/\.*' -prune \
-o -type d -print 2> /dev/null | fzf):-.}" -o -type d -print 2> /dev/null | fzf):-.}"
zle reset-prompt zle reset-prompt
} }
@@ -163,40 +216,137 @@ bindkey '\ec' fzf-cd-widget
# CTRL-R - Paste the selected command from history into the command line # CTRL-R - Paste the selected command from history into the command line
fzf-history-widget() { fzf-history-widget() {
LBUFFER=$(fc -l 1 | fzf +s | sed "s/ *[0-9]* *//") LBUFFER=$(fc -l 1 | fzf +s +m -n..,1,2.. | sed "s/ *[0-9*]* *//")
zle redisplay zle redisplay
} }
zle -N fzf-history-widget zle -N fzf-history-widget
bindkey '^R' fzf-history-widget bindkey '^R' fzf-history-widget
EOF fi
EOFZF
fi fi
fi fi
echo "OK" echo "OK"
done done
echo # fish
for shell in bash zsh; do has_fish=0
rc=~/.${shell}rc if [ -n "$(which fish)" ]; then
src="source ~/.fzf.${shell}" has_fish=1
echo -n "Generate ~/.config/fish/functions/fzf.fish ... "
mkdir -p ~/.config/fish/functions
cat > ~/.config/fish/functions/fzf.fish << EOFZF
function fzf
$fzf_cmd \$argv
end
EOFZF
echo "OK"
echo "Update $rc:" if [ $key_bindings -eq 0 ]; then
echo " - $src" echo -n "Generate ~/.config/fish/functions/fzf_key_bindings.fish ... "
if [ $(grep -F "$src" $rc | wc -l) -gt 0 ]; then cat > ~/.config/fish/functions/fzf_key_bindings.fish << "EOFZF"
echo " - Not added (already being sourced)" function fzf_key_bindings
function __fzf_select
find * -path '*/\.*' -prune \
-o -type f -print \
-o -type d -print \
-o -type l -print 2> /dev/null | fzf -m | while read item
echo -n (echo -n "$item" | sed 's/ /\\\\ /g')' '
end
echo
end
function __fzf_ctrl_t
if [ -n "$TMUX_PANE" -a "$FZF_TMUX" != "0" ]
tmux split-window (__fzf_tmux_height) "fish -c 'fzf_key_bindings; __fzf_ctrl_t_tmux \\$TMUX_PANE'"
else else
echo $src >> $rc __fzf_select > $TMPDIR/fzf.result
and commandline -i (cat $TMPDIR/fzf.result)
rm -f $TMPDIR/fzf.result
end
end
function __fzf_ctrl_t_tmux
__fzf_select > $TMPDIR/fzf.result
and tmux send-keys -t $argv[1] (cat $TMPDIR/fzf.result)
rm -f $TMPDIR/fzf.result
end
function __fzf_ctrl_r
if history | fzf +s +m > $TMPDIR/fzf.result
commandline (cat $TMPDIR/fzf.result)
else
commandline -f repaint
end
rm -f $TMPDIR/fzf.result
end
function __fzf_alt_c
find * -path '*/\.*' -prune -o -type d -print 2> /dev/null | fzf +m > $TMPDIR/fzf.result
if [ (cat $TMPDIR/fzf.result | wc -l) -gt 0 ]
cd (cat $TMPDIR/fzf.result)
end
commandline -f repaint
rm -f $TMPDIR/fzf.result
end
function __fzf_tmux_height
if set -q FZF_TMUX_HEIGHT
set height $FZF_TMUX_HEIGHT
else
set height 40%
end
if echo $height | grep -q -E '%$'
echo "-p "(echo $height | sed 's/%$//')
else
echo "-l $height"
end
set -e height
end
bind \ct '__fzf_ctrl_t'
bind \cr '__fzf_ctrl_r'
bind \ec '__fzf_alt_c'
end
EOFZF
echo "OK"
fi
fi
append_line() {
echo "Update $2:"
echo " - $1"
[ -f "$2" ] || touch "$2"
line=$(grep -nF "$1" "$2" | sed 's/:.*//')
if [ -n "$line" ]; then
echo " - Already exists (line #$line)"
else
echo "$1" >> "$2"
echo " - Added" echo " - Added"
fi fi
echo echo
}
echo
for shell in bash zsh; do
append_line "source ~/.fzf.${shell}" ~/.${shell}rc
done done
if [ $key_bindings -eq 0 -a $has_fish -eq 1 ]; then
bind_file=~/.config/fish/functions/fish_user_key_bindings.fish
append_line "fzf_key_bindings" "$bind_file"
fi
cat << EOF cat << EOF
Finished. Reload your .bashrc or .zshrc. Finished. Restart your shell or reload config file.
source ~/.bashrc # bash source ~/.bashrc # bash
source ~/.zshrc # zsh source ~/.zshrc # zsh
EOF
[ $has_fish -eq 1 ] && echo " fzf_key_bindings # fish"; cat << EOF
To uninstall fzf, simply remove the added line. Use uninstall script to remove fzf.
For more information, see: https://github.com/junegunn/fzf
EOF EOF

View File

@@ -1,4 +1,4 @@
" Copyright (c) 2013 Junegunn Choi " Copyright (c) 2014 Junegunn Choi
" "
" MIT License " MIT License
" "
@@ -21,39 +21,209 @@
" OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION " OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
" WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. " WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
let s:exec = expand('<sfile>:h:h').'/fzf' let s:min_tmux_width = 10
let s:min_tmux_height = 3
let s:default_tmux_height = '40%'
let s:launcher = 'xterm -e bash -ic %s'
let s:cpo_save = &cpo
set cpo&vim
call system('type fzf')
if v:shell_error
let s:fzf_rb = expand('<sfile>:h:h').'/fzf'
if executable(s:fzf_rb)
let s:exec = s:fzf_rb
else
echoerr 'fzf executable not found'
finish
endif
else
let s:exec = 'fzf'
endif
function! s:tmux_enabled()
if has('gui_running')
return 0
endif
if exists('s:tmux')
return s:tmux
endif
let s:tmux = 0
if exists('$TMUX')
let output = system('tmux -V')
let s:tmux = !v:shell_error && output >= 'tmux 1.7'
endif
return s:tmux
endfunction
function! s:shellesc(arg)
return '"'.substitute(a:arg, '"', '\\"', 'g').'"'
endfunction
function! s:escape(path) function! s:escape(path)
return substitute(a:path, ' ', '\\ ', 'g') return substitute(a:path, ' ', '\\ ', 'g')
endfunction endfunction
function! fzf#run(command, ...) function! fzf#run(...) abort
let cwd = getcwd() let dict = exists('a:1') ? a:1 : {}
try let temps = { 'result': tempname() }
let args = copy(a:000) let optstr = get(dict, 'options', '')
if len(args) > 0 && isdirectory(expand(args[-1]))
let dir = remove(args, -1) if has_key(dict, 'source')
execute 'chdir '.s:escape(dir) let source = dict.source
let type = type(source)
if type == 1
let prefix = source.'|'
elseif type == 3
let temps.input = tempname()
call writefile(source, temps.input)
let prefix = 'cat '.s:shellesc(temps.input).'|'
else
throw 'Invalid source type'
endif endif
let argstr = join(args) else
let tf = tempname() let prefix = ''
let prefix = exists('g:fzf_source') ? g:fzf_source.'|' : '' endif
let fzf = executable(s:exec) ? s:exec : 'fzf' let command = prefix.s:exec.' '.optstr.' > '.temps.result
let options = empty(argstr) ? get(g:, 'fzf_options', '') : argstr
execute "silent !".prefix.fzf.' '.options." > ".tf if s:tmux_enabled() && s:tmux_splittable(dict)
if !v:shell_error return s:execute_tmux(dict, command, temps)
for line in readfile(tf) else
if !empty(line) return s:execute(dict, command, temps)
execute a:command.' '.s:escape(line) endif
endfunction
function! s:tmux_splittable(dict)
return
\ min([&columns, get(a:dict, 'tmux_width', 0)]) >= s:min_tmux_width ||
\ min([&lines, get(a:dict, 'tmux_height', get(a:dict, 'tmux', 0))]) >= s:min_tmux_height
endfunction
function! s:pushd(dict)
if has_key(a:dict, 'dir')
let a:dict.prev_dir = getcwd()
execute 'chdir '.s:escape(a:dict.dir)
endif
endfunction
function! s:popd(dict)
if has_key(a:dict, 'prev_dir')
execute 'chdir '.s:escape(remove(a:dict, 'prev_dir'))
endif
endfunction
function! s:execute(dict, command, temps)
call s:pushd(a:dict)
silent !clear
if has('gui_running')
let launcher = get(a:dict, 'launcher', get(g:, 'fzf_launcher', s:launcher))
let command = printf(launcher, "'".substitute(a:command, "'", "'\"'\"'", 'g')."'")
else
let command = a:command
endif
execute 'silent !'.command
redraw!
if v:shell_error
echohl Error
echo 'Error running ' . command
return []
else
return s:callback(a:dict, a:temps, 0)
endif
endfunction
function! s:execute_tmux(dict, command, temps)
if has_key(a:dict, 'dir')
let command = 'cd '.s:escape(a:dict.dir).' && '.a:command
else
let command = a:command
endif
let splitopt = '-v'
if has_key(a:dict, 'tmux_width')
let splitopt = '-h'
let size = a:dict.tmux_width
else
let size = get(a:dict, 'tmux_height', get(a:dict, 'tmux'))
endif
if type(size) == 1 && size =~ '%$'
let sizeopt = '-p '.size[0:-2]
else
let sizeopt = '-l '.size
endif
let s:pane = substitute(
\ system(
\ printf(
\ 'tmux split-window %s %s -P -F "#{pane_id}" %s',
\ splitopt, sizeopt, s:shellesc(command))), '\n', '', 'g')
let s:dict = a:dict
let s:temps = a:temps
augroup fzf_tmux
autocmd!
autocmd VimResized * nested call s:tmux_check()
augroup END
endfunction
function! s:tmux_check()
let panes = split(system('tmux list-panes -a -F "#{pane_id}"'), '\n')
if index(panes, s:pane) < 0
augroup fzf_tmux
autocmd!
augroup END
call s:callback(s:dict, s:temps, 1)
redraw
endif
endfunction
function! s:callback(dict, temps, cd)
if !filereadable(a:temps.result)
let lines = []
else
if a:cd | call s:pushd(a:dict) | endif
let lines = readfile(a:temps.result)
if has_key(a:dict, 'sink')
for line in lines
if type(a:dict.sink) == 2
call a:dict.sink(line)
else
execute a:dict.sink.' '.s:escape(line)
endif endif
endfor endfor
endif endif
finally endif
execute 'chdir '.s:escape(cwd)
redraw! for tf in values(a:temps)
silent! call delete(tf) silent! call delete(tf)
endtry endfor
call s:popd(a:dict)
return lines
endfunction endfunction
command! -nargs=* -complete=dir FZF call fzf#run('silent e', <f-args>) function! s:cmd(bang, ...) abort
let args = copy(a:000)
let opts = {}
if len(args) > 0 && isdirectory(expand(args[-1]))
let opts.dir = remove(args, -1)
endif
if !a:bang
let opts.tmux = get(g:, 'fzf_tmux_height', s:default_tmux_height)
endif
call fzf#run(extend({ 'sink': 'e', 'options': join(args) }, opts))
endfunction
command! -nargs=* -complete=dir -bang FZF call s:cmd('<bang>' == '!', <f-args>)
let &cpo = s:cpo_save
unlet s:cpo_save

37
test/fzf.vader Normal file
View File

@@ -0,0 +1,37 @@
Execute (Setup):
let g:dir = fnamemodify(g:vader_file, ':p:h')
Log 'Test directory: ' . g:dir
Execute (fzf#run with dir option):
let result = fzf#run({ 'options': '--filter=vdr', 'dir': g:dir })
AssertEqual ['fzf.vader'], result
let result = sort(fzf#run({ 'options': '--filter e', 'dir': g:dir }))
AssertEqual ['fzf.vader', 'test_fzf.rb'], result
Execute (fzf#run with Funcref command):
let g:ret = []
function! g:proc(e)
call add(g:ret, a:e)
endfunction
let result = sort(fzf#run({ 'sink': function('g:proc'), 'options': '--filter e', 'dir': g:dir }))
AssertEqual ['fzf.vader', 'test_fzf.rb'], result
AssertEqual ['fzf.vader', 'test_fzf.rb'], sort(g:ret)
Execute (fzf#run with string source):
let result = sort(fzf#run({ 'source': 'echo hi', 'options': '-f i' }))
AssertEqual ['hi'], result
Execute (fzf#run with list source):
let result = sort(fzf#run({ 'source': ['hello', 'world'], 'options': '-f e' }))
AssertEqual ['hello'], result
let result = sort(fzf#run({ 'source': ['hello', 'world'], 'options': '-f o' }))
AssertEqual ['hello', 'world'], result
Execute (fzf#run with string source):
let result = sort(fzf#run({ 'source': 'echo hi', 'options': '-f i' }))
AssertEqual ['hi'], result
Execute (Cleanup):
unlet g:dir
Restore

View File

@@ -1,6 +1,9 @@
#!/usr/bin/env ruby #!/usr/bin/env ruby
# encoding: utf-8 # encoding: utf-8
require 'curses'
require 'timeout'
require 'stringio'
require 'minitest/autorun' require 'minitest/autorun'
$LOAD_PATH.unshift File.expand_path('../..', __FILE__) $LOAD_PATH.unshift File.expand_path('../..', __FILE__)
ENV['FZF_EXECUTABLE'] = '0' ENV['FZF_EXECUTABLE'] = '0'
@@ -20,6 +23,16 @@ class TestFZF < MiniTest::Unit::TestCase
assert_equal true, fzf.color assert_equal true, fzf.color
assert_equal nil, fzf.rxflag assert_equal nil, fzf.rxflag
assert_equal true, fzf.mouse assert_equal true, fzf.mouse
assert_equal nil, fzf.nth
assert_equal true, fzf.color
assert_equal false, fzf.black
assert_equal true, fzf.ansi256
assert_equal '', fzf.query.get
assert_equal false, fzf.select1
assert_equal false, fzf.exit0
assert_equal nil, fzf.filter
assert_equal nil, fzf.extended
assert_equal false, fzf.reverse
end end
def test_environment_variables def test_environment_variables
@@ -27,8 +40,11 @@ class TestFZF < MiniTest::Unit::TestCase
ENV['FZF_DEFAULT_SORT'] = '20000' ENV['FZF_DEFAULT_SORT'] = '20000'
fzf = FZF.new [] fzf = FZF.new []
assert_equal 20000, fzf.sort assert_equal 20000, fzf.sort
assert_equal nil, fzf.nth
ENV['FZF_DEFAULT_OPTS'] = '-x -m -s 10000 -q " hello world " +c +2 --no-mouse -f "goodbye world" --black' ENV['FZF_DEFAULT_OPTS'] =
'-x -m -s 10000 -q " hello world " +c +2 --select-1 -0 ' +
'--no-mouse -f "goodbye world" --black --nth=3,-1,2 --reverse'
fzf = FZF.new [] fzf = FZF.new []
assert_equal 10000, fzf.sort assert_equal 10000, fzf.sort
assert_equal ' hello world ', assert_equal ' hello world ',
@@ -41,12 +57,17 @@ class TestFZF < MiniTest::Unit::TestCase
assert_equal false, fzf.ansi256 assert_equal false, fzf.ansi256
assert_equal true, fzf.black assert_equal true, fzf.black
assert_equal false, fzf.mouse assert_equal false, fzf.mouse
assert_equal true, fzf.select1
assert_equal true, fzf.exit0
assert_equal true, fzf.reverse
assert_equal [2..2, -1..-1, 1..1], fzf.nth
end end
def test_option_parser def test_option_parser
# Long opts # Long opts
fzf = FZF.new %w[--sort=2000 --no-color --multi +i --query hello fzf = FZF.new %w[--sort=2000 --no-color --multi +i --query hello --select-1
--filter=howdy --extended-exact --no-mouse --no-256] --exit-0 --filter=howdy --extended-exact
--no-mouse --no-256 --nth=1 --reverse]
assert_equal 2000, fzf.sort assert_equal 2000, fzf.sort
assert_equal true, fzf.multi assert_equal true, fzf.multi
assert_equal false, fzf.color assert_equal false, fzf.color
@@ -55,12 +76,19 @@ class TestFZF < MiniTest::Unit::TestCase
assert_equal false, fzf.mouse assert_equal false, fzf.mouse
assert_equal 0, fzf.rxflag assert_equal 0, fzf.rxflag
assert_equal 'hello', fzf.query.get assert_equal 'hello', fzf.query.get
assert_equal true, fzf.select1
assert_equal true, fzf.exit0
assert_equal 'howdy', fzf.filter assert_equal 'howdy', fzf.filter
assert_equal :exact, fzf.extended assert_equal :exact, fzf.extended
assert_equal [0..0], fzf.nth
assert_equal true, fzf.reverse
fzf = FZF.new %w[--sort=2000 --no-color --multi +i --query hello # Long opts (left-to-right)
--filter a --filter b --no-256 --black fzf = FZF.new %w[--sort=2000 --no-color --multi +i --query=hello
--no-sort -i --color --no-multi --256] --filter a --filter b --no-256 --black --nth -1 --nth -2
--select-1 --exit-0 --no-select-1 --no-exit-0
--no-sort -i --color --no-multi --256
--reverse --no-reverse]
assert_equal nil, fzf.sort assert_equal nil, fzf.sort
assert_equal false, fzf.multi assert_equal false, fzf.multi
assert_equal true, fzf.color assert_equal true, fzf.color
@@ -70,10 +98,14 @@ class TestFZF < MiniTest::Unit::TestCase
assert_equal 1, fzf.rxflag assert_equal 1, fzf.rxflag
assert_equal 'b', fzf.filter assert_equal 'b', fzf.filter
assert_equal 'hello', fzf.query.get assert_equal 'hello', fzf.query.get
assert_equal false, fzf.select1
assert_equal false, fzf.exit0
assert_equal nil, fzf.extended assert_equal nil, fzf.extended
assert_equal [-2..-2], fzf.nth
assert_equal false, fzf.reverse
# Short opts # Short opts
fzf = FZF.new %w[-s 2000 +c -m +i -qhello -x -fhowdy +2] fzf = FZF.new %w[-s2000 +c -m +i -qhello -x -fhowdy +2 -n3 -1 -0]
assert_equal 2000, fzf.sort assert_equal 2000, fzf.sort
assert_equal true, fzf.multi assert_equal true, fzf.multi
assert_equal false, fzf.color assert_equal false, fzf.color
@@ -82,10 +114,15 @@ class TestFZF < MiniTest::Unit::TestCase
assert_equal 'hello', fzf.query.get assert_equal 'hello', fzf.query.get
assert_equal 'howdy', fzf.filter assert_equal 'howdy', fzf.filter
assert_equal :fuzzy, fzf.extended assert_equal :fuzzy, fzf.extended
assert_equal [2..2], fzf.nth
assert_equal true, fzf.select1
assert_equal true, fzf.exit0
# Left-to-right # Left-to-right
fzf = FZF.new %w[-s 2000 +c -m +i -qhello -x -fgoodbye +2 fzf = FZF.new %w[-s 2000 +c -m +i -qhello -x -fgoodbye +2 -n3 -n4,5
-s 3000 -c +m -i -q world +x -fworld -2 --black --no-black] -s 3000 -c +m -i -q world +x -fworld -2 --black --no-black
-1 -0 +1 +0
]
assert_equal 3000, fzf.sort assert_equal 3000, fzf.sort
assert_equal false, fzf.multi assert_equal false, fzf.multi
assert_equal true, fzf.color assert_equal true, fzf.color
@@ -93,18 +130,28 @@ class TestFZF < MiniTest::Unit::TestCase
assert_equal false, fzf.black assert_equal false, fzf.black
assert_equal 1, fzf.rxflag assert_equal 1, fzf.rxflag
assert_equal 'world', fzf.query.get assert_equal 'world', fzf.query.get
assert_equal false, fzf.select1
assert_equal false, fzf.exit0
assert_equal 'world', fzf.filter assert_equal 'world', fzf.filter
assert_equal nil, fzf.extended assert_equal nil, fzf.extended
assert_equal [3..3, 4..4], fzf.nth
fzf = FZF.new %w[--query hello +s -s 2000 --query=world]
assert_equal 2000, fzf.sort
assert_equal 'world', fzf.query.get
rescue SystemExit => e rescue SystemExit => e
assert false, "Exited" assert false, "Exited"
end end
def test_invalid_option def test_invalid_option
[%w[--unknown], %w[yo dawg]].each do |argv| [
%w[--unknown],
%w[yo dawg],
%w[--nth=0],
%w[-n 0],
%w[-n 1..2..3],
%w[-n 1....],
%w[-n ....3],
%w[-n 1....3],
%w[-n 1..0],
%w[--nth ..0],
].each do |argv|
assert_raises(SystemExit) do assert_raises(SystemExit) do
fzf = FZF.new argv fzf = FZF.new argv
end end
@@ -363,7 +410,7 @@ class TestFZF < MiniTest::Unit::TestCase
["0____1", [[0, 6]]], ["0____1", [[0, 6]]],
["0_____1", [[0, 7]]], ["0_____1", [[0, 7]]],
["0______1", [[0, 8]]]], ["0______1", [[0, 8]]]],
FZF.new([]).sort_by_rank(matcher.match(list, '01', '', ''))) FZF.sort(matcher.match(list, '01', '', '')))
assert_equal( assert_equal(
[["01", [[0, 1], [1, 2]]], [["01", [[0, 1], [1, 2]]],
@@ -374,16 +421,19 @@ class TestFZF < MiniTest::Unit::TestCase
["____0_1", [[4, 5], [6, 7]]], ["____0_1", [[4, 5], [6, 7]]],
["0______1", [[0, 1], [7, 8]]], ["0______1", [[0, 1], [7, 8]]],
["___01___", [[3, 4], [4, 5]]]], ["___01___", [[3, 4], [4, 5]]]],
FZF.new([]).sort_by_rank(xmatcher.match(list, '0 1', '', ''))) FZF.sort(xmatcher.match(list, '0 1', '', '')))
assert_equal( assert_equal(
[["_01_", [[1, 3], [0, 4]]], [["_01_", [[1, 3], [0, 4]], [4, 4, "_01_"]],
["0____1", [[0, 6], [1, 3]]], ["___01___", [[3, 5], [0, 2]], [4, 8, "___01___"]],
["0_____1", [[0, 7], [1, 3]]], ["____0_1", [[4, 7], [0, 2]], [5, 7, "____0_1"]],
["0______1", [[0, 8], [1, 3]]], ["0____1", [[0, 6], [1, 3]], [6, 6, "0____1"]],
["___01___", [[3, 5], [0, 2]]], ["0_____1", [[0, 7], [1, 3]], [7, 7, "0_____1"]],
["____0_1", [[4, 7], [0, 2]]]], ["0______1", [[0, 8], [1, 3]], [8, 8, "0______1"]]],
FZF.new([]).sort_by_rank(xmatcher.match(list, '01 __', '', ''))) FZF.sort(xmatcher.match(list, '01 __', '', '')).map { |tuple|
tuple << FZF.rank(tuple)
}
)
end end
def test_extended_exact_mode def test_extended_exact_mode
@@ -405,58 +455,11 @@ class TestFZF < MiniTest::Unit::TestCase
assert_equal 2, exact.match(list, "-fuzzy", '', '').length assert_equal 2, exact.match(list, "-fuzzy", '', '').length
end end
if RUBY_PLATFORM =~ /darwin/
NFD = '한글'
def test_nfc
assert_equal 6, NFD.length
assert_equal ["한글", [[0, 1], [1, 2]]],
FZF::UConv.nfc(NFD, [[0, 3], [3, 6]])
nfd2 = 'before' + NFD + 'after'
assert_equal 6 + 6 + 5, nfd2.length
nfc, offsets = FZF::UConv.nfc(nfd2, [[4, 14], [9, 13]])
o1, o2 = offsets
assert_equal 'before한글after', nfc
assert_equal 're한글af', nfc[(o1.first...o1.last)]
assert_equal '글a', nfc[(o2.first...o2.last)]
end
def test_nfd
nfc = '한글'
nfd = FZF::UConv.nfd(nfc)
assert_equal 2, nfd.length
assert_equal 6, nfd.join.length
assert_equal NFD, nfd.join
end
def test_nfd_fuzzy_matcher
matcher = FZF::FuzzyMatcher.new 0
assert_equal [], matcher.match([NFD + NFD], '할', '', '')
match = matcher.match([NFD + NFD], '글글', '', '')
assert_equal [[NFD + NFD, [[3, 12]]]], match
assert_equal ['한글한글', [[1, 4]]], FZF::UConv.nfc(*match.first)
end
def test_nfd_extended_fuzzy_matcher
matcher = FZF::ExtendedFuzzyMatcher.new 0
assert_equal [], matcher.match([NFD], "'글글", '', '')
match = matcher.match([NFD], "'한글", '', '')
assert_equal [[NFD, [[0, 6]]]], match
assert_equal ['한글', [[0, 2]]], FZF::UConv.nfc(*match.first)
end
end
def test_split
assert_equal ["a", "b", "c", "\xFF", "d", "e", "f"],
FZF::UConv.split("abc\xFFdef")
end
# ^$ -> matches empty item # ^$ -> matches empty item
def test_format_empty_item def test_format_empty_item
fzf = FZF.new [] fzf = FZF.new []
item = ['', [[0, 0]]] item = ['', [[0, 0]]]
line, offsets = fzf.convert_item item line, offsets = item
tokens = fzf.format line, 80, offsets tokens = fzf.format line, 80, offsets
assert_equal [], tokens assert_equal [], tokens
end end
@@ -476,5 +479,174 @@ class TestFZF < MiniTest::Unit::TestCase
sleep interval sleep interval
assert_equal false, me.double?(20) assert_equal false, me.double?(20)
end end
def test_nth_match
list = [
' first second third',
'fourth fifth sixth',
]
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE
assert_equal list, matcher.match(list, 'f', '', '').map(&:first)
assert_equal [
[list[0], [[2, 5]]],
[list[1], [[9, 17]]]], matcher.match(list, 'is', '', '')
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1..1]
assert_equal [[list[1], [[8, 9]]]], matcher.match(list, 'f', '', '')
assert_equal [[list[0], [[8, 9]]]], matcher.match(list, 's', '', '')
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [2..2]
assert_equal [[list[0], [[19, 20]]]], matcher.match(list, 'r', '', '')
# Comma-separated
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [2..2, 0..0]
assert_equal [[list[0], [[19, 20]]], [list[1], [[3, 4]]]], matcher.match(list, 'r', '', '')
# Ordered
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [0..0, 2..2]
assert_equal [[list[0], [[3, 4]]], [list[1], [[3, 4]]]], matcher.match(list, 'r', '', '')
regex = FZF.build_delim_regex "\t"
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [0..0], regex
assert_equal [[list[0], [[3, 10]]]], matcher.match(list, 're', '', '')
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1..1], regex
assert_equal [], matcher.match(list, 'r', '', '')
assert_equal [[list[1], [[9, 17]]]], matcher.match(list, 'is', '', '')
# Negative indexing
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [-1..-1], regex
assert_equal [[list[0], [[3, 6]]]], matcher.match(list, 'rt', '', '')
assert_equal [[list[0], [[2, 5]]], [list[1], [[9, 17]]]], matcher.match(list, 'is', '', '')
# Regex delimiter
regex = FZF.build_delim_regex "[ \t]+"
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [0..0], regex
assert_equal [list[1]], matcher.match(list, 'f', '', '').map(&:first)
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1..1], regex
assert_equal [[list[0], [[1, 2]]], [list[1], [[8, 9]]]], matcher.match(list, 'f', '', '')
end
def test_nth_match_range
list = [
' first second third',
'fourth fifth sixth',
]
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1..2]
assert_equal [[list[0], [[8, 20]]]], matcher.match(list, 'sr', '', '')
assert_equal [], matcher.match(list, 'fo', '', '')
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1..-1, 0..0]
assert_equal [[list[0], [[8, 20]]]], matcher.match(list, 'sr', '', '')
assert_equal [[list[1], [[0, 2]]]], matcher.match(list, 'fo', '', '')
matcher = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE, :fuzzy, [0..0, 1..2]
assert_equal [], matcher.match(list, '^t', '', '')
matcher = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE, :fuzzy, [0..1, 2..2]
assert_equal [[list[0], [[16, 17]]]], matcher.match(list, '^t', '', '')
matcher = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE, :fuzzy, [1..-1]
assert_equal [[list[0], [[8, 9]]]], matcher.match(list, '^s', '', '')
end
def stream_for str
StringIO.new(str).tap do |sio|
sio.instance_eval do
alias org_gets gets
def gets
org_gets.tap { |e| sleep 0.5 unless e.nil? }
end
end
end
end
def test_select_1
stream = stream_for "Hello\nWorld"
output = StringIO.new
begin
$stdout = output
FZF.new(%w[--query=ol --select-1], stream).start
rescue SystemExit => e
assert_equal 0, e.status
assert_equal 'World', output.string.chomp
ensure
$stdout = STDOUT
end
end
def test_select_1_without_query
stream = stream_for "Hello World"
output = StringIO.new
begin
$stdout = output
FZF.new(%w[--select-1], stream).start
rescue SystemExit => e
assert_equal 0, e.status
assert_equal 'Hello World', output.string.chomp
ensure
$stdout = STDOUT
end
end
def test_select_1_ambiguity
stream = stream_for "Hello\nWorld"
begin
Timeout::timeout(3) do
FZF.new(%w[--query=o --select-1], stream).start
end
flunk 'Should not reach here'
rescue Exception => e
Curses.close_screen
assert_instance_of Timeout::Error, e
end
end
def test_exit_0
stream = stream_for "Hello\nWorld"
output = StringIO.new
begin
$stdout = output
FZF.new(%w[--query=zz --exit-0], stream).start
rescue SystemExit => e
assert_equal 0, e.status
assert_equal '', output.string
ensure
$stdout = STDOUT
end
end
def test_exit_0_without_query
stream = stream_for ""
output = StringIO.new
begin
$stdout = output
FZF.new(%w[--exit-0], stream).start
rescue SystemExit => e
assert_equal 0, e.status
assert_equal '', output.string
ensure
$stdout = STDOUT
end
end
def test_ranking_overlap_match_regions
list = [
'1 3 4 2',
'1 2 3 4'
]
assert_equal [
['1 2 3 4', [[0, 13], [16, 22]]],
['1 3 4 2', [[0, 24], [12, 17]]],
], FZF.sort(FZF::ExtendedFuzzyMatcher.new(nil).match(list, '12 34', '', ''))
end
end end

70
uninstall Executable file
View File

@@ -0,0 +1,70 @@
#!/bin/bash
confirm() {
while [ 1 ]; do
read -p "$1" -n 1 -r
echo
if [[ "$REPLY" =~ ^[Yy] ]]; then
return 0
elif [[ "$REPLY" =~ ^[Nn] ]]; then
return 1
fi
done
}
remove() {
echo "Remove $1"
rm -f "$1"
}
remove_line() {
src=$(readlink "$2")
if [ $? -eq 0 ]; then
echo "Remove from $2 ($src):"
else
src=$2
echo "Remove from $2:"
fi
line_no=1
match=0
while [ 1 ]; do
line=$(sed -n "$line_no,\$p" "$src" | grep -m1 -nF "$1") || break
line_no=$(( $(sed 's/:.*//' <<< "$line") + line_no - 1 ))
content=$(sed 's/^[0-9]*://' <<< "$line")
match=1
echo " - Line #$line_no: $content"
[ "$content" = "$1" ] || confirm " - Remove (y/n) ? "
if [ $? -eq 0 ]; then
awk -v n=$line_no 'NR == n {next} {print}' "$src" > "$src.bak" &&
mv "$src.bak" "$src" || break
echo " - Removed"
else
echo " - Skipped"
line_no=$(( line_no + 1 ))
fi
done
[ $match -eq 0 ] && echo " - Nothing found"
echo
}
for shell in bash zsh; do
remove ~/.fzf.${shell}
remove_line "source ~/.fzf.${shell}" ~/.${shell}rc
done
bind_file=~/.config/fish/functions/fish_user_key_bindings.fish
if [ -f "$bind_file" ]; then
remove_line "fzf_key_bindings" "$bind_file"
fi
if [ -d ~/.config/fish/functions ]; then
remove ~/.config/fish/functions/fzf.fish
if [ "$(ls -A ~/.config/fish/functions)" ]; then
echo "Can't delete non-empty directory: \"~/.config/fish/functions\""
else
rmdir ~/.config/fish/functions
fi
fi