diff --git a/NEWS.md b/NEWS.md index f29ba016966e1..50b260e9d3d3c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -12,6 +12,14 @@ Language changes where it used to be incorrectly allowed. This is because `NTuple` refers only to homogeneous tuples (this meaning has not changed) ([#34272]). +* In docstrings, a level-1 markdown header "Extended help" is now + interpreted as a marker dividing "brief help" from "extended help." + The REPL help mode only shows the brief help (the content before the + "Extended help" header) by default; prepend the expression with '?' + (in addition to the one that enters the help mode) to see the full + docstring. ([#25903]) + + Multi-threading changes ----------------------- diff --git a/doc/src/manual/documentation.md b/doc/src/manual/documentation.md index e191e89ef7b6e..a15e852b054a0 100644 --- a/doc/src/manual/documentation.md +++ b/doc/src/manual/documentation.md @@ -197,6 +197,10 @@ As in the example above, we recommend following some simple conventions when wri rather than users, explaining e.g. which functions should be overridden and which functions automatically use appropriate fallbacks. Such details are best kept separate from the main description of the function's behavior. +5. For long docstrings, consider splitting the documentation with an + `# Extended help` header. The typical help-mode will show only the + material above the header; you can access the full help by adding a '?' + at the beginning of the expression (i.e., "??foo" rather than "?foo"). ## Accessing Documentation diff --git a/stdlib/REPL/src/docview.jl b/stdlib/REPL/src/docview.jl index a96e42317b065..ffbb86a1aa4a2 100644 --- a/stdlib/REPL/src/docview.jl +++ b/stdlib/REPL/src/docview.jl @@ -19,8 +19,18 @@ using InteractiveUtils: subtypes helpmode(io::IO, line::AbstractString) = :($REPL.insert_hlines($io, $(REPL._helpmode(io, line)))) helpmode(line::AbstractString) = helpmode(stdout, line) +const extended_help_on = Ref{Any}(nothing) + function _helpmode(io::IO, line::AbstractString) line = strip(line) + if startswith(line, '?') + line = line[2:end] + extended_help_on[] = line + brief = false + else + extended_help_on[] = nothing + brief = true + end x = Meta.parse(line, raise = false, depwarn = false) expr = if haskey(keywords, Symbol(line)) || isexpr(x, :error) || isexpr(x, :invalid) @@ -37,7 +47,7 @@ function _helpmode(io::IO, line::AbstractString) end # the following must call repl(io, expr) via the @repl macro # so that the resulting expressions are evaluated in the Base.Docs namespace - :($REPL.@repl $io $expr) + :($REPL.@repl $io $expr $brief) end _helpmode(line::AbstractString) = _helpmode(stdout, line) @@ -73,6 +83,48 @@ function parsedoc(d::DocStr) d.object end +## Trimming long help ("# Extended help") + +struct Message # For direct messages to the terminal + msg # AbstractString + fmt # keywords to `printstyled` +end +Message(msg) = Message(msg, ()) + +function Markdown.term(io::IO, msg::Message, columns) + printstyled(io, msg.msg; msg.fmt...) +end + +function trimdocs(md::Markdown.MD, brief::Bool) + brief || return md + md, trimmed = _trimdocs(md, brief) + if trimmed + line = extended_help_on[] + line = isa(line, AbstractString) ? line : "" + push!(md.content, Message("Extended help is available with `??$line`", (color=Base.info_color(), bold=true))) + end + return md +end + +function _trimdocs(md::Markdown.MD, brief::Bool) + content, trimmed = [], false + for c in md.content + if isa(c, Markdown.Header{1}) && isa(c.text, AbstractArray) && + lowercase(c.text[1]) ∈ ("extended help", + "extended documentation", + "extended docs") + trimmed = true + break + end + c, trm = _trimdocs(c, brief) + trimmed |= trm + push!(content, c) + end + return Markdown.MD(content, md.meta), trimmed +end + +_trimdocs(md, brief::Bool) = md, false + """ Docs.doc(binding, sig) @@ -273,10 +325,10 @@ function repl_latex(io::IO, s::String) end repl_latex(s::String) = repl_latex(stdout, s) -macro repl(ex) repl(ex) end -macro repl(io, ex) repl(io, ex) end +macro repl(ex, brief=false) repl(ex; brief=brief) end +macro repl(io, ex, brief) repl(io, ex; brief=brief) end -function repl(io::IO, s::Symbol) +function repl(io::IO, s::Symbol; brief::Bool=true) str = string(s) quote repl_latex($io, $str) @@ -284,18 +336,18 @@ function repl(io::IO, s::Symbol) $(if !isdefined(Main, s) && !haskey(keywords, s) :(repl_corrections($io, $str)) end) - $(_repl(s)) + $(_repl(s, brief)) end end isregex(x) = isexpr(x, :macrocall, 3) && x.args[1] === Symbol("@r_str") && !isempty(x.args[3]) -repl(io::IO, ex::Expr) = isregex(ex) ? :(apropos($io, $ex)) : _repl(ex) -repl(io::IO, str::AbstractString) = :(apropos($io, $str)) -repl(io::IO, other) = esc(:(@doc $other)) +repl(io::IO, ex::Expr; brief::Bool=true) = isregex(ex) ? :(apropos($io, $ex)) : _repl(ex, brief) +repl(io::IO, str::AbstractString; brief::Bool=true) = :(apropos($io, $str)) +repl(io::IO, other; brief::Bool=true) = esc(:(@doc $other)) #repl(io::IO, other) = lookup_doc(other) # TODO -repl(x) = repl(stdout, x) +repl(x; brief=true) = repl(stdout, x; brief=brief) -function _repl(x) +function _repl(x, brief=true) if isexpr(x, :call) # determine the types of the values kwargs = nothing @@ -349,7 +401,7 @@ function _repl(x) end #docs = lookup_doc(x) # TODO docs = esc(:(@doc $x)) - if isfield(x) + docs = if isfield(x) quote if isa($(esc(x.args[1])), DataType) fielddoc($(esc(x.args[1])), $(esc(x.args[2]))) @@ -360,6 +412,7 @@ function _repl(x) else docs end + :(REPL.trimdocs($docs, $brief)) end """ diff --git a/stdlib/REPL/test/repl.jl b/stdlib/REPL/test/repl.jl index b4c08dc821317..a2b06eaab701f 100644 --- a/stdlib/REPL/test/repl.jl +++ b/stdlib/REPL/test/repl.jl @@ -1040,6 +1040,49 @@ for line in ["′", "abstract", "type", "|=", ".="] sprint(show, Base.eval(REPL._helpmode(IOBuffer(), line))::Union{Markdown.MD,Nothing})) end +# Issue #25930 + +# Brief and extended docs (issue #25930) +let text = + """ + brief_extended() + + Short docs + + # Extended help + + Long docs + """, + md = Markdown.parse(text) + @test md == REPL.trimdocs(md, false) + @test !isa(md.content[end], REPL.Message) + mdbrief = REPL.trimdocs(md, true) + @test length(mdbrief.content) == 3 + @test isa(mdbrief.content[1], Markdown.Code) + @test isa(mdbrief.content[2], Markdown.Paragraph) + @test isa(mdbrief.content[3], REPL.Message) + @test occursin("??", mdbrief.content[3].msg) +end + +module BriefExtended +""" + f() + +Short docs + +# Extended help + +Long docs +""" +f() = nothing +end # module BriefExtended +buf = IOBuffer() +md = Base.eval(REPL._helpmode(buf, "$(@__MODULE__).BriefExtended.f")) +@test length(md.content) == 2 && isa(md.content[2], REPL.Message) +buf = IOBuffer() +md = Base.eval(REPL._helpmode(buf, "?$(@__MODULE__).BriefExtended.f")) +@test length(md.content) == 1 && length(md.content[1].content[1].content) == 4 + # PR #27562 fake_repl() do stdin_write, stdout_read, repl repltask = @async begin diff --git a/test/docs.jl b/test/docs.jl index 25d674e9ca082..510152f15be44 100644 --- a/test/docs.jl +++ b/test/docs.jl @@ -1011,28 +1011,42 @@ dynamic_test.x = "test 2" @test @doc(dynamic_test) == "test 2 Union{}" @test @doc(dynamic_test(::String)) == "test 2 Tuple{String}" -let dt1 = _repl(:(dynamic_test(1.0))) +# For testing purposes, strip off the `trimdocs(expr)` wrapper +function striptrimdocs(expr) + if Meta.isexpr(expr, :call) + fex = expr.args[1] + if Meta.isexpr(fex, :.) && fex.args[1] == :REPL + fmex = fex.args[2] + if isa(fmex, QuoteNode) && fmex.value == :trimdocs + expr = expr.args[2] + end + end + end + return expr +end + +let dt1 = striptrimdocs(_repl(:(dynamic_test(1.0)))) @test dt1 isa Expr @test dt1.args[1] isa Expr @test dt1.args[1].head === :macrocall @test dt1.args[1].args[1] == Symbol("@doc") @test dt1.args[1].args[3] == :(dynamic_test(::typeof(1.0))) end -let dt2 = _repl(:(dynamic_test(::String))) +let dt2 = striptrimdocs(_repl(:(dynamic_test(::String)))) @test dt2 isa Expr @test dt2.args[1] isa Expr @test dt2.args[1].head === :macrocall @test dt2.args[1].args[1] == Symbol("@doc") @test dt2.args[1].args[3] == :(dynamic_test(::String)) end -let dt3 = _repl(:(dynamic_test(a))) +let dt3 = striptrimdocs(_repl(:(dynamic_test(a)))) @test dt3 isa Expr @test dt3.args[1] isa Expr @test dt3.args[1].head === :macrocall @test dt3.args[1].args[1] == Symbol("@doc") @test dt3.args[1].args[3].args[2].head == :(::) # can't test equality due to line numbers end -let dt4 = _repl(:(dynamic_test(1.0,u=2.0))) +let dt4 = striptrimdocs(_repl(:(dynamic_test(1.0,u=2.0)))) @test dt4 isa Expr @test dt4.args[1] isa Expr @test dt4.args[1].head === :macrocall