Skip to content

Commit

Permalink
Support OTP 27 style docs (#1556)
Browse files Browse the repository at this point in the history
  • Loading branch information
plux authored Oct 2, 2024
1 parent d5bb5a8 commit 05a8477
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 81 deletions.
170 changes: 131 additions & 39 deletions apps/els_lsp/src/els_docs.erl
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,10 @@
-include("els_lsp.hrl").
-include_lib("kernel/include/logger.hrl").

-ifdef(OTP_RELEASE).
-if(?OTP_RELEASE >= 23).
-include_lib("kernel/include/eep48.hrl").
-export([eep48_docs/4]).
-export([eep59_docs/4]).
-type docs_v1() :: #docs_v1{}.
-endif.
-endif.

%%==============================================================================
%% Macro Definitions
Expand Down Expand Up @@ -135,23 +132,28 @@ function_docs(Type, M, F, A, true = _DocsMemo) ->
end;
function_docs(Type, M, F, A, false = _DocsMemo) ->
%% call via ?MODULE to enable mocking in tests
case ?MODULE:eep48_docs(function, M, F, A) of
case ?MODULE:eep59_docs(function, M, F, A) of
{ok, Docs} ->
[{text, Docs}];
{error, not_available} ->
%% We cannot fetch the EEP-48 style docs, so instead we create
%% something similar using the tools we have.
Sig = {h2, signature(Type, M, F, A)},
L = [
function_clauses(M, F, A),
specs(M, F, A),
edoc(M, F, A)
],
case lists:append(L) of
[] ->
[Sig];
Docs ->
[Sig, {text, "---"} | Docs]
case ?MODULE:eep48_docs(function, M, F, A) of
{ok, Docs} ->
[{text, Docs}];
{error, not_available} ->
%% We cannot fetch the EEP-48 style docs, so instead we create
%% something similar using the tools we have.
Sig = {h2, signature(Type, M, F, A)},
L = [
function_clauses(M, F, A),
specs(M, F, A),
edoc(M, F, A)
],
case lists:append(L) of
[] ->
[Sig];
Docs ->
[Sig, {text, "---"} | Docs]
end
end
end.

Expand Down Expand Up @@ -207,36 +209,24 @@ signature('remote', M, F, A) ->
%% If it is not available it tries to create the EEP-48 style docs
%% using edoc.
-ifdef(NATIVE_FORMAT).
-define(MARKDOWN_FORMAT, <<"text/markdown">>).

-spec eep48_docs(function | type, atom(), atom(), non_neg_integer()) ->
{ok, string()} | {error, not_available}.
eep48_docs(Type, M, F, A) ->
Render =
case Type of
function ->
render;
type ->
render_type
end,
GL = setup_group_leader_proxy(),
try get_doc_chunk(M) of
{ok,
#docs_v1{
format = ?NATIVE_FORMAT,
format = Format,
module_doc = MDoc
} = DocChunk} when MDoc =/= hidden ->
} = DocChunk} when
MDoc =/= hidden,
(Format == ?MARKDOWN_FORMAT orelse
Format == ?NATIVE_FORMAT)
->
flush_group_leader_proxy(GL),

case els_eep48_docs:Render(M, F, A, DocChunk) of
{error, _R0} ->
case els_eep48_docs:Render(M, F, DocChunk) of
{error, _R1} ->
{error, not_available};
Docs ->
{ok, els_utils:to_list(Docs)}
end;
Docs ->
{ok, els_utils:to_list(Docs)}
end;
render_doc(Type, M, F, A, DocChunk);
_R1 ->
?LOG_DEBUG(#{error => _R1}),
{error, not_available}
Expand All @@ -255,6 +245,108 @@ eep48_docs(Type, M, F, A) ->
{error, not_available}
end.

-spec eep59_docs(function | type, atom(), atom(), non_neg_integer()) ->
{ok, string()} | {error, not_available}.
eep59_docs(Type, M, F, A) ->
try get_doc(M) of
{ok,
#docs_v1{
format = Format,
module_doc = MDoc
} = DocChunk} when
MDoc =/= hidden,
(Format == ?MARKDOWN_FORMAT orelse
Format == ?NATIVE_FORMAT)
->
render_doc(Type, M, F, A, DocChunk);
_R1 ->
?LOG_DEBUG(#{error => _R1}),
{error, not_available}
catch
C:E:ST ->
%% code:get_doc/1 fails for escriptized modules, so fall back
%% reading docs from source. See #751 for details
?LOG_DEBUG(#{
slogan => "Error fetching docs, falling back to src.",
module => M,
error => {C, E},
st => ST
}),
{error, not_available}
end.

