• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

JuliaLang / julia / #37909

20 Sep 2024 04:59AM UTC coverage: 87.735% (+0.06%) from 87.675%
#37909

push

local

web-flow
move the test case added in #50174 to test/core.jl (#55811)

Also renames the name of the test function to avoid name collision.

78529 of 89507 relevant lines covered (87.74%)

16522189.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.98
/stdlib/REPL/src/docview.jl
1
# This file is a part of Julia. License is MIT: https://julialang.org/license
2

3
## Code for searching and viewing documentation
4

5
using Markdown
6

7
using Base.Docs: catdoc, modules, DocStr, Binding, MultiDoc, keywords, isfield, namify, bindingexpr,
8
    defined, resolve, getdoc, meta, aliasof, signature
9

10
import Base.Docs: doc, formatdoc, parsedoc, apropos
11

12
using Base: with_output_color, mapany, isdeprecated, isexported
13

14
using Base.Filesystem: _readdirx
15

16
using InteractiveUtils: subtypes
17

18
using Unicode: normalize
19

20
## Help mode ##
21

22
# This is split into helpmode and _helpmode to easier unittest _helpmode
23
function helpmode(io::IO, line::AbstractString, mod::Module=Main)
23✔
24
    internal_accesses = Set{Pair{Module,Symbol}}()
27✔
25
    quote
23✔
26
        docs = $Markdown.insert_hlines($(REPL._helpmode(io, line, mod, internal_accesses)))
27
        $REPL.insert_internal_warning(docs, $internal_accesses)
28
    end
29
end
30
helpmode(line::AbstractString, mod::Module=Main) = helpmode(stdout, line, mod)
2✔
31

32
# A hack to make the line entered at the REPL available at trimdocs without
33
# passing the string through the entire mechanism.
34
const extended_help_on = Ref{Any}(nothing)
35

36
function _helpmode(io::IO, line::AbstractString, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing)
85✔
37
    line = strip(line)
111✔
38
    ternary_operator_help = (line == "?" || line == "?:")
134✔
39
    if startswith(line, '?') && !ternary_operator_help
130✔
40
        line = line[2:end]
1✔
41
        extended_help_on[] = nothing
1✔
42
        brief = false
1✔
43
    else
44
        extended_help_on[] = line
66✔
45
        brief = true
66✔
46
    end
47
    # interpret anything starting with # or #= as asking for help on comments
48
    if startswith(line, "#")
67✔
49
        if startswith(line, "#=")
×
50
            line = "#="
×
51
        else
52
            line = "#"
×
53
        end
54
    end
55
    x = Meta.parse(line, raise = false, depwarn = false)
67✔
56
    assym = Symbol(line)
67✔
57
    expr =
184✔
58
        if haskey(keywords, assym) || Base.isoperator(assym) || isexpr(x, :error) ||
59
            isexpr(x, :invalid) || isexpr(x, :incomplete)
60
            # Docs for keywords must be treated separately since trying to parse a single
61
            # keyword such as `function` would throw a parse error due to the missing `end`.
62
            assym
13✔
63
        elseif isexpr(x, (:using, :import))
54✔
64
            (x::Expr).head
4✔
65
        else
66
            # Retrieving docs for macros requires us to make a distinction between the text
67
            # `@macroname` and `@macroname()`. These both parse the same, but are used by
68
            # the docsystem to return different results. The first returns all documentation
69
            # for `@macroname`, while the second returns *only* the docs for the 0-arg
70
            # definition if it exists.
71
            (isexpr(x, :macrocall, 1) && !endswith(line, "()")) ? quot(x) : x
121✔
72
        end
73
    # the following must call repl(io, expr) via the @repl macro
74
    # so that the resulting expressions are evaluated in the Base.Docs namespace
75
    :($REPL.@repl $io $expr $brief $mod $internal_accesses)
67✔
76
end
77
_helpmode(line::AbstractString, mod::Module=Main) = _helpmode(stdout, line, mod)
28✔
78

79
function formatdoc(d::DocStr)
6,299✔
80
    buffer = IOBuffer()
6,299✔
81
    for part in d.text
12,598✔
82
        formatdoc(buffer, d, part)
7,743✔
83
    end
9,187✔
84
    md = Markdown.MD(Any[Markdown.parse(seekstart(buffer))])
12,598✔
85
    assume_julia_code!(md)
6,299✔
86
end
87
@noinline formatdoc(buffer, d, part) = print(buffer, part)
7,741✔
88

89
function parsedoc(d::DocStr)
12,628✔
90
    if d.object === nothing
12,628✔
91
        md = formatdoc(d)
6,299✔
92
        md.meta[:module] = d.data[:module]
6,299✔
93
        md.meta[:path]   = d.data[:path]
6,299✔
94
        d.object = md
6,299✔
95
    end
96
    d.object
12,628✔
97
end
98

99
"""
100
    assume_julia_code!(doc::Markdown.MD) -> doc
101

102
Assume that code blocks with no language specified are Julia code.
103
"""
104
function assume_julia_code!(doc::Markdown.MD)
6,299✔
105
    assume_julia_code!(doc.content)
12,598✔
106
    doc
×
107
end
108

109
function assume_julia_code!(blocks::Vector)
12,598✔
110
    for (i, block) in enumerate(blocks)
12,598✔
111
        if block isa Markdown.Code && block.language == ""
30,638✔
112
            blocks[i] = Markdown.Code("julia", block.code)
6,208✔
113
        elseif block isa Vector || block isa Markdown.MD
48,860✔
114
            assume_julia_code!(block)
6,299✔
115
        end
116
    end
30,638✔
117
    blocks
×
118
end
119

120
## Trimming long help ("# Extended help")
121

122
struct Message  # For direct messages to the terminal
123
    msg    # AbstractString
2✔
124
    fmt    # keywords to `printstyled`
