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

JuliaLang / julia / 1392

31 Dec 2025 01:18AM UTC coverage: 76.653% (+0.02%) from 76.638%
1392

push

buildkite

web-flow
update utf8proc to 2.11.3 (#60207)

62458 of 81482 relevant lines covered (76.65%)

23210278.09 hits per line

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

92.87
/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)
56✔
24
    internal_accesses = Set{Pair{Module,Symbol}}()
72✔
25
    quote
56✔
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)
4✔
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)
180✔
37
    line = strip(line)
232✔
38
    ternary_operator_help = (line == "?" || line == "?:")
288✔
39
    if startswith(line, '?') && !ternary_operator_help
280✔
40
        line = line[2:end]
2✔
41
        extended_help_on[] = nothing
2✔
42
        brief = false
2✔
43
    else
44
        extended_help_on[] = line
142✔
45
        brief = true
142✔
46
    end
47
    # interpret anything starting with # or #= as asking for help on comments
48
    if startswith(line, "#")
144✔
49
        if startswith(line, "#=")
×
50
            line = "#="
×
51
        else
52
            line = "#"
×
53
        end
54
    end
55
    x = Meta.parse(line, raise = false, depwarn = false)
144✔
56
    assym = Symbol(line)
144✔
57
    expr =
398✔
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
26✔
63
        elseif isexpr(x, (:using, :import))
118✔
64
            (x::Expr).head
8✔
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
262✔
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)
144✔
76
end
77
_helpmode(line::AbstractString, mod::Module=Main) = _helpmode(stdout, line, mod)
56✔
78

79
function formatdoc(d::DocStr)
18,283✔
80
    buffer = IOBuffer()
18,283✔
81
    for part in d.text
36,566✔
82
        formatdoc(buffer, d, part)
24,536✔
83
    end
30,789✔
84
    md = Markdown.MD(Any[Markdown.parse(seekstart(buffer))])
36,566✔
85
    assume_julia_code!(md)
18,283✔
86
end
87
@noinline formatdoc(buffer, d, part) = print(buffer, part)
24,530✔
88

89
function parsedoc(d::DocStr)
36,796✔
90
    if d.object === nothing
36,796✔
91
        md = formatdoc(d)
18,283✔
92
        md.meta[:module] = d.data[:module]
18,283✔
93
        md.meta[:path]   = d.data[:path]
18,283✔
94
        d.object = md
18,283✔
95
    end
96
    d.object
36,796✔
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)
18,283✔
105
    assume_julia_code!(doc.content)
36,566✔
106
    doc
36,566✔
107
end
108

109
function assume_julia_code!(blocks::Vector)
36,566✔
110
    for (i, block) in enumerate(blocks)
36,584✔
111
        if block isa Markdown.Code && block.language == ""
87,973✔
112
            blocks[i] = Markdown.Code("julia", block.code)
17,771✔
113
        elseif block isa Vector || block isa Markdown.MD
140,404✔
114
            assume_julia_code!(block)
18,283✔
115
        end
116
    end
139,380✔
117
    blocks
36,566✔
118
end
119

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

122
struct Message  # For direct messages to the terminal
123
    msg    # AbstractString
4✔
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
4✔
133

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

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

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

167

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

171
struct Logged{F}
172
    f::F
102✔
173
    mod::Module
174
    collection::Set{Pair{Module,Symbol}}
175
end
176
function (la::Logged)(m::Module, s::Symbol)
70✔
177
    m !== la.mod && Base.isdefined(m, s) && !Base.ispublic(m, s) && push!(la.collection, m => s)
70✔
178
    la.f(m, s)
70✔
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}})
536✔
183
    if expr.head === :. && length(expr.args) == 2 && !is_tuple(expr.args[2])
536✔
184
        Expr(:call, Logged(getproperty, mod, internal_access), log_nonpublic_access.(expr.args, (mod,), (internal_access,))...)
48✔
185
    elseif expr.head === :call && expr.args[1] === Base.Docs.Binding
488✔
186
        Expr(:call, Logged(Base.Docs.Binding, mod, internal_access), log_nonpublic_access.(expr.args[2:end], (mod,), (internal_access,))...)
54✔
187
    else
188
        Expr(expr.head, log_nonpublic_access.(expr.args, (mod,), (internal_access,))...)
434✔
189
    end
190
end
191
log_nonpublic_access(expr, ::Module, _) = expr
617✔
192

193
function insert_internal_warning(md::Markdown.MD, internal_access::Set{Pair{Module,Symbol}})
56✔
194
    if !isempty(internal_access)
56✔
195
        items = Any[Any[Markdown.Paragraph(Any[Markdown.Code("", s)])] for s in sort!(["$mod.$sym" for (mod, sym) in internal_access])]
12✔
196
        admonition = Markdown.Admonition("warning", "Warning", Any[
24✔
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)
24✔
200
    end
201
    md
56✔
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{})
1,678✔
210
    if defined(binding)
1,678✔
211
        result = getdoc(resolve(binding), sig)
1,317✔
212
        result === nothing || return result
1,326✔
213
    end
214
    results, groups = DocStr[], MultiDoc[]
1,386✔
215
    # Lookup `binding` and `sig` for matches in all modules of the docsystem.
216
    for mod in modules
