complete: improve zsh completion

- Use groups with _arguments
- Conditionally complete 'negation' options (--messages, --no-column, &c.)
- Improve option exclusivity
- Improve option descriptions
- Improve completion of colour 'whens'
- Improve completion of colour specs
- Remove some unnecessary work-arounds
- Use more idiomatic conventions
This commit is contained in:
dana 2018-06-25 20:09:24 -05:00 committed by Andrew Gallant
parent 62b4813b8a
commit fca9709d94

View File

@ -6,164 +6,269 @@
# Run ci/test_complete.sh after building to ensure that the options supported by # Run ci/test_complete.sh after building to ensure that the options supported by
# this function stay in synch with the `rg` binary. # this function stay in synch with the `rg` binary.
# #
# @see http://zsh.sourceforge.net/Doc/Release/Completion-System.html
# @see https://github.com/zsh-users/zsh/blob/master/Etc/completion-style-guide # @see https://github.com/zsh-users/zsh/blob/master/Etc/completion-style-guide
# #
# Based on code from the zsh-users project — see copyright notice below. # Originally based on code from the zsh-users project — see copyright notice
# below.
_rg() { _rg() {
local state_descr ret curcontext="${curcontext:-}" local curcontext=$curcontext no='!' descr ret=1
local -a context line state local -a context line state state_descr args tmp suf
local -A opt_args val_args local -A opt_args
local -a rg_args
# Sort by long option name to match `rg --help` # ripgrep has many options which negate the effect of a more common one — for
rg_args=( # example, `--no-column` to negate `--column`, and `--messages` to negate
'(-A -C --after-context --context)'{-A+,--after-context=}'[specify lines to show after each match]:number of lines' # `--no-messages`. There are so many of these, and they're so infrequently
'(-B -C --before-context --context)'{-B+,--before-context=}'[specify lines to show before each match]:number of lines' # used, that some users will probably find it irritating if they're completed
'(-i -s -S --ignore-case --case-sensitive --smart-case)'{-s,--case-sensitive}'[search case-sensitively]' # indiscriminately, so let's not do that unless either the current prefix
'--color=[specify when to use colors in output]:when:( never auto always ansi )' # matches one of those negation options or the user has the `complete-all`
'*--colors=[specify color settings and styles]: :->colorspec' # style set. Note that this prefix check has to be updated manually to account
'--column[show column numbers]' # for all of the potential negation options listed below!
'(-A -B -C --after-context --before-context --context)'{-C+,--context=}'[specify lines to show before and after each match]:number of lines' if
'(-b --byte-offset)'{-b,--byte-offset}'[print the 0-based byte offset for each matching line]' # (--[imn]* => --ignore*, --messages, --no-*)
'--context-separator=[specify string used to separate non-continuous context lines in output]:separator' [[ $PREFIX$SUFFIX == --[imn]* ]] ||
'(-c --count --count-matches --passthrough --passthru)'{-c,--count}'[only show count of matching lines for each file]' zstyle -t ":complete:$curcontext:*" complete-all
'(--count-matches -c --count --passthrough --passthru)--count-matches[only show count of individual matches for each file]' then
'--debug[show debug messages]' no=
'--dfa-size-limit=[specify upper size limit of generated DFA]:DFA size' fi
'(-E --encoding)'{-E+,--encoding=}'[specify text encoding of files to search]: :_rg_encodings'
'*'{-f+,--file=}'[specify file containing patterns to search for]:file:_files' # We make heavy use of argument groups here to prevent the option specs from
"(1)--files[show each file that would be searched (but don't search)]" # growing unwieldy. These aren't supported in zsh <5.4, though, so we'll strip
'(-l --files-with-matches --files-without-match)'{-l,--files-with-matches}'[only show names of files with matches]' # them out below if necessary. This makes the exclusions inaccurate on those
'(-l --files-with-matches --files-without-match)--files-without-match[only show names of files without matches]' # older versions, but oh well — it's not that big a deal
'(-F --fixed-strings)'{-F,--fixed-strings}'[treat pattern as literal string instead of regular expression]' args=(
'(-L --follow)'{-L,--follow}'[follow symlinks]' + '(exclusive)' # Misc. fully exclusive options
'*'{-g+,--glob=}'[include or exclude files for searching that match the specified glob]:glob' '(: * -)'{-h,--help}'[display help information]'
'(: -)'{-h,--help}'[display help information]' '(: * -)'{-V,--version}'[display version information]'
'(-p --no-heading --pretty --vimgrep)--heading[show matches grouped by file name]'
+ '(case)' # Case-sensitivity options
{-i,--ignore-case}'[search case-insensitively]'
{-s,--case-sensitive}'[search case-sensitively]'
{-S,--smart-case}'[search case-insensitively if pattern is all lowercase]'
+ '(context-a)' # Context (after) options
'(context-c)'{-A+,--after-context=}'[specify lines to show after each match]:number of lines'
+ '(context-b)' # Context (before) options
'(context-c)'{-B+,--before-context=}'[specify lines to show before each match]:number of lines'
+ '(context-c)' # Context (combined) options
'(context-a context-b)'{-C+,--context=}'[specify lines to show before and after each match]:number of lines'
+ '(column)' # Column options
'--column[show column numbers for matches]'
$no"--no-column[don't show column numbers for matches]"
+ '(count)' # Counting options
'(passthru)'{-c,--count}'[only show count of matching lines for each file]'
'(passthru)--count-matches[only show count of individual matches for each file]'
+ file # File-input options
'*'{-f+,--file=}'[specify file containing patterns to search for]: :_files'
+ '(file-match)' # Files with/without match options
'(stats)'{-l,--files-with-matches}'[only show names of files with matches]'
'(stats)--files-without-match[only show names of files without matches]'
+ '(file-name)' # File-name options
{-H,--with-filename}'[show file name for matches]'
"--no-filename[don't show file name for matches]"
+ '(fixed)' # Fixed-string options
{-F,--fixed-strings}'[treat pattern as literal string instead of regular expression]'
$no"--no-fixed-strings[don't treat pattern as literal string]"
+ '(follow)' # Symlink-following options
{-L,--follow}'[follow symlinks]'
$no"--no-follow[don't follow symlinks]"
+ glob # File-glob options
'*'{-g+,--glob=}'[include/exclude files matching specified glob]:glob'
'*--iglob=[include/exclude files matching specified case-insensitive glob]:glob'
+ '(heading)' # Heading options
'(pretty-vimgrep)--heading[show matches grouped by file name]'
"(pretty-vimgrep)--no-heading[don't show matches grouped by file name]"
+ '(hidden)' # Hidden-file options
'--hidden[search hidden files and directories]' '--hidden[search hidden files and directories]'
'*--iglob=[include or exclude files for searching that match the specified case-insensitive glob]:glob' $no"--no-hidden[don't search hidden files and directories]"
'(-i -s -S --case-sensitive --ignore-case --smart-case)'{-i,--ignore-case}'[search case-insensitively]'
'--ignore-file=[specify additional ignore file]:file:_files' + '(ignore)' # Ignore-file options
'(-v --invert-match)'{-v,--invert-match}'[invert matching]' "(--no-ignore-parent --no-ignore-vcs)--no-ignore[don't respect ignore files]"
'(-n -N --line-number --no-line-number)'{-n,--line-number}'[show line numbers]' $no'(--ignore-parent --ignore-vcs)--ignore[respect ignore files]'
'(-w -x --line-regexp --word-regexp)'{-x,--line-regexp}'[only show matches surrounded by line boundaries]'
'(-M --max-columns)'{-M+,--max-columns=}'[specify max length of lines to print]:number of bytes' + '(ignore-parent)' # Parent ignore-file options
'(-m --max-count)'{-m+,--max-count=}'[specify max number of matches per file]:number of matches' "--no-ignore-parent[don't respect ignore files in parent directories]"
'--max-filesize=[specify size above which files should be ignored]:file size' $no'--ignore-parent[respect ignore files in parent directories]'
+ '(ignore-vcs)' # VCS ignore-file options
"--no-ignore-vcs[don't respect version control ignore files]"
$no'--ignore-vcs[respect version control ignore files]'
+ '(line)' # Line-number options
{-n,--line-number}'[show line numbers for matches]'
{-N,--no-line-number}"[don't show line numbers for matches]"
+ '(max-depth)' # Directory-depth options
'--max-depth=[specify max number of directories to descend]:number of directories' '--max-depth=[specify max number of directories to descend]:number of directories'
'!--maxdepth=:number of directories' '!--maxdepth=:number of directories'
'(--mmap --no-mmap)--mmap[search using memory maps when possible]'
'(-H --with-filename --no-filename)--no-filename[suppress all file names]' + '(messages)' # Error-message options
"(-p --heading --pretty --vimgrep)--no-heading[don't group matches by file name]" '(--no-ignore-messages)--no-messages[suppress some error messages]'
"--no-config[don't load configuration files]" $no"--messages[don't suppress error messages affected by --no-messages]"
'--no-ignore-messages[suppress gitignore parse error messages]'
"(--no-ignore-parent)--no-ignore[don't respect ignore files]" + '(messages-ignore)' # Ignore-error message options
"--no-ignore-parent[don't respect ignore files in parent directories]" "--no-ignore-messages[don't show ignore-file parse error messages]"
"--no-ignore-vcs[don't respect version control ignore files]" $no'--ignore-messages[show ignore-file parse error messages]'
'(-n -N --line-number --no-line-number)'{-N,--no-line-number}'[suppress line numbers]'
'--no-messages[suppress some error messages]' + '(mmap)' # mmap options
"(--mmap --no-mmap)--no-mmap[don't search using memory maps]" '--mmap[search using memory maps when possible]'
'(-0 --null)'{-0,--null}'[print NUL byte after file names]' "--no-mmap[don't search using memory maps]"
'(-o -r --only-matching --passthrough --passthru --replace)'{-o,--only-matching}'[show only matching part of each line]'
'(-c -o -r --count --only-matching --passthrough --replace)--passthru[show both matching and non-matching lines]' + '(only)' # Only-match options
'!(-c -o -r --count --only-matching --passthru --replace)--passthrough' '(passthru replace)'{-o,--only-matching}'[show only matching part of each line]'
'--path-separator=[specify path separator to use when printing file names]:separator'
'(-p --heading --no-heading --pretty --vimgrep)'{-p,--pretty}'[alias for --color=always --heading -n]' + '(passthru)' # Pass-through options
'(-q --quiet)'{-q,--quiet}'[suppress normal output]' '(--vimgrep count only replace)--passthru[show both matching and non-matching lines]'
'--regex-size-limit=[specify upper size limit of compiled regex]:regex size' '!(--vimgrep count only replace)--passthrough'
'(1 -f --file)*'{-e+,--regexp=}'[specify pattern]:pattern'
'(-c -o -r --count --only-matching --passthrough --passthru --replace)'{-r+,--replace=}'[specify string used to replace matches]:replace string' + '(pretty-vimgrep)' # Pretty/vimgrep display options
'(-i -s -S --ignore-case --case-sensitive --smart-case)'{-S,--smart-case}'[search case-insensitively if the pattern is all lowercase]' '(heading)'{-p,--pretty}'[alias for --color=always --heading -n]'
'(-j --threads)--sort-files[sort results by file path (disables parallelism)]' '(heading passthru)--vimgrep[show results in vim-compatible format]'
'(-a --text)'{-a,--text}'[search binary files as if they were text]'
'(-j --sort-files --threads)'{-j+,--threads=}'[specify approximate number of threads to use]:number of threads' + regexp # Explicit pattern options
'(1 file)*'{-e+,--regexp=}'[specify pattern]:pattern'
+ '(replace)' # Replacement options
'(count only passthru)'{-r+,--replace=}'[specify string used to replace matches]:replace string'
+ '(sort)' # File-sorting options
'(threads)--sort-files[sort results by file path (disables parallelism)]'
$no"--no-sort-files[don't sort results by file path]"
+ stats # Statistics options
'(--files file-match)--stats[show search statistics]'
+ '(text)' # Binary-search options
{-a,--text}'[search binary files as if they were text]'
$no"--no-text[don't search binary files as if they were text]"
+ '(threads)' # Thread-count options
'(--sort-files)'{-j+,--threads=}'[specify approximate number of threads to use]:number of threads'
+ type # Type options
'*'{-t+,--type=}'[only search files matching specified type]: :_rg_types' '*'{-t+,--type=}'[only search files matching specified type]: :_rg_types'
'*--type-add=[add new glob for file type]: :->typespec' '*--type-add=[add new glob for specified file type]: :->typespec'
'*--type-clear=[clear globs previously defined for specified file type]: :_rg_types' '*--type-clear=[clear globs previously defined for specified file type]: :_rg_types'
# This should actually be exclusive with everything but other type options # This should actually be exclusive with everything but other type options
'(:)--type-list[show all supported file types and their associated globs]' '(: *)--type-list[show all supported file types and their associated globs]'
'*'{-T+,--type-not=}"[don't search files matching specified type]: :_rg_types" '*'{-T+,--type-not=}"[don't search files matching specified file type]: :_rg_types"
+ '(word-line)' # Whole-word/line match options
{-w,--word-regexp}'[only show matches surrounded by word boundaries]'
{-x,--line-regexp}'[only show matches surrounded by line boundaries]'
+ '(zip)' # Compressed-file options
{-z,--search-zip}'[search in compressed files]'
$no"--no-search-zip[don't search in compressed files]"
+ misc # Other options — no need to separate these at the moment
'(-b --byte-offset)'{-b,--byte-offset}'[show 0-based byte offset for each matching line]'
'--color=[specify when to use colors in output]:when:((
never\:"never use colors"
auto\:"use colors or not based on stdout, TERM, etc."
always\:"always use colors"
ansi\:"always use ANSI colors (even on Windows)"
))'
'*--colors=[specify color and style settings]: :->colorspec'
'--context-separator=[specify string used to separate non-continuous context lines in output]:separator'
'--debug[show debug messages]'
'--dfa-size-limit=[specify upper size limit of generated DFA]:DFA size (bytes)'
'(-E --encoding)'{-E+,--encoding=}'[specify text encoding of files to search]: :_rg_encodings'
"(1 stats)--files[show each file that would be searched (but don't search)]"
'*--ignore-file=[specify additional ignore file]:ignore file:_files'
'(-v --invert-match)'{-v,--invert-match}'[invert matching]'
'(-M --max-columns)'{-M+,--max-columns=}'[specify max length of lines to print]:number of bytes'
'(-m --max-count)'{-m+,--max-count=}'[specify max number of matches per file]:number of matches'
'--max-filesize=[specify size above which files should be ignored]:file size (bytes)'
"--no-config[don't load configuration files]"
'(-0 --null)'{-0,--null}'[print NUL byte after file names]'
'--path-separator=[specify path separator to use when printing file names]:separator'
'(-q --quiet)'{-q,--quiet}'[suppress normal output]'
'--regex-size-limit=[specify upper size limit of compiled regex]:regex size (bytes)'
'*'{-u,--unrestricted}'[reduce level of "smart" searching]' '*'{-u,--unrestricted}'[reduce level of "smart" searching]'
'(: -)'{-V,--version}'[display version information]'
'(-p --heading --no-heading --pretty)--vimgrep[show results in vim-compatible format]' + operand # Operands
'(-H --no-filename --with-filename)'{-H,--with-filename}'[display the file name for matches]' '(--files --type-list file regexp)1: :_guard "^-*" pattern'
'(-w -x --line-regexp --word-regexp)'{-w,--word-regexp}'[only show matches surrounded by word boundaries]' '(--type-list)*: :_files'
'(-e -f --file --files --regexp --type-list)1: :_rg_pattern'
'(--type-list)*:file:_files'
'(-z --search-zip)'{-z,--search-zip}'[search in compressed files]'
"(--stats)--stats[print stats about this search]"
) )
[[ ${_RG_COMPLETE_LIST_ARGS:-} == (1|t*|y*) ]] && { # This is used with test_complete.sh to verify that there are no options
printf '%s\n' "${rg_args[@]}" # listed in the help output that aren't also defined here
[[ $_RG_COMPLETE_LIST_ARGS == (1|t*|y*) ]] && {
print -rl - $args
return 0 return 0
} }
_arguments -s -S : "${rg_args[@]}" && return 0 # Strip out argument groups where unsupported (see above)
[[ $ZSH_VERSION == (4|5.<0-3>)(.*)# ]] &&
args=( ${(@)args:#(#i)(+|[a-z0-9][a-z0-9_-]#|\([a-z0-9][a-z0-9_-]#\))} )
while (( $#state )); do _arguments -C -s -S : $args && ret=0
case "${state[1]}" in
colorspec)
# @todo I don't like this because it allows you to do weird things like
# `line:line:bg:`. Also, i would like the `compadd -q` behaviour
[[ -prefix *:none: ]] && return 1
[[ -prefix *:*:*:* ]] && return 1
_values -S ':' 'color/style type' \ case $state in
'column[specify coloring for column numbers]: :->attribute' \ colorspec)
'line[specify coloring for line numbers]: :->attribute' \ if [[ ${IPREFIX#--*=}$PREFIX == [^:]# ]]; then
'match[specify coloring for match text]: :->attribute' \ suf=( -qS: )
'path[specify color for file names]: :->attribute' && return 0 tmp=(
'column:specify coloring for column numbers'
'line:specify coloring for line numbers'
'match:specify coloring for match text'
'path:specify coloring for file names'
)
descr='color/style type'
elif [[ ${IPREFIX#--*=}$PREFIX == (column|line|match|path):[^:]# ]]; then
suf=( -qS: )
tmp=(
'none:clear color/style for type'
'bg:specify background color'
'fg:specify foreground color'
'style:specify text style'
)
descr='color/style attribute'
elif [[ ${IPREFIX#--*=}$PREFIX == [^:]##:(bg|fg):[^:]# ]]; then
tmp=( black blue green red cyan magenta yellow white )
descr='color name or r,g,b'
elif [[ ${IPREFIX#--*=}$PREFIX == [^:]##:style:[^:]# ]]; then
tmp=( {,no}bold {,no}intense {,no}underline )
descr='style name'
else
_message -e colorspec 'no more arguments'
fi
[[ "${state}" == 'attribute' ]] && (( $#tmp )) && {
_values -S ':' 'color/style attribute' \ compset -P '*:'
'none[clear color/style for type]' \ _describe -t colorspec $descr tmp $suf && ret=0
'bg[specify background color]: :->color' \ }
'fg[specify foreground color]: :->color' \ ;;
'style[specify text style]: :->style' && return 0
[[ "${state}" == 'color' ]] && typespec)
_values -S ':' 'color value' \ if compset -P '[^:]##:include:'; then
black blue green red cyan magenta yellow white && return 0 _sequence -s , _rg_types && ret=0
# @todo This bit in particular could be better, but it's a little
# complex, and attempting to solve it seems to run us up against a crash
# bug — zsh # 40362
elif compset -P '[^:]##:'; then
_message 'glob or include directive' && ret=1
elif [[ ! -prefix *:* ]]; then
_rg_types -qS : && ret=0
fi
;;
esac
[[ "${state}" == 'style' ]] && return ret
_values -S ':' 'style value' \
bold nobold intense nointense underline nounderline && return 0
;;
typespec)
if compset -P '[^:]##:include:'; then
_sequence -s ',' _rg_types && return 0
# @todo This bit in particular could be better, but it's a little
# complex, and attempting to solve it seems to run us up against a crash
# bug — zsh # 40362
elif compset -P '[^:]##:'; then
_message 'glob or include directive' && return 1
elif [[ ! -prefix *:* ]]; then
_rg_types -qS ':' && return 0
fi
;;
esac
shift state
done
return 1
}
# zsh 5.1 refuses to complete options if a 'match-less' operand like our pattern
# could be 'completed' instead. We can use _guard() to avoid this problem, but
# it introduces another one: zsh won't print the message if we try to complete
# the pattern after having passed `--`. To work around *that* problem, we can
# use this function to bypass the _guard() when `--` is on the command line.
# This is inaccurate (it'd get confused by e.g. `rg -e --`), but zsh's handling
# of `--` isn't accurate anyway
_rg_pattern() {
if (( ${words[(I)--]} )); then
_message 'pattern'
else
_guard '^-*' 'pattern'
fi
} }
# Complete encodings # Complete encodings
@ -199,7 +304,7 @@ _rg_encodings() {
x-user-defined auto x-user-defined auto
) )
_wanted rg-encodings expl 'encoding' compadd -a "${@}" - _encodings _wanted encodings expl encoding compadd -a "$@" - _encodings
} }
# Complete file types # Complete file types
@ -207,12 +312,12 @@ _rg_types() {
local -a expl local -a expl
local -aU _types local -aU _types
_types=( ${${(f)"$( _call_program rg-types rg --type-list )"}%%:*} ) _types=( ${(@)${(f)"$( _call_program types rg --type-list )"}%%:*} )
_wanted rg-types expl 'file type' compadd -a "${@}" - _types _wanted types expl 'file type' compadd -a "$@" - _types
} }
_rg "${@}" _rg "$@"
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Copyright (c) 2011 Github zsh-users - http://github.com/zsh-users # Copyright (c) 2011 Github zsh-users - http://github.com/zsh-users