%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2005-2017. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% %CopyrightEnd%
%%
-module(edlin_expand).
%% a default expand function for edlin, expanding modules and functions
-export([expand/1, format_matches/1]).
-import(lists, [reverse/1, prefix/2]).
%% expand(CurrentBefore) ->
%% {yes, Expansion, Matches} | {no, Matches}
%% Try to expand the word before as either a module name or a function
%% name. We can handle white space around the seperating ':' but the
%% function name must be on the same line. CurrentBefore is reversed
%% and over_word/3 reverses the characters it finds. In certain cases
%% possible expansions are printed.
expand(Bef0) ->
{Bef1,Word,_} = edlin:over_word(Bef0, [], 0),
case over_white(Bef1, [], 0) of
{[$:|Bef2],_White,_Nwh} ->
{Bef3,_White1,_Nwh1} = over_white(Bef2, [], 0),
{_,Mod,_Nm} = edlin:over_word(Bef3, [], 0),
expand_function_name(Mod, Word);
{_,_,_} ->
expand_module_name(Word)
end.
expand_module_name(Prefix) ->
match(Prefix, code:all_loaded(), ":").
expand_function_name(ModStr, FuncPrefix) ->
case to_atom(ModStr) of
{ok, Mod} ->
case erlang:module_loaded(Mod) of
true ->
L = Mod:module_info(),
case lists:keyfind(exports, 1, L) of
{_, Exports} ->
match(FuncPrefix, Exports, "(");
_ ->
{no, [], []}
end;
false ->
{no, [], []}
end;
error ->
{no, [], []}
end.
%% if it's a quoted atom, atom_to_list/1 will do the wrong thing.
to_atom(Str) ->
case erl_scan:string(Str) of
{ok, [{atom,_,A}], _} ->
{ok, A};
_ ->
error
end.
match(Prefix, Alts, Extra0) ->
Len = string:length(Prefix),
Matches = lists:sort(
[{S, A} || {H, A} <- Alts,
prefix(Prefix, S=flat_write(H))]),
case longest_common_head([N || {N, _} <- Matches]) of
{partial, []} ->
{no, [], Matches}; % format_matches(Matches)};
{partial, Str} ->
case string:slice(Str, Len) of
[] ->
{yes, [], Matches}; % format_matches(Matches)};
Remain ->
{yes, Remain, []}
end;
{complete, Str} ->
Extra = case {Extra0,Matches} of
{"(",[{Str,0}]} -> "()";
{_,_} -> Extra0
end,
{yes, string:slice(Str, Len) ++ Extra, []};
no ->
{no, [], []}
end.
flat_write(T) ->
lists:flatten(io_lib:fwrite("~tw",[T])).
%% Return the list of names L in multiple columns.
format_matches(L) ->
{S1, Dots} = format_col(lists:sort(L), []),
S = case Dots of
true ->
{_, Prefix} = longest_common_head(vals(L)),
PrefixLen = string:length(Prefix),
case PrefixLen =< 3 of
true -> S1; % Do not replace the prefix with "...".
false ->
LeadingDotsL = leading_dots(L, PrefixLen),
{S2, _} = format_col(lists:sort(LeadingDotsL), []),
S2
end;
false -> S1
end,
["\n" | S].
format_col([], _) -> [];
format_col(L, Acc) ->
LL = 79,
format_col(L, field_width(L, LL), 0, Acc, LL, false).
format_col(X, Width, Len, Acc, LL, Dots) when Width + Len > LL ->
format_col(X, Width, 0, ["\n" | Acc], LL, Dots);
format_col([A|T], Width, Len, Acc0, LL, Dots) ->
{H0, R} = format_val(A),
Hmax = LL - length(R),
{H, NewDots} =
case string:length(H0) > Hmax of
true -> {io_lib:format("~-*ts", [Hmax - 3, H0]) ++ "...", true};
false -> {H0, Dots}
end,
Acc = [io_lib:format("~-*ts", [Width, H ++ R]) | Acc0],
format_col(T, Width, Len+Width, Acc, LL, NewDots);
format_col([], _, _, Acc, _LL, Dots) ->
{lists:reverse(Acc, "\n"), Dots}.
format_val({H, I}) when is_integer(I) ->
%% If it's a tuple {string(), integer()}, we assume it's an
%% arity, and meant to be printed.
{H, "/" ++ integer_to_list(I)};
format_val({H, _}) ->
{H, ""};
format_val(H) ->
{H, ""}.
field_width(L, LL) -> field_width(L, 0, LL).
field_width([{H,_}|T], W, LL) ->
case string:length(H) of
L when L > W -> field_width(T, L, LL);
_ -> field_width(T, W, LL)
end;
field_width([H|T], W, LL) ->
case string:length(H) of
L when L > W -> field_width(T, L, LL);
_ -> field_width(T, W, LL)
end;
field_width([], W, LL) when W < LL - 3 ->
W + 4;
field_width([], _, LL) ->
LL.
vals([]) -> [];
vals([{S, _}|L]) -> [S|vals(L)];
vals([S|L]) -> [S|vals(L)].
leading_dots([], _Len) -> [];
leading_dots([{H, I}|L], Len) ->
[{"..." ++ string:slice(H, Len), I}|leading_dots(L, Len)];
leading_dots([H|L], Len) ->
["..." ++ string:slice(H, Len)|leading_dots(L, Len)].
%% Strings are handled naively, but it should be OK here.
longest_common_head([]) ->
no;
longest_common_head(LL) ->
longest_common_head(LL, []).
longest_common_head([[]|_], L) ->
{partial, reverse(L)};
longest_common_head(LL, L) ->
case same_head(LL) of
true ->
[[H|_]|_] = LL,
LL1 = all_tails(LL),
case all_nil(LL1) of
false ->
longest_common_head(LL1, [H|L]);
true ->
{complete, reverse([H|L])}
end;
false ->
{partial, reverse(L)}
end.
same_head([[H|_]|T1]) -> same_head(H, T1).
same_head(H, [[H|_]|T]) -> same_head(H, T);
same_head(_, []) -> true;
same_head(_, _) -> false.
all_tails(LL) -> all_tails(LL, []).
all_tails([[_|T]|T1], L) -> all_tails(T1, [T|L]);
all_tails([], L) -> L.
all_nil([]) -> true;
all_nil([[] | Rest]) -> all_nil(Rest);
all_nil(_) -> false.
%% over_white(Chars, InitialStack, InitialCount) ->
%% {RemainingChars,CharStack,Count}.
over_white([$\s|Cs], Stack, N) ->
over_white(Cs, [$\s|Stack], N+1);
over_white([$\t|Cs], Stack, N) ->
over_white(Cs, [$\t|Stack], N+1);
over_white(Cs, Stack, N) when is_list(Cs) ->
{Cs,Stack,N}.