Skip to content

Commit

Permalink
Merge pull request #2858 from MarkoMin/completion
Browse files Browse the repository at this point in the history
add a new shell completion provider
  • Loading branch information
ferd authored Mar 5, 2024
2 parents 17f6861 + 4ad437a commit 59238ca
Show file tree
Hide file tree
Showing 7 changed files with 557 additions and 0 deletions.
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()) -> iolist().
generate(Commands, #{shell:=bash}=CmplOpts) ->
rebar_completion_bash:generate(Commands,CmplOpts);
generate(Commands, #{shell:=zsh}=CmplOpts) ->
rebar_completion_zsh:generate(Commands,CmplOpts).

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

0 comments on commit 59238ca

Please sign in to comment.