diff --git a/base/docs/Docs.jl b/base/docs/Docs.jl index 473070114b0a2a..02356f3914bd26 100644 --- a/base/docs/Docs.jl +++ b/base/docs/Docs.jl @@ -312,7 +312,7 @@ astname(@nospecialize(other), ismacro::Bool) = other macroname(s::Symbol) = Symbol('@', s) macroname(x::Expr) = Expr(x.head, x.args[1], macroname(x.args[end].value)) -isfield(@nospecialize x) = isexpr(x, :.) && +isfield(@nospecialize x) = isexpr(x, :.) && length(x.args) == 2 && (isa(x.args[1], Symbol) || isfield(x.args[1])) && (isa(x.args[2], QuoteNode) || isexpr(x.args[2], :quote)) diff --git a/stdlib/REPL/src/docview.jl b/stdlib/REPL/src/docview.jl index 6b6b720bf0ee3f..a483765a2bd6ba 100644 --- a/stdlib/REPL/src/docview.jl +++ b/stdlib/REPL/src/docview.jl @@ -25,17 +25,8 @@ helpmode(line::AbstractString) = helpmode(stdout, line) const extended_help_on = Ref{Any}(nothing) -function _helpmode(io::IO, line::AbstractString) +function _helpmode_parse(line::AbstractString) line = strip(line) - ternary_operator_help = (line == "?" || line == "?:") - if startswith(line, '?') && !ternary_operator_help - line = line[2:end] - extended_help_on[] = line - brief = false - else - extended_help_on[] = nothing - brief = true - end # interpret anything starting with # or #= as asking for help on comments if startswith(line, "#") if startswith(line, "#=") @@ -45,26 +36,52 @@ function _helpmode(io::IO, line::AbstractString) end end x = Meta.parse(line, raise = false, depwarn = false) - assym = Symbol(line) - expr = - if haskey(keywords, Symbol(line)) || Base.isoperator(assym) || isexpr(x, :error) || - isexpr(x, :invalid) || isexpr(x, :incomplete) - # Docs for keywords must be treated separately since trying to parse a single - # keyword such as `function` would throw a parse error due to the missing `end`. - assym - elseif isexpr(x, (:using, :import)) - (x::Expr).head - else - # Retrieving docs for macros requires us to make a distinction between the text - # `@macroname` and `@macroname()`. These both parse the same, but are used by - # the docsystem to return different results. The first returns all documentation - # for `@macroname`, while the second returns *only* the docs for the 0-arg - # definition if it exists. - (isexpr(x, :macrocall, 1) && !endswith(line, "()")) ? quot(x) : x + if Meta.isexpr(x, :error) + # handle operators like += + asinfix = Meta.parse("x $line x", raise = false, depwarn = false) + if asinfix isa Expr && length(asinfix.args) == 2 && asinfix.args[1] == asinfix.args[2] == :x + x = asinfix.head end + end + if Meta.isexpr(x, :.) && length(x.args) == 1 && x.args[1] isa Symbol + # handle broadcast operators + x = Symbol(string(x.head) * string(x.args[1])) + end + assym = Symbol(line) + if haskey(keywords, assym) || isexpr(x, :error) || isexpr(x, :invalid) || isexpr(x, :incomplete) + # Docs for keywords must be treated separately since trying to parse a single + # keyword such as `function` would throw a parse error due to the missing `end`. + assym + elseif isexpr(x, (:using, :import)) + (x::Expr).head + else + # Retrieving docs for macros requires us to make a distinction between the text + # `@macroname` and `@macroname()`. These both parse the same, but are used by + # the docsystem to return different results. The first returns all documentation + # for `@macroname`, while the second returns *only* the docs for the 0-arg + # definition if it exists. + (isexpr(x, :macrocall, 1) && !endswith(line, "()")) ? quot(x) : x + end +end + +function _helpmode(io::IO, line::AbstractString) + line = strip(line) + ternary_operator_help = (line == "?" || line == "?:") + if startswith(line, '?') && !ternary_operator_help + line = line[2:end] + extended_help_on[] = line + brief = false + else + extended_help_on[] = nothing + brief = true + end + expr = _helpmode_parse(line) # 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 $brief) + quote + $REPL.repl_latex($io, $line) + $REPL.@repl $io $expr $brief + end end _helpmode(line::AbstractString) = _helpmode(stdout, line) @@ -221,7 +238,7 @@ function lookup_doc(ex) str = string(ex) isdotted = startswith(str, ".") if endswith(str, "=") && Base.operator_precedence(ex) == Base.prec_assignment && ex !== :(:=) - op = str[1:end-1] + op = first(rsplit(str, c->true, limit=2)) eq = isdotted ? ".=" : "=" return Markdown.parse("`x $op= y` is a synonym for `x $eq x $op y`") elseif isdotted && ex !== :(..) @@ -402,7 +419,7 @@ function symbol_latex(s::String) return get(symbols_latex, s, "") end -function repl_latex(io::IO, s::String) +function repl_latex(io::IO, s::AbstractString) # decompose NFC-normalized identifier to match tab-completion input s = normalize(s, :NFD) latex = symbol_latex(s) @@ -450,23 +467,14 @@ function repl_latex(io::IO, s::String) println(io, '\n') end end -repl_latex(s::String) = repl_latex(stdout, s) - -function normalize_symbol(s::Symbol) - # parse to apply string->normsymbol - normalized = Meta.parse(string(s), raise = false, depwarn = false) - normalized isa Symbol ? normalized : s -end +repl_latex(s::AbstractString) = repl_latex(stdout, s) macro repl(ex, brief::Bool=false) repl(ex; brief=brief) end macro repl(io, ex, brief) repl(io, ex; brief=brief) end function repl(io::IO, s::Symbol; brief::Bool=true) - str_orig = string(s) - s = normalize_symbol(s) str = string(s) quote - repl_latex($io, $str_orig) repl_search($io, $str) $(if !isdefined(Main, s) && !haskey(keywords, s) && !Base.isoperator(s) :(repl_corrections($io, $str)) diff --git a/stdlib/REPL/test/docview.jl b/stdlib/REPL/test/docview.jl index 50b8aa7f8f4bfd..58fccd15534d21 100644 --- a/stdlib/REPL/test/docview.jl +++ b/stdlib/REPL/test/docview.jl @@ -32,11 +32,13 @@ import Markdown @test all(duplicates .∈ Ref(keys(REPLCompletions.symbols_latex_canonical))) end -@testset "unicode normalization" begin +@testset "unicode operators" begin @test contains(let buf = IOBuffer() Core.eval(Main, REPL.helpmode(buf, "\u2212")) String(take!(buf)) end, "\nsearch: - ") + + @test REPL.lookup_doc(:⊻=) == Markdown.parse("`x ⊻= y` is a synonym for `x = x ⊻ y`") end @testset "quoting in doc search" begin diff --git a/stdlib/REPL/test/repl.jl b/stdlib/REPL/test/repl.jl index 8c4ee75850fbff..caea8222a094cd 100644 --- a/stdlib/REPL/test/repl.jl +++ b/stdlib/REPL/test/repl.jl @@ -1100,8 +1100,29 @@ for (line, expr) in Pair[ "r\"...\"" => Expr(:macrocall, Symbol("@r_str"), LineNumberNode(1, :none), "..."), "using Foo" => :using, "import Foo" => :import, + "# comment" => Symbol("#"), + "#= foo\n=#" => Symbol("#="), + "=#" => Symbol("=#"), + "+" => :+, + "+=" => :+=, + ".+" => :.+, + ".+=" => :.+=, + "⊻" => :⊻, + "⊻=" => :⊻=, + ".⊻" => :.⊻, + ".⊻=" => :.⊻=, + "\u2212" => :-, + "\u2212=" => :-=, + ".\u2212" => :.-, + ".\u2212=" => :.-=, + "1+2" => :(1 + 2), + "1\u22122" => :(1 - 2), + "2e-2" => 2e-2, + "2e\u22122" => 2e-2, + "\ub5" => Symbol("\u03bc"), # µ (U+00B5 micro) -> μ (U+03BC greek small letter mu) + "\ub7" => Symbol("\u22c5"), # \cdotp -> \cdot ] - @test REPL._helpmode(line).args[4] == expr + @test REPL._helpmode_parse(line) == expr @test help_result(line) isa Union{Markdown.MD,Nothing} end