Skip to content

Commit 9790a8d

Browse files
authored
Support brief and extended docs (closes #25930) (#34226)
1 parent 0234e00 commit 9790a8d

File tree

5 files changed

+137
-15
lines changed

5 files changed

+137
-15
lines changed

NEWS.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ Language changes
1212
where it used to be incorrectly allowed. This is because `NTuple` refers only to homogeneous
1313
tuples (this meaning has not changed) ([#34272]).
1414

15+
* In docstrings, a level-1 markdown header "Extended help" is now
16+
interpreted as a marker dividing "brief help" from "extended help."
17+
The REPL help mode only shows the brief help (the content before the
18+
"Extended help" header) by default; prepend the expression with '?'
19+
(in addition to the one that enters the help mode) to see the full
20+
docstring. ([#25903])
21+
22+
1523
Multi-threading changes
1624
-----------------------
1725

doc/src/manual/documentation.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,10 @@ As in the example above, we recommend following some simple conventions when wri
197197
rather than users, explaining e.g. which functions should be overridden and which
198198
functions automatically use appropriate fallbacks. Such details are best kept separate
199199
from the main description of the function's behavior.
200+
5. For long docstrings, consider splitting the documentation with an
201+
`# Extended help` header. The typical help-mode will show only the
202+
material above the header; you can access the full help by adding a '?'
203+
at the beginning of the expression (i.e., "??foo" rather than "?foo").
200204

201205
## Accessing Documentation
202206

stdlib/REPL/src/docview.jl

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,18 @@ using InteractiveUtils: subtypes
1919
helpmode(io::IO, line::AbstractString) = :($REPL.insert_hlines($io, $(REPL._helpmode(io, line))))
2020
helpmode(line::AbstractString) = helpmode(stdout, line)
2121

22+
const extended_help_on = Ref{Any}(nothing)
23+
2224
function _helpmode(io::IO, line::AbstractString)
2325
line = strip(line)
26+
if startswith(line, '?')
27+
line = line[2:end]
28+
extended_help_on[] = line
29+
brief = false
30+
else
31+
extended_help_on[] = nothing
32+
brief = true
33+
end
2434
x = Meta.parse(line, raise = false, depwarn = false)
2535
expr =
2636
if haskey(keywords, Symbol(line)) || isexpr(x, :error) || isexpr(x, :invalid)
@@ -37,7 +47,7 @@ function _helpmode(io::IO, line::AbstractString)
3747
end
3848
# the following must call repl(io, expr) via the @repl macro
3949
# so that the resulting expressions are evaluated in the Base.Docs namespace
40-
:($REPL.@repl $io $expr)
50+
:($REPL.@repl $io $expr $brief)
4151
end
4252
_helpmode(line::AbstractString) = _helpmode(stdout, line)
4353

@@ -73,6 +83,48 @@ function parsedoc(d::DocStr)
7383
d.object
7484
end
7585

86+
## Trimming long help ("# Extended help")
87+
88+
struct Message # For direct messages to the terminal
89+
msg # AbstractString
90+
fmt # keywords to `printstyled`
91+
end
92+
Message(msg) = Message(msg, ())
93+
94+
function Markdown.term(io::IO, msg::Message, columns)
95+
printstyled(io, msg.msg; msg.fmt...)
96+
end
97+
98+
function trimdocs(md::Markdown.MD, brief::Bool)
99+
brief || return md
100+
md, trimmed = _trimdocs(md, brief)
101+
if trimmed
102+
line = extended_help_on[]
103+
line = isa(line, AbstractString) ? line : ""
104+
push!(md.content, Message("Extended help is available with `??$line`", (color=Base.info_color(), bold=true)))
105+
end
106+
return md
107+
end
108+
109+
function _trimdocs(md::Markdown.MD, brief::Bool)
110+
content, trimmed = [], false
111+
for c in md.content
112+
if isa(c, Markdown.Header{1}) && isa(c.text, AbstractArray) &&
113+
lowercase(c.text[1]) ("extended help",
114+
"extended documentation",
115+
"extended docs")
116+
trimmed = true
117+
break
118+
end
119+
c, trm = _trimdocs(c, brief)
120+
trimmed |= trm
121+
push!(content, c)
122+
end
123+
return Markdown.MD(content, md.meta), trimmed
124+
end
125+
126+
_trimdocs(md, brief::Bool) = md, false
127+
76128
"""
77129
Docs.doc(binding, sig)
78130
@@ -273,29 +325,29 @@ function repl_latex(io::IO, s::String)
273325
end
274326
repl_latex(s::String) = repl_latex(stdout, s)
275327

276-
macro repl(ex) repl(ex) end
277-
macro repl(io, ex) repl(io, ex) end
328+
macro repl(ex, brief=false) repl(ex; brief=brief) end
329+
macro repl(io, ex, brief) repl(io, ex; brief=brief) end
278330

279-
function repl(io::IO, s::Symbol)
331+
function repl(io::IO, s::Symbol; brief::Bool=true)
280332
str = string(s)
281333
quote
282334
repl_latex($io, $str)
283335
repl_search($io, $str)
284336
$(if !isdefined(Main, s) && !haskey(keywords, s)
285337
:(repl_corrections($io, $str))
286338
end)
287-
$(_repl(s))
339+
$(_repl(s, brief))
288340
end
289341
end
290342
isregex(x) = isexpr(x, :macrocall, 3) && x.args[1] === Symbol("@r_str") && !isempty(x.args[3])
291-
repl(io::IO, ex::Expr) = isregex(ex) ? :(apropos($io, $ex)) : _repl(ex)
292-
repl(io::IO, str::AbstractString) = :(apropos($io, $str))
293-
repl(io::IO, other) = esc(:(@doc $other))
343+
repl(io::IO, ex::Expr; brief::Bool=true) = isregex(ex) ? :(apropos($io, $ex)) : _repl(ex, brief)
344+
repl(io::IO, str::AbstractString; brief::Bool=true) = :(apropos($io, $str))
345+
repl(io::IO, other; brief::Bool=true) = esc(:(@doc $other))
294346
#repl(io::IO, other) = lookup_doc(other) # TODO
295347

296-
repl(x) = repl(stdout, x)
348+
repl(x; brief=true) = repl(stdout, x; brief=brief)
297349

298-
function _repl(x)
350+
function _repl(x, brief=true)
299351
if isexpr(x, :call)
300352
# determine the types of the values
301353
kwargs = nothing
@@ -349,7 +401,7 @@ function _repl(x)
349401
end
350402
#docs = lookup_doc(x) # TODO
351403
docs = esc(:(@doc $x))
352-
if isfield(x)
404+
docs = if isfield(x)
353405
quote
354406
if isa($(esc(x.args[1])), DataType)
355407
fielddoc($(esc(x.args[1])), $(esc(x.args[2])))
@@ -360,6 +412,7 @@ function _repl(x)
360412
else
361413
docs
362414
end
415+
:(REPL.trimdocs($docs, $brief))
363416
end
364417

365418
"""

stdlib/REPL/test/repl.jl

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,6 +1040,49 @@ for line in ["′", "abstract", "type", "|=", ".="]
10401040
sprint(show, Base.eval(REPL._helpmode(IOBuffer(), line))::Union{Markdown.MD,Nothing}))
10411041
end
10421042

1043+
# Issue #25930
1044+
1045+
# Brief and extended docs (issue #25930)
1046+
let text =
1047+
"""
1048+
brief_extended()
1049+
1050+
Short docs
1051+
1052+
# Extended help
1053+
1054+
Long docs
1055+
""",
1056+
md = Markdown.parse(text)
1057+
@test md == REPL.trimdocs(md, false)
1058+
@test !isa(md.content[end], REPL.Message)
1059+
mdbrief = REPL.trimdocs(md, true)
1060+
@test length(mdbrief.content) == 3
1061+
@test isa(mdbrief.content[1], Markdown.Code)
1062+
@test isa(mdbrief.content[2], Markdown.Paragraph)
1063+
@test isa(mdbrief.content[3], REPL.Message)
1064+
@test occursin("??", mdbrief.content[3].msg)
1065+
end
1066+
1067+
module BriefExtended
1068+
"""
1069+
f()
1070+
1071+
Short docs
1072+
1073+
# Extended help
1074+
1075+
Long docs
1076+
"""
1077+
f() = nothing
1078+
end # module BriefExtended
1079+
buf = IOBuffer()
1080+
md = Base.eval(REPL._helpmode(buf, "$(@__MODULE__).BriefExtended.f"))
1081+
@test length(md.content) == 2 && isa(md.content[2], REPL.Message)
1082+
buf = IOBuffer()
1083+
md = Base.eval(REPL._helpmode(buf, "?$(@__MODULE__).BriefExtended.f"))
1084+
@test length(md.content) == 1 && length(md.content[1].content[1].content) == 4
1085+
10431086
# PR #27562
10441087
fake_repl() do stdin_write, stdout_read, repl
10451088
repltask = @async begin

test/docs.jl

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1011,28 +1011,42 @@ dynamic_test.x = "test 2"
10111011
@test @doc(dynamic_test) == "test 2 Union{}"
10121012
@test @doc(dynamic_test(::String)) == "test 2 Tuple{String}"
10131013

1014-
let dt1 = _repl(:(dynamic_test(1.0)))
1014+
# For testing purposes, strip off the `trimdocs(expr)` wrapper
1015+
function striptrimdocs(expr)
1016+
if Meta.isexpr(expr, :call)
1017+
fex = expr.args[1]
1018+
if Meta.isexpr(fex, :.) && fex.args[1] == :REPL
1019+
fmex = fex.args[2]
1020+
if isa(fmex, QuoteNode) && fmex.value == :trimdocs
1021+
expr = expr.args[2]
1022+
end
1023+
end
1024+
end
1025+
return expr
1026+
end
1027+
1028+
let dt1 = striptrimdocs(_repl(:(dynamic_test(1.0))))
10151029
@test dt1 isa Expr
10161030
@test dt1.args[1] isa Expr
10171031
@test dt1.args[1].head === :macrocall
10181032
@test dt1.args[1].args[1] == Symbol("@doc")
10191033
@test dt1.args[1].args[3] == :(dynamic_test(::typeof(1.0)))
10201034
end
1021-
let dt2 = _repl(:(dynamic_test(::String)))
1035+
let dt2 = striptrimdocs(_repl(:(dynamic_test(::String))))
10221036
@test dt2 isa Expr
10231037
@test dt2.args[1] isa Expr
10241038
@test dt2.args[1].head === :macrocall
10251039
@test dt2.args[1].args[1] == Symbol("@doc")
10261040
@test dt2.args[1].args[3] == :(dynamic_test(::String))
10271041
end
1028-
let dt3 = _repl(:(dynamic_test(a)))
1042+
let dt3 = striptrimdocs(_repl(:(dynamic_test(a))))
10291043
@test dt3 isa Expr
10301044
@test dt3.args[1] isa Expr
10311045
@test dt3.args[1].head === :macrocall
10321046
@test dt3.args[1].args[1] == Symbol("@doc")
10331047
@test dt3.args[1].args[3].args[2].head == :(::) # can't test equality due to line numbers
10341048
end
1035-
let dt4 = _repl(:(dynamic_test(1.0,u=2.0)))
1049+
let dt4 = striptrimdocs(_repl(:(dynamic_test(1.0,u=2.0))))
10361050
@test dt4 isa Expr
10371051
@test dt4.args[1] isa Expr
10381052
@test dt4.args[1].head === :macrocall

0 commit comments

Comments
 (0)