Skip to content

Commit 05a8477

Browse files
authored
Support OTP 27 style docs (#1556)
1 parent d5bb5a8 commit 05a8477

File tree

4 files changed

+216
-81
lines changed

4 files changed

+216
-81
lines changed

apps/els_lsp/src/els_docs.erl

+131-39
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,10 @@
1818
-include("els_lsp.hrl").
1919
-include_lib("kernel/include/logger.hrl").
2020

21-
-ifdef(OTP_RELEASE).
22-
-if(?OTP_RELEASE >= 23).
2321
-include_lib("kernel/include/eep48.hrl").
2422
-export([eep48_docs/4]).
23+
-export([eep59_docs/4]).
2524
-type docs_v1() :: #docs_v1{}.
26-
-endif.
27-
-endif.
2825

2926
%%==============================================================================
3027
%% Macro Definitions
@@ -135,23 +132,28 @@ function_docs(Type, M, F, A, true = _DocsMemo) ->
135132
end;
136133
function_docs(Type, M, F, A, false = _DocsMemo) ->
137134
%% call via ?MODULE to enable mocking in tests
138-
case ?MODULE:eep48_docs(function, M, F, A) of
135+
case ?MODULE:eep59_docs(function, M, F, A) of
139136
{ok, Docs} ->
140137
[{text, Docs}];
141138
{error, not_available} ->
142-
%% We cannot fetch the EEP-48 style docs, so instead we create
143-
%% something similar using the tools we have.
144-
Sig = {h2, signature(Type, M, F, A)},
145-
L = [
146-
function_clauses(M, F, A),
147-
specs(M, F, A),
148-
edoc(M, F, A)
149-
],
150-
case lists:append(L) of
151-
[] ->
152-
[Sig];
153-
Docs ->
154-
[Sig, {text, "---"} | Docs]
139+
case ?MODULE:eep48_docs(function, M, F, A) of
140+
{ok, Docs} ->
141+
[{text, Docs}];
142+
{error, not_available} ->
143+
%% We cannot fetch the EEP-48 style docs, so instead we create
144+
%% something similar using the tools we have.
145+
Sig = {h2, signature(Type, M, F, A)},
146+
L = [
147+
function_clauses(M, F, A),
148+
specs(M, F, A),
149+
edoc(M, F, A)
150+
],
151+
case lists:append(L) of
152+
[] ->
153+
[Sig];
154+
Docs ->
155+
[Sig, {text, "---"} | Docs]
156+
end
155157
end
156158
end.
157159

@@ -207,36 +209,24 @@ signature('remote', M, F, A) ->
207209
%% If it is not available it tries to create the EEP-48 style docs
208210
%% using edoc.
209211
-ifdef(NATIVE_FORMAT).
212+
-define(MARKDOWN_FORMAT, <<"text/markdown">>).
213+
210214
-spec eep48_docs(function | type, atom(), atom(), non_neg_integer()) ->
211215
{ok, string()} | {error, not_available}.
212216
eep48_docs(Type, M, F, A) ->
213-
Render =
214-
case Type of
215-
function ->
216-
render;
217-
type ->
218-
render_type
219-
end,
220217
GL = setup_group_leader_proxy(),
221218
try get_doc_chunk(M) of
222219
{ok,
223220
#docs_v1{
224-
format = ?NATIVE_FORMAT,
221+
format = Format,
225222
module_doc = MDoc
226-
} = DocChunk} when MDoc =/= hidden ->
223+
} = DocChunk} when
224+
MDoc =/= hidden,
225+
(Format == ?MARKDOWN_FORMAT orelse
226+
Format == ?NATIVE_FORMAT)
227+
->
227228
flush_group_leader_proxy(GL),
228-
229-
case els_eep48_docs:Render(M, F, A, DocChunk) of
230-
{error, _R0} ->
231-
case els_eep48_docs:Render(M, F, DocChunk) of
232-
{error, _R1} ->
233-
{error, not_available};
234-
Docs ->
235-
{ok, els_utils:to_list(Docs)}
236-
end;
237-
Docs ->
238-
{ok, els_utils:to_list(Docs)}
239-
end;
229+
render_doc(Type, M, F, A, DocChunk);
240230
_R1 ->
241231
?LOG_DEBUG(#{error => _R1}),
242232
{error, not_available}
@@ -255,6 +245,108 @@ eep48_docs(Type, M, F, A) ->
255245
{error, not_available}
256246
end.
257247

248+
-spec eep59_docs(function | type, atom(), atom(), non_neg_integer()) ->
249+
{ok, string()} | {error, not_available}.
250+
eep59_docs(Type, M, F, A) ->
251+
try get_doc(M) of
252+
{ok,
253+
#docs_v1{
254+
format = Format,
255+
module_doc = MDoc
256+
} = DocChunk} when
257+
MDoc =/= hidden,
258+
(Format == ?MARKDOWN_FORMAT orelse
259+
Format == ?NATIVE_FORMAT)
260+
->
261+
render_doc(Type, M, F, A, DocChunk);
262+
_R1 ->
263+
?LOG_DEBUG(#{error => _R1}),
264+
{error, not_available}
265+
catch
266+
C:E:ST ->
267+
%% code:get_doc/1 fails for escriptized modules, so fall back
268+
%% reading docs from source. See #751 for details
269+
?LOG_DEBUG(#{
270+
slogan => "Error fetching docs, falling back to src.",
271+
module => M,
272+
error => {C, E},
273+
st => ST
274+
}),
275+
{error, not_available}
276+
end.
277+
278+
-spec get_doc(module()) -> {ok, docs_v1()} | {error, not_available}.
279+
get_doc(Module) when is_atom(Module) ->
280+
%% This will error if module isn't loaded
281+
try code:get_doc(Module) of
282+
{ok, DocChunk} ->
283+
{ok, DocChunk};
284+
{error, _} ->
285+
%% If the module isn't loaded, we try
286+
%% to find the doc chunks from any .beam files
287+
%% matching the module name.
288+
Beams = find_beams(Module),
289+
get_doc(Beams, Module)
290+
catch
291+
C:E:ST ->
292+
%% code:get_doc/1 fails for escriptized modules, so fall back
293+
%% reading docs from source. See #751 for details
294+
?LOG_INFO(#{
295+
slogan => "Error fetching docs, falling back to src.",
296+
module => Module,
297+
error => {C, E},
298+
st => ST
299+
}),
300+
{error, not_available}
301+
end.
302+
303+
-spec get_doc([file:filename()], module()) ->
304+
{ok, docs_v1()} | {error, not_available}.
305+
get_doc([], _Module) ->
306+
{error, not_available};
307+
get_doc([Beam | T], Module) ->
308+
case beam_lib:chunks(Beam, ["Docs"]) of
309+
{ok, {Module, [{"Docs", Bin}]}} ->
310+
{ok, binary_to_term(Bin)};
311+
_ ->
312+
get_doc(T, Module)
313+
end.
314+
315+
-spec find_beams(module()) -> [file:filename()].
316+
find_beams(Module) ->
317+
%% Look for matching .beam files under the project root
318+
RootUri = els_config:get(root_uri),
319+
Root = binary_to_list(els_uri:path(RootUri)),
320+
Beams0 = filelib:wildcard(
321+
filename:join([Root, "**", atom_to_list(Module) ++ ".beam"])
322+
),
323+
%% Sort the beams, to ensure we try the newest beam first
324+
TimeBeams = [{filelib:last_modified(Beam), Beam} || Beam <- Beams0],
325+
{_, Beams} = lists:unzip(lists:reverse(lists:sort(TimeBeams))),
326+
Beams.
327+
328+
-spec render_doc(function | type, module(), atom(), arity(), docs_v1()) ->
329+
{ok, string()} | {error, not_available}.
330+
render_doc(Type, M, F, A, DocChunk) ->
331+
Render =
332+
case Type of
333+
function ->
334+
render;
335+
type ->
336+
render_type
337+
end,
338+
case els_eep48_docs:Render(M, F, A, DocChunk) of
339+
{error, _R0} ->
340+
case els_eep48_docs:Render(M, F, DocChunk) of
341+
{error, _R1} ->
342+
{error, not_available};
343+
Docs ->
344+
{ok, els_utils:to_list(Docs)}
345+
end;
346+
Docs ->
347+
{ok, els_utils:to_list(Docs)}
348+
end.
349+
258350
%% This function first tries to read the doc chunk from the .beam file
259351
%% and if that fails it attempts to find the .chunk file.
260352
-spec get_doc_chunk(M :: module()) -> {ok, term()} | error.