1,386✔
217
        dict = meta(mod; autoinit=false)
130,182✔
218
        isnothing(dict) && continue
130,182✔
219
        if haskey(dict, binding)
130,182✔
220
            multidoc = dict[binding]
824✔
221
            push!(groups, multidoc)
824✔
222
            for msig in multidoc.order
824✔
223
                sig <: msig && push!(results, multidoc.docs[msig])
965✔
224
            end
965✔
225
        end
226
    end
130,182✔
227
    if isempty(groups)
1,386✔
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)
578✔
233
        alias == binding ? summarize(alias, sig) : doc(alias, sig)
578✔
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)
808✔
238
            for group in groups, each in group.order
48✔
239
                push!(results, group.docs[each])
57✔
240
            end
57✔
241
        end
242
        # Get parsed docs and concatenate them.
243
        md = catdoc(mapany(parsedoc, results)...)
857✔
244
        # Save metadata in the generated markdown.
245
        if isa(md, Markdown.MD)
808✔
246
            md.meta[:results] = results
801✔
247
            md.meta[:binding] = binding
801✔
248
            md.meta[:typesig] = sig
801✔
249
        end
250
        return md
808✔
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))
6✔
256
doc(object, sig::Type = Union{}) = doc(aliasof(object, typeof(object)), sig)
1,935✔
257
doc(object, sig...)              = doc(object, Tuple{sig...})
×
258

259
function lookup_doc(@nospecialize(ex))
480✔
260
    if isa(ex, Expr) && ex.head !== :(.) && Base.isoperator(ex.head)
655✔
261
        # handle syntactic operators, e.g. +=, ::, .=
262
        ex = ex.head
×
263
    end
264
    if haskey(keywords, ex)
480✔
265
        return parsedoc(keywords[ex])
12✔
266
    elseif Meta.isexpr(ex, :incomplete)
468✔
267
        return :($(Markdown.md"No documentation found."))
3✔
268
    elseif !isa(ex, Expr) && !isa(ex, Symbol)
465✔
269
        return :($(doc)($(typeof)($(esc(ex)))))
5✔
270
    end
271
    if isa(ex, Symbol) && Base.isoperator(ex)
550✔
272
        str = string(ex)
12✔
273
        isdotted = startswith(str, ".")
12✔
274
        if endswith(str, "=") && Base.operator_precedence(ex) == Base.prec_assignment && ex !== :(:=)
12✔
275
            op = chop(str)
4✔
276
            eq = isdotted ? ".=" : "="
4✔
277
            return Markdown.parse("`x $op= y` is a synonym for `x $eq x $op y`")
4✔
278
        elseif isdotted && ex !== :(..)
8✔
279
            op = str[2:end]
4✔
280
            if op in ("&&", "||")
4✔
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).")
2✔
284
            end
285
        end
286
    end
287
    name = namify(ex)
550✔
288
    # If namify couldn't extract a meaningful name and returned an Expr
289
    # that can't be converted to a binding, treat it like a value
290
    if isa(name, Expr) && !isexpr(name, :(.))
454✔
291
        return :($(doc)($(typeof)($(esc(ex)))))
3✔
292
    end
293
    binding = esc(bindingexpr(name))
451✔
294
    if isexpr(ex, :call) || isexpr(ex, :macrocall) || isexpr(ex, :where)
914✔
295
        sig = esc(signature(ex))
149✔
296
        :($(doc)($binding, $sig))
149✔
297
    else
298
        :($(doc)($binding))
302✔
299
    end
300
end
301

302
# Object Summaries.
303
# =================
304

305
function summarize(binding::Binding, sig)
580✔
306
    io = IOBuffer()
580✔
307
    if defined(binding)
580✔
308
        binding_res = resolve(binding)
551✔
309
        if !isa(binding_res, Module)
551✔
310
            varstr = "$(binding.mod).$(binding.var)"
68✔
311
            if Base.ispublic(binding.mod, binding.var)
68✔
312
                println(io, "No documentation found for public binding `$varstr`.\n")
15✔
313
            else
314
                println(io, "No documentation found for private binding `$varstr`.\n")
53✔
315
            end
316
        end
317
        summarize(io, binding_res, binding)
551✔
318
    else
319
        println(io, "No documentation found.\n")
29✔
320
        quot = any(isspace, sprint(print, binding)) ? "'" : ""
29✔
321
        bpart = Base.lookup_binding_partition(Base.tls_world_age(), convert(Core.Binding, GlobalRef(binding.mod, binding.var)))
29✔
322
        if Base.binding_kind(bpart) === Base.PARTITION_KIND_GUARD
29✔
323
            println(io, "Binding ", quot, "`", binding, "`", quot, " does not exist.")
27✔
324
        else
325
            println(io, "Binding ", quot, "`", binding, "`", quot, " exists, but has not been assigned a value.")
2✔
326
        end
327
    end
328
    md = Markdown.parse(seekstart(io))
1,160✔
329
    # Save metadata in the generated markdown.
330
    md.meta[:results] = DocStr[]
580✔
331
    md.meta[:binding] = binding
580✔
332
    md.meta[:typesig] = sig
580✔
333
    return md
580✔
334
end
335

336
function summarize(io::IO, λ::Function, binding::Binding)
14✔
337
    kind = startswith(string(binding.var), '@') ? "macro" : "`Function`"
