# This file is a part of Julia. License is MIT: https://julialang.org/license # Text / HTML objects import Base: print, show, ==, hash export HTML, @html_str export HTML, Text, apropos """ `HTML(s)`: Create an object that renders `s` as html. HTML("
foo
") You can also use a stream for large amounts of data: HTML() do io println(io, "
foo
") end """ mutable struct HTML{T} content::T end function HTML(xs...) HTML() do io for x in xs show(io, MIME"text/html"(), x) end end end show(io::IO, ::MIME"text/html", h::HTML) = print(io, h.content) show(io::IO, ::MIME"text/html", h::HTML{<:Function}) = h.content(io) """ @html_str -> Docs.HTML Create an `HTML` object from a literal string. """ macro html_str(s) :(HTML($s)) end function catdoc(xs::HTML...) HTML() do io for x in xs show(io, MIME"text/html"(), x) end end end export Text, @text_str """ `Text(s)`: Create an object that renders `s` as plain text. Text("foo") You can also use a stream for large amounts of data: Text() do io println(io, "foo") end """ mutable struct Text{T} content::T end print(io::IO, t::Text) = print(io, t.content) print(io::IO, t::Text{<:Function}) = t.content(io) show(io::IO, t::Text) = print(io, t) ==(t1::T, t2::T) where {T<:Union{HTML,Text}} = t1.content == t2.content hash(t::T, h::UInt) where {T<:Union{HTML,Text}} = hash(T, hash(t.content, h)) """ @text_str -> Docs.Text Create a `Text` object from a literal string. """ macro text_str(s) :(Text($s)) end function catdoc(xs::Text...) Text() do io for x in xs show(io, MIME"text/plain"(), x) end end end # REPL help function helpmode(io::IO, line::AbstractString) line = strip(line) expr = if haskey(keywords, Symbol(line)) # 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`. Symbol(line) else x = Base.syntax_deprecation_warnings(false) do parse(line, raise = false) end # 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 # the following must call repl(io, expr) via the @repl macro # so that the resulting expressions are evaluated in the Base.Docs namespace :(Base.Docs.@repl $io $expr) end helpmode(line::AbstractString) = helpmode(STDOUT, line) function repl_search(io::IO, s) pre = "search:" print(io, pre) printmatches(io, s, completions(s), cols = displaysize(io)[2] - length(pre)) println(io, "\n") end repl_search(s) = repl_search(STDOUT, s) function repl_corrections(io::IO, s) print(io, "Couldn't find ") Markdown.with_output_format(:cyan, io) do io println(io, s) end print_correction(io, s) end repl_corrections(s) = repl_corrections(STDOUT, s) # inverse of latex_symbols Dict, lazily created as needed const symbols_latex = Dict{String,String}() function symbol_latex(s::String) if isempty(symbols_latex) for (k,v) in Base.REPLCompletions.latex_symbols symbols_latex[v] = k end end return get(symbols_latex, s, "") end function repl_latex(io::IO, s::String) latex = symbol_latex(s) if !isempty(latex) print(io, "\"") Markdown.with_output_format(:cyan, io) do io print(io, s) end print(io, "\" can be typed by ") Markdown.with_output_format(:cyan, io) do io print(io, latex, "") end println(io, '\n') elseif any(c -> haskey(symbols_latex, string(c)), s) print(io, "\"") Markdown.with_output_format(:cyan, io) do io print(io, s) end print(io, "\" can be typed by ") Markdown.with_output_format(:cyan, io) do io for c in s cstr = string(c) if haskey(symbols_latex, cstr) print(io, symbols_latex[cstr], "") else print(io, c) end end end println(io, '\n') end end repl_latex(s::String) = repl_latex(STDOUT, s) macro repl(ex) repl(ex) end macro repl(io, ex) repl(io, ex) end function repl(io::IO, s::Symbol) str = string(s) quote repl_latex($io, $str) repl_search($io, $str) ($(isdefined(s) || haskey(keywords, s))) || repl_corrections($io, $str) $(_repl(s)) end end isregex(x) = isexpr(x, :macrocall, 2) && x.args[1] === Symbol("@r_str") && !isempty(x.args[2]) repl(io::IO, ex::Expr) = isregex(ex) ? :(apropos($io, $ex)) : _repl(ex) repl(io::IO, str::AbstractString) = :(apropos($io, $str)) repl(io::IO, other) = :(@doc $(esc(other))) repl(x) = repl(STDOUT, x) function _repl(x) if (isexpr(x, :call) && !any(isexpr(x, :(::)) for x in x.args)) x.args[2:end] = [:(::typeof($arg)) for arg in x.args[2:end]] end docs = :(@doc $(esc(x))) if isfield(x) quote if isa($(esc(x.args[1])), DataType) fielddoc($(esc(x.args[1])), $(esc(x.args[2]))) else $docs end end else docs end end # Search & Rescue # Utilities for correcting user mistakes and (eventually) # doing full documentation searches from the repl. # Fuzzy Search Algorithm function matchinds(needle, haystack; acronym = false) chars = collect(needle) is = Int[] lastc = '\0' for (i, char) in enumerate(haystack) isempty(chars) && break while chars[1] == ' ' shift!(chars) end # skip spaces if lowercase(char) == lowercase(chars[1]) && (!acronym || !isalpha(lastc)) push!(is, i) shift!(chars) end lastc = char end return is end longer(x, y) = length(x) ≥ length(y) ? (x, true) : (y, false) bestmatch(needle, haystack) = longer(matchinds(needle, haystack, acronym = true), matchinds(needle, haystack)) avgdistance(xs) = isempty(xs) ? 0 : (xs[end] - xs[1] - length(xs)+1)/length(xs) function fuzzyscore(needle, haystack) score = 0. is, acro = bestmatch(needle, haystack) score += (acro?2:1)*length(is) # Matched characters score -= 2(length(needle)-length(is)) # Missing characters !acro && (score -= avgdistance(is)/10) # Contiguous !isempty(is) && (score -= mean(is)/100) # Closer to beginning return score end function fuzzysort(search, candidates) scores = map(cand -> (fuzzyscore(search, cand), -levenshtein(search, cand)), candidates) candidates[sortperm(scores)] |> reverse end # Levenshtein Distance function levenshtein(s1, s2) a, b = collect(s1), collect(s2) m = length(a) n = length(b) d = Matrix{Int}(m+1, n+1) d[1:m+1, 1] = 0:m d[1, 1:n+1] = 0:n for i = 1:m, j = 1:n d[i+1,j+1] = min(d[i , j+1] + 1, d[i+1, j ] + 1, d[i , j ] + (a[i] != b[j])) end return d[m+1, n+1] end function levsort(search, candidates) scores = map(cand -> (levenshtein(search, cand), -fuzzyscore(search, cand)), candidates) candidates = candidates[sortperm(scores)] i = 0 for i = 1:length(candidates) levenshtein(search, candidates[i]) > 3 && break end return candidates[1:i] end # Result printing function printmatch(io::IO, word, match) is, _ = bestmatch(word, match) Markdown.with_output_format(:fade, io) do io for (i, char) = enumerate(match) if i in is Markdown.with_output_format(print, :bold, io, char) else print(io, char) end end end end printmatch(args...) = printfuzzy(STDOUT, args...) function printmatches(io::IO, word, matches; cols = displaysize(io)[2]) total = 0 for match in matches total + length(match) + 1 > cols && break fuzzyscore(word, match) < 0 && break print(io, " ") printmatch(io, word, match) total += length(match) + 1 end end printmatches(args...; cols = displaysize(STDOUT)[2]) = printmatches(STDOUT, args..., cols = cols) function print_joined_cols(io::IO, ss, delim = "", last = delim; cols = displaysize(io)[2]) i = 0 total = 0 for i = 1:length(ss) total += length(ss[i]) total + max(i-2,0)*length(delim) + (i>1?1:0)*length(last) > cols && (i-=1; break) end join(io, ss[1:i], delim, last) end print_joined_cols(args...; cols = displaysize(STDOUT)[2]) = print_joined_cols(STDOUT, args...; cols=cols) function print_correction(io, word) cors = levsort(word, accessible(current_module())) pre = "Perhaps you meant " print(io, pre) print_joined_cols(io, cors, ", ", " or "; cols = displaysize(io)[2] - length(pre)) println(io) return end print_correction(word) = print_correction(STDOUT, word) # Completion data const builtins = ["abstract type", "baremodule", "begin", "break", "catch", "ccall", "const", "continue", "do", "else", "elseif", "end", "export", "finally", "for", "function", "global", "if", "import", "importall", "let", "local", "macro", "module", "mutable struct", "primitive type", "quote", "return", "struct", "try", "using", "while"] moduleusings(mod) = ccall(:jl_module_usings, Any, (Any,), mod) filtervalid(names) = filter(x->!ismatch(r"#", x), map(string, names)) accessible(mod::Module) = [filter!(s->Base.isdeprecated(mod, s), names(mod, true, true)); map(names, moduleusings(mod))...; builtins] |> unique |> filtervalid completions(name) = fuzzysort(name, accessible(current_module())) completions(name::Symbol) = completions(string(name)) # Searching and apropos # Docsearch simply returns true or false if an object contains the given needle docsearch(haystack::AbstractString, needle) = !isempty(search(haystack, needle)) docsearch(haystack::Symbol, needle) = docsearch(string(haystack), needle) docsearch(::Void, needle) = false function docsearch(haystack::Array, needle) for elt in haystack docsearch(elt, needle) && return true end false end function docsearch(haystack, needle) Base.warn_once("unable to search documentation of type $(typeof(haystack))") false end ## Searching specific documentation objects function docsearch(haystack::MultiDoc, needle) for v in values(haystack.docs) docsearch(v, needle) && return true end false end function docsearch(haystack::DocStr, needle) docsearch(parsedoc(haystack), needle) && return true if haskey(haystack.data, :fields) for doc in values(haystack.data[:fields]) docsearch(doc, needle) && return true end end false end ## Markdown search simply strips all markup and searches plain text version docsearch(haystack::Markdown.MD, needle) = docsearch(stripmd(haystack.content), needle) """ stripmd(x) Strip all Markdown markup from x, leaving the result in plain text. Used internally by apropos to make docstrings containing more than one markdown element searchable. """ stripmd(x::ANY) = string(x) # for random objects interpolated into the docstring stripmd(x::AbstractString) = x # base case stripmd(x::Void) = " " stripmd(x::Vector) = string(map(stripmd, x)...) stripmd(x::Markdown.BlockQuote) = "$(stripmd(x.content))" stripmd(x::Markdown.Admonition) = "$(stripmd(x.content))" stripmd(x::Markdown.Bold) = "$(stripmd(x.text))" stripmd(x::Markdown.Code) = "$(stripmd(x.code))" stripmd(x::Markdown.Header) = stripmd(x.text) stripmd(x::Markdown.HorizontalRule) = " " stripmd(x::Markdown.Image) = "$(stripmd(x.alt)) $(x.url)" stripmd(x::Markdown.Italic) = "$(stripmd(x.text))" stripmd(x::Markdown.LaTeX) = "$(x.formula)" stripmd(x::Markdown.LineBreak) = " " stripmd(x::Markdown.Link) = "$(stripmd(x.text)) $(x.url)" stripmd(x::Markdown.List) = join(map(stripmd, x.items), " ") stripmd(x::Markdown.MD) = join(map(stripmd, x.content), " ") stripmd(x::Markdown.Paragraph) = stripmd(x.content) stripmd(x::Markdown.Footnote) = "$(stripmd(x.id)) $(stripmd(x.text))" stripmd(x::Markdown.Table) = join([join(map(stripmd, r), " ") for r in x.rows], " ") # Apropos searches through all available documentation for some string or regex """ apropos(string) Search through all documentation for a string, ignoring case. """ apropos(string) = apropos(STDOUT, string) apropos(io::IO, string) = apropos(io, Regex("\\Q$string", "i")) function apropos(io::IO, needle::Regex) for mod in modules # Module doc might be in README.md instead of the META dict docsearch(doc(mod), needle) && println(io, mod) for (k, v) in meta(mod) docsearch(v, needle) && println(io, k) end end end