-spec get_doc(module()) -> {ok, docs_v1()} | {error, not_available}.
get_doc(Module) when is_atom(Module) ->
%% This will error if module isn't loaded
try code:get_doc(Module) of
{ok, DocChunk} ->
{ok, DocChunk};
{error, _} ->
%% If the module isn't loaded, we try
%% to find the doc chunks from any .beam files
%% matching the module name.
Beams = find_beams(Module),
get_doc(Beams, Module)
catch
C:E:ST ->
%% code:get_doc/1 fails for escriptized modules, so fall back
%% reading docs from source. See #751 for details
?LOG_INFO(#{
slogan => "Error fetching docs, falling back to src.",
module => Module,
error => {C, E},
st => ST
}),
{error, not_available}
end.

-spec get_doc([file:filename()], module()) ->
{ok, docs_v1()} | {error, not_available}.
get_doc([], _Module) ->
{error, not_available};
get_doc([Beam | T], Module) ->
case beam_lib:chunks(Beam, ["Docs"]) of
{ok, {Module, [{"Docs", Bin}]}} ->
{ok, binary_to_term(Bin)};
_ ->
get_doc(T, Module)
end.

-spec find_beams(module()) -> [file:filename()].
find_beams(Module) ->
%% Look for matching .beam files under the project root
RootUri = els_config:get(root_uri),
Root = binary_to_list(els_uri:path(RootUri)),
Beams0 = filelib:wildcard(
filename:join([Root, "**", atom_to_list(Module) ++ ".beam"])
),
%% Sort the beams, to ensure we try the newest beam first
TimeBeams = [{filelib:last_modified(Beam), Beam} || Beam <- Beams0],
{_, Beams} = lists:unzip(lists:reverse(lists:sort(TimeBeams))),
Beams.

-spec render_doc(function | type, module(), atom(), arity(), docs_v1()) ->
{ok, string()} | {error, not_available}.
render_doc(Type, M, F, A, DocChunk) ->
Render =
case Type of
function ->
render;
type ->
render_type
end,
case els_eep48_docs:Render(M, F, A, DocChunk) of
{error, _R0} ->
case els_eep48_docs:Render(M, F, DocChunk) of
{error, _R1} ->
{error, not_available};
Docs ->
{ok, els_utils:to_list(Docs)}
end;
Docs ->
{ok, els_utils:to_list(Docs)}
end.

%% This function first tries to read the doc chunk from the .beam file
%% and if that fails it attempts to find the .chunk file.
-spec get_doc_chunk(M :: module()) -> {ok, term()} | error.
Expand Down
63 changes: 42 additions & 21 deletions apps/els_lsp/src/els_eep48_docs.erl
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ render(Module, Function, #docs_v1{docs = Docs} = D, Config) when
Docs
),
D,
Module,
Config
);
render(_Module, Function, Arity, #docs_v1{} = D) ->
Expand All @@ -183,6 +184,7 @@ render(Module, Function, Arity, #docs_v1{docs = Docs} = D, Config) when
Docs
),
D,
Module,
Config
).

