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.
This commit is contained in:
Philip Sequeira 2017-11-22 19:09:06 -05:00 committed by wm4
parent 21a5c416d5
commit 2712db8238
5 changed files with 253 additions and 307 deletions

View File

@ -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 <http://www.gnu.org/licenses/>.
#
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;
}

251
etc/_mpv.zsh Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
#
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

View File

@ -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__

View File

@ -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',

View File

@ -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',