125
end
126
Message(msg) = Message(msg, ())
×
127

128
function Markdown.term(io::IO, msg::Message, columns)
×
129
    printstyled(io, msg.msg; msg.fmt...)
×
130
end
131

132
trimdocs(doc, brief::Bool) = doc
2✔
133

134
function trimdocs(md::Markdown.MD, brief::Bool)
59✔
135
    brief || return md
68✔
136
    md, trimmed = _trimdocs(md, brief)
50✔
137
    if trimmed
50✔
138
        line = extended_help_on[]
2✔
139
        line = isa(line, AbstractString) ? line : ""
2✔
140
        push!(md.content, Message("Extended help is available with `??$line`", (color=Base.info_color(), bold=true)))
2✔
141
    end
142
    return md
50✔
143
end
144

145
function _trimdocs(md::Markdown.MD, brief::Bool)
90✔
146
    content, trimmed = [], false
90✔
147
    for c in md.content
90✔
148
        if isa(c, Markdown.Header{1}) && isa(c.text, AbstractArray) && !isempty(c.text)
219✔
149
            item = c.text[1]
20✔
150
            if isa(item, AbstractString) &&
20✔
151
                lowercase(item) ∈ ("extended help",
152
                                   "extended documentation",
153
                                   "extended docs")
154
                trimmed = true
×
155
                break
2✔
156
            end
157
        end
158
        c, trm = _trimdocs(c, brief)
217✔
159
        trimmed |= trm
217✔
160
        push!(content, c)
217✔
161
    end
217✔
162
    return Markdown.MD(content, md.meta), trimmed
90✔
163
end
164

165
_trimdocs(md, brief::Bool) = md, false
177✔
166

167

168
is_tuple(expr) = false
×
169
is_tuple(expr::Expr) = expr.head == :tuple
×
170

171
struct Logged{F}
172
    f::F
46✔
173
    mod::Module
174
    collection::Set{Pair{Module,Symbol}}
175
end
176
function (la::Logged)(m::Module, s::Symbol)
34✔
177
    m !== la.mod && Base.isdefined(m, s) && !Base.ispublic(m, s) && push!(la.collection, m => s)
34✔
178
    la.f(m, s)
34✔
179
end
180
(la::Logged)(args...) = la.f(args...)
×
181

182
function log_nonpublic_access(expr::Expr, mod::Module, internal_access::Set{Pair{Module,Symbol}})
217✔
183
    if expr.head === :. && length(expr.args) == 2 && !is_tuple(expr.args[2])
217✔
184
        Expr(:call, Logged(getproperty, mod, internal_access), log_nonpublic_access.(expr.args, (mod,), (internal_access,))...)
24✔
185
    elseif expr.head === :call && expr.args[1] === Base.Docs.Binding
193✔
186
        Expr(:call, Logged(Base.Docs.Binding, mod, internal_access), log_nonpublic_access.(expr.args[2:end], (mod,), (internal_access,))...)
22✔
187
    else
188
        Expr(expr.head, log_nonpublic_access.(expr.args, (mod,), (internal_access,))...)
171✔
189
    end
190
end
191
log_nonpublic_access(expr, ::Module, _) = expr
163✔
192

193
function insert_internal_warning(md::Markdown.MD, internal_access::Set{Pair{Module,Symbol}})
23✔
194
    if !isempty(internal_access)
23✔
195
        items = Any[Any[Markdown.Paragraph(Any[Markdown.Code("", s)])] for s in sort!(["$mod.$sym" for (mod, sym) in internal_access])]
6✔
196
        admonition = Markdown.Admonition("warning", "Warning", Any[
12✔
197
            Markdown.Paragraph(Any["The following bindings may be internal; they may change or be removed in future versions:"]),
198
            Markdown.List(items, -1, false)])
199
        pushfirst!(md.content, admonition)
6✔
200
    end
201
    md
×
202
end
203
function insert_internal_warning(other, internal_access::Set{Pair{Module,Symbol}})
×
204
    # We don't know how to insert an internal symbol warning into non-markdown
205
    # content, so we don't.
206
    other
×
207
end
208

209
function doc(binding::Binding, sig::Type = Union{})
567✔
210
    if defined(binding)
567✔
211
        result = getdoc(resolve(binding), sig)
461✔
212
        result === nothing || return result
17✔
213
    end
214
    results, groups = DocStr[], MultiDoc[]
470✔
215
    # Lookup `binding` and `sig` for matches in all modules of the docsystem.
216
    for mod in modules
470✔
217
        dict = meta(mod; autoinit=false)
79,108✔
218
        isnothing(dict) && continue
79,108✔
219
        if haskey(dict, binding)
39,554✔
220
            multidoc = dict[binding]
272✔
221
            push!(groups, multidoc)
272✔
222
            for msig in multidoc.order
272✔
223
                sig <: msig && push!(results, multidoc.docs[msig])
319✔
224
            end
319✔
225
        end
226
    end
39,554✔
227
    if isempty(groups)
470✔
228
        # When no `MultiDoc`s are found that match `binding` then we check whether `binding`
229
        # is an alias of some other `Binding`. When it is we then re-run `doc` with that
230
        # `Binding`, otherwise if it's not an alias then we generate a summary for the
231
        # `binding` and display that to the user instead.
232
        alias = aliasof(binding)
204✔
233
        alias == binding ? summarize(alias, sig) : doc(alias, sig)
204✔
234
    else
235
        # There was at least one match for `binding` while searching. If there weren't any
236
        # matches for `sig` then we concatenate *all* the docs from the matching `Binding`s.
237
        if isempty(results)
266✔
238
            for group in groups, each in group.order
13✔
239
                push!(results, group.docs[each])
16✔
240
            end
16✔
241
        end
242
        # Get parsed docs and concatenate them.
243
        md = catdoc(mapany(parsedoc, results)...)
283✔
244
        # Save metadata in the generated markdown.
