From 2712db8238846f7b4e24052dbe091ea87defc5ae Mon Sep 17 00:00:00 2001 From: Philip Sequeira Date: Wed, 22 Nov 2017 19:09:06 -0500 Subject: zsh completion: move generation to runtime and improve The completion function itself now parses --list-options on the first tab press and caches the results. This does mean a slight delay on that first tab press, but it will only do this if the argument being completed looks like an option (i.e. starts with "-"), so there is never a delay when just completing a file name. I've also put some effort into making it reasonably fast; on my machine it's consistently under 100 ms, more than half of which is mpv itself. Installation of zsh completion is now done unconditionally because it's nothing more than copying a file. If you really don't want it installed, set zshdir to empty: `./waf configure --zshdir= ...` Improvements in functionality compared to the old script: * Produces the right results for mpv binaries other than the one it was installed with (like a dev build for testing changes). * Does not require running mpv at build time, so it won't cause problems with cross compilation. * Handles aliases. * Slightly nicer handling of options that take comma-separated values and/or sub-options: A space is now inserted at the end instead of a comma, allowing you to immediately start typing the next argument, but typing a comma will still remove the automatically added space, and = and : will now do that too, so you can immediately add a sub-option. * More general/flexible handling of values for options that print their possible values with --option=help. The code as is could handle quite a few more options (*scale, demuxers, decoders, ...), but nobody wants to maintain that list here so we'll just stick with what the old completion script already did. --- etc/_mpv.zsh | 251 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 etc/_mpv.zsh (limited to 'etc') diff --git a/etc/_mpv.zsh b/etc/_mpv.zsh new file mode 100644 index 0000000000..265c20acbf --- /dev/null +++ b/etc/_mpv.zsh @@ -0,0 +1,251 @@ +#compdef mpv + +# ZSH completion for mpv +# +# For customization, see: +# https://github.com/mpv-player/mpv/wiki/Zsh-completion-customization + +# +# This file is part of mpv. +# +# mpv is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# mpv is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with mpv. If not, see . +# + +local curcontext="$curcontext" state state_descr line +typeset -A opt_args + +local -a match mbegin mend +local MATCH MBEGIN MEND + +# By default, don't complete URLs unless no files match +local -a tag_order +zstyle -a ":completion:*:*:$service:*" tag-order tag_order || + zstyle ":completion:*:*:$service:*" tag-order '!urls' + +# Use PCRE for regular expression matching if possible. This approximately +# halves the execution time of generate_arguments compared to the default POSIX +# regex, which translates to a more responsive first tab press. However, we +# can't rely on PCRE being available, so we keep all our patterns +# POSIX-compatible. +setopt re_match_pcre &>/dev/null + +typeset -ga _mpv_completion_arguments _mpv_completion_protocols + +function generate_arguments { + + _mpv_completion_arguments=() + + local -a option_aliases=() + + local list_options_line + for list_options_line in "${(@f)$($words[1] --list-options)}"; do + + [[ $list_options_line =~ '^\s+--(\S+)\s*(.*)' ]] || continue + + local name=$match[1] desc=$match[2] + + if [[ $desc == Flag* ]]; then + + _mpv_completion_arguments+="$name" + if [[ $name != (\{|\}|v|list-options) ]]; then + # Negated version + _mpv_completion_arguments+="no-$name" + fi + + elif [[ -z $desc ]]; then + + # Sub-option for list option + + if [[ $name == *-(clr|help) ]]; then + # Like a flag + _mpv_completion_arguments+="$name" + else + # Find the parent option and use that with this option's name + _mpv_completion_arguments+="${_mpv_completion_arguments[(R)${name%-*}=*]/*=/$name=}" + fi + + elif [[ $desc == Print* ]]; then + + _mpv_completion_arguments+="$name" + + elif [[ $desc =~ '^alias for --(\S+)' ]]; then + + # Save this for later; we might not have parsed the target option yet + option_aliases+="$name $match[1]" + + else + + # Option takes argument + + local entry="$name=-:${desc//:/\\:}:" + + if [[ $desc =~ '^Choices: ([^(]*)' ]]; then + + local -a choices=(${(s: :)match[1]}) + entry+="($choices)" + # If "no" is one of the choices, it can also be negated like a flag + # (--no-whatever is equivalent to --whatever=no). + if (( ${+choices[(r)no]} )); then + _mpv_completion_arguments+="no-$name" + fi + + elif [[ $desc == *'[file]'* ]]; then + + entry+='->files' + + elif [[ $name == (ao|vo|af|vf|profile|audio-device|vulkan-device) ]]; then + + entry+="->parse-help-$name" + + elif [[ $name == show-profile ]]; then + + entry+="->parse-help-profile" + + fi + + _mpv_completion_arguments+="$entry" + + fi + + done + + # Process aliases + local to_from real_name arg_spec + for to_from in $option_aliases; do + # to_from='alias-name real-name' + real_name=${to_from##* } + for arg_spec in "$real_name" "$real_name=*" "no-$real_name"; do + arg_spec=${_mpv_completion_arguments[(r)$arg_spec]} + [[ -n $arg_spec ]] && + _mpv_completion_arguments+="${arg_spec/$real_name/${to_from%% *}}" + done + done + + # Older versions of zsh have a bug where they won't complete an option listed + # after one that's a prefix of it. To work around this, we can sort the + # options by length, longest first, so that any prefix of an option will be + # listed after it. On newer versions of zsh where the bug is fixed, we skip + # this to avoid slowing down the first tab press any more than we have to. + autoload -Uz is-at-least + if ! is-at-least 5.2; then + # If this were a real language, we wouldn't have to sort by prepending the + # length, sorting the whole thing numerically, and then removing it again. + local -a sort_tmp=() + for arg_spec in $_mpv_completion_arguments; do + sort_tmp+=${#arg_spec%%=*}_$arg_spec + done + _mpv_completion_arguments=(${${(On)sort_tmp}/#*_}) + fi + +} + +function generate_protocols { + _mpv_completion_protocols=() + local list_protos_line + for list_protos_line in "${(@f)$($words[1] --list-protocols)}"; do + if [[ $list_protos_line =~ '^\s+(.*)' ]]; then + _mpv_completion_protocols+="$match[1]" + fi + done +} + +function generate_if_changed { + # Called with $1 = 'arguments' or 'protocols'. Generates the respective list + # on the first run and re-generates it if the executable being completed for + # is different than the one we used to generate the cached list. + typeset -gA _mpv_completion_binary + local current_binary=${words[1]:c} + zmodload -F zsh/stat b:zstat + current_binary+=T$(zstat +mtime $current_binary) + if [[ $_mpv_completion_binary[$1] != $current_binary ]]; then + generate_$1 + _mpv_completion_binary[$1]=$current_binary + fi +} + +# Only consider generating arguments if the argument being completed looks like +# an option. This way, the user should never see a delay when just completing a +# filename. +if [[ $words[$CURRENT] == -* ]]; then + generate_if_changed arguments +fi + +local rc=1 + +_arguments -C -S \*--$_mpv_completion_arguments '*:files:->mfiles' && rc=0 + +case $state in + + parse-help-*) + local option_name=${state#parse-help-} + # Can't do non-capturing groups without pcre, so we index the ones we want + local pattern name_group=1 desc_group=2 + case $option_name in + audio-device|vulkan-device) + pattern='^\s+'\''([^'\'']*)'\''\s+\((.*)\)' + ;; + profile) + # The generic pattern would actually work in most cases for --profile, + # but would break if a profile name contained spaces. This stricter one + # only breaks if a profile name contains tabs. + pattern=$'^\t([^\t]*)\t(.*)' + ;; + *) + pattern='^\s+(--'${option_name}'=)?(\S+)\s*[-:]?\s*(.*)' + name_group=2 desc_group=3 + ;; + esac + local -a values + local current + for current in "${(@f)$($words[1] --${option_name}=help)}"; do + [[ $current =~ $pattern ]] || continue; + local name=${match[name_group]//:/\\:} desc=${match[desc_group]} + if [[ -n $desc ]]; then + values+="${name}:${desc}" + else + values+="${name}" + fi + done + (( $#values )) && { + compset -P '*,' + compset -S ',*' + _describe "$state_descr" values -r ',=: \t\n\-' && rc=0 + } + ;; + + files) + compset -P '*,' + compset -S ',*' + _files -r ',/ \t\n\-' && rc=0 + ;; + + mfiles) + local expl + _tags files urls + while _tags; do + _requested files expl 'media file' _files && rc=0 + if _requested urls; then + while _next_label urls expl URL; do + _urls "$expl[@]" && rc=0 + generate_if_changed protocols + compadd -S '' "$expl[@]" $_mpv_completion_protocols && rc=0 + done + fi + (( rc )) || return 0 + done + ;; + +esac + +return rc -- cgit v1.2.3