Expand Down Expand Up @@ -210,7 +212,7 @@ render_type(Module, Type, D = #docs_v1{}) ->
Arity :: arity(),
Docs :: docs_v1(),
Res :: unicode:chardata() | {error, type_missing}.
render_type(_Module, Type, #docs_v1{docs = Docs} = D, Config) ->
render_type(Module, Type, #docs_v1{docs = Docs} = D, Config) ->
render_typecb_docs(
lists:filter(
fun
Expand All @@ -222,6 +224,7 @@ render_type(_Module, Type, #docs_v1{docs = Docs} = D, Config) ->
Docs
),
D,
Module,
Config
);
render_type(_Module, Type, Arity, #docs_v1{} = D) ->
Expand All @@ -234,7 +237,7 @@ render_type(_Module, Type, Arity, #docs_v1{} = D) ->
Docs :: docs_v1(),
Config :: config(),
Res :: unicode:chardata() | {error, type_missing}.
render_type(_Module, Type, Arity, #docs_v1{docs = Docs} = D, Config) ->
render_type(Module, Type, Arity, #docs_v1{docs = Docs} = D, Config) ->
render_typecb_docs(
lists:filter(
fun
Expand All @@ -246,6 +249,7 @@ render_type(_Module, Type, Arity, #docs_v1{docs = Docs} = D, Config) ->
Docs
),
D,
Module,
Config
).

Expand Down Expand Up @@ -275,7 +279,7 @@ render_callback(_Module, Callback, #docs_v1{} = D) ->
Res :: unicode:chardata() | {error, callback_missing}.
render_callback(_Module, Callback, Arity, #docs_v1{} = D) ->
render_callback(_Module, Callback, Arity, D, #{});
render_callback(_Module, Callback, #docs_v1{docs = Docs} = D, Config) ->
render_callback(Module, Callback, #docs_v1{docs = Docs} = D, Config) ->
render_typecb_docs(
lists:filter(
fun
Expand All @@ -287,6 +291,7 @@ render_callback(_Module, Callback, #docs_v1{docs = Docs} = D, Config) ->
Docs
),
D,
Module,
Config
).

Expand All @@ -297,7 +302,7 @@ render_callback(_Module, Callback, #docs_v1{docs = Docs} = D, Config) ->
Docs :: docs_v1(),
Config :: config(),
Res :: unicode:chardata() | {error, callback_missing}.
render_callback(_Module, Callback, Arity, #docs_v1{docs = Docs} = D, Config) ->
render_callback(Module, Callback, Arity, #docs_v1{docs = Docs} = D, Config) ->
render_typecb_docs(
lists:filter(
fun
Expand All @@ -309,6 +314,7 @@ render_callback(_Module, Callback, Arity, #docs_v1{docs = Docs} = D, Config) ->
Docs
),
D,
Module,
Config
).

Expand Down Expand Up @@ -353,11 +359,11 @@ normalize_format(Docs, #docs_v1{format = <<"text/", _/binary>>}) when is_binary(
[{pre, [], [Docs]}].

%%% Functions for rendering reference documentation
-spec render_function([chunk_entry()], #docs_v1{}, map()) ->
-spec render_function([chunk_entry()], #docs_v1{}, atom(), map()) ->
unicode:chardata() | {'error', 'function_missing'}.
render_function([], _D, _Config) ->
render_function([], _D, _Module, _Config) ->
{error, function_missing};
render_function(FDocs, #docs_v1{docs = Docs} = D, Config) ->
render_function(FDocs, #docs_v1{docs = Docs} = D, Module, Config) ->
Grouping =
lists:foldl(
fun
Expand All @@ -375,7 +381,7 @@ render_function(FDocs, #docs_v1{docs = Docs} = D, Config) ->
fun({Group, Members}) ->
lists:map(
fun(Member = {_, _, _, Doc, _}) ->
Sig = render_signature(Member),
Sig = render_signature(Member, Module),
LocalDoc =
if
Doc =:= #{} ->
Expand All @@ -399,8 +405,8 @@ render_function(FDocs, #docs_v1{docs = Docs} = D, Config) ->
).

%% Render the signature of either function, type, or anything else really.
-spec render_signature(chunk_entry()) -> chunk_elements().
render_signature({{_Type, _F, _A}, _Anno, _Sigs, _Docs, #{signature := Specs} = Meta}) ->
-spec render_signature(chunk_entry(), module()) -> chunk_elements() | els_poi:poi().
render_signature({{_Type, _F, _A}, _Anno, _Sigs, _Docs, #{signature := Specs} = Meta}, _Module) ->
lists:flatmap(
fun(ASTSpec) ->
PPSpec = erl_pp:attribute(ASTSpec, [{encoding, utf8}]),
Expand All @@ -424,8 +430,13 @@ render_signature({{_Type, _F, _A}, _Anno, _Sigs, _Docs, #{signature := Specs} =
end,
Specs
);
render_signature({{_Type, _F, _A}, _Anno, Sigs, _Docs, Meta}) ->
[{pre, [], Sigs}, {hr, [], []} | render_meta(Meta)].
render_signature({{_Type, F, A}, _Anno, Sigs, _Docs, Meta}, Module) ->
case els_dt_signatures:lookup({Module, F, A}) of
{ok, [#{spec := <<"-spec ", Spec/binary>>}]} ->
[{pre, [], Spec}, {hr, [], []} | render_meta(Meta)];
{ok, _} ->
[{pre, [], Sigs}, {hr, [], []} | render_meta(Meta)]
end.

-spec trim_spec(unicode:chardata()) -> unicode:chardata().
trim_spec(Spec) ->
Expand Down Expand Up @@ -499,7 +510,7 @@ render_headers_and_docs(Headers, DocContents, #config{} = Config) ->
render_docs(DocContents, 0, Config)
].

-spec render_typecb_docs([TypeCB] | TypeCB, #config{}) ->
-spec render_typecb_docs([TypeCB] | TypeCB, module(), #config{}) ->
unicode:chardata() | {'error', 'type_missing'}
when
TypeCB :: {
Expand All @@ -508,16 +519,20 @@ when
Sig :: [binary()],
none | hidden | #{binary() => chunk_elements()}
}.
render_typecb_docs([], _C) ->
render_typecb_docs([], _Module, _C) ->
{error, type_missing};
render_typecb_docs(TypeCBs, #config{} = C) when is_list(TypeCBs) ->
[render_typecb_docs(TypeCB, C) || TypeCB <- TypeCBs];
render_typecb_docs({F, _, _Sig, Docs, _Meta} = TypeCB, #config{docs = D} = C) ->
render_headers_and_docs(render_signature(TypeCB), get_local_doc(F, Docs, D), C).
-spec render_typecb_docs(chunk_elements(), #docs_v1{}, _) ->
render_typecb_docs(TypeCBs, Module, #config{} = C) when is_list(TypeCBs) ->
[render_typecb_docs(TypeCB, Module, C) || TypeCB <- TypeCBs];
render_typecb_docs({F, _, _Sig, Docs, _Meta} = TypeCB, Module, #config{docs = D} = C) ->
render_headers_and_docs(
render_signature(TypeCB, Module),
get_local_doc(F, Docs, D),
C
).
-spec render_typecb_docs(chunk_elements(), #docs_v1{}, module(), _) ->
unicode:chardata() | {'error', 'type_missing'}.
render_typecb_docs(Docs, D, Config) ->
render_typecb_docs(Docs, init_config(D, Config)).
render_typecb_docs(Docs, D, Module, Config) ->
render_typecb_docs(Docs, Module, init_config(D, Config)).

%%% General rendering functions
-spec render_docs([chunk_element()], #config{}) -> unicode:chardata().
Expand All @@ -540,6 +555,12 @@ init_config(D, _Config) ->
#config{}
) ->
{unicode:chardata(), non_neg_integer()}.
render_docs(Str, State, Pos, Ind, D) when
is_list(Str),
is_integer(hd(Str))
->
%% This is a string, convert it to binary.
render_docs([unicode:characters_to_binary(Str)], State, Pos, Ind, D);
render_docs(Elems, State, Pos, Ind, D) when is_list(Elems) ->
lists:mapfoldl(
fun(Elem, P) ->
Expand Down
Loading

0 comments on commit 05a8477

Please sign in to comment.