245
        if isa(md, Markdown.MD)
266✔
246
            md.meta[:results] = results
263✔
247
            md.meta[:binding] = binding
263✔
248
            md.meta[:typesig] = sig
263✔
249
        end
250
        return md
266✔
251
    end
252
end
253

254
# Some additional convenience `doc` methods that take objects rather than `Binding`s.
255
doc(obj::UnionAll) = doc(Base.unwrap_unionall(obj))
2✔
256
doc(object, sig::Type = Union{}) = doc(aliasof(object, typeof(object)), sig)
679✔
257
doc(object, sig...)              = doc(object, Tuple{sig...})
×
258

259
function lookup_doc(ex)
152✔
260
    if isa(ex, Expr) && ex.head !== :(.) && Base.isoperator(ex.head)
161✔
261
        # handle syntactic operators, e.g. +=, ::, .=
262
        ex = ex.head
×
263
    end
264
    if haskey(keywords, ex)
194✔
265
        return parsedoc(keywords[ex])
6✔
266
    elseif Meta.isexpr(ex, :incomplete)
112✔
267
        return :($(Markdown.md"No documentation found."))
1✔
268
    elseif !isa(ex, Expr) && !isa(ex, Symbol)
111✔
269
        return :($(doc)($(typeof)($(esc(ex)))))
2✔
270
    end
271
    if isa(ex, Symbol) && Base.isoperator(ex)
171✔
272
        str = string(ex)
6✔
273
        isdotted = startswith(str, ".")
6✔
274
        if endswith(str, "=") && Base.operator_precedence(ex) == Base.prec_assignment && ex !== :(:=)
6✔
275
            op = chop(str)
2✔
276
            eq = isdotted ? ".=" : "="
2✔
277
            return Markdown.parse("`x $op= y` is a synonym for `x $eq x $op y`")
2✔
278
        elseif isdotted && ex !== :(..)
4✔
279
            op = str[2:end]
2✔
280
            if op in ("&&", "||")
2✔
281
                return Markdown.parse("`x $ex y` broadcasts the boolean operator `$op` to `x` and `y`. See [`broadcast`](@ref).")
×
282
            else
283
                return Markdown.parse("`x $ex y` is akin to `broadcast($op, x, y)`. See [`broadcast`](@ref).")
1✔
284
            end
285
        end
286
    end
287
    binding = esc(bindingexpr(namify(ex)))
169✔
288
    if isexpr(ex, :call) || isexpr(ex, :macrocall) || isexpr(ex, :where)
200✔
289
        sig = esc(signature(ex))
41✔
290
        :($(doc)($binding, $sig))
41✔
291
    else
292
        :($(doc)($binding))
99✔
293
    end
294
end
295

296
# Object Summaries.
297
# =================
298

299
function summarize(binding::Binding, sig)
205✔
300
    io = IOBuffer()
205✔
301
    if defined(binding)
205✔
302
        binding_res = resolve(binding)
192✔
303
        if !isa(binding_res, Module)
192✔
304
            if Base.ispublic(binding.mod, binding.var)
24✔
305
                println(io, "No documentation found for public symbol.\n")
5✔
306
            else
307
                println(io, "No documentation found for private symbol.\n")
19✔
308
            end
309
        end
310
        summarize(io, binding_res, binding)
192✔
311
    else
312
        println(io, "No documentation found.\n")
13✔
313
        quot = any(isspace, sprint(print, binding)) ? "'" : ""
13✔
314
        if Base.isbindingresolved(binding.mod, binding.var)
13✔
315
            println(io, "Binding ", quot, "`", binding, "`", quot, " exists, but has not been assigned a value.")
1✔
316
        else
317
            println(io, "Binding ", quot, "`", binding, "`", quot, " does not exist.")
12✔
318
        end
319
    end
320
    md = Markdown.parse(seekstart(io))
410✔
321
    # Save metadata in the generated markdown.
322
    md.meta[:results] = DocStr[]
205✔
323
    md.meta[:binding] = binding
205✔
324
    md.meta[:typesig] = sig
205✔
325
    return md
205✔
326
end
327

328
function summarize(io::IO, λ::Function, binding::Binding)
5✔
329
    kind = startswith(string(binding.var), '@') ? "macro" : "`Function`"
10✔
330
    println(io, "`", binding, "` is a ", kind, ".")
5✔
331
    println(io, "```\n", methods(λ), "\n```")
5✔
332
end
333

334
function summarize(io::IO, TT::Type, binding::Binding)
19✔
335
    println(io, "# Summary")
19✔
336
    T = Base.unwrap_unionall(TT)
19✔
337
    if T isa DataType
19✔
338
        println(io, "```")
16✔
339
        print(io,
27✔
340
            Base.isabstracttype(T) ? "abstract type " :
341
            Base.ismutabletype(T)  ? "mutable struct " :
342
            Base.isstructtype(T) ? "struct " :
343
            "primitive type ")
344
        supert = supertype(T)
16✔
345
        println(io, T)
16✔
346
        println(io, "```")
16✔
347
        if !Base.isabstracttype(T) && T.name !== Tuple.name && !isempty(fieldnames(T))
16✔
348
            println(io, "# Fields")
6✔
349
            println(io, "```")
6✔
350
            pad = maximum(length(string(f)) for f in fieldnames(T))
6✔
351
            for (f, t) in zip(fieldnames(T), fieldtypes(T))
6✔
352
                println(io, rpad(f, pad), " :: ", t)
11✔
353
            end
11✔
354
            println(io, "```")
6✔
355
        end
356
        subt = subtypes(TT)
16✔
357
        if !isempty(subt)
21✔
358
            println(io, "# Subtypes")
5✔
359
            println(io, "```")
5✔
360
            for t in subt
5✔
361
                println(io, Base.unwrap_unionall(t))
12✔
362
            end
12✔
363
            println(io, "```")
