Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add a new shell completion provider #2858

Merged
merged 6 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions THANKS
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,4 @@ Justin Wood
Guilherme Andrade
Manas Chaudhari
Luís Rascão
Marko Minđek
1 change: 1 addition & 0 deletions apps/rebar/src/rebar.app.src.script
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
rebar_prv_clean,
rebar_prv_common_test,
rebar_prv_compile,
rebar_prv_completion,
rebar_prv_cover,
rebar_prv_deps,
rebar_prv_deps_tree,
Expand Down
34 changes: 34 additions & 0 deletions apps/rebar/src/rebar_completion.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
-module(rebar_completion).

-export([generate/2]).

-type arg_type() :: atom | binary | boolean | float | interger | string.

-type cmpl_arg() :: #{short => char() | undefined,
long => string() | undefined,
type => arg_type(),
help => string()}.

-type cmpl_cmd() :: #{name := string(),
help := string() | undefined,
args := [cmpl_arg()],
cmds => [cmpl_cmd()]}.

-type cmpl_opts() :: #{aliases => [string()],
file => file:filename(),
%% TODO support fish and maybe some more shells
shell => bash | zsh}.
-export([prelude/1]).

-export_type([cmpl_opts/0, cmpl_cmd/0, cmpl_arg/0]).

-callback generate([cmpl_cmd()], cmpl_opts()) -> iolist().