28✔
338
    println(io, "`", binding, "` is a ", kind, ".")
14✔
339
    println(io, "```\n", methods(λ), "\n```")
14✔
340
end
341

342
function summarize(io::IO, TT::Type, binding::Binding)
54✔
343
    println(io, "# Summary")
54✔
344
    T = Base.unwrap_unionall(TT)
54✔
345
    if T isa DataType
54✔
346
        println(io, "```")
45✔
347
        print(io,
75✔
348
            Base.isabstracttype(T) ? "abstract type " :
349
            Base.ismutabletype(T)  ? "mutable struct " :
350
            Base.isstructtype(T) ? "struct " :
351
            "primitive type ")
352
        supert = supertype(T)
45✔
353
        println(io, T)
45✔
354
        println(io, "```")
45✔
355
        if !Base.isabstracttype(T) && T.name !== Tuple.name && !isempty(fieldnames(T))
45✔
356
            println(io, "# Fields")
18✔
357
            println(io, "```")
18✔
358
            pad = maximum(length(string(f)) for f in fieldnames(T))
18✔
359
            for (f, t) in zip(fieldnames(T), fieldtypes(T))
18✔
360
                println(io, rpad(f, pad), " :: ", t)
33✔
361
            end
33✔
362
            println(io, "```")
18✔
363
        end
364
        subt = subtypes(TT)
45✔
365
        if !isempty(subt)
60✔
366
            println(io, "# Subtypes")
15✔
367
            println(io, "```")
15✔
368
            for t in subt
15✔
369
                println(io, Base.unwrap_unionall(t))
36✔
370
            end
36✔
371
            println(io, "```")
15✔
372
        end
373
        if supert != Any
45✔
374
            println(io, "# Supertype Hierarchy")
33✔
375
            println(io, "```")
33✔
376
            Base.show_supertypes(io, T)
33✔
377
            println(io)
33✔
378
            println(io, "```")
33✔
379
        end
380
    elseif T isa Union
9✔
381
        println(io, "`", binding, "` is of type `", typeof(TT), "`.\n")
9✔
382
        println(io, "# Union Composed of Types")
9✔
383
        for T1 in Base.uniontypes(T)
9✔
384
            println(io, " - `", Base.rewrap_unionall(T1, TT), "`")
30✔
385
        end
30✔
386
    else # unreachable?
387
        println(io, "`", binding, "` is of type `", typeof(TT), "`.\n")
×
388
    end
389
end
390

391
function find_readme(m::Module)::Union{String, Nothing}
483✔
392
    mpath = pathof(m)
483✔
393
    isnothing(mpath) && return nothing
595✔
394
    !isfile(mpath) && return nothing # modules in sysimage, where src files are omitted
112✔
395
    path = dirname(mpath)
112✔
396
    top_path = pkgdir(m)
112✔
397
    while true
224✔
398
        for entry in _readdirx(path; sort=true)
224✔
399
            isfile(entry) && (lowercase(entry.name) in ["readme.md", "readme"]) || continue
4,492✔
400
            return entry.path
64✔
401
        end
1,704✔
402
        path == top_path && break # go no further than pkgdir
320✔
403
        path = dirname(path) # work up through nested modules
112✔
404
    end
112✔
405
    return nothing
48✔
406
end
407
function summarize(io::IO, m::Module, binding::Binding; nlines::Int = 200)
966✔
408
    readme_path = find_readme(m)
483✔
409
    public = Base.ispublic(binding.mod, binding.var) ? "public" : "internal"
483✔
410
    if isnothing(readme_path)
547✔
411
        println(io, "No docstring or readme file found for $public module `$m`.\n")
419✔
412
    else
413
        println(io, "No docstring found for $public module `$m`.")
64✔
414
    end
415
    exports = filter!(!=(nameof(m)), names(m))
966✔
416
    if isempty(exports)
483✔
417
        println(io, "Module does not have any public names.")
157✔
418
    else
419
        println(io, "# Public names")
326✔
420
        print(io, "  `")
326✔
421
        join(io, exports, "`, `")
326✔
422
        println(io, "`\n")
326✔
423
    end
424
    if !isnothing(readme_path)
547✔
425
        readme_lines = readlines(readme_path)
64✔
426
        isempty(readme_lines) && return  # don't say we are going to print empty file
64✔
427
        println(io)
64✔
428
        println(io, "---")
64✔
429
        println(io, "_Package description from `$(basename(readme_path))`:_")
64✔
430
        for line in first(readme_lines, nlines)
64✔
431
            println(io, line)
3,292✔
432
        end
3,292✔
433
        length(readme_lines) > nlines && println(io, "\n[output truncated to first $nlines lines]")
64✔
434
    end
435
end
436

437
function summarize(io::IO, @nospecialize(T), binding::Binding)
9✔
438
    T = typeof(T)
9✔
439
    println(io, "`", binding, "` is of type `", T, "`.\n")
9✔
440
    summarize(io, T, binding)
9✔
441
end
442

443
# repl search and completions for help
444

