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. --- TOOLS/zsh.pl | 283 ----------------------------------------- etc/_mpv.zsh | 251 ++++++++++++++++++++++++++++++++++++ waftools/generators/sources.py | 13 -- wscript | 6 - wscript_build.py | 7 +- 5 files changed, 253 insertions(+), 307 deletions(-) delete mode 100755 TOOLS/zsh.pl create mode 100644 etc/_mpv.zsh diff --git a/TOOLS/zsh.pl b/TOOLS/zsh.pl deleted file mode 100755 index 482aa5e125..0000000000 --- a/TOOLS/zsh.pl +++ /dev/null @@ -1,283 +0,0 @@ -#!/usr/bin/perl - -# Generate ZSH completion - -# -# 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 . -# - -use strict; -use warnings; -use warnings FATAL => 'uninitialized'; - -my $mpv = $ARGV[0] || 'mpv'; - -my @opts = parse_main_opts('--list-options', '^ (\-\-[^\s\*]*)\*?\s*(.*)'); - -die "Couldn't find any options" unless (@opts); - -my @ao = parse_opts('--ao=help', '^ ([^\s\:]*)\s*(.*)'); -my @vo = parse_opts('--vo=help', '^ ([^\s\:]*)\s*(.*)'); - -my @af = parse_opts('--af=help', '^ ([^\s\:]*)\s*(.*)'); -my @vf = parse_opts('--vf=help', '^ ([^\s\:]*)\s*(.*)'); - -my @protos = parse_opts('--list-protocols', '^ ([^\s]*)'); - -my ($opts_str, $ao_str, $vo_str, $af_str, $vf_str, $protos_str); - -$opts_str .= qq{ '$_' \\\n} foreach (@opts); -chomp $opts_str; - -$ao_str .= qq{ '$_' \\\n} foreach (@ao); -chomp $ao_str; - -$vo_str .= qq{ '$_' \\\n} foreach (@vo); -chomp $vo_str; - -$af_str .= qq{ '$_' \\\n} foreach (@af); -chomp $af_str; - -$vf_str .= qq{ '$_' \\\n} foreach (@vf); -chomp $vf_str; - -$protos_str = join(' ', @protos); - -my $runtime_completions = <<'EOS'; - profile|show-profile) - local -a profiles - local current - for current in "${(@f)$($words[1] --profile=help)}"; do - current=${current//\*/\\\*} - current=${current//\:/\\\:} - current=${current//\[/\\\[} - current=${current//\]/\\\]} - if [[ $current =~ $'\t'([^$'\t']*)$'\t'(.*) ]]; then - if [[ -n $match[2] ]]; then - current="$match[1][$match[2]]" - else - current="$match[1]" - fi - profiles=($profiles $current) - fi - done - if [[ $state == show-profile ]]; then - # For --show-profile, only one allowed - if (( ${#profiles} > 0 )); then - _values 'profile' $profiles && rc=0 - fi - else - # For --profile, multiple allowed - profiles=($profiles 'help[list profiles]') - _values -s , 'profile(s)' $profiles && rc=0 - fi - ;; - - audio-device) - local -a audio_devices - local current - for current in "${(@f)$($words[1] --audio-device=help)}"; do - current=${current//\*/\\\*} - current=${current//\:/\\\:} - current=${current//\[/\\\[} - current=${current//\]/\\\]} - if [[ $current =~ ' '\'([^\']*)\'' \('(.*)'\)' ]]; then - audio_devices=($audio_devices "$match[1][$match[2]]") - fi - done - audio_devices=($audio_devices 'help[list audio devices]') - _values 'audio device' $audio_devices && rc=0 - ;; -EOS -chomp $runtime_completions; - -my $tmpl = <<"EOS"; -#compdef mpv - -# For customization, see: -# https://github.com/mpv-player/mpv/wiki/Zsh-completion-customization - -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' - -local rc=1 - -_arguments -C -S \\ -$opts_str - '*:files:->mfiles' && rc=0 - -case \$state in - ao) - _values -s , 'audio outputs' \\ -$ao_str - && rc=0 - ;; - - vo) - _values -s , 'video outputs' \\ -$vo_str - && rc=0 - ;; - - af) - _values -s , 'audio filters' \\ -$af_str - && rc=0 - ;; - - vf) - _values -s , 'video filters' \\ -$vf_str - && rc=0 - ;; - -$runtime_completions - - 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 - compadd -S '' "\$expl[@]" $protos_str && rc=0 - done - fi - (( rc )) || return 0 - done - ;; -esac - -return rc -EOS - -print $tmpl; - -sub parse_main_opts { - my ($cmd, $regex) = @_; - - my @list; - my @lines = call_mpv($cmd); - - foreach my $line (@lines) { - my ($name, $desc) = ($line =~ /^$regex/) or next; - - next if ($desc eq 'removed' || $desc eq 'alias'); - - if ($desc =~ /^Flag/) { - - push @list, $name; - - $name =~ /^--(.*)/; - if ($1 !~ /^(\{|\}|v|list-options|really-quiet|no-.*)$/) { - push @list, "--no-$1"; - } - - } elsif ($desc =~ /^Print/) { - - push @list, $name; - - } else { - - # Option takes argument - - my $entry = $name; - - $desc =~ s/\:/\\:/g; - $entry .= "=-:$desc:"; - - if ($desc =~ /^Choices\\: ([^(]*)/) { - my $choices = $1; - $choices =~ s/ +$//; # strip trailing space - $entry .= "($choices)"; - - # If "no" is one of the choices, it can also be - # negated like a flag (--no-whatever). - if ($choices =~ /\bno\b/) { - $name =~ s/^--/--no-/; - push @list, $name; - } - } elsif ($line =~ /\[file\]/) { - $entry .= '->files'; - } elsif ($name =~ /^--(ao|vo|af|vf|profile|show-profile|audio-device)$/) { - $entry .= "->$1"; - } - push @list, $entry; - } - } - - # Sort longest first, because zsh won't complete an option listed - # after one that's a prefix of it. - @list = sort { - $a =~ /([^=]*)/; my $ma = $1; - $b =~ /([^=]*)/; my $mb = $1; - - length($mb) <=> length($ma) - } @list; - - return @list; -} - -sub parse_opts { - my ($cmd, $regex) = @_; - - my @list; - my @lines = call_mpv($cmd); - - foreach my $line (@lines) { - if ($line !~ /^$regex/) { - next; - } - - my $entry = $1; - - if (defined $2) { - my $desc = $2; - $desc =~ s/\:/\\:/g; - $entry .= "[$desc]"; - } - - push @list, $entry - } - - return @list; -} - -sub call_mpv { - my ($cmd) = @_; - my $output = `"$mpv" --no-config $cmd`; - if ($? == -1) { - die "Could not run mpv: $!"; - } elsif ((my $exit_code = $? >> 8) != 0) { - die "mpv returned $exit_code with output:\n$output"; - } - return split /\n/, $output; -} 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 diff --git a/waftools/generators/sources.py b/waftools/generators/sources.py index bbca38c06d..0f5b993758 100644 --- a/waftools/generators/sources.py +++ b/waftools/generators/sources.py @@ -5,10 +5,6 @@ from TOOLS.matroska import generate_C_header, generate_C_definitions from TOOLS.file2string import file2string import os -def __zshcomp_cmd__(ctx, argument): - return '"${{BIN_PERL}}" "{0}/TOOLS/zsh.pl" "{1}" > "${{TGT}}"' \ - .format(ctx.srcnode.abspath(), argument) - def __wayland_scanner_cmd__(ctx, mode, dir, src, vendored_file): return "${{WAYSCAN}} {0} < {1} > ${{TGT}}".format( mode, @@ -49,14 +45,6 @@ def ebml_header(self): def ebml_definitions(self): execf(self, generate_C_definitions) -def __zshcomp__(ctx, **kwargs): - ctx( - rule = __zshcomp_cmd__(ctx, ctx.bldnode.abspath() + '/mpv'), - after = ("c", "cprogram",), - name = os.path.basename(kwargs['target']), - **kwargs - ) - def __wayland_protocol_code__(ctx, **kwargs): protocol_is_vendored = kwargs.get("vendored_protocol", False) file_name = kwargs['protocol'] + '.xml' @@ -105,4 +93,3 @@ def handle_add_object(tgen): BuildContext.file2string = __file2string__ BuildContext.wayland_protocol_code = __wayland_protocol_code__ BuildContext.wayland_protocol_header = __wayland_protocol_header__ -BuildContext.zshcomp = __zshcomp__ diff --git a/wscript b/wscript index 93144f9edd..752b400dc3 100644 --- a/wscript +++ b/wscript @@ -103,12 +103,6 @@ build_options = [ 'desc': 'C plugins', 'deps': 'libdl && !os-win32', 'func': check_cc(linkflags=['-rdynamic']), - }, { - 'name': '--zsh-comp', - 'desc': 'zsh completion', - 'func': check_ctx_vars('BIN_PERL'), - 'func': check_true, - 'default': 'disable', }, { # does nothing - left for backward and forward compatibility 'name': '--asm', diff --git a/wscript_build.py b/wscript_build.py index 50f8e641ea..301e8819e4 100644 --- a/wscript_build.py +++ b/wscript_build.py @@ -717,11 +717,8 @@ def build(ctx): if ctx.dependency_satisfied('cplayer'): - if ctx.dependency_satisfied('zsh-comp'): - ctx.zshcomp(target = "etc/_mpv", source = "TOOLS/zsh.pl") - ctx.install_files( - ctx.env.ZSHDIR, - ['etc/_mpv']) + if ctx.env.ZSHDIR: + ctx.install_as(ctx.env.ZSHDIR + '/_mpv', 'etc/_mpv.zsh') ctx.install_files( ctx.env.DATADIR + '/applications', -- cgit v1.2.3