5✔
364
        end
365
        if supert != Any
16✔
366
            println(io, "# Supertype Hierarchy")
12✔
367
            println(io, "```")
12✔
368
            Base.show_supertypes(io, T)
12✔
369
            println(io)
12✔
370
            println(io, "```")
12✔
371
        end
372
    elseif T isa Union
3✔
373
        println(io, "`", binding, "` is of type `", typeof(TT), "`.\n")
3✔
374
        println(io, "# Union Composed of Types")
3✔
375
        for T1 in Base.uniontypes(T)
3✔
376
            println(io, " - `", Base.rewrap_unionall(T1, TT), "`")
10✔
377
        end
10✔
378
    else # unreachable?
379
        println(io, "`", binding, "` is of type `", typeof(TT), "`.\n")
×
380
    end
381
end
382

383
function find_readme(m::Module)::Union{String, Nothing}
168✔
384
    mpath = pathof(m)
168✔
385
    isnothing(mpath) && return nothing
176✔
386
    !isfile(mpath) && return nothing # modules in sysimage, where src files are omitted
8✔
387
    path = dirname(mpath)
8✔
388
    top_path = pkgdir(m)
8✔
389
    while true
16✔
390
        for entry in _readdirx(path; sort=true)
16✔
391
            isfile(entry) && (lowercase(entry.name) in ["readme.md", "readme"]) || continue
184✔
392
            return entry.path
8✔
393
        end
64✔
394
        path == top_path && break # go no further than pkgdir
16✔
395
        path = dirname(path) # work up through nested modules
8✔
396
    end
8✔
397
    return nothing
×
398
end
399
function summarize(io::IO, m::Module, binding::Binding; nlines::Int = 200)
336✔
400
    readme_path = find_readme(m)
168✔
401
    public = Base.ispublic(binding.mod, binding.var) ? "public" : "internal"
168✔
402
    if isnothing(readme_path)
176✔
403
        println(io, "No docstring or readme file found for $public module `$m`.\n")
160✔
404
    else
405
        println(io, "No docstring found for $public module `$m`.")
8✔
406
    end
407
    exports = filter!(!=(nameof(m)), names(m))
336✔
408
    if isempty(exports)
168✔
409
        println(io, "Module does not have any public names.")
51✔
410
    else
411
        println(io, "# Public names")
117✔
412
        print(io, "  `")
117✔
413
        join(io, exports, "`, `")
117✔
414
        println(io, "`\n")
117✔
415
    end
416
    if !isnothing(readme_path)
176✔
417
        readme_lines = readlines(readme_path)
8✔
418
        isempty(readme_lines) && return  # don't say we are going to print empty file
8✔
419
        println(io, "# Displaying contents of readme found at `$(readme_path)`")
8✔
420
        for line in first(readme_lines, nlines)
8✔
421
            println(io, line)
208✔
422
        end
208✔
423
        length(readme_lines) > nlines && println(io, "\n[output truncated to first $nlines lines]")
8✔
424
    end
425
end
426

427
function summarize(io::IO, @nospecialize(T), binding::Binding)
4✔
428
    T = typeof(T)
4✔
429
    println(io, "`", binding, "` is of type `", T, "`.\n")
4✔
430
    summarize(io, T, binding)
4✔
431
end
432

433
# repl search and completions for help
434

435
# This type is returned from `accessible` and denotes a binding that is accessible within
436
# some context. It differs from `Base.Docs.Binding`, which is also used by the REPL, in
437
# that it doesn't track the defining module for a symbol unless the symbol is public but
438
# not exported, i.e. it's accessible but requires qualification. Using this type rather
439
# than `Base.Docs.Binding` simplifies things considerably, partially because REPL searching
440
# is based on `String`s, which this type stores, but `Base.Docs.Binding` stores a module
441
# and symbol and does not have any notion of the context from which the binding is accessed.
442
struct AccessibleBinding
443
    source::Union{String,Nothing}
234,838✔
444
    name::String
445
end
446

447
function AccessibleBinding(mod::Module, name::Symbol)
56,186✔
448
    m = isexported(mod, name) ? nothing : String(nameof(mod))
59,512✔
449
    return AccessibleBinding(m, String(name))
56,186✔
450
end
451
AccessibleBinding(name::Symbol) = AccessibleBinding(nothing, String(name))
111,810✔
452

453
function Base.show(io::IO, b::AccessibleBinding)
454
    b.source === nothing || print(io, b.source, '.')
31,275✔
455
    print(io, b.name)
29,367✔
456
end
457

458
quote_spaces(x) = any(isspace, x) ? "'" * x * "'" : x
66,253✔
459
quote_spaces(x::AccessibleBinding) = AccessibleBinding(x.source, quote_spaces(x.name))
67,236✔
460

461
function repl_search(io::IO, s::Union{Symbol,String}, mod::Module)
25✔
462
    pre = "search:"
25✔
463
    print(io, pre)
25✔
464
    printmatches(io, s, map(quote_spaces, doc_completions(s, mod)), cols = _displaysize(io)[2] - length(pre))
25✔
465
    println(io, "\n")
25✔
466
end
467

468
# TODO: document where this is used
469
repl_search(s, mod::Module) = repl_search(stdout, s, mod)
×
470

471
function repl_corrections(io::IO, s, mod::Module)
7✔
472
    print(io, "Couldn't find ")
7✔
473
    quot = any(isspace, s) ? "'" : ""
7✔
474
    print(io, quot)
7✔
475
    printstyled(io, s, color=:cyan)
7✔
476
    print(io, quot, '\n')
7✔
477
    print_correction(io, s, mod)
7✔
478
end
479
repl_corrections(s) = repl_corrections(stdout, s)
×
480

481
# inverse of latex_symbols Dict, lazily created as needed
482
const symbols_latex = Dict{String,String}()
483
function symbol_latex(s::String)
53✔
484
    if isempty(symbols_latex)