445
# This type is returned from `accessible` and denotes a binding that is accessible within
446
# some context. It differs from `Base.Docs.Binding`, which is also used by the REPL, in
447
# that it doesn't track the defining module for a symbol unless the symbol is public but
448
# not exported, i.e. it's accessible but requires qualification. Using this type rather
449
# than `Base.Docs.Binding` simplifies things considerably, partially because REPL searching
450
# is based on `String`s, which this type stores, but `Base.Docs.Binding` stores a module
451
# and symbol and does not have any notion of the context from which the binding is accessed.
452
struct AccessibleBinding
453
    source::Union{String,Nothing}
512,194✔
454
    name::String
455
end
456

457
function AccessibleBinding(mod::Module, name::Symbol)
123,407✔
458
    m = isexported(mod, name) ? nothing : String(nameof(mod))
131,945✔
459
    return AccessibleBinding(m, String(name))
123,407✔
460
end
461
AccessibleBinding(name::Symbol) = AccessibleBinding(nothing, String(name))
244,183✔
462

463
function Base.show(io::IO, b::AccessibleBinding)
464
    b.source === nothing || print(io, b.source, '.')
66,245✔
465
    print(io, b.name)
61,677✔
466
end
467

468
quote_spaces(x) = any(isspace, x) ? "'" * x * "'" : x
143,400✔
469
quote_spaces(x::AccessibleBinding) = AccessibleBinding(x.source, quote_spaces(x.name))
145,920✔
470

471
function repl_search(io::IO, s::Union{Symbol,String}, mod::Module)
52✔
472
    pre = "search:"
52✔
473
    print(io, pre)
52✔
474
    printmatches(io, s, map(quote_spaces, doc_completions(s, mod)), cols = _displaysize(io)[2] - length(pre))
52✔
475
    println(io, "\n")
52✔
476
end
477

478
# TODO: document where this is used
479
repl_search(s, mod::Module) = repl_search(stdout, s, mod)
×
480

481
function repl_corrections(io::IO, s, mod::Module)
18✔
482
    print(io, "Couldn't find ")
18✔
483
    quot = any(isspace, s) ? "'" : ""
18✔
484
    print(io, quot)
18✔
485
    printstyled(io, s, color=:cyan)
18✔
486
    print(io, quot)
18✔
487
    if Base.identify_package(s) === nothing
18✔
488
        print(io, '\n')
16✔
489
    else
490
        print(io, ", but a loadable package with that name exists. If you are looking for the package docs load the package first.\n")
2✔
491
    end
492
    print_correction(io, s, mod)
18✔
493
end
494
repl_corrections(s) = repl_corrections(stdout, s)
×
495

496
# inverse of latex_symbols Dict, lazily created as needed
497
const symbols_latex = Dict{String,String}()
498
function symbol_latex(s::String)
124✔
499
    if isempty(symbols_latex)
124✔
500
        for (k,v) in Iterators.flatten((REPLCompletions.latex_symbols,
×
501
                                        REPLCompletions.emoji_symbols))
502
            symbols_latex[v] = k
×
503
        end
×
504

505
        # Overwrite with canonical mapping when a symbol has several completions (#39148)
506
        merge!(symbols_latex, REPLCompletions.symbols_latex_canonical)
×
507
    end
508

509
    return get(symbols_latex, s, "")
124✔
510
end
511
function repl_latex(io::IO, s0::String)
64✔
512
    # This has rampant `Core.Box` problems (#15276). Use the tricks of
513
    # https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-captured
514
    # We're changing some of the values so the `let` trick isn't applicable.
515
    s::String = s0
64✔
516
    latex::String = symbol_latex(s)
64✔
517
    if isempty(latex)
64✔
518
        # Decompose NFC-normalized identifier to match tab-completion
519
        # input if the first search came up empty.
520
        s = normalize(s, :NFD)
52✔
521
        latex = symbol_latex(s)
52✔
522
    end
523
    if !isempty(latex)
64✔
524
        print(io, "\"")
12✔
525
        printstyled(io, s, color=:cyan)
12✔
526
        print(io, "\" can be typed by ")
12✔
527
        printstyled(io, latex, "<tab>", color=:cyan)
12✔
528
        println(io, '\n')
12✔
529
    elseif any(c -> haskey(symbols_latex, string(c)), s)
268✔
530
        print(io, "\"")
8✔
531
        printstyled(io, s, color=:cyan)
8✔
532
        print(io, "\" can be typed by ")
8✔
533
        s_to_print = s
8✔
534
        with_output_color(:cyan, io) do io
8✔
535
            state::Char = '\0'
8✔
536
            for c in s_to_print
14✔
537
                cstr = string(c)
31✔
538
                if haskey(symbols_latex, cstr)
62✔
539
                    latex_symbol = symbols_latex[cstr]
25✔
540
                    if length(latex_symbol) == 3 && latex_symbol[2] in ('^','_')
34✔
541
                        # coalesce runs of sub/superscripts
542
                        if state != latex_symbol[2]
30✔
543
                            '\0' != state && print(io, "<tab>")
7✔
544
                            print(io, latex_symbol[1:2])
14✔
545
                            state = latex_symbol[2]
14✔
546
                        end
547
                        print(io, latex_symbol[3])
30✔
548
                    else
549
                        if '\0' != state
10✔
550
                            print(io, "<tab>")
2✔
551
                            state = '\0'
2✔
552
                        end
553
                        print(io, latex_symbol, "<tab>")
