├── .gitignore
├── fuzzy-match.zsh
├── LICENSE
├── functions
├── fuzzy-match-key-handlers
├── fuzzy-match-file-glob
└── fuzzy-match
└── README.mkd
/.gitignore:
--------------------------------------------------------------------------------
1 | *.zwc
2 |
--------------------------------------------------------------------------------
/fuzzy-match.zsh:
--------------------------------------------------------------------------------
1 | ### this plugin contains snippets/ideas taken from zaw (github.com/zsh-users/zaw)
2 | function () {
3 |
4 | zmodload zsh/parameter
5 | autoload -U is-at-least
6 |
7 | local this_file="${funcsourcetrace[1]%:*}"
8 | if is-at-least 4.3.10; then
9 | # "A" flag (turn a file name into an absolute path with symlink
10 | # resolution) is only available on 4.3.10 and latter
11 | local cur_dir="${this_file:A:h}"
12 | else
13 | local cur_dir="${this_file:h}"
14 | fi
15 | fpath+=("${cur_dir}/functions")
16 |
17 | local file
18 | for file in "$cur_dir/functions/"*(.); do
19 | autoload -U ${file:t}
20 | ${file:t}
21 | done
22 |
23 | local default_keys='^T' map
24 |
25 | for map in emacs viins vicmd; do
26 | bindkey -M $map | "grep" -q 'fuzzy-match' || bindkey -M $map $default_keys fuzzy-match
27 | done
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | This is free and unencumbered software released into the public domain.
2 |
3 | Anyone is free to copy, modify, publish, use, compile, sell, or
4 | distribute this software, either in source code form or as a compiled
5 | binary, for any purpose, commercial or non-commercial, and by any
6 | means.
7 |
8 | In jurisdictions that recognize copyright laws, the author or authors
9 | of this software dedicate any and all copyright interest in the
10 | software to the public domain. We make this dedication for the benefit
11 | of the public at large and to the detriment of our heirs and
12 | successors. We intend this dedication to be an overt act of
13 | relinquishment in perpetuity of all present and future rights to this
14 | software under copyright law.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
24 | For more information, please refer to
25 |
--------------------------------------------------------------------------------
/functions/fuzzy-match-key-handlers:
--------------------------------------------------------------------------------
1 | # functions that handle user key presses
2 |
3 | _fuzzy-match-update-line-cursor() {
4 | integer selected_start_idx selected_end_idx selected_line=$state[selected_line]\
5 | buffer_len=${#BUFFER}
6 | local hi_selected=$state[hi_selected] nl=$state[nl]
7 | # highlight the selected line
8 | # start index to highlight (zshparam(1) for help on the subscript
9 | # syntax)
10 | selected_start_idx=${POSTDISPLAY[(n:((selected_line)):i)$nl]}
11 | (( selected_start_idx += buffer_len ))
12 | # end index
13 | selected_end_idx=$(( ${POSTDISPLAY[(n:((selected_line + 1)):i)$nl]} - 1))
14 | (( selected_end_idx += buffer_len ))
15 | state[last_selected_line]=$state[selected_line]
16 | region_highlight[1]="$selected_start_idx $selected_end_idx $hi_selected"
17 | zle -Rc
18 | }
19 |
20 | _fuzzy-match-go-up() {
21 | if (( state[selected_line] > 1)); then
22 | (( state[selected_line]-- ))
23 | _fuzzy-match-update-line-cursor
24 | fi
25 | }
26 |
27 | _fuzzy-match-go-down() {
28 | if (( state[selected_line] < state[lines] )); then
29 | (( state[selected_line]++ ))
30 | _fuzzy-match-update-line-cursor
31 | fi
32 | }
33 |
34 | _fuzzy-match-self-insert() {
35 | LBUFFER+=${KEYS[-1]}
36 | print -p -N 'RESTART'
37 | _fuzzy-match-update-line-cursor
38 | }
39 |
40 | _fuzzy-match-backward-delete-char() {
41 | if (( $#LBUFFER )); then
42 | LBUFFER=${LBUFFER[1,-2]}
43 | print -p -N 'RESTART'
44 | _fuzzy-match-update-line-cursor
45 | fi
46 | }
47 |
48 | _fuzzy-match-delete-char() {
49 | if (( $#RBUFFER )); then
50 | RBUFFER=${RBUFFER[2,-1]}
51 | print -p -N 'RESTART'
52 | _fuzzy-match-update-line-cursor
53 | fi
54 | }
55 |
56 | _fuzzy-match-accept-line() {
57 | # extract the highlighted line
58 | local highlighted start end buffer_len=$#BUFFER
59 | highlighted=(${(z)region_highlight[1]})
60 | start=$(( highlighted[1] + 1 - buffer_len ))
61 | end=$(( highlighted[2] - buffer_len ))
62 | state[text]="${POSTDISPLAY[$start,$end]}"
63 | # exit the nested zle editor
64 | zle .accept-line
65 | }
66 |
67 | unfunction fuzzy-match-key-handlers
68 |
--------------------------------------------------------------------------------
/README.mkd:
--------------------------------------------------------------------------------
1 | This project alpha software and not being maintained, I don't recommend using it. Here are better alternatives:
2 |
3 | - [fzf](https://github.com/junegunn/fzf)
4 | - [selecta](https://github.com/garybernhardt/selecta)
5 |
6 | ##Zsh-Fuzzy-Match
7 |
8 | Zsh-Fuzzy-Match is a ZLE widget for interactively finding stuff, inspired by Vim's [CtrlP][control-p] and [Command-T][command-t] plugins.
9 |
10 | [control-p]: https://github.com/kien/ctrlp.vim
11 | [command-t]: https://github.com/wincent/Command-T
12 |
13 | ###Installation
14 |
15 | ```zsh
16 | $ git clone git://github.com/tarruda/zsh-fuzzy-match.git $HOME/.zsh-fuzzy-match
17 | $ echo 'source $HOME/.zsh-fuzzy-match/fuzzy-match.zsh' >> $HOME/.zshrc
18 | ```
19 |
20 | ###Basic Usage
21 |
22 | CTRL+T is the default binding to activate the widget, it can be overridden
23 | by invoking `'bindkey [KEYS] fuzzy-match'` before sourcing the main file.
24 |
25 | When the widget is running, type parts of a filename and it will list zsh glob
26 | matches, which can be selected using arrows.
27 |
28 | If you invoke the widget with an empty zle buffer, it will open the file in your
29 | $EDITOR.
30 |
31 | You can also start typing a command(eg: 'rm' or 'vi') and then invoke the
32 | widget, when the filename is selected it will be appended to your command line.
33 |
34 |
35 | ###Configuration
36 |
37 | Ignore patterns can be specified with a '.fuzzy-match-ignore' in the working
38 | directory or '.global.fuzzy-match-ignore' in home directory.
39 |
40 | Each line should contain a zsh-like ignore pattern, with the following remarks:
41 |
42 | - If starts with a '/', the pattern is considered relative to the starting
43 | directory instead of the directory currently being scanned
44 | - If ends with a '/', the pattern is a directory-ignore pattern(it wont descend
45 | into directories that match the pattern)
46 | - Single line comments are allowed with '#'
47 |
48 | ####Examples
49 |
50 | Ignore all css files inside the 'compiled' directory in the project root:
51 |
52 | ```
53 | /compiled/*.css
54 | ```
55 |
56 | Ignore all .min.js/.min.css files:
57 |
58 | ```
59 | *.min.(css|js)
60 | ```
61 |
62 | Here's a good .global.fuzzy-match-ignore:
63 |
64 | ```
65 | /.(git|svn|bzr|hg)/ # VCS special dir
66 | /node_modules/ # node.js dependencies directory
67 | *.min.(js|css) # minified files
68 | *~ # vim undo files
69 | *.swp # vim swap files
70 | *.pyc # python compiled
71 | *.zwc # zsh compiled
72 | ```
73 |
74 |
--------------------------------------------------------------------------------
/functions/fuzzy-match-file-glob:
--------------------------------------------------------------------------------
1 | # This is the default data source for fuzzy-match, it will
2 | # recursively search for files that matched the user-entered pattern
3 | #
4 |
5 | _fuzzy-match-find() {
6 | print -N "CLEAR"
7 | if [[ -n $1 ]]; then
8 | # FIXME Need to propertly escape so it wont break the constructed
9 | # 'fpattern'. For now just replacing characters like '\' with ?
10 | # Also, probably need to do some kind of processing on user input, so
11 | # characters like '\' will in fact match sub directories
12 | local escaped=${${1:q}//\//?}
13 | _fuzzy-match-walk $CWD $escaped 0
14 | else
15 | _fuzzy-match-walk $CWD "" 0
16 | fi
17 | }
18 |
19 | _fuzzy-match-walk() {
20 | local cwd=$1
21 | local pattern=$2
22 | local matches=$3
23 | local fpattern=
24 | if [[ -n $pattern ]]; then
25 | # explicitly set the scope of the '#ia1' flag so it wont apply to
26 | # the negated patterns
27 | fpattern="$cwd/(((#ia1)*${pattern}*)${PRUNE_REL_FILES})${PRUNE_FILES}(.N)"
28 | else
29 | fpattern="$cwd/(*${PRUNE_REL_FILES})${PRUNE_FILES}(.N)"
30 | fi
31 | local dpattern="$cwd/(*${PRUNE_REL_DIRS})${PRUNE_DIRS}(/N)"
32 | for file in ${~fpattern}; do
33 | print -N "APPEND:${file#$CWD/}"
34 | # no need to keep searching after the screen is filled with results
35 | (( ++matches >= MAX_RESULTS )) && exit
36 | done
37 | for dir in ${~dpattern}; do
38 | _fuzzy-match-walk "$dir" "$pattern" "$matches"
39 | done
40 | }
41 |
42 | _fuzzy-match-load-ignore-patterns() {
43 | # Load the user ignore patterns
44 | PRUNE_REL_DIRS=""
45 | PRUNE_REL_FILES=""
46 | PRUNE_DIRS=""
47 | PRUNE_FILES=""
48 | local rc
49 | for rc in "$HOME/.global.fuzzy-match-ignore" "$CWD/.fuzzy-match-ignore"; do
50 | if [[ -r $rc ]]; then
51 | exec 3<$rc
52 | while read -u 3 ignore_pattern; do
53 | # strip line comments in the ignore file
54 | local ignore_pattern="${ignore_pattern%%\#*}"
55 | ignore_pattern="${ignore_pattern%% #}"
56 | [[ -z $ignore_pattern ]] && continue
57 | if [[ $ignore_pattern -pcre-match ^/ ]]; then
58 | # relative to the base directory
59 | if [[ $ignore_pattern -pcre-match /$ ]]; then
60 | PRUNE_DIRS="$PRUNE_DIRS~$CWD${ignore_pattern%/}"
61 | else
62 | PRUNE_FILES="$PRUNE_FILES~$CWD$ignore_pattern"
63 | fi
64 | else
65 | # relative to the current directory being scanned
66 | if [[ $ignore_pattern -pcre-match /$ ]]; then
67 | PRUNE_REL_DIRS="$PRUNE_REL_DIRS~${ignore_pattern%/}"
68 | else
69 | PRUNE_REL_FILES="$PRUNE_REL_FILES~$ignore_pattern"
70 | fi
71 | fi
72 | done
73 | exec 3>&-
74 | fi
75 | done
76 | # shared this with the finder process
77 | export PRUNE_DIRS PRUNE_FILES PRUNE_REL_DIRS PRUNE_REL_FILES
78 | }
79 |
80 | _fuzzy-match-file-glob() {
81 | zmodload zsh/pcre
82 | setopt extendedglob globdots
83 |
84 | export MAX_RESULTS=$1
85 | export CWD="$(pwd)"
86 | _fuzzy-match-load-ignore-patterns
87 |
88 | local cmd finder_pid
89 | integer restarting=0 # semaphore for avoiding stacked RESTART commands
90 |
91 | # start the finder without a pattern, so the list will be filled when the
92 | # widget starts
93 | _fuzzy-match-find &!
94 | finder_pid=$!
95 | while read -d $'\0' -r cmd &> /dev/null; do
96 | case $cmd in
97 | PATTERN*)
98 | restarting=0
99 | # restart the find using this pattern
100 | _fuzzy-match-find ${cmd#PATTERN\:} &!
101 | finder_pid=$!
102 | ;;
103 | RESTART)
104 | kill $finder_pid &> /dev/null
105 | (( restarting == 1 )) && continue # already restarting, just ignore
106 | if kill -0 $finder_pid &> /dev/null; then
107 | restarting=1
108 | {
109 | # only run a single instance of the finder process at a time,
110 | # so we wait until it exits before requesting the search
111 | # pattern
112 | #
113 | # this is done in the background so zle can continue to read keys
114 | # without buffering RESTART commands
115 | while kill -0 $finder_pid &> /dev/null; do
116 | sleep 0.2
117 | done
118 | # Done waiting for the finder, request the current pattern and
119 | # continue
120 | print -N 'GET'
121 | } &!
122 | else
123 | # finder has already exited, so we request the pattern and wait for
124 | # response
125 | print -N 'GET'
126 | fi
127 | ;;
128 | esac
129 | done
130 | }
131 |
132 | unfunction fuzzy-match-file-glob
133 |
--------------------------------------------------------------------------------
/functions/fuzzy-match:
--------------------------------------------------------------------------------
1 | # Main widget logic.
2 | #
3 | # Theoretically, this can be extended by providing a source,
4 | # which can be any program that understands a simple protocol via stdio
5 | #
6 | # It can send:
7 | #
8 | # CLEAR - Clears the output list
9 | # APPEND:[result] - Appends a single result to the output list
10 | # GET - Queries for the currently typed pattern
11 | #
12 | # It should also accept:
13 | #
14 | # PATTERN:[pattern] - This is the response to GET
15 | # RESTART - Stop yielding matches and start again(this happens when the
16 | # user modified the text)
17 | #
18 | # Every message in the protocol should be terminated with the null byte(0)
19 | fuzzy-match() {
20 | emulate -L zsh
21 | setopt local_options extended_glob
22 | unsetopt monitor
23 |
24 | # Save current zle state
25 | typeset -Ag old_state state
26 | old_state=()
27 | old_state[LBUFFER]=$LBUFFER
28 | old_state[RBUFFER]=$RBUFFER
29 | old_state[PREDISPLAY]=$PREDISPLAY
30 | old_state[POSTDISPLAY]=$POSTDISPLAY
31 | if [[ ${#region_highlight} -ne 0 ]]; then
32 | set -a "old_state[region_highlight]" ("${region_highlight[@]}")
33 | fi
34 | BUFFER=''
35 | local action=$2
36 |
37 | # TODO find a way to match newlines in subcript glob patterns without using
38 | # this hack
39 | IFS=$'\n'
40 | # widget-specific state, global for easy sharing across functions.
41 | state=(
42 | nl '[[:IFS:]]'
43 | selected_line 0
44 | last_selected_line 0
45 | lines 0
46 | max_lines $(( LINES - 3 ))
47 | text ''
48 | )
49 |
50 | # initialize configuration
51 | zstyle -s ':fuzzy-match:highlight' selected "state[hi_selected]" ||\
52 | state[hi_selected]='standout'
53 | zstyle -s ':fuzzy-match:highlight' matched "state[hi_matched]" ||\
54 | state[hi_matched]='fg=green,underline'
55 | zstyle -s ':fuzzy-match:highlight' title "state[hi_title]" ||\
56 | state[hi_title]='bold'
57 |
58 | # The first item in 'region_highlight' is used for the line cursor, second for
59 | # the title
60 | PREDISPLAY=$'\nFuzzy match: '
61 | region_highlight=("" "P1 13 $state[hi_title]")
62 |
63 | # initialize source program
64 | local source_prog=$1
65 | if [[ -z $source_prog ]]; then
66 | source_prog="_fuzzy-match-file-glob"
67 | fi
68 |
69 | # start the source prog as a coprocess
70 | coproc $source_prog $state[max_lines]
71 | local source_pid=$!
72 | disown
73 | local coproc_out
74 |
75 | # copy coproc fd to $coproc_out so it can be used by zle -F
76 | exec {coproc_out}<&p
77 |
78 | # make zle invoke _fuzzy-match-coproc-receive
79 | zle -F $coproc_out _fuzzy-match-coproc-receive
80 |
81 | # Intercept basic editing widgets
82 | zle -N accept-line _fuzzy-match-accept-line
83 | zle -N self-insert _fuzzy-match-self-insert
84 | zle -N backward-delete-char _fuzzy-match-backward-delete-char
85 | zle -N vi-backward-delete-char _fuzzy-match-backward-delete-char
86 | zle -N delete-char _fuzzy-match-delete-char
87 | zle -N vi-delete-char _fuzzy-match-delete-char
88 | zle -N up-line-or-history _fuzzy-match-go-up
89 | zle -N down-line-or-history _fuzzy-match-go-down
90 |
91 | # use recursive edit, so we inherit the user keymap and override only what is
92 | # needed
93 | zle recursive-edit
94 |
95 | # restore default widgets
96 | zle -A .accept-line accept-line
97 | zle -A .self-insert self-insert
98 | zle -A .backward-delete-char backward-delete-char
99 | zle -A .vi-backward-delete-char vi-backward-delete-char
100 | zle -A .delete-char delete-char
101 | zle -A .vi-delete-char vi-delete-char
102 | zle -A .up-line-or-history up-line-or-history
103 | zle -A .down-line-or-history down-line-or-history
104 |
105 | # restore initial zle state
106 | LBUFFER=$old_state[LBUFFER]
107 | RBUFFER=$old_state[RBUFFER]
108 | PREDISPLAY=$old_state[PREDISPLAY]
109 | POSTDISPLAY=$old_state[POSTDISPLAY]
110 | if [[ -n $old_state[region_highlight] ]]; then
111 | region_highlight=($old_state[region_highlight])
112 | else
113 | region_highlight=()
114 | fi
115 |
116 | # refresh
117 | zle -Rc
118 |
119 | # if something was selected, save before deleting the state hashtable
120 | local text=$state[text]
121 |
122 | # delete globals set
123 | unset old_state state IFS
124 |
125 | # cleanup source process
126 | kill $source_pid &> /dev/null
127 |
128 | echo "$@" > /tmp/args
129 | if [[ -n $text ]]; then
130 | if [[ -n $3 ]]; then
131 | # set REPLY and return to the calling widget
132 | REPLY=$text
133 | elif [[ -z $LBUFFER ]]; then
134 | # Nothing was typed, if an action was provided as second argument, invoke
135 | # it, else just edit the file
136 | if [[ -n $2 ]]; then
137 | LBUFFER="$2 ${text:q}"
138 | else
139 | LBUFFER="${EDITOR:-vi} ${text:q}"
140 | fi
141 | zle accept-line
142 | else
143 | # append the filename and let the user confirm
144 | if [[ $LBUFFER[-1] != $' ' ]]; then
145 | LBUFFER+=$' '
146 | fi
147 | LBUFFER+=${text:q}
148 | zle -Rc
149 | fi
150 | else
151 | zle send-break
152 | fi
153 | }
154 |
155 | _fuzzy-match-coproc-receive() {
156 | local cmd item hi_selected=$state[hi_selected]
157 | integer buffer_len base_idx
158 | if read -d $'\0' -r -u $1 cmd; then
159 | case $cmd in
160 | CLEAR)
161 | POSTDISPLAY=''
162 | state[lines]=0
163 | state[selected_line]=0
164 | state[last_selected_line]=0
165 | region_highlight=$region_highlight[1,2]
166 | ;;
167 | APPEND*)
168 | (( state[lines] == state[max_lines] )) && return
169 | buffer_len=${#BUFFER}
170 | (( base_idx = buffer_len + 1 ))
171 | item=${cmd#APPEND\:}
172 | _fuzzy-match-highlight-item $(( buffer_len + $#POSTDISPLAY )) $item
173 | POSTDISPLAY+=$'\n'
174 | POSTDISPLAY+=$item
175 | (( state[lines]++ ))
176 | if (( state[selected_line] == 0 )); then
177 | # automatically select the first line
178 | state[selected_line]=1
179 | region_highlight[1]="$base_idx $(( base_idx + $#item )) $hi_selected"
180 | fi
181 | state[incomplete_line]=''
182 | ;;
183 | GET)
184 | print -p -N "PATTERN:$BUFFER"
185 | ;;
186 | esac
187 | zle -Rc
188 | fi
189 | }
190 |
191 | # highlight a single result. this constructs a pattern with the buffer escaped
192 | # contents, and appends the #ia1 flags for approximate/case insensitive
193 | # matching. also, since there seems to be no easy way to find the exact match
194 | # length, we use the buffer length for limiting the highlighted text
195 | _fuzzy-match-highlight-item() {
196 | local idx len end_idx start_idx=$1 item=$2 hi_matched=$state[hi_matched]\
197 | pattern="(#ia1)(${${BUFFER:q}//\//?})"
198 | # find the index of the first match after the last '/' (the filename)
199 | idx=${${item##*/}[(i)$pattern]}
200 | if (( ${item[(i)/]} <= $#item )); then
201 | (( idx += ${#${item%/*}} + 1 ))
202 | fi
203 | (( idx > $#item )) && return
204 | (( start_idx += idx ))
205 | (( end_idx = start_idx + $#BUFFER ))
206 | region_highlight+="$start_idx $end_idx $hi_matched"
207 | }
208 |
209 |
210 | zle -N fuzzy-match
211 |
--------------------------------------------------------------------------------