53✔
485
        for (k,v) in Iterators.flatten((REPLCompletions.latex_symbols,
×
486
                                        REPLCompletions.emoji_symbols))
487
            symbols_latex[v] = k
×
488
        end
×
489

490
        # Overwrite with canonical mapping when a symbol has several completions (#39148)
491
        merge!(symbols_latex, REPLCompletions.symbols_latex_canonical)
×
492
    end
493

494
    return get(symbols_latex, s, "")
53✔
495
end
496
function repl_latex(io::IO, s0::String)
29✔
497
    # This has rampant `Core.Box` problems (#15276). Use the tricks of
498
    # https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-captured
499
    # We're changing some of the values so the `let` trick isn't applicable.
500
    s::String = s0
29✔
501
    latex::String = symbol_latex(s)
29✔
502
    if isempty(latex)
29✔
503
        # Decompose NFC-normalized identifier to match tab-completion
504
        # input if the first search came up empty.
505
        s = normalize(s, :NFD)
24✔
506
        latex = symbol_latex(s)
24✔
507
    end
508
    if !isempty(latex)
29✔
509
        print(io, "\"")
5✔
510
        printstyled(io, s, color=:cyan)
5✔
511
        print(io, "\" can be typed by ")
5✔
512
        printstyled(io, latex, "<tab>", color=:cyan)
5✔
513
        println(io, '\n')
5✔
514
    elseif any(c -> haskey(symbols_latex, string(c)), s)
120✔
515
        print(io, "\"")
3✔
516
        printstyled(io, s, color=:cyan)
3✔
517
        print(io, "\" can be typed by ")
3✔
518
        state::Char = '\0'
3✔
519
        with_output_color(:cyan, io) do io
3✔
520
            for c in s
5✔
521
                cstr = string(c)
13✔
522
                if haskey(symbols_latex, cstr)
13✔
523
                    latex = symbols_latex[cstr]
11✔
524
                    if length(latex) == 3 && latex[2] in ('^','_')
15✔
525
                        # coalesce runs of sub/superscripts
526
                        if state != latex[2]
14✔
527
                            '\0' != state && print(io, "<tab>")
3✔
528
                            print(io, latex[1:2])
6✔
529
                            state = latex[2]
6✔
530
                        end
531
                        print(io, latex[3])
14✔
532
                    else
533
                        if '\0' != state
4✔
534
                            print(io, "<tab>")
1✔
535
                            state = '\0'
1✔
536
                        end
537
                        print(io, latex, "<tab>")
4✔
538
                    end
539
                else
540
                    if '\0' != state
2✔
541
                        print(io, "<tab>")
×
542
                        state = '\0'
×
543
                    end
544
                    print(io, c)
2✔
545
                end
546
            end
13✔
547
            '\0' != state && print(io, "<tab>")
3✔
548
        end
549
        println(io, '\n')
3✔
550
    end
551
end
552
repl_latex(s::String) = repl_latex(stdout, s)
×
553

554
macro repl(ex, brief::Bool=false, mod::Module=Main) repl(ex; brief, mod) end
12✔
555
macro repl(io, ex, brief, mod, internal_accesses) repl(io, ex; brief, mod, internal_accesses) end
53✔
556

557
function repl(io::IO, s::Symbol; brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing)
50✔
558
    str = string(s)
25✔
559
    quote
32✔
560
        repl_latex($io, $str)
561
        repl_search($io, $str, $mod)
562
        $(if !isdefined(mod, s) && !Base.isbindingresolved(mod, s) && !haskey(keywords, s) && !Base.isoperator(s)
563
               # n.b. we call isdefined for the side-effect of resolving the binding, if possible
564
               :(repl_corrections($io, $str, $mod))
7✔
565
          end)
566
        $(_repl(s, brief, mod, internal_accesses))
567
    end
568
end
569
isregex(x) = isexpr(x, :macrocall, 3) && x.args[1] === Symbol("@r_str") && !isempty(x.args[3])
31✔
570

571
repl(io::IO, ex::Expr; brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing) = isregex(ex) ? :(apropos($io, $ex)) : _repl(ex, brief, mod, internal_accesses)
62✔
572
repl(io::IO, str::AbstractString; brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing) = :(apropos($io, $str))
2✔
573
repl(io::IO, other; brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing) = esc(:(@doc $other)) # TODO: track internal_accesses
4✔
574
#repl(io::IO, other) = lookup_doc(other) # TODO
575

576
repl(x; brief::Bool=true, mod::Module=Main) = repl(stdout, x; brief, mod)
12✔
577

578
function _repl(x, brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing)
63✔
579
    if isexpr(x, :call)
54✔
580
        x = x::Expr
6✔
581
        # determine the types of the values
582
        kwargs = nothing
6✔
583
        pargs = Any[]
6✔
584
        for arg in x.args[2:end]
6✔
585
            if isexpr(arg, :parameters)
9✔
586
                kwargs = mapany(arg.args) do kwarg
×
587
                    if kwarg isa Symbol
588
                        kwarg = :($kwarg::Any)
589
                    elseif isexpr(kwarg, :kw)
590
                        lhs = kwarg.args[1]
591
                        rhs = kwarg.args[2]
592
                        if lhs isa Symbol
593
                            if rhs isa Symbol
594
                                kwarg.args[1] = :($lhs::(@isdefined($rhs) ? typeof($rhs) : Any))
595
                            else
596
                                kwarg.args[1] = :($lhs::typeof($rhs))
597
                            end
598
                        end
599
                    end
600
                    kwarg
601
                end
602
            elseif isexpr(arg, :kw)
9✔
603
                if kwargs === nothing
1✔
604
                    kwargs = Any[]
1✔
605
                end
606
                lhs = arg.args[1]
1✔
607
                rhs = arg.args[2]
1✔
608
                if lhs isa Symbol
