summaryrefslogtreecommitdiff
path: root/lib/stdlib/src/edlin_expand.erl
diff options
context:
space:
mode:
Diffstat (limited to 'lib/stdlib/src/edlin_expand.erl')
-rw-r--r--lib/stdlib/src/edlin_expand.erl1192
1 files changed, 1068 insertions, 124 deletions
diff --git a/lib/stdlib/src/edlin_expand.erl b/lib/stdlib/src/edlin_expand.erl
index bc3de13750..9823534e9b 100644
--- a/lib/stdlib/src/edlin_expand.erl
+++ b/lib/stdlib/src/edlin_expand.erl
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2005-2021. All Rights Reserved.
+%% Copyright Ericsson AB 2005-2023. 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.
@@ -18,47 +18,784 @@
%% %CopyrightEnd%
%%
-module(edlin_expand).
+%% a default expand function for edlin, expanding modules, functions
+%% filepaths, variable binding, record names, function parameter values,
+%% record fields and map keys and record field values.
+-include_lib("kernel/include/eep48.hrl").
+-export([expand/1, expand/2, expand/3, format_matches/2, number_matches/1, get_exports/1,
+ shell_default_or_bif/1, bif/1, over_word/1]).
+-export([is_type/3, match_arguments1/3]).
+-record(shell_state,{
+ bindings = [],
+ records = [],
+ functions = []
+ }).
-%% a default expand function for edlin, expanding modules and functions
+-spec expand(Bef0) -> {Res, Completion, Matches} when
+ Bef0 :: string(), %% a line of erlang expressions in reverse
+ Res :: 'yes' | 'no',
+ Completion :: string(),
+ Matches :: [Element] | [Section],
+ Element :: {string(), [ElementOption]},
+ ElementOption :: {ending, string()},
+ Section :: #{title:=string(), elems:=Matches, options:=SectionOption},
+ SectionOption :: {highlight_all} %% highlight the whole title
+ | {highlight, string()} %% highlight this part of the title
+ | {highlight_param, integer()} %% highlight this parameter
+ | {hide, title} %% hide the title
+ | {hide, result} %% hide the results
+ | {separator, string()}. %% specify another separator between title and result
+expand(Bef0) ->
+ expand(Bef0, [{legacy_output, true}]).
+
+-spec expand(Bef0, Opts) -> {Res, Completion, Matches} when
+ Bef0 :: string(), %% a line of erlang expressions in reverse
+ Opts :: [Option],
+ Option :: {legacy_output, boolean()},
+ Res :: 'yes' | 'no',
+ Completion :: string(),
+ Matches :: [Element] | [Section],
+ Element :: {string(), [ElementOption]},
+ ElementOption :: {ending, string()},
+ Section :: #{title:=string(), elems:=Matches, options:=SectionOption},
+ SectionOption :: {highlight_all} %% highlight the whole title
+ | {highlight, string()} %% highlight this part of the title
+ | {highlight_param, integer()} %% highlight this parameter
+ | {hide, title} %% hide the title
+ | {hide, result} %% hide the results
+ | {separator, string()}. %% specify another separator between title and result
+expand(Bef0, Opts) ->
+ ShellState = try
+ shell:get_state()
+ catch
+ _:_ ->
+ %% Running on a shell that does not support get_state()
+ #shell_state{bindings=[],records=[],functions=[]}
+ end,
+ expand(Bef0, Opts, ShellState).
+
+%% Only used for testing
+expand(Bef0, Opts, #shell_state{bindings = Bs, records = RT, functions = FT}) ->
+ LegacyOutput = proplists:get_value(legacy_output, Opts, false),
+ {_Bef1, Word} = over_word(Bef0),
+ {Res, Expansion, Matches} = case edlin_context:get_context(Bef0) of
+
+ {string} -> expand_string(Bef0);
+
+ {binding} -> expand_binding(Word, Bs);
+
+ {term} -> expand_module_function(Bef0, FT);
+ {term, _, {_, Unfinished}} -> expand_module_function(lists:reverse(Unfinished), FT);
+ {error, _Column} ->
+ {no, [], []};
+ {function} -> expand_module_function(Bef0, FT);
+ {fun_} -> expand_module_function(Bef0, FT);
+
+ {fun_, Mod} -> expand_function_name(Mod, Word, "/", FT);
+
+ %% Complete with arity in a 'fun mod:fun/' expression
+ {fun_, Mod, Fun} ->
+ Arities = [integer_to_list(A) || A <- get_arities(Mod, Fun)],
+ match(Word, Arities, "");
+ {new_fun, _ArgsString} -> {no, [], []};
+ %% Suggest type of function parameter
+ %% Complete an unfinished list, tuple or map using type of function parameter
+ {function, Mod, Fun, Args, Unfinished, Nesting} ->
+ Mod2 = case Mod of
+ "user_defined" -> "shell_default";
+ _ -> Mod
+ end,
+ FunExpansion = expand_function_type(Mod2, Fun, Args, Unfinished, Nesting, FT),
+ case Word of
+ [] -> FunExpansion;
+ _ ->
+ ModuleOrBifs = expand_helper(FT, module, Word, ":"),
+ Functions = case Args =/= [] andalso lists:last(Args) of
+ {atom, MaybeMod} -> expand_function_name(MaybeMod, Word, "", FT);
+ _ -> {no, [], []}
+ end,
+ fold_results([FunExpansion] ++ ModuleOrBifs ++ [Functions])
+ end;
+
+ %% Complete an unfinished key or suggest valid keys of a map binding
+ {map, Binding, Keys} -> expand_map(Word, Bs, Binding, Keys);
+
+ {map_or_record} ->
+ {[$#|Bef2], _} = over_word(Bef0),
+ {_, Var} = over_word(Bef2),
+ case Bs of
+ [] -> expand_record(Word, RT);
+ _ ->
+ case proplists:get_value(list_to_atom(Var), Bs) of
+ undefined ->
+ expand_record(Word, RT);
+ Map when is_map(Map) -> {yes, "{", []};
+ RecordTuple when is_tuple(RecordTuple), tuple_size(RecordTuple) > 0 ->
+ Atom = erlang:element(1, RecordTuple),
+ case (is_atom(Atom) andalso lists:keysearch(Atom, 1, RT)) of
+ {value, {Atom, _}} -> match(Word, [Atom], "{");
+ _ -> {no, [], []}
+ end;
+ _ -> {no, [], []}
+ end
+ end;
+
+ {record} -> expand_record(Word, RT);
+
+ {record, Record, Fields, FieldToComplete, Args, Unfinished, Nestings} ->
+ RecordExpansion = expand_record_fields(FieldToComplete, Unfinished, Record, Fields, RT, Args, Nestings, FT),
+ case Word of
+ [] -> RecordExpansion;
+ _ ->
+ ModuleOrBifs = expand_helper(FT, module,Word,":"),
+ fold_results([RecordExpansion] ++ ModuleOrBifs)
+ end;
+ _ -> {no, [], []}
+
+ end,
+ Matches1 = case {Res,number_matches(Matches)} of
+ {yes, 1} -> [];
+ _ -> Matches
+ end,
+ case LegacyOutput of
+ true -> {Res, Expansion, to_legacy_format(Matches1)};
+ false -> {Res, Expansion, Matches1}
+ end.
+expand_map(_, [], _, _) ->
+ {no, [], []};
+expand_map(Word, Bs, Binding, Keys) ->
+ case proplists:get_value(list_to_atom(Binding), Bs) of
+ Map when is_map(Map) ->
+ K1 = sets:from_list(maps:keys(Map)),
+ K2 = sets:subtract(K1, sets:from_list([list_to_atom(K) || K <- Keys])),
+ match(Word, sets:to_list(K2), "=>");
+ _ -> {no, [], []}
+ end.
+
+over_word(Bef) ->
+ {Bef1,_,_} = over_white(Bef, [], 0),
+ {Bef2, Word, _} = edlin:over_word(Bef1, [], 0),
+ {Bef2, Word}.
+
+
+expand_binding(Prefix, Bindings) ->
+ Alts = [strip_quotes(K) || {K,_} <- Bindings],
+ case match(Prefix, Alts, "") of
+ {_Res,_Expansion,[]}=M -> M;
+ {Res, Expansion, Matches} -> {Res,Expansion,[#{title=>"bindings", elems=>Matches, options=>[highlight_all]}]}
+ end.
+
+expand_record(Prefix, RT) ->
+ Alts = [Name || {Name, _} <- RT],
+ case match(Prefix, Alts, "{") of
+ {_Res,_Expansion,[]}=M -> M;
+ {Res, Expansion, Matches} -> {Res,Expansion,[#{title=>"records", elems=>Matches, options=>[highlight_all]}]}
+ end.
+
+expand_record_fields(FieldToComplete, Word, Record, Fields, RT, _Args, Nestings, FT) ->
+ Record2 = list_to_atom(Record),
+ FieldSet2 = sets:from_list([list_to_atom(F) || F <- Fields]),
+ FieldToComplete2 = list_to_atom(FieldToComplete),
+ Word1 = case Word of
+ {_, Word2} -> Word2;
+ [] -> []
+ end,
+ case [RecordSpec || {Record3, RecordSpec} <- RT, Record2 =:= Record3] of
+ [RecordType|_] ->
+ case sets:is_element(FieldToComplete2, FieldSet2) of
+ true ->
+ expand_record_field_content(FieldToComplete2, RecordType, Word1, Nestings, FT);
+ false ->
+ expand_record_field_name(Record2, FieldSet2, RecordType, Word1)
+ end;
+ _ ->
+ {no, [], []}
+ end.
+
+expand_record_field_name(Record, Fields, RecordType, Word) ->
+ RecordFieldsList = extract_record_fields(Record, RecordType),
+ RecordFieldsSet = sets:from_list(RecordFieldsList),
+ RecordFields = sets:subtract(RecordFieldsSet, Fields),
+ Alts = sets:to_list(RecordFields),
+ case match(Word, Alts, "=") of
+ {_Res,_Expansion,[]}=M -> M;
+ {Res, Expansion, Matches} -> {Res,Expansion,[#{title=>"fields", elems=>Matches, options=>[highlight_all]}]}
+ end.
--export([expand/1, format_matches/1]).
+expand_record_field_content(Field,
+ {attribute, _, record,
+ {_Record, FieldTypes}}, Word, Nestings, FT) ->
+ FieldTypesFiltered = [Type1 || {typed_record_field, {record_field, _, {_,_, F}}, Type1} <- FieldTypes, F == Field] ++
+ [Type1 || {typed_record_field, {record_field, _, {_,_, F}, _}, Type1} <- FieldTypes, F == Field],
+ case FieldTypesFiltered of
+ [] -> {no, [], []};
+ [Type] ->
+ T = edlin_type_suggestion:type_tree(erlang, Type, Nestings, FT),
+ Types = edlin_type_suggestion:get_types([], T, Nestings),
+ case Nestings of
+ [] ->
+ Atoms = edlin_type_suggestion:get_atoms([], T, Nestings),
+ case {Word, match(Word, Atoms, ", ")} of
+ {[],{_Res,_Expansion,_}} -> {_Res, _Expansion, [#{title=>"types", elems=>Types, options=>[{hide, title}]}]};
+ {_,{_Res,_Expansion,[]}=M} -> M;
+ {_,{Res,Expansion,Matches}} -> {Res, Expansion, [#{title=>"matches", elems=>Matches, options=>[highlight_all]}]}
+ end;
+ _ ->
+ expand_nesting_content(T, [], Nestings, #{title=>"types", elems=>Types, options=>[{hide, title}]})
+ end
+ end.
+
+%% Check that the actual type on previous arguments
+%% matches with the expected types
+%% Since we are not doing any evaluations at this point we
+%% don't know if a parenthesis, keyword, var, call or fun returns
+%% a value with the wrong type.
+match_arguments({function, {{parameters, Ps}, _}, Cs}, As) ->
+ match_arguments1(Ps, Cs, As);
+match_arguments({{parameters, Ps}, _}, As) ->
+ match_arguments1(Ps, [], As).
+match_arguments1(_,_,[]) -> true;
+%% Just assume that it will evaluate to the correct type.
+match_arguments1([_|Ps], Cs, [{parenthesis, _}|As]) ->
+ match_arguments1(Ps, Cs, As);
+match_arguments1([_|Ps], Cs, [{operation, _}|As]) ->
+ match_arguments1(Ps, Cs, As);
+match_arguments1([_|Ps], Cs, [{keyword, _}|As]) ->
+ match_arguments1(Ps, Cs, As);
+match_arguments1([_|Ps], Cs, [{var, _}|As]) ->
+ match_arguments1(Ps, Cs, As);
+match_arguments1([_|Ps], Cs, [{call, _}|As]) ->
+ match_arguments1(Ps, Cs, As);
+match_arguments1([_|Ps], Cs, [{fun_, _}|As]) ->
+ match_arguments1(Ps, Cs, As);
+match_arguments1([P|Ps], Cs, [{atom, [$'|_]=String}|As]) ->
+ case edlin_context:odd_quotes($', lists:reverse(String)) of
+ true -> false; % we know that the atom is unfinished, and thus cannot match any valid atom
+ _ -> case is_type(P, Cs, String) of
+ true -> match_arguments1(Ps, Cs, As);
+ false -> false
+ end
+ end;
+match_arguments1([P|Ps], Cs, [{_, String}|As]) ->
+ case is_type(P, Cs, String) of
+ true -> match_arguments1(Ps, Cs, As);
+ false -> false
+ end.
+
+is_type(Type, Cs, String) ->
+ {ok, A, _} = erl_scan:string(String++"."),
+ Types = [T || T <- edlin_type_suggestion:get_types(Cs, Type, [], [no_print]) ],
+ try
+ {ok, Term} = erl_parse:parse_term(A),
+ case Term of
+ Atom when is_atom(Atom) ->
+ Atoms = edlin_type_suggestion:get_atoms(Cs, Type, []),
+ lists:member(to_list(Atom), Atoms) orelse
+ lists:member(atom_to_list(Atom), Atoms) orelse
+ find_type(Types, [atom, node, module, 'fun']);
+ Tuple when is_tuple(Tuple) -> find_type(Types, [tuple]);
+ Map when is_map(Map) -> find_type(Types, [map]);
+ Binary when is_binary(Binary) -> find_type(Types, [binary]);
+ Float when is_float(Float) -> find_type(Types, [float]);
+ Integer when is_integer(Integer) -> check_integer_type(Types, Integer);
+ List when is_list(List), length(List) > 0 ->
+ find_type(Types, [list, string, nonempty_list,maybe_improper_list, nonempty_improper_list]);
+ List when is_list(List) -> find_type(Types, [list, string, maybe_improper_list])
+ end
+ catch
+ _:_ ->
+ %% Types not possible to deduce with erl_parse
+ % If string contains variables, erl_parse:parse_term will fail, but we
+ % consider them valid sooo.. lets replace them with the atom var
+ B = [(fun({var, Anno, _}) -> {atom, Anno, var}; (Token) -> Token end)(X) || X <- A],
+ try
+ {ok, Term2} = erl_parse:parse_term(B),
+ case Term2 of
+ Tuple2 when is_tuple(Tuple2) -> find_type(Types, [tuple]);
+ Map2 when is_map(Map2) -> find_type(Types, [map]);
+ Binary2 when is_binary(Binary2) -> find_type(Types, [binary]);
+ List2 when is_list(List2), length(List2) > 0 ->
+ find_type(Types, [list, string, nonempty_list,maybe_improper_list, nonempty_improper_list]);
+ List2 when is_list(List2) -> find_type(Types, [list, string, maybe_improper_list])
+ end
+ catch
+ _:_ ->
+ case A of
+ [{'#',_},{var,_,'Port'},{'<',_},{float,_,_},{'>',_},{dot,_}] -> find_type(Types, [port]);
+ [{'#',_},{var,_,'Ref'},{'<',_},{float,_,_},{'.',_},{float,_,_},{'>',_},{dot,_}] -> find_type(Types, [reference]);
+ [{'fun',_},{'(',_} | _] -> find_type(Types, [parameters, function, 'fun']);
+ [{'#',_},{var,_,'Fun'},{'<',_},{atom,_,erl_eval},{'.',_},{float,_,_},{'>',_}] -> find_type(Types, [parameters, function, 'fun']);
+ [{'<', _}, {float, _, _}, {'.', _}, {integer, _, _}, {'>', _}, {dot, _}] -> find_type(Types, [pid]);
+ [{'#', _}, {atom, _, RecordName},{'{', _}| _] -> find_type(Types, [{record, RecordName}]);
+ _ -> false
+ end
+ end
+ end.
+
+find_type([],_) -> false;
+find_type([any|_], _) -> true; % If we find any then every type is valid
+find_type([{type, any, []}|_], _) -> true;
+find_type([{{parameters, _},_}|Types], ValidTypes) ->
+ case lists:member(parameters, ValidTypes) of
+ true -> true;
+ false -> find_type(Types, ValidTypes)
+ end;
+find_type([{record, _}=Type|Types], ValidTypes) ->
+ case lists:member(Type, ValidTypes) of
+ true -> true;
+ false -> find_type(Types, ValidTypes)
+ end;
+find_type([{Type, _}|Types], ValidTypes) ->
+ case lists:member(Type, ValidTypes) of
+ true -> true;
+ false -> find_type(Types, ValidTypes)
+ end;
+find_type([{type, Type, _}|Types], ValidTypes) ->
+ case lists:member(Type, ValidTypes) of
+ true -> true;
+ false -> find_type(Types, ValidTypes)
+ end;
+find_type([{type, Type, _, any}|Types], ValidTypes) ->
+ case lists:member(Type, ValidTypes) of
+ true -> true;
+ false -> find_type(Types, ValidTypes)
+ end;
+find_type([_|Types], ValidTypes) -> find_type(Types, ValidTypes).
+
+in_range(_, []) -> false;
+in_range(Integer, [{type, range, [{integer, Start}, {integer, End}]}|_]) when Start =< Integer, Integer =< End -> true;
+in_range(Integer, [_|Types]) -> in_range(Integer, Types).
+
+check_integer_type(Types, Integer) when Integer == 0 -> find_type(Types, [integer, non_neg_integer, arity]) orelse in_range(Integer, Types);
+check_integer_type(Types, Integer) when Integer < 0 -> find_type(Types, [integer, neg_integer]) orelse in_range(Integer, Types);
+check_integer_type(Types, Integer) when Integer > 0 -> find_type(Types, [integer, non_neg_integer, pos_integer]) orelse in_range(Integer, Types).
--import(lists, [reverse/1, prefix/2]).
+add_to_last_nesting(Term, Nesting) ->
+ Last = lists:last(Nesting),
+ List = lists:droplast(Nesting),
+ case Last of
+ {tuple, Args, U} ->
+ List ++ [{tuple, Args ++ [Term], U}];
+ {list, Args, U} ->
+ List ++ [{list, Args ++ [Term], U}];
+ {map, F, Fs, Args, U} ->
+ List ++ [{map, F, Fs, Args ++ [Term], U}]
+ end.
+
+close_nesting(Nesting) ->
+ Last = lists:last(Nesting),
+ case Last of
+ {tuple, _Args, _} ->
+ "}";
+ {list, _Args, _} ->
+ "]";
+ {map, _F, _Fs, _Args, _} ->
+ "}"
+ end.
+expand_function_parameter_type(Mod, MFA, FunType, Args, Unfinished, Nestings, FT) ->
+ TypeTree = edlin_type_suggestion:type_tree(Mod, FunType, Nestings, FT),
+
+ {Parameters, Constraints1} = case TypeTree of
+ {function, {{parameters, Parameters1},_}, Constraints} ->
+ {Parameters1, Constraints};
+ {{parameters, Parameters1},_}=_F ->
+ {Parameters1, []}
+ end,
+ case match_arguments(TypeTree, Args) of
+ false -> {no, [], []};
+ true when Parameters == [] -> {yes, ")", [#{title=>MFA, elems=>[")"], options=>[]}]};
+ true ->
+ Parameter = lists:nth(length(Args)+1, Parameters),
+ {T, _Name} = case Parameter of
+ Atom when is_atom(Atom) -> {Atom, atom_to_list(Atom)};
+ {var, Name1}=T1 -> {T1, atom_to_list(Name1)};
+ {ann_type, {var, Name1}, T1} -> {T1, atom_to_list(Name1)};
+ T1 -> {T1, edlin_type_suggestion:print_type(T1, [], [{first_only, true}])}
+ end,
+ Ts = edlin_type_suggestion:get_types(Constraints1, T, Nestings),
+ Types = case Ts of
+ [] -> [];
+ _ ->
+ SectionTypes = [S || #{}=S <- Ts],
+ Types1 = case [E || {_, _}=E<-Ts] of
+ [] -> SectionTypes;
+ Elems ->
+ case SectionTypes of
+ [] -> Elems;
+ ST -> [#{title=>"simple types", elems=>Elems, options=>[{hide, title}]}|ST]
+ end
+ end,
+
+ [#{title=>"types", elems=>(Types1), options=>[{hide, title}]}]
+ end,
+ case Nestings of
+ [] -> %% Expand function type
+ case Unfinished of
+ [] ->
+ case T of
+ Atom1 when is_atom(Atom1) ->
+ CC = case length(Args)+1 < length(Parameters) of
+ true -> ", ";
+ false ->")"
+ end,
+ {Res, Expansion, Matches} = match([], [Atom1], CC),
+ case Matches of
+ [] -> {no, [], []};
+ _ -> {Res, Expansion, [#{title=>MFA, elems=>[], options=>[{highlight_param, length(Args)+1}]}]}
+ end;
+ _ when Types == [] ->
+ {no, [], []};
+ _ ->
+ {no, [], [#{title=>MFA, elems=>Types, options=>[{highlight_param, length(Args)+1}]}]}
+ end;
+ {_, Word} when is_atom(T) ->
+ CC = case length(Args)+1 < length(Parameters) of
+ true -> ", ";
+ false ->")"
+ end,
+ {Res, Expansion, Matches} = match(Word, [T], CC),
+ case Matches of
+ [] -> {no, [], []};
+ _ -> {Res, Expansion, [#{title=>MFA, elems=>[], options=>[{highlight_param, length(Args)+1}]}]}
+ end;
+ {_, Word} ->
+ {Res, Expansion, Matches} = begin
+ CC = case length(Args)+1 < length(Parameters) of
+ true -> ", ";
+ false ->")"
+ end,
+ Atoms1 = edlin_type_suggestion:get_atoms(Constraints1, T, Nestings),
+ {Res1, Expansion1, Matches1} = match(Word, Atoms1, CC),
+ case Matches1 of
+ [] ->
+ case match_arguments(TypeTree, Args ++ [Unfinished]) of
+ false -> {Res1, Expansion1, Matches1};
+ true ->
+ {yes, CC, [{CC, []}]}
+ end;
+ _ ->
+ {Res1, Expansion1, Matches1}
+ end
+ end,
+ Match1 = case Matches of
+ [] -> [];
+ _ -> Atoms = [#{title=>"atoms", elems=>Matches, options=>[{hide, title}]}],
+ [#{title=>MFA, elems=>Atoms, options=>[{highlight_param, length(Args)+1}]}]
+ end,
+ {Res, Expansion,Match1}
+ end;
+ _ -> %% Expand last nesting types
+ expand_nesting_content(T, Constraints1, Nestings, #{title=>MFA, elems=>Types, options=>[{highlight_param, length(Args)+1}]})
+ end
+ end.
+expand_nesting_content(T, Constraints, Nestings, Section) ->
+ {NestingType, UnfinishedNestingArg, NestingArgs} = case lists:last(Nestings) of
+ {tuple, NestingArgs1, Unfinished1} -> {tuple, Unfinished1, NestingArgs1};
+ {list, NestingArgs1, Unfinished1} -> {list, Unfinished1, NestingArgs1};
+ {map, _, _, NestingArgs1, Unfinished1} -> {map, Unfinished1, NestingArgs1}
+ end,
+ %% in the case of
+ %% erlang:system_info({allocator, )
+ %% we have a tuple nesting with an atom
+ %% this should give us "allocator" in the nestingsargs, and empty unfinished part
+ %% but we also know that we have a nesting, if we expect something other than a tuple, we shouldnt print that function
+ %% lets call it NestingType
+ %% now when that is fixed, how do we filter {allocator_sizes, ...} and others
+ Types = [Ts || Ts <- edlin_type_suggestion:get_types(Constraints, T, lists:droplast(Nestings), [no_print]) ],
+ case UnfinishedNestingArg of
+ [] ->
+ case find_type(Types, [NestingType]) of
+ true ->
+ %% if we know had a tuple, {allocator_sizes, } will be allowed
+ %% probably get_arity will return none
+ Nestings2 = add_to_last_nesting({var, "Var"}, Nestings),
+ NestingArities = edlin_type_suggestion:get_arity(Constraints, T, Nestings2),
+
+ fold_results([begin
+ case NestingArity of
+ none -> {no, [], []};
+ _ -> {no, [], [Section]}
+ end
+ end || NestingArity <- NestingArities]);
+ false -> {no, [], []}
+ end;
+ {_, Word} ->
+ Atoms1 = edlin_type_suggestion:get_atoms(Constraints, T, Nestings),
+ {Res1, Expansion1, Matches1} = match(Word, Atoms1, ""),
+ {Res, Expansion, Matches} = case Matches1 of
+ [] ->
+ Nestings2 = add_to_last_nesting(UnfinishedNestingArg, Nestings),
+ NestingArities = edlin_type_suggestion:get_arity(Constraints, T, Nestings2),
+ fold_results([begin
+ case NestingArity of
+ none -> {no, [], []};
+ _ when NestingType =:= tuple ->
+ CC = case length(NestingArgs)+1 < NestingArity of
+ true -> ", ";
+ false -> close_nesting(Nestings)
+ end,
+ {yes, CC, [{CC, []}]};
+ _ when NestingType =:= list ->
+ {no, [], [{", ", []}, {"]", []}]};
+ _ when NestingType =:= map ->
+ {no, [], [{", ",[]},{"}", []}]};
+ _ ->
+ {no, [], []}
+ end
+ end || NestingArity <- NestingArities]);
+ [{Word2,_}] ->
+ Nestings2 = add_to_last_nesting({atom, Word2}, Nestings),
+ NestingArities = edlin_type_suggestion:get_arity(Constraints, T, Nestings2),
+ fold_results([begin
+ case NestingArity of
+ none -> {no, [], []};
+ _ when NestingType =:= tuple ->
+ CC = case length(NestingArgs)+1 < NestingArity of
+ true -> ", ";
+ false -> close_nesting(Nestings)
+ end,
+ {yes, Expansion1++CC, [{Word2, [{ending, CC}]}]};
+ _ ->
+ {Res1, Expansion1, Matches1}
+ end
+ end || NestingArity <- NestingArities]);
+ _ -> {Res1, Expansion1, Matches1}
+ end,
+ Match1 = case Matches of
+ [] -> [];
+ _ -> Atoms = [#{title=>"atoms", elems=>Matches, options=>[{hide, title}]}],
+ [Section#{elems:=Atoms}]
+ end,
+ {Res, Expansion, Match1}
+ end.
+
+extract_record_fields(Record, {attribute,_,record,{Record, Fields}})->
+ [X || X <- [extract_record_field(F) || F <- Fields], X /= []];
+extract_record_fields(_, _)-> error.
+extract_record_field({typed_record_field, {_, _,{atom, _, Field}},_})->
+ Field;
+extract_record_field({typed_record_field, {_, _,{atom, _, Field}, _},_})->
+ Field;
+extract_record_field({record_field, _,{atom, _, Field},_})->
+ Field;
+extract_record_field({record_field, _,{atom, _, Field}})->
+ Field;
+extract_record_field(_) -> [].
+
+fold_results([]) -> {no, [], []};
+fold_results([R|Results]) ->
+ lists:foldl(fun fold_completion_result/2, R, Results).
+
+fold_completion_result({yes, Cmp1, Matches1}, {yes, Cmp2, Matches2}) ->
+ {_, Cmp} = longest_common_head([Cmp1,Cmp2]),
+ case Cmp of
+ [] -> {no, [], ordsets:union([Matches1,Matches2])};
+ _ -> {yes, Cmp, ordsets:union([Matches1,Matches2])}
+ end;
+fold_completion_result({yes, Cmp, Matches}, {no, [], []}) ->
+ {yes, Cmp, Matches};
+fold_completion_result({no, [], []},{yes, Cmp, Matches}) ->
+ {yes, Cmp, Matches};
+fold_completion_result({_, _, Matches1}, {_, [], Matches2}) ->
+ {no, [], ordsets:union([Matches1,Matches2])};
+fold_completion_result(A, B) ->
+ fold_completion_result(B,A).
+
+expand_function_type(ModStr, FunStr, Args, Unfinished, Nestings, FT) ->
+ Mod = list_to_atom(ModStr),
+ Fun = list_to_atom(FunStr),
+ MinArity = if Unfinished =:= [], length(Args) =:= 0 -> 0;
+ true -> length(Args)+1
+ end,
+ case [A || A <- get_arities(ModStr, FunStr, FT), A >= MinArity] of
+ [] -> {no, [], []};
+ Arities ->
+ {Res, Expansion, Matches} = fold_results([begin
+ FunTypes = edlin_type_suggestion:get_function_type(Mod, Fun, Arity, FT),
+ case FunTypes of
+ [] -> MFA = print_function_head(ModStr, FunStr, Arity),
+ case Unfinished of
+ [] -> {no, [], [#{title=>MFA, elems=>[], options=>[]}]};
+ _ -> {no, [], []}
+ end;
+ _ ->
+ fold_results([begin
+ MFA = print_function_head(ModStr, FunStr, FunType, FT),
+ expand_function_parameter_type(Mod, MFA, FunType, Args, Unfinished, Nestings, FT)
+ end || FunType <- FunTypes])
+ end
+ end || Arity <- Arities]),
+ case Matches of
+ [] -> {Res, Expansion, Matches};
+ _ -> {Res, Expansion, [#{title=>"typespecs", elems=>Matches, options=>[highlight_all]}]}
+ end
+ end.
-%% expand(CurrentBefore) ->
-%% {yes, Expansion, Matches} | {no, Matches}
+%% Behaves like zsh
+%% filters all files starting with . unless Word starts with .
+%% outputs / on end of folders
+expand_filepath(PathPrefix, Word) ->
+ Path = case PathPrefix of
+ [$/|_] -> PathPrefix;
+ _ ->
+ {ok, Cwd} = file:get_cwd(),
+ Cwd ++ "/" ++ PathPrefix
+ end,
+ ShowHidden = case Word of
+ "." ++ _ -> true;
+ _ -> false
+ end,
+ Entries = case file:list_dir(Path) of
+ {ok, E} -> lists:map(
+ fun(X)->
+ case filelib:is_dir(Path ++ "/" ++ X) of
+ true -> X ++ "/";
+ false -> X
+ end
+ end, [".."|E]);
+ _ -> []
+ end,
+ EntriesFiltered = [File || File <- Entries,
+ case File of
+ [$.|_] -> ShowHidden;
+ _ -> true
+ end],
+ case match(Word, EntriesFiltered, []) of
+ {yes, Cmp, [Match]} ->
+ case filelib:is_dir(Path ++ "/" ++ Word ++ Cmp) of
+ true -> {yes, Cmp, [Match]};
+ false -> {yes, Cmp ++ "\"", [Match]}
+ end;
+ X -> X
+ end.
+
+shell(Fun) ->
+ case shell:local_func(list_to_atom(Fun)) of
+ true -> "shell";
+ false -> "user_defined"
+ end.
+
+shell_default_or_bif(Fun) ->
+ case lists:member(list_to_atom(Fun), [E || {E,_}<-get_exports(shell_default)]) of
+ true -> "shell_default";
+ _ -> bif(Fun)
+ end.
+bif(Fun) ->
+ case lists:member(list_to_atom(Fun), [E || {E,A}<-get_exports(erlang), erl_internal:bif(E,A)]) of
+ true -> "erlang";
+ _ -> shell(Fun)
+ end.
+
+expand_string(Bef0) ->
+ case over_filepath(Bef0, []) of
+ {_, Filepath} ->
+ {Path, File} = split_at_last_slash(Filepath),
+ expand_filepath(Path, File);
+ _ -> {no, [], []}
+ end.
+%% Extract a whole filepath
+%% Stops as soon as we hit a double quote (")
+%% and returns everything it found before stopping.
+%% assumes the string is not a filepath if it contains unescaped spaces
+over_filepath([],_) -> none;
+over_filepath([$", $\\|Bef1], Filepath) -> over_filepath(Bef1, [$" | Filepath]);
+over_filepath([$"|Bef1], Filepath) -> {Bef1, Filepath};
+over_filepath([$\ ,$\\|Bef1], Filepath) -> over_filepath(Bef1, [$\ |Filepath]);
+over_filepath([$\ |_], _) -> none;
+over_filepath([C|Bef1], Filepath) ->
+ over_filepath(Bef1, [C|Filepath]).
+split_at_last_slash(Filepath) ->
+ {File, Path} = lists:splitwith(fun(X)->X/=$/ end, lists:reverse(Filepath)),
+ {lists:reverse(Path), lists:reverse(File)}.
+
+print_function_head(ModStr, FunStr, Arity) ->
+ lists:flatten(ModStr ++ ":" ++ FunStr ++ "/" ++ integer_to_list(Arity)).
+print_function_head(ModStr, FunStr, FunType, FT) ->
+ lists:flatten(print_function_head_from_type(ModStr, FunStr, FunType, FT)).
+
+print_function_head1(Mod, Fun, Par, _Ret) ->
+ Mod++":"++Fun++"("++lists:join(", ",
+ [case P of
+ Atom when is_atom(Atom) -> atom_to_list(Atom);
+ {var, V} -> atom_to_list(V);
+ {ann_type, {var, V}, _T} -> atom_to_list(V);
+ T -> edlin_type_suggestion:print_type(T, [], [{first_only, true}])
+ end || {_N,P} <- lists:enumerate(Par)])++")".
+print_function_head_from_type(Mod, Fun, FunType, FT) ->
+ case edlin_type_suggestion:type_tree(list_to_atom(Mod), FunType, [], FT) of
+ {function, {{parameters, Parameters},{return, Return}}, _} ->
+ print_function_head1(Mod, Fun, Parameters, Return);
+ {{parameters, Parameters},{return, Return}} ->
+ print_function_head1(Mod, Fun, Parameters, Return)
+ end.
+
+%% expand_module_function(CurrentBefore, FT) -> {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.
+%% possible expansions are printed.ยดยดยด
%%
-%% The function also handles expansion with "h(" for module and functions.
-expand(Bef0) ->
+%% The function also handles expansion with "h(" and "ht("" for module and functions.
+expand_module_function(Bef0, FT) ->
{Bef1,Word,_} = edlin:over_word(Bef0, [], 0),
case over_white(Bef1, [], 0) of
{[$,|Bef2],_White,_Nwh} ->
- {Bef3,_White1,_Nwh1} = over_white(Bef2, [], 0),
- {Bef4,Mod,_Nm} = edlin:over_word(Bef3, [], 0),
+ {Bef3,_White1,_Nwh1} = over_white(Bef2, [], 0),
+ {Bef4,Mod,_Nm} = edlin:over_word(Bef3, [], 0),
case expand_function(Bef4) of
help ->
- expand_function_name(Mod, Word, ",");
+ expand_function_name(Mod, Word, ", ", FT);
+ help_type ->
+ expand_type_name(Mod, Word, ", ");
_ ->
- expand_module_name(Word, ",")
+ fold_results(expand_helper(FT, module, Word, ":"))
end;
{[$:|Bef2],_White,_Nwh} ->
- {Bef3,_White1,_Nwh1} = over_white(Bef2, [], 0),
- {_,Mod,_Nm} = edlin:over_word(Bef3, [], 0),
- expand_function_name(Mod, Word, "(");
- {_,_,_} ->
+ {Bef3,_White1,_Nwh1} = over_white(Bef2, [], 0),
+ {_,Mod,_Nm} = edlin:over_word(Bef3, [], 0),
+ expand_function_name(Mod, Word, "(", FT);
+ {[CC, N_Esc|_], _White, _Nwh} when (CC =:= $] orelse CC =:= $) orelse CC =:= $> orelse CC =:= $}
+ orelse CC =:= $" orelse CC =:= $'),
+ N_Esc =/= $$, N_Esc =/= $- ->
+ {no, [], []};
+ {[], _, _} ->
+ case Word of
+ [] -> {no, [], []}; %fold_results([expand_shell_default(Word), expand_user_defined_functions(FT, Word)]);
+ _ -> fold_results(expand_helper(FT, all, Word, ":"))
+ end;
+ {_,_,_} ->
+ case Word of
+ [] -> {no, [], []};
+ _ ->
+ TypeOfExpand = expand_function(Bef1),
CompleteChar
- = case expand_function(Bef1) of
- help -> ",";
+ = case TypeOfExpand of
+ help -> ", ";
+ help_type -> ", ";
_ -> ":"
end,
- expand_module_name(Word, CompleteChar)
+ fold_results(expand_helper(FT, TypeOfExpand, Word, CompleteChar))
+ end
end.
-
+expand_keyword(Word) ->
+ Keywords = ["begin", "case", "of", "receive", "after", "maybe", "try", "catch", "throw", "if", "fun", "when", "end"],
+ {Res, Expansion, Matches} = match(Word, Keywords, ""),
+ case Matches of
+ [] -> {no, [], []};
+ [{Word, _}] -> {no, [], []}; %% exact match
+ _ -> {Res,Expansion,[#{title=>"keywords", elems=>Matches, options=>[highlight_all]}]}
+ end.
+expand_helper(_, help, Word, CompleteChar) ->
+ [expand_module_name(Word, CompleteChar)];
+expand_helper(_, help_type, Word, CompleteChar) ->
+ [expand_module_name(Word, CompleteChar)];
+expand_helper(FT, all, Word, CompleteChar) ->
+ [expand_module_name(Word, CompleteChar), expand_bifs(Word), expand_shell_default(Word),
+ expand_user_defined_functions(FT, Word), expand_keyword(Word)];
+expand_helper(FT, _, Word, CompleteChar) ->
+ [expand_module_name(Word, CompleteChar), expand_bifs(Word),
+ expand_user_defined_functions(FT, Word), expand_keyword(Word)].
expand_function("("++Str) ->
case edlin:over_word(Str, [], 0) of
{_,"h",_} ->
@@ -71,68 +808,157 @@ expand_function("("++Str) ->
expand_function(_) ->
module.
+expand_bifs(Prefix) ->
+ Alts = [EA || {E,A}=EA <- get_exports(erlang), erl_internal:bif(E,A)],
+ CC = "(",
+ case match(Prefix, Alts, CC) of
+ {_Res,_Expansion,[]}=M -> M;
+ {Res,Expansion, Matches} -> {Res,Expansion,[#{title=>"bifs", elems=>Matches, options=>[highlight_all]}]}
+ end.
+
+expand_shell_default(Prefix) ->
+ Alts = get_exports(shell_default) ++ shell:local_func(),
+ CC = "(",
+ case match(Prefix, Alts, CC) of
+ {_Res,_Expansion,[]}=M -> M;
+ {Res,Expansion, Matches} -> {Res,Expansion,[#{title=>"commands",elems=>Matches, options=>[highlight_all]}]}
+ end.
+
+expand_user_defined_functions(FT, Prefix) ->
+ Alts = [{Name, Arity}||{{function, {_, Name, Arity}}, _} <- FT],
+ CC = "(",
+ case match(Prefix, Alts, CC) of
+ {_Res,_Expansion,[]}=M -> M;
+ {Res,Expansion, Matches} -> {Res,Expansion,[#{title=>"user_defined", elems=>Matches, options=>[highlight_all]}]}
+ end.
+
expand_module_name("",_) ->
{no, [], []};
-expand_module_name(Prefix,CompleteChar) ->
- match(Prefix, [{list_to_atom(M),P} || {M,P,_} <- code:all_available()], CompleteChar).
+expand_module_name(Prefix,CC) ->
+ Alts = [{list_to_atom(M),""} || {M,_,_} <- code:all_available()],
+ case match(Prefix, Alts, CC) of
+ {_Res,_Expansion,[]}=M -> M;
+ {Res,Expansion, Matches} -> {Res,Expansion,[#{title=>"modules", elems=>Matches, options=>[highlight_all]}]}
+ end.
-expand_function_name(ModStr, FuncPrefix, CompleteChar) ->
+get_arities("shell_default"=ModStr, FuncStr, FT) ->
+ case [A || {{function, {_, Fun, A}}, _} <- FT, Fun =:= list_to_atom(FuncStr)] of
+ [] -> get_arities(ModStr, FuncStr);
+ Arities -> Arities
+ end;
+get_arities(ModStr, FuncStr, _) ->
+ get_arities(ModStr, FuncStr).
+get_arities(ModStr, FuncStr) ->
case to_atom(ModStr) of
- {ok, Mod} ->
- Exports =
- case erlang:module_loaded(Mod) of
- true ->
- Mod:module_info(exports);
- false ->
- case beam_lib:chunks(code:which(Mod), [exports]) of
- {ok, {Mod, [{exports,E}]}} ->
- E;
- _ ->
- {no, [], []}
- end
- end,
- case Exports of
+ {ok, Mod} ->
+ Exports = get_exports(Mod),
+ lists:sort(
+ [A || {H, A} <- Exports, string:equal(FuncStr, flat_write(H))]);
+ error ->
+ {no, [], []}
+ end.
+
+get_exports(Mod) ->
+ case erlang:module_loaded(Mod) of
+ true ->
+ Mod:module_info(exports);
+ false ->
+ case beam_lib:chunks(code:which(Mod), [exports]) of
+ {ok, {Mod, [{exports,E}]}} ->
+ E;
+ _ ->
+ []
+ end
+ end.
+
+expand_function_name(ModStr, FuncPrefix, CompleteChar, FT) ->
+ case to_atom(ModStr) of
+ {ok, Mod} ->
+ Extra = case Mod of
+ shell_default -> [{Name, Arity}||{{function, {_, Name, Arity}}, _} <- FT];
+ _ -> []
+ end,
+ Exports = get_exports(Mod) ++ Extra,
+ {Res, Expansion, Matches}=Result = match(FuncPrefix, Exports, CompleteChar),
+ case Matches of
+ [] -> Result;
+ _ -> {Res, Expansion, [#{title=>"functions", elems=>Matches, options=>[highlight_all]}]}
+ end;
+ error ->
+ {no, [], []}
+ end.
+
+get_module_types(Mod) ->
+ case code:get_doc(Mod, #{sources => [debug_info]}) of
+ {ok, #docs_v1{ docs = Docs } } ->
+ [{T, A} || {{type, T, A},_Anno,_Sig,_Doc,_Meta} <- Docs];
+ _ -> {no, [], []}
+ end.
+
+expand_type_name(ModStr, TypePrefix, CompleteChar) ->
+ case to_atom(ModStr) of
+ {ok, Mod} ->
+ case get_module_types(Mod) of
{no, [], []} ->
{no, [], []};
- Exports ->
- match(FuncPrefix, Exports, CompleteChar)
+ Types ->
+ {Res, Expansion, Matches}=Result = match(TypePrefix, Types, CompleteChar),
+ case Matches of
+ [] -> Result;
+ _ -> {Res, Expansion, [#{title=>"types", elems=>Matches, options=>[highlight_all]}]}
+ end
end;
- error ->
- {no, [], []}
+ 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
+ {ok, [{atom,_,A}], _} ->
+ {ok, A};
+ _ ->
+ error
end.
+to_list(Atom) ->
+ io_lib:write_atom(Atom).
+
+strip_quotes(Atom) ->
+ [C || C<-atom_to_list(Atom), C/=$'].
+
+match_preprocess_alt({_,_}=Alt) -> Alt;
+match_preprocess_alt(X) -> {X, ""}.
+
match(Prefix, Alts, Extra0) ->
+ Alts2 = [match_preprocess_alt(A) || A <- Alts],
Len = string:length(Prefix),
Matches = lists:sort(
- [{S, A} || {H, A} <- Alts,
- prefix(Prefix, S=flat_write(H))]),
+ [{S, A} || {H, A} <- Alts2,
+ lists:prefix(Prefix, S=flat_write(H))]),
+ Matches2 = lists:usort(
+ case Extra0 of
+ [] -> [{S,[]} || {S,_} <- Matches];
+ _ -> [{S,[{ending, Extra0}]} || {S,_} <- Matches]
+ end),
case longest_common_head([N || {N, _} <- Matches]) of
- {partial, []} ->
- {no, [], Matches}; % format_matches(Matches)};
- {partial, Str} ->
+ {partial, []} ->
+ {no, [], Matches2};
+ {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, [], []}
+ [] ->
+ {yes, [], Matches2};
+ Remain ->
+ {yes, Remain, Matches2}
+ end;
+ {complete, Str} ->
+ Extra = case {Extra0,Matches} of
+ {"/",[{Str,N}]} when is_integer(N) -> "/"++integer_to_list(N);
+ {"(",[{Str,0}]} -> "()";
+ {_,_} -> Extra0
+ end,
+ {yes, string:slice(Str, Len) ++ Extra, ordsets:from_list(Matches2)};
+ no ->
+ {no, [], []}
end.
flat_write(T) when is_atom(T) ->
@@ -140,79 +966,198 @@ flat_write(T) when is_atom(T) ->
flat_write(S) ->
S.
-%% 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
+special_sort1([C|A], B) when C == ${ ; C == $. ; C == $# ->
+ special_sort1(A, B);
+special_sort1(A, [C|B]) when C == ${ ; C == $. ; C == $# ->
+ special_sort1(A,B);
+special_sort1(A,B) ->
+ string:lowercase(A) =< string:lowercase(B).
+special_sort(#{title:=A}, #{title:=B}) ->
+ special_sort1(A,B);
+%% Sections and elemts should not be in the same list
+special_sort(#{}, {}) ->
+ error;
+special_sort({}, #{}) ->
+ error;
+special_sort({A,_},{B,_}) ->
+ special_sort1(A,B);
+special_sort(A,B) ->
+ special_sort1(A,B).
+
+to_legacy_format([]) -> [];
+to_legacy_format([#{title:=Title}|Rest]) when Title =:= "commands"; Title =:= "bifs" ->
+ to_legacy_format(Rest);
+to_legacy_format([#{title:=Title, elems:=Elems}|Rest])
+ when Title =:= "modules"; Title =:= "functions"; Title =:= "bindings";
+ Title =:= "user_defined", Title =:= "records"; Title =:= "fields";
+ Title =:= "types"; Title =:= "atoms"; Title =:= "matches";
+ Title =:= "keywords"; Title =:= "typespecs" ->
+ Elems1 = to_legacy_format(Elems),
+ Elems1 ++ to_legacy_format(Rest);
+to_legacy_format([#{title:=Title, elems:=_Elems}|Rest]) ->
+ [Title] ++ to_legacy_format(Rest);
+to_legacy_format([{Val, _}|Rest]) ->
+ [{Val, ""}] ++ to_legacy_format(Rest).
+
+format_matches([], _LineWidth) -> [];
+format_matches([#{}|_]=FF, LineWidth) ->
+ %% Group function head that have the exact same Type suggestion
+ Groups = maps:groups_from_list(
+ fun(#{title:=Title, elems:=T, options:=Opts}) ->
+ Separator = proplists:get_value(separator, Opts, "\n"),
+ case lists:last(string:split(Title++Separator, "\n", all)) of
+ [] -> format_section_matches(T, LineWidth);
+ Chars -> %% we have chars that compete with the results on the first line
+ Len = length(Chars),
+ format_section_matches(T, LineWidth, Len)
+ end
+ end,
+ fun(F) ->
+ format_title(F, LineWidth)
+ end, FF),
+ S = lists:flatten(
+ [lists:join("", F)++Matches ||
+ {Matches, F}<-lists:sort(fun({_,A},{_,B}) -> A =< B end, maps:to_list(Groups))]),
+ lists:flatten(string:trim(S, trailing)++"\n");
+format_matches(Elems, LineWidth) ->
+ S = format_section_matches1(Elems, LineWidth, 0),
+ lists:flatten(string:trim(S, trailing)++"\n").
+format_title(#{title:=MFA, options:=Options}, _LineWidth) ->
+ case proplists:get_value(hide, Options) of
+ title -> "";
+ _ ->
+ Separator = proplists:get_value(separator, Options, "\n"),
+ HighlightAll = proplists:is_defined(highlight_all, Options),
+ case HighlightAll of
+ true -> "\033[;1;4m"++MFA++"\033[0m"++Separator;
+ _ ->
+ HighlightParam = proplists:get_value(highlight_param, Options, false),
+
+ MFA2 = case HighlightParam of
+ false -> MFA;
+ _ ->
+ PreviousParams = HighlightParam -1,
+ TuplePattern = "(?:\\{[^\\}]+\\})",
+ AtomVarPattern = "(?:\\w+)",
+ TypePattern="(?:(?:"++AtomVarPattern++":)?(?:"++AtomVarPattern++"\\(\\))(?:\\s[><=]+\\s\\d+)?)",
+ SimplePatterns = "(?:"++TuplePattern++"|"++TypePattern++"|"++AtomVarPattern++")",
+ UnionPattern = "(?:"++SimplePatterns++"(?:\\s\\|\\s"++SimplePatterns++")*)",
+ FunPattern="(?:fun\\(\\(" ++ UnionPattern ++ "\\)\\s*->\\s*" ++ UnionPattern ++ "\\))",
+ ArgPattern3 = "(?:"++FunPattern++"|"++UnionPattern++")",
+ PrevArgs="(?:"++ArgPattern3++",\\s){"++integer_to_list(PreviousParams) ++ "}",
+ FunctionHeadStart="^([^\\(]+\\("++PrevArgs++")", %% \\1
+
+ HighlightArg="("++ArgPattern3++")", %\\2
+ NextArgs="(?:,\\s"++ArgPattern3++")*",
+ FunctionHeadEnd="("++NextArgs++"\\)(?:.*))$", % \\3
+
+ re:replace(MFA,
+ FunctionHeadStart ++ HighlightArg ++ FunctionHeadEnd,
+ "\\1\033[;1;4m\\2\033[0m\\3",
+ [global, {return, list}, unicode])
+ end,
+ Highlight = proplists:get_value(highlight, Options, false),
+ case Highlight of
+ false -> MFA2;
+ _ -> re:replace(MFA2, "(\\Q"++Highlight++"\\E)", "\033[;1;4m\\1\033[0m", [global, {return, list}, unicode])
+ end ++ Separator
+ end
+ end;
+format_title(_Elems, _LineWidth) ->
+ %% not a section, old interface
+ %% output empty list
+ "".
+
+format_section_matches(LS, LineWidth) -> format_section_matches(LS, LineWidth, 0).
+format_section_matches([], _, _) -> "\n";
+format_section_matches([#{}|_]=FF, LineWidth, Acc) ->
+ Groups = maps:groups_from_list(
+ fun(#{title:=Title, elems:=T, options:=Opts}) ->
+ Separator = proplists:get_value(separator, Opts, "\n"),
+ case lists:last(string:split(Title++Separator, "\n", trailing)) of
+ [] -> format_section_matches(T, LineWidth);
+ Chars -> %% we have chars that compete with the results on the first line
+ Len = string:length(Chars),
+ format_section_matches(T, LineWidth, Len+Acc)
+ end
end,
- ["\n" | S].
+ fun(F) ->
+ format_title(F, LineWidth)
+ end, FF),
+ lists:flatten(
+ [lists:join("", F)++Matches ||
+ {Matches, F}<-lists:sort(fun({_,A},{_,B}) -> A =< B end, maps:to_list(Groups))]);
+format_section_matches(Elems, LineWidth, Acc) ->
+ format_section_matches1(Elems, LineWidth, Acc).
-format_col([], _) -> [];
-format_col(L, Acc) ->
- LL = 79,
- format_col(L, field_width(L, LL), 0, Acc, LL, false).
+format_section_matches1([], _, _) -> [];
+format_section_matches1(LS, LineWidth, Len) ->
+ L = lists:usort(fun special_sort/2, ordsets:to_list(LS)),
+ Opt = case Len == 0 of
+ true -> [];
+ false -> [{title, Len}]
+ end,
+ S1 = format_col(Opt ++ L, field_width(Opt ++ L, LineWidth), Len, [], LineWidth, Opt),
+ S2 = lists:map(
+ fun(Line) ->
+ case string:length(Line) of
+ Len1 when Len1 > LineWidth ->
+ string:sub_string(Line, 1, LineWidth-4) ++ "...\n";
+ _ -> Line
+ end
+ end,S1),
+ lists:flatten(string:trim(S2, trailing)++"\n").
-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) ->
+format_col(X, Width, Len, Acc, LL, Opt) when Width + Len > LL ->
+ format_col(X, Width, 0, ["\n" | Acc], LL, Opt);
+format_col([{title,TitleLen}|T], Width, Len, Acc0, LL, Opt) ->
+ Acc = [io_lib:format("~-*ts", [Width-TitleLen, ""])|Acc0],
+ format_col(T, Width, Len+Width, Acc, LL, Opt);
+format_col([A|T], Width, Len, Acc0, LL, _Opt) ->
{H0, R} = format_val(A),
- Hmax = LL - length(R),
- {H, NewDots} =
+ Hmax = LL - string:length(R),
+ {H, _} =
case string:length(H0) > Hmax of
true -> {io_lib:format("~-*ts", [Hmax - 3, H0]) ++ "...", true};
- false -> {H0, Dots}
+ false -> {H0, false}
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_col(T, Width, Len+Width, Acc, LL, []);
+format_col([], _, _, Acc, _LL, _Opt) ->
+ lists:reverse(Acc).
+format_val({H, L}) when is_list(L) ->
+ {H, proplists:get_value(ending, L, "")};
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)};
+ {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
+field_width([{title, Len}|T], W, LL) ->
+ case Len 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
+ {H1, Ending} = format_val(H),
+ case string:length(H1++Ending) of
L when L > W -> field_width(T, L, LL);
_ -> field_width(T, W, LL)
end;
-field_width([], W, LL) when W < LL - 3 ->
+field_width([], W, LL) when W < LL ->
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)].
+number_matches([#{ elems := Matches }|T]) ->
+ number_matches(Matches) + number_matches(T);
+number_matches([_|T]) ->
+ 1 + number_matches(T);
+number_matches([]) ->
+ 0.
%% Strings are handled naively, but it should be OK here.
longest_common_head([]) ->
@@ -221,24 +1166,23 @@ longest_common_head(LL) ->
longest_common_head(LL, []).
longest_common_head([[]|_], L) ->
- {partial, reverse(L)};
+ {partial, lists: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)}
+ true ->
+ [[H|_]|_] = LL,
+ LL1 = all_tails(LL),
+ case all_nil(LL1) of
+ false ->
+ longest_common_head(LL1, [H|L]);
+ true ->
+ {complete, lists:reverse([H|L])}
+ end;
+ false ->
+ {partial, lists: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.