10✔
554
                    end
555
                else
556
                    if '\0' != state
6✔
557
                        print(io, "<tab>")
×
558
                        state = '\0'
×
559
                    end
560
                    print(io, c)
6✔
561
                end
562
            end
31✔
563
            '\0' != state && print(io, "<tab>")
8✔
564
        end
565
        println(io, '\n')
8✔
566
    end
567
end
568
repl_latex(s::String) = repl_latex(stdout, s)
×
569

570
macro repl(ex, brief::Bool=false, mod::Module=Main) repl(ex; brief, mod) end
36✔
571
macro repl(io, ex, brief, mod, internal_accesses) repl(io, ex; brief, mod, internal_accesses) end
116✔
572

573
function repl(io::IO, s::Symbol; brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing)
104✔
574
    str = string(s)
52✔
575
    quote
70✔
576
        repl_latex($io, $str)
577
        repl_search($io, $str, $mod)
578
        $(if !isdefined(mod, s) && !haskey(keywords, s) && !Base.isoperator(s)
579
               :(repl_corrections($io, $str, $mod))
18✔
580
          end)
581
        $(_repl(s, brief, mod, internal_accesses))
582
    end
583
end
584
isregex(x) = isexpr(x, :macrocall, 3) && x.args[1] === Symbol("@r_str") && !isempty(x.args[3])
75✔
585

586
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)
150✔
587
repl(io::IO, str::AbstractString; brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing) = :(apropos($io, $str))
4✔
588
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
10✔
589

590
repl(x; brief::Bool=true, mod::Module=Main) = repl(stdout, x; brief, mod)
36✔
591

592
function _repl(x, brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing)
149✔
593
    if isexpr(x, :call)
149✔
594
        x = x::Expr
18✔
595
        # determine the types of the values
596
        kwargs = nothing
18✔
597
        pargs = Any[]
18✔
598
        for arg in x.args[2:end]
18✔
599
            if isexpr(arg, :parameters)
27✔
600
                kwargs = mapany(arg.args) do kwarg
×
601
                    if kwarg isa Symbol
602
                        kwarg = :($kwarg::Any)
603
                    elseif isexpr(kwarg, :kw)
604
                        let kw_lhs = kwarg.args[1],
605
                            kw_rhs = kwarg.args[2]
606
                            if kw_lhs isa Symbol
607
                                if kw_rhs isa Symbol
608
                                    kwarg.args[1] = :($kw_lhs::(@isdefined($kw_rhs) ? typeof($kw_rhs) : Any))
609
                                else
610
                                    kwarg.args[1] = :($kw_lhs::typeof($kw_rhs))
611
                                end
612
                            end
613
                        end
614
                    end
615
                    kwarg
616
                end
617
            elseif isexpr(arg, :kw)
27✔
618
                if kwargs === nothing
3✔
619
                    kwargs = Any[]
3✔
620
                end
621
                arg_lhs = arg.args[1]
3✔
622
                arg_rhs = arg.args[2]
3✔
623
                if arg_lhs isa Symbol
3✔
624
                    if arg_rhs isa Symbol
3✔
625
                        arg.args[1] = :($arg_lhs::(@isdefined($arg_rhs) ? typeof($arg_rhs) : Any))
×
626
                    else
627
                        arg.args[1] = :($arg_lhs::typeof($arg_rhs))
3✔
628
                    end
629
                end
630
                push!(kwargs, arg)
3✔
631
            else
632
                if arg isa Symbol
24✔
633
                    arg = :($arg::(@isdefined($arg) ? typeof($arg) : Any))
3✔
634
                elseif !isexpr(arg, :(::))
21✔
635
                    arg = :(::typeof($arg))
12✔
636
                end
637
                push!(pargs, arg)
24✔
638
            end
639
        end
27✔
640
        if kwargs === nothing
18✔
641
            x.args = Any[x.args[1], pargs...]
15✔
642
        else
643
            x.args = Any[x.args[1], Expr(:parameters, kwargs...), pargs...]
3✔
644
        end
645
    end
646
    #docs = lookup_doc(x) # TODO
647
    docs = esc(:(@doc $x))
137✔
648
    docs = if isfield(x)
137✔
649
        quote
51✔
650
            if $(esc(x.args[1])) isa Type
651
                fielddoc($(esc(x.args[1])), $(esc(x.args[2])))
652
            else
653
                $docs
654
            end
655
        end
656
    else
657
        docs
171✔
658
    end
659
    docs = log_nonpublic_access(macroexpand(mod, docs), mod, internal_accesses)
137✔
660
    :(REPL.trimdocs($docs, $brief))
137✔
661
end
662

663
"""
664
    fielddoc(binding, field)
665

666
Return documentation for a particular `field` of a type if it exists.
667
"""
668
function fielddoc(binding::Binding, field::Symbol)
19✔
669
    for mod in modules
19✔
670
        dict = meta(mod; autoinit=false)
1,745✔
671
        isnothing(dict) && continue
1,745✔
672
        multidoc = get(dict, binding, nothing)
1,745✔
673
        if multidoc !== nothing
1,745✔
674
            structdoc = get(multidoc.docs, Union{}, nothing)
11✔
675
            if structdoc !== nothing