-spec generate([cmpl_cmd()], cmpl_opts()) -> string().
generate(Commands, #{shell:=bash}=CmplOpts) ->
rebar_completion_bash:generate(Commands,CmplOpts);
generate(Commands, #{shell:=zsh}=CmplOpts) ->
rebar_completion_zsh:generate(Commands,CmplOpts).
MarkoMin marked this conversation as resolved.
Show resolved Hide resolved

prelude(#{shell:=Shell}) ->
"# "++atom_to_list(Shell)++" completion file for rebar3 (autogenerated by rebar3).\n".
113 changes: 113 additions & 0 deletions apps/rebar/src/rebar_completion_bash.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
%% @doc Completion file generator for bash
%% @end
-module(rebar_completion_bash).

-behavior(rebar_completion).

-export([generate/2]).

-define(str(N), integer_to_list(N)).

-spec generate([rebar_completion:cmpl_cmd()], rebar_completion:cmpl_opts()) -> iolist().
generate(Commands, #{shell:=bash}=CmplOpts) ->
[rebar_completion:prelude(CmplOpts),
io_lib:nl(),
main(Commands, CmplOpts),
complete(CmplOpts),
io_lib:nl()].

cmd_clause(Cmd) ->
nested_cmd_clause(Cmd, [], 1).

-spec nested_cmd_clause(rebar_completion:cmpl_cmd(), [string()], pos_integer()) -> iolist().
nested_cmd_clause(#{name:=Name,args:=Args,cmds:=Cmds},Prevs,Depth) ->
Opts = [{S,L} || #{short:=S, long:=L} <- Args],
{Shorts0,Longs0} = lists:unzip(Opts),
Defined = fun(Opt) -> Opt =/= undefined end,
Shorts = lists:filter(Defined, Shorts0),
Longs = lists:filter(Defined, Longs0),
SOpts = lists:join(" ",
[[$-,S] || S <- Shorts]),
LOpts = lists:join(" ",
["--"++L || L <- Longs]),
Cmdsnvars = lists:join(" ",
[N || #{name:=N} <- Cmds]),
IfBody = match_prev_if_body([Name | Prevs]),
ClauseHead = ["elif [[ ",IfBody," ]] ; then\n"],
ClauseBody = [" sopts=\"",SOpts,"\"\n",
" lopts=\"",LOpts,"\"\n",
" cmdsnvars=\"",Cmdsnvars,"\"\n"],
Nested = [nested_cmd_clause(C, [Name | Prevs], Depth+1) || C <- Cmds],
[ClauseHead,ClauseBody,Nested].

match_prev_if_body([P | Rest]) ->
lists:join(" && ",
do_match_prev_if_body([P | Rest],1)).

do_match_prev_if_body([],_) ->
[];
do_match_prev_if_body([P | Rest],Cnt) ->
[["${prev",?str(Cnt),"} == ",P] | do_match_prev_if_body(Rest,Cnt+1)].

main(Commands, #{shell:=bash, aliases:=Aliases}) ->
MaxDepth=cmd_depth(Commands,1,0),
CmdNames = [Name || #{name:=Name} <- Commands],
Triggers = ["rebar3" | Aliases],
TriggerConds = [["${prev1} == \"",T,"\""] || T <- Triggers],
Trigger = lists:join(" || ", TriggerConds),
IfTriggerThen = ["if [[ ",Trigger," ]] ; then\n"],

["_rebar3_ref_idx() {\n",
" startc=$1\n",
" # is at least one of the two previous words a flag?\n",
" prev=${COMP_CWORD}-${startc}+",?str(MaxDepth-1),"\n",
" if [[ ${COMP_WORDS[${prev}]} == -* || ${COMP_WORDS[${prev}-1]} == -* ]] ; then\n",
" startc=$((startc+1))\n",
" _rebar3_ref_idx $startc\n",
" fi\n",
" return $startc\n",
"}\n",
"\n",
"_rebar3(){\n",
" local cur sopts lopts cmdsnvars refidx \n",
" local ",lists:join(" ", ["prev"++?str(I) || I <- lists:seq(1, MaxDepth)]),"\n",
" COMPREPLY=()\n",
" _rebar3_ref_idx ",?str(MaxDepth),"\n",
" refidx=$?\n",
" cur=\"${COMP_WORDS[COMP_CWORD]}\"\n",
prev_definitions(MaxDepth,1),
" ",IfTriggerThen,
" sopts=\"-h -v\"\n"
" lopts=\"--help --version\"\n",
" cmdsnvars=\"",lists:join(" \\\n", CmdNames),"\"\n",
" ",[cmd_clause(Cmd) || Cmd <- Commands],
" fi\n",
" COMPREPLY=( $(compgen -W \"${sopts} ${lopts} ${cmdsnvars} \" -- ${cur}) )\n",
" if [ -n \"$COMPREPLY\" ] ; then\n",
" # append space if matched\n",
" COMPREPLY=\"${COMPREPLY} \"\n",
" # remove trailing space after equal sign\n",
" COMPREPLY=${COMPREPLY/%= /=}\n",
" fi\n",
" return 0\n",
"}\n"].

prev_definitions(MaxDepth, Cnt) when (Cnt-1)=:=MaxDepth ->
[];
prev_definitions(MaxDepth, Cnt) ->
P = [" prev",?str(Cnt),"=\"${COMP_WORDS[COMP_CWORD-${refidx}+",?str((MaxDepth-Cnt)),"]}\"\n"],
[P | prev_definitions(MaxDepth,Cnt+1)].

cmd_depth([], _, Max) ->
Max;
cmd_depth([#{cmds:=[]} | Rest],Depth,Max) ->
cmd_depth(Rest,Depth,max(Depth,Max));
cmd_depth([#{cmds:=Cmds} | Rest],Depth, Max) ->
D = cmd_depth(Cmds, Depth+1, Max),
cmd_depth(Rest, Depth, max(D,Max));
cmd_depth([_ | Rest],Depth,Max) ->
cmd_depth(Rest,Depth,max(Depth,Max)).

complete(#{shell:=bash, aliases:=Aliases}) ->
Triggers = ["rebar3" | Aliases],
[["complete -o nospace -F _rebar3 ", Trigger, "\n"] || Trigger <- Triggers].
119 changes: 119 additions & 0 deletions apps/rebar/src/rebar_completion_zsh.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
%% @doc Completion file generator for zsh
%% @end
-module(rebar_completion_zsh).

-behavior(rebar_completion).

-export([generate/2]).

-spec generate([rebar_completion:cmpl_cmd()], rebar_completion:cmpl_opts()) -> iolist().
generate(Commands, #{shell:=zsh}=CmplOpts) ->
["#compdef _rebar3 rebar3\n",
rebar_completion:prelude(CmplOpts),
io_lib:nl(),
main(Commands, CmplOpts),
io_lib:nl()].

main(Commands, CmplOpts) ->
H = #{short=>$s,
long=>"help",
help=>"rebar3 help",
type=>boolean},
V = #{short=>$v,
long=>"version",
help=>"Version of rebar3",
type=>boolean},
Rebar = #{name=>"rebar3",
cmds=>Commands,
args=>[H,V],
help=>"Erlang build tool"},
cmd_to_fun(Rebar, [], CmplOpts).

cmd_to_fun(#{name:=Name,cmds:=Nested}=Cmd, Prev, CmplOpts) ->
["function "++function_name(Prev, Name)++" {\n",
" local -a commands\n",
io_lib:nl(),
args(Cmd, CmplOpts),
io_lib:nl(),
nested_cmds(Nested,[Name|Prev],CmplOpts),
"}\n",
io_lib:nl(),
[cmd_to_fun(C, [Name|Prev], CmplOpts) || C <- Nested]].

function_name(Prev,Name) ->
["_",
string:join(
lists:reverse([Name | Prev]),
"_")].

nested_cmds([],_,_) ->
io_lib:nl();
nested_cmds(Cmds,Prev,CmplOpts) ->
[" case $state in\n",
" cmnds)\n",
" commands=(\n",
[[" ",cmd_str(Cmd, CmplOpts),io_lib:nl()] || Cmd <- Cmds],
" )\n",
" _describe \"command\" commands\n",
" ;;\n",
" esac\n",
"\n",
" case \"$words[1]\" in\n",
[cmd_call_case(Cmd, Prev, CmplOpts) || Cmd <- Cmds],
" esac\n"].

cmd_str(#{name:=N,help:=H}, _CmplOpts) ->
["\"",N,":",help(H),"\""].

cmd_call_case(#{name:=Name}, Prev, _CmplOpts) ->
[" ",Name,")\n",
" ",function_name(Prev, Name),"\n",
" ;;\n"].

args(#{args:=Args,cmds:=Cmds}, _CmplOpts) ->
NoMore = (Args=:=[]) and (Cmds=:=[]),
case NoMore of
true ->
" _message 'no more arguments'\n";
false ->
[" _arguments \\\n",
[arg_str(Arg) || Arg <- Args],
case Cmds of
[] ->
"";
_ ->
[" \"1: :->cmnds\" \\\n",
" \"*::arg:->args\"\n"]
end]
end.

arg_str(#{short:=undefined,long:=undefined,help:=H}) ->
[" ","'1:",H,":' \\\n"];
arg_str(#{help:=H}=Arg) ->
[" ",spec(Arg),"[",help(H),"]' \\\n"].

spec(#{short:=undefined,long:=L}) ->
["'(--",L,")--",L,""];
spec(#{short:=S,long:=undefined}) ->
["'(-",[S],")-",[S],""];
spec(#{short:=S,long:=L}) ->
["'(-",[S]," --",L,")'{-",[S],",--",L,"}'"].

help(undefined) -> "";
help(H) -> help_escape(H).

help_escape([]) ->
[];
help_escape([40 | Rest]) ->
["\\(",help_escape(Rest)];
help_escape([41 | Rest]) ->
["\\)",help_escape(Rest)];
help_escape([91| Rest]) ->
["\\[",help_escape(Rest)];
help_escape([93| Rest]) ->
["\\]",help_escape(Rest)];
%% escaping single quotes by doubling them
help_escape([$' | Rest]) ->
["''",help_escape(Rest)];
help_escape([C | Rest]) ->
[C | help_escape(Rest)].
Loading
Loading