1✔
609
                    if rhs isa Symbol
1✔
610
                        arg.args[1] = :($lhs::(@isdefined($rhs) ? typeof($rhs) : Any))
×
611
                    else
612
                        arg.args[1] = :($lhs::typeof($rhs))
1✔
613
                    end
614
                end
615
                push!(kwargs, arg)
1✔
616
            else
617
                if arg isa Symbol
8✔
618
                    arg = :($arg::(@isdefined($arg) ? typeof($arg) : Any))
1✔
619
                elseif !isexpr(arg, :(::))
7✔
620
                    arg = :(::typeof($arg))
4✔
621
                end
622
                push!(pargs, arg)
8✔
623
            end
624
        end
9✔
625
        if kwargs === nothing
6✔
626
            x.args = Any[x.args[1], pargs...]
5✔
627
        else
628
            x.args = Any[x.args[1], Expr(:parameters, kwargs...), pargs...]
1✔
629
        end
630
    end
631
    #docs = lookup_doc(x) # TODO
632
    docs = esc(:(@doc $x))
59✔
633
    docs = if isfield(x)
50✔
634
        quote
21✔
635
            if isa($(esc(x.args[1])), DataType)
636
                fielddoc($(esc(x.args[1])), $(esc(x.args[2])))
637
            else
638
                $docs
639
            end
640
        end
641
    else
642
        docs
63✔
643
    end
644
    docs = log_nonpublic_access(macroexpand(mod, docs), mod, internal_accesses)
59✔
645
    :(REPL.trimdocs($docs, $brief))
59✔
646
end
647

648
"""
649
    fielddoc(binding, field)
650

651
Return documentation for a particular `field` of a type if it exists.
652
"""
653
function fielddoc(binding::Binding, field::Symbol)
5✔
654
    for mod in modules
5✔
655
        dict = meta(mod; autoinit=false)
770✔
656
        isnothing(dict) && continue
770✔
657
        if haskey(dict, binding)
385✔
658
            multidoc = dict[binding]
1✔
659
            if haskey(multidoc.docs, Union{})
1✔
660
                fields = multidoc.docs[Union{}].data[:fields]
1✔
661
                if haskey(fields, field)
1✔
662
                    doc = fields[field]
1✔
663
                    return isa(doc, Markdown.MD) ? doc : Markdown.parse(doc)
1✔
664
                end
665
            end
666
        end
667
    end
384✔
668
    fs = fieldnames(resolve(binding))
4✔
669
    fields = isempty(fs) ? "no fields" : (length(fs) == 1 ? "field " : "fields ") *
7✔
670
                                          join(("`$f`" for f in fs), ", ", ", and ")
671
    Markdown.parse("`$(resolve(binding))` has $fields.")
4✔
672
end
673

674
# As with the additional `doc` methods, this converts an object to a `Binding` first.
675
fielddoc(object, field::Symbol) = fielddoc(aliasof(object, typeof(object)), field)
5✔
676

677

678
# Search & Rescue
679
# Utilities for correcting user mistakes and (eventually)
680
# doing full documentation searches from the repl.
681

682
# Fuzzy Search Algorithm
683

684
function matchinds(needle, haystack; acronym::Bool = false)
510✔
685
    chars = collect(needle)
510✔
686
    is = Int[]
255✔
687
    lastc = '\0'
×
688
    for (i, char) in enumerate(haystack)
510✔
689
        while !isempty(chars) && isspace(first(chars))
2,225✔
690
            popfirst!(chars) # skip spaces
4✔
691
        end
4✔
692
        isempty(chars) && break
1,122✔
693
        if lowercase(char) == lowercase(chars[1]) &&
1,239✔
694
           (!acronym || !isletter(lastc))
695
            push!(is, i)
337✔
696
            popfirst!(chars)
337✔
697
        end
698
        lastc = char
×
699
    end
1,099✔
700
    return is
255✔
701
end
702

703
matchinds(needle, (; name)::AccessibleBinding; acronym::Bool=false) =
×
704
    matchinds(needle, name; acronym)
705

706
longer(x, y) = length(x) ≥ length(y) ? (x, true) : (y, false)
127✔
707

708
bestmatch(needle, haystack) =
127✔
709
    longer(matchinds(needle, haystack, acronym = true),
710
           matchinds(needle, haystack))
711

712
# Optimal string distance: Counts the minimum number of insertions, deletions,
713
# transpositions or substitutions to go from one string to the other.
714
function string_distance(a::AbstractString, lena::Integer, b::AbstractString, lenb::Integer)
107,317✔
715
    if lena > lenb
107,317✔
716
        a, b = b, a
×
717
        lena, lenb = lenb, lena
×
718
    end
719
    start = 0
107,317✔
720
    for (i, j) in zip(a, b)
213,200✔
721
        if a == b
106,054✔
722
            start += 1
228✔
723
        else
724
            break
×
725
        end
726
    end
399✔
727
    start == lena && return lenb - start
107,317✔
728
    vzero = collect(1:(lenb - start))
1,071,762✔
729
    vone = similar(vzero)
211,652✔
730
    prev_a, prev_b = first(a), first(b)
209,741✔
731
    current = 0
×
732
    for (i, ai) in enumerate(a)
211,652✔
733
        i > start || (prev_a = ai; continue)
339,945✔
734
        left = i - start - 1
339,945✔
735
        current = i - start
339,945✔
736
        transition_next = 0
×
737
        for (j, bj) in enumerate(b)
679,890✔
738
            j > start || (prev_b = bj; continue)
3,642,042✔
739
            # No need to look beyond window of lower right diagonal
740
            above = current
×
741
            this_transition = transition_next
×
742
            transition_next = vone[j - start]
3,642,042✔
743
            vone[j - start] = current = left
3,642,042✔
744
            left = vzero[j - start]
3,642,042✔
745
            if ai != bj