11✔
676
                fieldsdoc = get(structdoc.data, :fields, nothing)
11✔
677
                if fieldsdoc !== nothing
11✔
678
                    fielddoc = get(fieldsdoc, field, nothing)
9✔
679
                    if fielddoc !== nothing
9✔
680
                        return isa(fielddoc, Markdown.MD) ?
7✔
681
                            fielddoc : Markdown.parse(fielddoc)
682
                    end
683
                end
684
            end
685
        end
686
    end
1,738✔
687
    fs = fieldnames(resolve(binding))
12✔
688
    fields = isempty(fs) ? "no fields" : (length(fs) == 1 ? "field " : "fields ") *
20✔
689
                                          join(("`$f`" for f in fs), ", ", ", and ")
690
    Markdown.parse("`$(resolve(binding))` has $fields.")
12✔
691
end
692

693
# As with the additional `doc` methods, this converts an object to a `Binding` first.
694
fielddoc(obj::UnionAll, field::Symbol) = fielddoc(Base.unwrap_unionall(obj), field)
4✔
695
fielddoc(object, field::Symbol) = fielddoc(aliasof(object, typeof(object)), field)
19✔
696

697

698
# Search & Rescue
699
# Utilities for correcting user mistakes and (eventually)
700
# doing full documentation searches from the repl.
701

702
# Fuzzy Search Algorithm
703

704
function matchinds(needle, haystack; acronym::Bool = false)
1,028✔
705
    chars = collect(needle)
514✔
706
    is = Int[]
514✔
707
    lastc = '\0'
514✔
708
    for (i, char) in enumerate(haystack)
1,028✔
709
        while !isempty(chars) && isspace(first(chars))
4,608✔
710
            popfirst!(chars) # skip spaces
8✔
711
        end
8✔
712
        isempty(chars) && break
2,322✔
713
        if lowercase(char) == lowercase(chars[1]) &&
2,558✔
714
           (!acronym || !isletter(lastc))
715
            push!(is, i)
673✔
716
            popfirst!(chars)
673✔
717
        end
718
        lastc = char
2,278✔
719
    end
2,278✔
720
    return is
514✔
721
end
722

723
matchinds(needle, (; name)::AccessibleBinding; acronym::Bool=false) =
×
724
    matchinds(needle, name; acronym)
725

726
longer(x, y) = length(x) ≥ length(y) ? (x, true) : (y, false)
256✔
727

728
bestmatch(needle, haystack) =
256✔
729
    longer(matchinds(needle, haystack, acronym = true),
730
           matchinds(needle, haystack))
731

732
# Optimal string distance: Counts the minimum number of insertions, deletions,
733
# transpositions or substitutions to go from one string to the other.
734
function string_distance(a::AbstractString, lena::Integer, b::AbstractString, lenb::Integer)
233,980✔
735
    if lena > lenb
233,980✔
736
        a, b = b, a
30,941✔
737
        lena, lenb = lenb, lena
30,941✔
738
    end
739
    start = 0
233,980✔
740
    for (i, j) in zip(a, b)
465,014✔
741
        if a == b
231,408✔
742
            start += 1
490✔
743
        else
744
            break
230,918✔
745
        end
746
    end
864✔
747
    start == lena && return lenb - start
233,980✔
748
    vzero = collect(1:(lenb - start))
2,429,497✔
749
    vone = similar(vzero)
230,918✔
750
    prev_a, prev_b = first(a), first(b)
457,970✔
751
    current = 0
230,918✔
752
    for (i, ai) in enumerate(a)
461,836✔
753
        i > start || (prev_a = ai; continue)
777,298✔
754
        left = i - start - 1
777,298✔
755
        current = i - start
777,298✔
756
        transition_next = 0
777,298✔
757
        for (j, bj) in enumerate(b)
1,554,596✔
758
            j > start || (prev_b = bj; continue)
8,700,994✔
759
            # No need to look beyond window of lower right diagonal
760
            above = current
8,700,994✔
761
            this_transition = transition_next
8,700,994✔
762
            transition_next = vone[j - start]
8,700,994✔
763
            vone[j - start] = current = left
8,700,994✔
764
            left = vzero[j - start]
8,700,994✔
765
            if ai != bj
8,700,994✔
766
                # Minimum between substitution, deletion and insertion
767
                current = min(current + 1, above + 1, left + 1)
8,416,044✔
768
                if i > start + 1 && j > start + 1 && ai == prev_b && prev_a == bj
8,416,044✔
769
                    current = min(current, (this_transition += 1))
10,571✔
770
                end
771
            end
772
            vzero[j - start] = current
8,700,994✔
773
            prev_b = bj
8,700,994✔
774
        end
16,624,690✔
775
        prev_a = ai
777,298✔
776
    end
1,323,678✔
777
    current
230,918✔
778
end
779

780
function fuzzyscore(needle::AbstractString, haystack::AbstractString)
8✔
781
    lena, lenb = length(needle), length(haystack)
233,980✔
782
    1 - (string_distance(needle, lena, haystack, lenb) / max(lena, lenb))
233,980✔
783
end
784

785
function fuzzyscore(needle::AbstractString, haystack::AccessibleBinding)
233,972✔
786
    score = fuzzyscore(needle, haystack.name)
233,972✔
787
    haystack.source === nothing && return score