apps/els_lsp/src/els_eep48_docs.erl

+42-21
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ render(Module, Function, #docs_v1{docs = Docs} = D, Config) when
157157
Docs
158158
),
159159
D,
160+
Module,
160161
Config
161162
);
162163
render(_Module, Function, Arity, #docs_v1{} = D) ->
@@ -183,6 +184,7 @@ render(Module, Function, Arity, #docs_v1{docs = Docs} = D, Config) when
183184
Docs
184185
),
185186
D,
187+
Module,
186188
Config
187189
).
188190

@@ -210,7 +212,7 @@ render_type(Module, Type, D = #docs_v1{}) ->
210212
Arity :: arity(),
211213
Docs :: docs_v1(),
212214
Res :: unicode:chardata() | {error, type_missing}.
213-
render_type(_Module, Type, #docs_v1{docs = Docs} = D, Config) ->
215+
render_type(Module, Type, #docs_v1{docs = Docs} = D, Config) ->
214216
render_typecb_docs(
215217
lists:filter(
216218
fun
@@ -222,6 +224,7 @@ render_type(_Module, Type, #docs_v1{docs = Docs} = D, Config) ->
222224
Docs
223225
),
224226
D,
227+
Module,
225228
Config
226229
);
227230
render_type(_Module, Type, Arity, #docs_v1{} = D) ->
@@ -234,7 +237,7 @@ render_type(_Module, Type, Arity, #docs_v1{} = D) ->
234237
Docs :: docs_v1(),
235238
Config :: config(),
236239
Res :: unicode:chardata() | {error, type_missing}.
237-
render_type(_Module, Type, Arity, #docs_v1{docs = Docs} = D, Config) ->
240+
render_type(Module, Type, Arity, #docs_v1{docs = Docs} = D, Config) ->
238241
render_typecb_docs(
239242
lists:filter(
240243
fun
@@ -246,6 +249,7 @@ render_type(_Module, Type, Arity, #docs_v1{docs = Docs} = D, Config) ->
246249
Docs
247250
),
248251
D,
252+
Module,
249253
Config
250254
).
251255

@@ -275,7 +279,7 @@ render_callback(_Module, Callback, #docs_v1{} = D) ->
275279
Res :: unicode:chardata() | {error, callback_missing}.
276280
render_callback(_Module, Callback, Arity, #docs_v1{} = D) ->
277281
render_callback(_Module, Callback, Arity, D, #{});
278-
render_callback(_Module, Callback, #docs_v1{docs = Docs} = D, Config) ->
282+
render_callback(Module, Callback, #docs_v1{docs = Docs} = D, Config) ->
279283
render_typecb_docs(
280284
lists:filter(
281285
fun
@@ -287,6 +291,7 @@ render_callback(_Module, Callback, #docs_v1{docs = Docs} = D, Config) ->
287291
Docs
288292
),
289293
D,
294+
Module,
290295
Config
291296
).
292297

@@ -297,7 +302,7 @@ render_callback(_Module, Callback, #docs_v1{docs = Docs} = D, Config) ->
297302
Docs :: docs_v1(),
298303
Config :: config(),
299304
Res :: unicode:chardata() | {error, callback_missing}.
300-
render_callback(_Module, Callback, Arity, #docs_v1{docs = Docs} = D, Config) ->
305+
render_callback(Module, Callback, Arity, #docs_v1{docs = Docs} = D, Config) ->
301306
render_typecb_docs(
302307
lists:filter(
303308
fun
@@ -309,6 +314,7 @@ render_callback(_Module, Callback, Arity, #docs_v1{docs = Docs} = D, Config) ->
309314
Docs
310315
),
311316
D,
317+
Module,
312318
Config
313319
).
314320

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

355361
%%% Functions for rendering reference documentation
356-
-spec render_function([chunk_entry()], #docs_v1{}, map()) ->
362+
-spec render_function([chunk_entry()], #docs_v1{}, atom(), map()) ->
357363
unicode:chardata() | {'error', 'function_missing'}.
358-
render_function([], _D, _Config) ->
364+
render_function([], _D, _Module, _Config) ->
359365
{error, function_missing};
360-
render_function(FDocs, #docs_v1{docs = Docs} = D, Config) ->
366+
render_function(FDocs, #docs_v1{docs = Docs} = D, Module, Config) ->
361367
Grouping =
362368
lists:foldl(
363369
fun
@@ -375,7 +381,7 @@ render_function(FDocs, #docs_v1{docs = Docs} = D, Config) ->
375381
fun({Group, Members}) ->
376382
lists:map(
377383
fun(Member = {_, _, _, Doc, _}) ->
378-
Sig = render_signature(Member),
384+
Sig = render_signature(Member, Module),
379385
LocalDoc =
380386
if
381387
Doc =:= #{} ->
@@ -399,8 +405,8 @@ render_function(FDocs, #docs_v1{docs = Docs} = D, Config) ->
399405
).
400406

401407
%% Render the signature of either function, type, or anything else really.
402-
-spec render_signature(chunk_entry()) -> chunk_elements().
403-
render_signature({{_Type, _F, _A}, _Anno, _Sigs, _Docs, #{signature := Specs} = Meta}) ->
408+
-spec render_signature(chunk_entry(), module()) -> chunk_elements() | els_poi:poi().
409+
render_signature({{_Type, _F, _A}, _Anno, _Sigs, _Docs, #{signature := Specs} = Meta}, _Module) ->
404410
lists:flatmap(
405411
fun(ASTSpec) ->
406412
PPSpec = erl_pp:attribute(ASTSpec, [{encoding, utf8}]),
@@ -424,8 +430,13 @@ render_signature({{_Type, _F, _A}, _Anno, _Sigs, _Docs, #{signature := Specs} =
424430
end,
425431
Specs
426432
);
427-
render_signature({{_Type, _F, _A}, _Anno, Sigs, _Docs, Meta}) ->
428-
[{pre, [], Sigs}, {hr, [], []} | render_meta(Meta)].
433+
render_signature({{_Type, F, A}, _Anno, Sigs, _Docs, Meta}, Module) ->
434+
case els_dt_signatures:lookup({Module, F, A}) of
435+
{ok, [#{spec := <<"-spec ", Spec/binary>>}]} ->
436+
[{pre, [], Spec}, {hr, [], []} | render_meta(Meta)];
437+
{ok, _} ->
438+
[{pre, [], Sigs}, {hr, [], []} | render_meta(Meta)]
439+
end.
429440

430441
-spec trim_spec(unicode:chardata()) -> unicode:chardata().
431442
trim_spec(Spec) ->
@@ -499,7 +510,7 @@ render_headers_and_docs(Headers, DocContents, #config{} = Config) ->
499510
render_docs(DocContents, 0, Config)
500511
].
501512

502-
-spec render_typecb_docs([TypeCB] | TypeCB, #config{}) ->
513+
-spec render_typecb_docs([TypeCB] | TypeCB, module(), #config{}) ->
503514
unicode:chardata() | {'error', 'type_missing'}
504515
when
505516
TypeCB :: {
@@ -508,16 +519,20 @@ when
508519
Sig :: [binary()],
509520
none | hidden | #{binary() => chunk_elements()}
510521
}.
511-
render_typecb_docs([], _C) ->
522+
render_typecb_docs([], _Module, _C) ->
512523
{error, type_missing};
513-
render_typecb_docs(TypeCBs, #config{} = C) when is_list(TypeCBs) ->
514-
[render_typecb_docs(TypeCB, C) || TypeCB <- TypeCBs];
515-
render_typecb_docs({F, _, _Sig, Docs, _Meta} = TypeCB, #config{docs = D} = C) ->
516-
render_headers_and_docs(render_signature(TypeCB), get_local_doc(F, Docs, D), C).
517-
-spec render_typecb_docs(chunk_elements(), #docs_v1{}, _) ->
524+
render_typecb_docs(TypeCBs, Module, #config{} = C) when is_list(TypeCBs) ->
525+
[render_typecb_docs(TypeCB, Module, C) || TypeCB <- TypeCBs];
526+
render_typecb_docs({F, _, _Sig, Docs, _Meta} = TypeCB, Module, #config{docs = D} = C) ->
527+
render_headers_and_docs(
528+
render_signature(TypeCB, Module),
529+
get_local_doc(F, Docs, D),
530+
C
531+
).
532+
-spec render_typecb_docs(chunk_elements(), #docs_v1{}, module(), _) ->
518533
unicode:chardata() | {'error', 'type_missing'}.
519-
render_typecb_docs(Docs, D, Config) ->
520-
render_typecb_docs(Docs, init_config(D, Config)).
534+
render_typecb_docs(Docs, D, Module, Config) ->
535+
render_typecb_docs(Docs, Module, init_config(D, Config)).
521536

522537
%%% General rendering functions
523538
-spec render_docs([chunk_element()], #config{}) -> unicode:chardata().
@@ -540,6 +555,12 @@ init_config(D, _Config) ->
540555
#config{}
541556
) ->
542557
{unicode:chardata(), non_neg_integer()}.
558+
render_docs(Str, State, Pos, Ind, D) when
559+
is_list(Str),
560+
is_integer(hd(Str))
561+
->
562+
%% This is a string, convert it to binary.
563+
render_docs([unicode:characters_to_binary(Str)], State, Pos, Ind, D);
543564
render_docs(Elems, State, Pos, Ind, D) when is_list(Elems) ->
544565
lists:mapfoldl(
545566
fun(Elem, P) ->

0 commit comments

Comments
 (0)