3,642,042✔
746
                # Minimum between substitution, deletion and insertion
747
                current = min(current + 1, above + 1, left + 1)
3,526,024✔
748
                if i > start + 1 && j > start + 1 && ai == prev_b && prev_a == bj
3,526,024✔
749
                    current = min(current, (this_transition += 1))
4,532✔
750
                end
751
            end
752
            vzero[j - start] = current
3,642,042✔
753
            prev_b = bj
×
754
        end
6,944,139✔
755
        prev_a = ai
339,945✔
756
    end
574,064✔
757
    current
105,826✔
758
end
759

760
function fuzzyscore(needle::AbstractString, haystack::AbstractString)
4✔
761
    lena, lenb = length(needle), length(haystack)
107,317✔
762
    1 - (string_distance(needle, lena, haystack, lenb) / max(lena, lenb))
107,317✔
763
end
764

765
function fuzzyscore(needle::AbstractString, haystack::AccessibleBinding)
107,313✔
766
    score = fuzzyscore(needle, haystack.name)
107,313✔
767
    haystack.source === nothing && return score
107,313✔
768
    # Apply a "penalty" of half an edit if the comparator binding is public but not
769
    # exported so that exported/local names that exactly match the search query are
770
    # listed first
771
    penalty = 1 / (2 * max(length(needle), length(haystack.name)))
3,230✔
772
    return max(score - penalty, 0)
3,230✔
773
end
774

775
function fuzzysort(search::String, candidates::Vector{AccessibleBinding})
45✔
776
    scores = map(cand -> fuzzyscore(search, cand), candidates)
93,159✔
777
    candidates[sortperm(scores)] |> reverse
45✔
778
end
779

780
# Levenshtein Distance
781

782
function levenshtein(s1, s2)
14,513✔
783
    a, b = collect(s1), collect(s2)
29,017✔
784
    m = length(a)
14,513✔
785
    n = length(b)
14,513✔
786
    d = Matrix{Int}(undef, m+1, n+1)
29,026✔
787

788
    d[1:m+1, 1] = 0:m
70,328✔
789
    d[1, 1:n+1] = 0:n
150,923✔
790

791
    for i = 1:m, j = 1:n
14,513✔
792
        d[i+1,j+1] = min(d[i  , j+1] + 1,
495,169✔
793
                         d[i+1, j  ] + 1,
794
                         d[i  , j  ] + (a[i] != b[j]))
795
    end
536,471✔
796

797
    return d[m+1, n+1]
14,513✔
798
end
799

800
function levsort(search::String, candidates::Vector{AccessibleBinding})
7✔
801
    scores = map(candidates) do cand
7✔
802
        (Float64(levenshtein(search, cand.name)), -fuzzyscore(search, cand))
14,049✔
803
    end
804
    candidates = candidates[sortperm(scores)]
7✔
805
    i = 0
7✔
806
    for outer i = 1:length(candidates)
7✔
807
        levenshtein(search, candidates[i].name) > 3 && break
464✔
808
    end
457✔
809
    return candidates[1:i]
7✔
810
end
811

812
# Result printing
813

814
function printmatch(io::IO, word, match)
127✔
815
    is, _ = bestmatch(word, match)
127✔
816
    for (i, char) = enumerate(match)
254✔
817
        if i in is
1,571✔
818
            printstyled(io, char, bold=true)
266✔
819
        else
820
            print(io, char)
305✔
821
        end
822
    end
571✔
823
end
824

825
function printmatch(io::IO, word, match::AccessibleBinding)
826
    match.source === nothing || print(io, match.source, '.')
133✔
827
    printmatch(io, word, match.name)
127✔
828
end
829

830
function matchlength(x::AccessibleBinding)
831
    n = length(x.name)
221✔
832
    if x.source !== nothing
221✔
833
        n += length(x.source) + 1  # the +1 is for the `.` separator
10✔
834
    end
835
    return n
221✔
836
end
837
matchlength(x) = length(x)
×
838

839
function printmatches(io::IO, word, matches; cols::Int = _displaysize(io)[2])
50✔
840
    total = 0
25✔
841
    for match in matches
25✔
842
        ml = matchlength(match)
152✔
843
        total + ml + 1 > cols && break
152✔
844
        fuzzyscore(word, match) < 0.5 && break
146✔
845
        print(io, " ")
127✔
846
        printmatch(io, word, match)
133✔
847
        total += ml + 1
127✔
848
    end
127✔
849
end
850

851
printmatches(args...; cols::Int = _displaysize(stdout)[2]) = printmatches(stdout, args..., cols = cols)
×
852

853
function print_joined_cols(io::IO, ss::Vector{AccessibleBinding}, delim = "", last = delim; cols::Int = _displaysize(io)[2])
14✔
854
    i = 0
7✔
855
    total = 0
7✔
856
    for outer i = 1:length(ss)
7✔
857
        total += matchlength(ss[i])
69✔
858
        total + max(i-2,0)*length(delim) + (i>1 ? 1 : 0)*length(last) > cols && (i-=1; break)
69✔
859
    end
64✔
860
    join(io, ss[1:i], delim, last)
7✔
861
end
862

863
print_joined_cols(args...; cols::Int = _displaysize(stdout)[2]) = print_joined_cols(stdout, args...; cols=cols)
×
864

865
function print_correction(io::IO, word::String, mod::Module)
7✔
866
    cors = map(quote_spaces, levsort(word, accessible(mod)))
7✔
867
    pre = "Perhaps you meant "
7✔
868
    print(io, pre)
7✔
869
    print_joined_cols(io, cors, ", ", " or "; cols = _displaysize(io)[2] - length(pre))
7✔
870
    println(io)
7✔
871
    return
7✔
872
end
873

874
# TODO: document where this is used
875
print_correction(word, mod::Module) = print_correction(stdout, word, mod)
×
876

877
# Completion data
878