233,972✔
788
    # Apply a "penalty" of half an edit if the comparator binding is public but not
789
    # exported so that exported/local names that exactly match the search query are
790
    # listed first
791
    penalty = 1 / (2 * max(length(needle), length(haystack.name)))
8,196✔
792
    return max(score - penalty, 0)
8,196✔
793
end
794

795
function fuzzysort(search::String, candidates::Vector{AccessibleBinding})
92✔
796
    scores = map(cand -> fuzzyscore(search, cand), candidates)
198,622✔
797
    candidates[sortperm(scores)] |> reverse
92✔
798
end
799

800
# Levenshtein Distance
801

802
function levenshtein(s1, s2)
36,078✔
803
    a, b = collect(s1), collect(s2)
36,078✔
804
    m = length(a)
36,078✔
805
    n = length(b)
36,078✔
806
    d = Matrix{Int}(undef, m+1, n+1)
36,078✔
807

808
    d[1:m+1, 1] = 0:m
228,440✔
809
    d[1, 1:n+1] = 0:n
376,011✔
810

811
    for i = 1:m, j = 1:n
36,078✔
812
        d[i+1,j+1] = min(d[i  , j+1] + 1,
1,661,928✔
813
                         d[i+1, j  ] + 1,
814
                         d[i  , j  ] + (a[i] != b[j]))
815
    end
1,818,212✔
816

817
    return d[m+1, n+1]
36,078✔
818
end
819

820
function levsort(search::String, candidates::Vector{AccessibleBinding})
18✔
821
    scores = map(candidates) do cand
18✔
822
        (Float64(levenshtein(search, cand.name)), -fuzzyscore(search, cand))
35,138✔
823
    end
824
    candidates = candidates[sortperm(scores)]
18✔
825
    i = 0
18✔
826
    for outer i = 1:length(candidates)
18✔
827
        levenshtein(search, candidates[i].name) > 3 && break
940✔
828
    end
922✔
829
    return candidates[1:i]
18✔
830
end
831

832
# Result printing
833

834
function printmatch(io::IO, word, match)
256✔
835
    is, _ = bestmatch(word, match)
256✔
836
    for (i, char) = enumerate(match)
512✔
837
        if i in is
3,220✔
838
            printstyled(io, char, bold=true)
531✔
839
        else
840
            print(io, char)
648✔
841
        end
842
    end
1,179✔
843
end
844

845
function printmatch(io::IO, word, match::AccessibleBinding)
846
    match.source === nothing || print(io, match.source, '.')
268✔
847
    printmatch(io, word, match.name)
256✔
848
end
849

850
function matchlength(x::AccessibleBinding)
851
    n = length(x.name)
453✔
852
    if x.source !== nothing
453✔
853
        n += length(x.source) + 1  # the +1 is for the `.` separator
22✔
854
    end
855
    return n
453✔
856
end
857
matchlength(x) = length(x)
×
858

859
function printmatches(io::IO, word, matches; cols::Int = _displaysize(io)[2])
104✔
860
    total = 0
52✔
861
    for match in matches
52✔
862
        ml = matchlength(match)
308✔
863
        total + ml + 1 > cols && break
308✔
864
        fuzzyscore(word, match) < 0.5 && break
296✔
865
        print(io, " ")
256✔
866
        printmatch(io, word, match)
268✔
867
        total += ml + 1
256✔
868
    end
256✔
869
end
870

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

873
function print_joined_cols(io::IO, ss::Vector{AccessibleBinding}, delim = "", last = delim; cols::Int = _displaysize(io)[2])
36✔
874
    i = 0
18✔
875
    total = 0
18✔
876
    for outer i = 1:length(ss)
18✔
877
        total += matchlength(ss[i])
145✔
878
        total + max(i-2,0)*length(delim) + (i>1 ? 1 : 0)*length(last) > cols && (i-=1; break)
145✔
879
    end
135✔
880
    join(io, ss[1:i], delim, last)
18✔
881
end
882

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

885
function print_correction(io::IO, word::String, mod::Module)
18✔
886
    cors = map(quote_spaces, levsort(word, accessible(mod)))
18✔
887
    pre = "Perhaps you meant "
18✔
888
    print(io, pre)
18✔
889
    print_joined_cols(io, cors, ", ", " or "; cols = _displaysize(io)[2] - length(pre))
18✔
890
    println(io)
18✔
891
    return
18✔
892
end
893

894
# TODO: document where this is used
895
print_correction(word, mod::Module) = print_correction(stdout, word, mod)
×
896

897
# Completion data
898

899
moduleusings(mod) = ccall(:jl_module_usings, Any, (Any,), mod)
113✔
900

901
function accessible(mod::Module)
113✔
902
    bindings = Set(AccessibleBinding(s) for s in names(mod; all=true, imported=true)
113✔
903
                   if !isdeprecated(mod, s))
904
    for used in moduleusings(mod)
113✔
905
        union!(bindings, (AccessibleBinding(used, s) for s in names(used)
1,260✔
906
                          if !isdeprecated(used, s)))
907
    end
1,260✔
908
    union!(bindings, (AccessibleBinding(k) for k in keys(Base.Docs.keywords)))
113✔
909
    filter!(b -> !occursin('#', b.name), bindings)
474,365✔
910
    return collect(bindings)
113✔
911
end
912

913
function doc_completions(name, mod::Module=Main)
98✔
914
    res = fuzzysort(name, accessible(mod))
98✔
915

916
    # to insert an entry like `raw""` for `"@raw_str"` in `res`
917
    ms = map(c -> match(r"^@(.*?)_str$", c.name), res)
198,622✔
918
    idxs = findall(!isnothing, ms)
184✔
919

920
    # avoid messing up the order while inserting
921
    for i in reverse!(idxs)
92✔
922
        c = only((ms[i]::AbstractMatch).captures)
1,200✔
923
        insert!(res, i, AccessibleBinding(res[i].source, "$(c)\"\""))
1,200✔
924
    end
1,200✔
925
    res
92✔
926
end
927
doc_completions(name::Symbol) = doc_completions(string(name), mod)
×
928

929

930
# Searching and apropos
931

932
# Docsearch simply returns true or false if an object contains the given needle
933
docsearch(haystack::AbstractString, needle) = occursin(needle, haystack)
36,953✔
934
docsearch(haystack::Symbol, needle) = docsearch(string(haystack), needle)
×
935
docsearch(::Nothing, needle) = false
×
936
function docsearch(haystack::Array, needle)
×
937
    for elt in haystack
×
938
        docsearch(elt, needle) && return true
×
939
    end
×
940
    false
×
941
end
942
function docsearch(haystack, needle)
×
943
    @warn "Unable to search documentation of type $(typeof(haystack))" maxlog=1
×
944
    false
×
945
end
946

947
## Searching specific documentation objects
948
function docsearch(haystack::MultiDoc, needle)
31,937✔
949
    for v in values(haystack.docs)
63,874✔
950
        docsearch(v, needle) && return true
35,864✔
951
    end
27,701✔
952
    false
953
end
954

955
function docsearch(haystack::DocStr, needle)
35,864✔
956
    docsearch(parsedoc(haystack), needle) && return true
35,864✔
957
    if haskey(haystack.data, :fields)
55,402✔
958
        for doc in values(haystack.data[:fields])
2,346✔
959
            docsearch(doc, needle) && return true
152✔
960
        end
152✔
961
    end
962
    false
963
end
964

965
## doc search
966

967
## Markdown search simply strips all markup and searches plain text version
968
docsearch(haystack::Markdown.MD, needle) = docsearch(stripmd(haystack.content), needle)
36,795✔
969

970
"""
971
    stripmd(x)
972

973
Strip all Markdown markup from x, leaving the result in plain text. Used
974
internally by apropos to make docstrings containing more than one markdown
975
element searchable.
976
"""
977
stripmd(@nospecialize x) = string(x) # for random objects interpolated into the docstring
18✔
978
stripmd(x::AbstractString) = x  # base case
542,949✔
979
stripmd(x::Nothing) = " "
262✔
980
stripmd(x::Vector) = string(map(stripmd, x)...)
195,821✔
981

982
stripmd(x::Markdown.BlockQuote) = "$(stripmd(x.content))"
10✔
983
stripmd(x::Markdown.Admonition) = "$(stripmd(x.content))"
7,966✔
984
stripmd(x::Markdown.Bold) = "$(stripmd(x.text))"
261✔
985
stripmd(x::Markdown.Code) = "$(stripmd(x.code))"
249,054✔
986
stripmd(x::Markdown.Header) = stripmd(x.text)
13,792✔
987
stripmd(x::Markdown.HorizontalRule) = " "
414✔
988
stripmd(x::Markdown.Image) = "$(stripmd(x.alt)) $(x.url)"
92✔
989
stripmd(x::Markdown.Italic) = "$(stripmd(x.text))"
1,738✔
990
stripmd(x::Markdown.LaTeX) = "$(x.formula)"
2,227✔
991
stripmd(x::Markdown.LineBreak) = " "
200✔
992
stripmd(x::Markdown.Link) = "$(stripmd(x.text)) $(x.url)"
35,665✔
993
stripmd(x::Markdown.List) = join(map(stripmd, x.items), " ")
3,307✔
994
stripmd(x::Markdown.MD) = join(map(stripmd, x.content), " ")
36,780✔
995
stripmd(x::Markdown.Paragraph) = stripmd(x.content)
84,713✔
996
stripmd(x::Markdown.Footnote) = "$(stripmd(x.id)) $(stripmd(x.text))"
510✔
997
stripmd(x::Markdown.Table) =
182✔
998
    join([join(map(stripmd, r), " ") for r in x.rows], " ")
999

1000
apropos(string) = apropos(stdout, string)
×
1001
apropos(io::IO, string) = apropos(io, Regex("\\Q$string", "i"))
5✔
1002

1003
function apropos(io::IO, needle::Regex)
10✔
1004
    for mod in modules
10✔
1005
        # Module doc might be in README.md instead of the META dict
1006
        docsearch(doc(mod), needle) && println(io, mod)
937✔
1007
        dict = meta(mod; autoinit=false)
937✔
1008
        isnothing(dict) && continue
937✔
1009
        for (k, v) in dict
937✔
1010
            docsearch(v, needle) && println(io, k)
31,937✔
1011
        end
31,937✔
1012
    end
937✔
1013
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