879
moduleusings(mod) = ccall(:jl_module_usings, Any, (Any,), mod)
53✔
880

881
function accessible(mod::Module)
53✔
882
    bindings = Set(AccessibleBinding(s) for s in names(mod; all=true, imported=true)
53✔
883
                   if !isdeprecated(mod, s))
884
    for used in moduleusings(mod)
53✔
885
        union!(bindings, (AccessibleBinding(used, s) for s in names(used)
646✔
886
                          if !isdeprecated(used, s)))
887
    end
646✔
888
    union!(bindings, (AccessibleBinding(k) for k in keys(Base.Docs.keywords)))
53✔
889
    filter!(b -> !occursin('#', b.name), bindings)
219,311✔
890
    return collect(bindings)
53✔
891
end
892

893
function doc_completions(name, mod::Module=Main)
45✔
894
    res = fuzzysort(name, accessible(mod))
48✔
895

896
    # to insert an entry like `raw""` for `"@raw_str"` in `res`
897
    ms = map(c -> match(r"^@(.*?)_str$", c.name), res)
93,159✔
898
    idxs = findall(!isnothing, ms)
90✔
899

900
    # avoid messing up the order while inserting
901
    for i in reverse!(idxs)
45✔
902
        c = only((ms[i]::AbstractMatch).captures)
587✔
903
        insert!(res, i, AccessibleBinding(res[i].source, "$(c)\"\""))
587✔
904
    end
587✔
905
    res
×
906
end
907
doc_completions(name::Symbol) = doc_completions(string(name), mod)
×
908

909

910
# Searching and apropos
911

912
# Docsearch simply returns true or false if an object contains the given needle
913
docsearch(haystack::AbstractString, needle) = occursin(needle, haystack)
12,705✔
914
docsearch(haystack::Symbol, needle) = docsearch(string(haystack), needle)
×
915
docsearch(::Nothing, needle) = false
×
916
function docsearch(haystack::Array, needle)
×
917
    for elt in haystack
×
918
        docsearch(elt, needle) && return true
×
919
    end
×
920
    false
×
921
end
922
function docsearch(haystack, needle)
×
923
    @warn "Unable to search documentation of type $(typeof(haystack))" maxlog=1
×
924
    false
×
925
end
926

927
## Searching specific documentation objects
928
function docsearch(haystack::MultiDoc, needle)
11,086✔
929
    for v in values(haystack.docs)
22,172✔
930
        docsearch(v, needle) && return true
12,318✔
931
    end
9,003✔
932
    false
933
end
934

935
function docsearch(haystack::DocStr, needle)
12,318✔
936
    docsearch(parsedoc(haystack), needle) && return true
12,318✔
937
    if haskey(haystack.data, :fields)
18,006✔
938
        for doc in values(haystack.data[:fields])
792✔
939
            docsearch(doc, needle) && return true
54✔
940
        end
54✔
941
    end
942
    false
943
end
944

945
## doc search
946

947
## Markdown search simply strips all markup and searches plain text version
948
docsearch(haystack::Markdown.MD, needle) = docsearch(stripmd(haystack.content), needle)
12,649✔
949

950
"""
951
    stripmd(x)
952

953
Strip all Markdown markup from x, leaving the result in plain text. Used
954
internally by apropos to make docstrings containing more than one markdown
955
element searchable.
956
"""
957
stripmd(@nospecialize x) = string(x) # for random objects interpolated into the docstring
6✔
958
stripmd(x::AbstractString) = x  # base case
188,106✔
959
stripmd(x::Nothing) = " "
100✔
960
stripmd(x::Vector) = string(map(stripmd, x)...)
66,831✔
961

962
stripmd(x::Markdown.BlockQuote) = "$(stripmd(x.content))"
4✔
963
stripmd(x::Markdown.Admonition) = "$(stripmd(x.content))"
2,664✔
964
stripmd(x::Markdown.Bold) = "$(stripmd(x.text))"
74✔
965
stripmd(x::Markdown.Code) = "$(stripmd(x.code))"
86,544✔
966
stripmd(x::Markdown.Header) = stripmd(x.text)
4,777✔
967
stripmd(x::Markdown.HorizontalRule) = " "
136✔
968
stripmd(x::Markdown.Image) = "$(stripmd(x.alt)) $(x.url)"
32✔
969
stripmd(x::Markdown.Italic) = "$(stripmd(x.text))"
580✔
970
stripmd(x::Markdown.LaTeX) = "$(x.formula)"
821✔
971
stripmd(x::Markdown.LineBreak) = " "
76✔
972
stripmd(x::Markdown.Link) = "$(stripmd(x.text)) $(x.url)"
12,889✔
973
stripmd(x::Markdown.List) = join(map(stripmd, x.items), " ")
1,078✔
974
stripmd(x::Markdown.MD) = join(map(stripmd, x.content), " ")
12,654✔
975
stripmd(x::Markdown.Paragraph) = stripmd(x.content)
28,653✔
976
stripmd(x::Markdown.Footnote) = "$(stripmd(x.id)) $(stripmd(x.text))"
196✔
977
stripmd(x::Markdown.Table) =
45✔
978
    join([join(map(stripmd, r), " ") for r in x.rows], " ")
979

980
apropos(string) = apropos(stdout, string)
×
981
apropos(io::IO, string) = apropos(io, Regex("\\Q$string", "i"))
2✔
982

983
function apropos(io::IO, needle::Regex)
4✔
984
    for mod in modules
4✔
985
        # Module doc might be in README.md instead of the META dict
986
        docsearch(doc(mod), needle) && println(io, mod)
333✔
987
        dict = meta(mod; autoinit=false)
666✔
988
        isnothing(dict) && continue
666✔
989
        for (k, v) in dict
664✔
990
            docsearch(v, needle) && println(io, k)
11,086✔
991
        end
11,086✔
992
    end
333✔
993
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc