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

JuliaLang / julia / #37621

15 Sep 2023 01:19PM UTC coverage: 86.217% (+0.2%) from 85.982%
#37621

push

local

web-flow
Tweaks to repl tab complete hints (#51321)

3 of 3 new or added lines in 1 file covered. (100.0%)

73278 of 84993 relevant lines covered (86.22%)

10500566.5 hits per line

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

78.61
/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
13

14
import REPL
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)
20✔
24
    internal_accesses = Set{Pair{Module,Symbol}}()
20✔
25
    quote
16✔
26
        docs = $REPL.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)
10✔
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)
104✔
37
    line = strip(line)
104✔
38
    ternary_operator_help = (line == "?" || line == "?:")
120✔
39
    if startswith(line, '?') && !ternary_operator_help
60✔
40
        line = line[2:end]
1✔
41
        extended_help_on[] = nothing
1✔
42
        brief = false
1✔
43
    else
44
        extended_help_on[] = line
59✔
45
        brief = true
59✔
46
    end
47
    # interpret anything starting with # or #= as asking for help on comments
48
    if startswith(line, "#")
120✔
49
        if startswith(line, "#=")
×
50
            line = "#="
×
51
        else
52
            line = "#"
×
53
        end
54
    end
55
    x = Meta.parse(line, raise = false, depwarn = false)
60✔
56
    assym = Symbol(line)
60✔
57
    expr =
129✔
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))
62✔
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
107✔
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)
60✔
76
end
77
_helpmode(line::AbstractString, mod::Module=Main) = _helpmode(stdout, line, mod)
28✔
78

79
# Print vertical lines along each docstring if there are multiple docs
80
function insert_hlines(docs)
18✔
81
    if !isa(docs, Markdown.MD) || !haskey(docs.meta, :results) || isempty(docs.meta[:results])
33✔
82
        return docs
14✔
83
    end
84
    docs = docs::Markdown.MD
×
85
    v = Any[]
4✔
86
    for (n, doc) in enumerate(docs.content)
8✔
87
        push!(v, doc)
4✔
88
        n == length(docs.content) || push!(v, Markdown.HorizontalRule())
4✔
89
    end
4✔
90
    return Markdown.MD(v)
4✔
91
end
92

93
function formatdoc(d::DocStr)
3,342✔
94
    buffer = IOBuffer()
3,342✔
95
    for part in d.text
6,684✔
96
        formatdoc(buffer, d, part)
4,397✔
97
    end
5,452✔
98
    Markdown.MD(Any[Markdown.parse(seekstart(buffer))])
3,342✔
99
end
100
@noinline formatdoc(buffer, d, part) = print(buffer, part)
4,397✔
101

102
function parsedoc(d::DocStr)
6,397✔
103
    if d.object === nothing
6,397✔
104
        md = formatdoc(d)
3,342✔
105
        md.meta[:module] = d.data[:module]
3,342✔
106
        md.meta[:path]   = d.data[:path]
3,342✔
107
        d.object = md
3,342✔
108
    end
109
    d.object
6,397✔
110
end
111

112
## Trimming long help ("# Extended help")
113

114
struct Message  # For direct messages to the terminal
115
    msg    # AbstractString
2✔
116
    fmt    # keywords to `printstyled`
117
end
118
Message(msg) = Message(msg, ())
×
119

120
function Markdown.term(io::IO, msg::Message, columns)
×
121
    printstyled(io, msg.msg; msg.fmt...)
×
122
end
123

124
trimdocs(doc, brief::Bool) = doc
2✔
125

126
function trimdocs(md::Markdown.MD, brief::Bool)
47✔
127
    brief || return md
51✔
128
    md, trimmed = _trimdocs(md, brief)
43✔
129
    if trimmed
43✔
130
        line = extended_help_on[]
2✔
131
        line = isa(line, AbstractString) ? line : ""
2✔
132
        push!(md.content, Message("Extended help is available with `??$line`", (color=Base.info_color(), bold=true)))
2✔
133
    end
134
    return md
43✔
135
end
136

137
function _trimdocs(md::Markdown.MD, brief::Bool)
83✔
138
    content, trimmed = [], false
83✔
139
    for c in md.content
83✔
140
        if isa(c, Markdown.Header{1}) && isa(c.text, AbstractArray) && !isempty(c.text)
199✔
141
            item = c.text[1]
20✔
142
            if isa(item, AbstractString) &&
71✔
143
                lowercase(item) ∈ ("extended help",
144
                                   "extended documentation",
145
                                   "extended docs")
146
                trimmed = true
×
147
                break
2✔
148
            end
149
        end
150
        c, trm = _trimdocs(c, brief)
197✔
151
        trimmed |= trm
197✔
152
        push!(content, c)
197✔
153
    end
197✔
154
    return Markdown.MD(content, md.meta), trimmed
83✔
155
end
156

157
_trimdocs(md, brief::Bool) = md, false
157✔
158

159

160
is_tuple(expr) = false
24✔
161
is_tuple(expr::Expr) = expr.head == :tuple
×
162

163
struct Logged{F}
164
    f::F
39✔
165
    mod::Module
166
    collection::Set{Pair{Module,Symbol}}
167
end
168
function (la::Logged)(m::Module, s::Symbol)
31✔
169
    m !== la.mod && !Base.ispublic(m, s) && push!(la.collection, m => s)
31✔
170
    la.f(m, s)
31✔
171
end
172
(la::Logged)(args...) = la.f(args...)
×
173

174
function log_nonpublic_access(expr::Expr, mod::Module, internal_access::Set{Pair{Module,Symbol}})
151✔
175
    if expr.head === :. && length(expr.args) == 2 && !is_tuple(expr.args[2])
151✔
176
        Expr(:call, Logged(getproperty, mod, internal_access), log_nonpublic_access.(expr.args, (mod,), (internal_access,))...)
24✔
177
    elseif expr.head === :call && expr.args[1] === Base.Docs.Binding
127✔
178
        Expr(:call, Logged(Base.Docs.Binding, mod, internal_access), log_nonpublic_access.(expr.args[2:end], (mod,), (internal_access,))...)
15✔
179
    else
180
        Expr(expr.head, log_nonpublic_access.(expr.args, (mod,), (internal_access,))...)
112✔
181
    end
182
end
183
log_nonpublic_access(expr, ::Module, _) = expr
27✔
184

185
function insert_internal_warning(md::Markdown.MD, internal_access::Set{Pair{Module,Symbol}})
16✔
186
    if !isempty(internal_access)
16✔
187
        items = Any[Any[Markdown.Paragraph(Any[Markdown.Code("", s)])] for s in sort("$mod.$sym" for (mod, sym) in internal_access)]
12✔
188
        admonition = Markdown.Admonition("warning", "Warning", Any[
12✔
189
            Markdown.Paragraph(Any["The following bindings may be internal; they may change or be removed in future versions:"]),
190
            Markdown.List(items, -1, false)])
191
        pushfirst!(md.content, admonition)
6✔
192
    end
193
    md
16✔
194
end
195
function insert_internal_warning(other, internal_access::Set{Pair{Module,Symbol}})
×
196
    println("oops.")
×
197
    other
×
198
end
199

200
"""
201
    Docs.doc(binding, sig)
202

203
Return all documentation that matches both `binding` and `sig`.
204

205
If `getdoc` returns a non-`nothing` result on the value of the binding, then a
206
dynamic docstring is returned instead of one based on the binding itself.
207
"""
208
function doc(binding::Binding, sig::Type = Union{})
251✔
209
    if defined(binding)
251✔
210
        result = getdoc(resolve(binding), sig)
211✔
211
        result === nothing || return result
×
212
    end
213
    results, groups = DocStr[], MultiDoc[]
220✔
214
    # Lookup `binding` and `sig` for matches in all modules of the docsystem.
215
    for mod in modules
220✔
216
        dict = meta(mod; autoinit=false)
39,694✔
217
        isnothing(dict) && continue
39,694✔
218
        if haskey(dict, binding)
19,949✔
219
            multidoc = dict[binding]
102✔
220
            push!(groups, multidoc)
102✔
221
            for msig in multidoc.order
102✔
222
                sig <: msig && push!(results, multidoc.docs[msig])
102✔
223
            end
102✔
224
        end
225
    end
20,067✔
226
    if isempty(groups)
220✔
227
        # When no `MultiDoc`s are found that match `binding` then we check whether `binding`
228
        # is an alias of some other `Binding`. When it is we then re-run `doc` with that
229
        # `Binding`, otherwise if it's not an alias then we generate a summary for the
230
        # `binding` and display that to the user instead.
231
        alias = aliasof(binding)
120✔
232
        alias == binding ? summarize(alias, sig) : doc(alias, sig)
120✔
233
    else
234
        # There was at least one match for `binding` while searching. If there weren't any
235
        # matches for `sig` then we concatenate *all* the docs from the matching `Binding`s.
236
        if isempty(results)
100✔
237
            for group in groups, each in group.order
3✔
238
                push!(results, group.docs[each])
3✔
239
            end
6✔
240
        end
241
        # Get parsed docs and concatenate them.
242
        md = catdoc(mapany(parsedoc, results)...)
100✔
243
        # Save metadata in the generated markdown.
244
        if isa(md, Markdown.MD)
100✔
245
            # We don't know how to insert an internal symbol warning into non-markdown
246
            # content, so we don't.
247
            md.meta[:results] = results
98✔
248
            md.meta[:binding] = binding
98✔
249
            md.meta[:typesig] = sig
98✔
250
        end
251
        return md
100✔
252
    end
253
end
254

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

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

297
# Object Summaries.
298
# =================
299

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

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

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

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

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

434
# repl search and completions for help
435

436

437
quote_spaces(x) = any(isspace, x) ? "'" * x * "'" : x
120,563✔
438

439
function repl_search(io::IO, s::Union{Symbol,String}, mod::Module)
23✔
440
    pre = "search:"
23✔
441
    print(io, pre)
23✔
442
    printmatches(io, s, map(quote_spaces, doc_completions(s, mod)), cols = _displaysize(io)[2] - length(pre))
23✔
443
    println(io, "\n")
23✔
444
end
445

446
# TODO: document where this is used
447
repl_search(s, mod::Module) = repl_search(stdout, s, mod)
×
448

449
function repl_corrections(io::IO, s, mod::Module)
6✔
450
    print(io, "Couldn't find ")
6✔
451
    quot = any(isspace, s) ? "'" : ""
6✔
452
    print(io, quot)
6✔
453
    printstyled(io, s, color=:cyan)
6✔
454
    print(io, quot, '\n')
6✔
455
    print_correction(io, s, mod)
6✔
456
end
457
repl_corrections(s) = repl_corrections(stdout, s)
×
458

459
# inverse of latex_symbols Dict, lazily created as needed
460
const symbols_latex = Dict{String,String}()
461
function symbol_latex(s::String)
43✔
462
    if isempty(symbols_latex) && isassigned(Base.REPL_MODULE_REF)
43✔
463
        for (k,v) in Iterators.flatten((REPLCompletions.latex_symbols,
1✔
464
                                        REPLCompletions.emoji_symbols))
465
            symbols_latex[v] = k
3,699✔
466
        end
3,701✔
467

468
        # Overwrite with canonical mapping when a symbol has several completions (#39148)
469
        merge!(symbols_latex, REPLCompletions.symbols_latex_canonical)
1✔
470
    end
471

472
    return get(symbols_latex, s, "")
43✔
473
end
474
function repl_latex(io::IO, s0::String)
23✔
475
    # This has rampant `Core.Box` problems (#15276). Use the tricks of
476
    # https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-captured
477
    # We're changing some of the values so the `let` trick isn't applicable.
478
    s::String = s0
23✔
479
    latex::String = symbol_latex(s)
23✔
480
    if isempty(latex)
23✔
481
        # Decompose NFC-normalized identifier to match tab-completion
482
        # input if the first search came up empty.
483
        s = normalize(s, :NFD)
20✔
484
        latex = symbol_latex(s)
20✔
485
    end
486
    if !isempty(latex)
23✔
487
        print(io, "\"")
3✔
488
        printstyled(io, s, color=:cyan)
3✔
489
        print(io, "\" can be typed by ")
3✔
490
        printstyled(io, latex, "<tab>", color=:cyan)
3✔
491
        println(io, '\n')
3✔
492
    elseif any(c -> haskey(symbols_latex, string(c)), s)
102✔
493
        print(io, "\"")
1✔
494
        printstyled(io, s, color=:cyan)
1✔
495
        print(io, "\" can be typed by ")
1✔
496
        state::Char = '\0'
1✔
497
        with_output_color(:cyan, io) do io
1✔
498
            for c in s
1✔
499
                cstr = string(c)
8✔
500
                if haskey(symbols_latex, cstr)
8✔
501
                    latex = symbols_latex[cstr]
8✔
502
                    if length(latex) == 3 && latex[2] in ('^','_')
11✔
503
                        # coalesce runs of sub/superscripts
504
                        if state != latex[2]
12✔
505
                            '\0' != state && print(io, "<tab>")
2✔
506
                            print(io, latex[1:2])
2✔
507
                            state = latex[2]
4✔
508
                        end
509
                        print(io, latex[3])
6✔
510
                    else
511
                        if '\0' != state
2✔
512
                            print(io, "<tab>")
1✔
513
                            state = '\0'
1✔
514
                        end
515
                        print(io, latex, "<tab>")
10✔
516
                    end
517
                else
518
                    if '\0' != state
×
519
                        print(io, "<tab>")
×
520
                        state = '\0'
×
521
                    end
522
                    print(io, c)
×
523
                end
524
            end
8✔
525
            '\0' != state && print(io, "<tab>")
1✔
526
        end
527
        println(io, '\n')
20✔
528
    end
529
end
530
repl_latex(s::String) = repl_latex(stdout, s)
×
531

532
macro repl(ex, brief::Bool=false, mod::Module=Main) repl(ex; brief, mod) end
533
macro repl(io, ex, brief, mod, internal_accesses) repl(io, ex; brief, mod, internal_accesses) end
46✔
534

535
function repl(io::IO, s::Symbol; brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing)
46✔
536
    str = string(s)
23✔
537
    quote
29✔
538
        repl_latex($io, $str)
539
        repl_search($io, $str, $mod)
540
        $(if !isdefined(mod, s) && !Base.isbindingresolved(mod, s) && !haskey(keywords, s) && !Base.isoperator(s)
541
               # n.b. we call isdefined for the side-effect of resolving the binding, if possible
542
               :(repl_corrections($io, $str, $mod))
6✔
543
          end)
544
        $(_repl(s, brief, mod, internal_accesses))
545
    end
546
end
547
isregex(x) = isexpr(x, :macrocall, 3) && x.args[1] === Symbol("@r_str") && !isempty(x.args[3])
21✔
548

549
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)
42✔
550
repl(io::IO, str::AbstractString; brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing) = :(apropos($io, $str))
2✔
551
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
2✔
552
#repl(io::IO, other) = lookup_doc(other) # TODO
553

554
repl(x; brief::Bool=true, mod::Module=Main) = repl(stdout, x; brief, mod)
×
555

556
function _repl(x, brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing)
43✔
557
    if isexpr(x, :call)
36✔
558
        x = x::Expr
×
559
        # determine the types of the values
560
        kwargs = nothing
×
561
        pargs = Any[]
×
562
        for arg in x.args[2:end]
×
563
            if isexpr(arg, :parameters)
×
564
                kwargs = mapany(arg.args) do kwarg
×
565
                    if kwarg isa Symbol
566
                        kwarg = :($kwarg::Any)
567
                    elseif isexpr(kwarg, :kw)
568
                        lhs = kwarg.args[1]
569
                        rhs = kwarg.args[2]
570
                        if lhs isa Symbol
571
                            if rhs isa Symbol
572
                                kwarg.args[1] = :($lhs::(@isdefined($rhs) ? typeof($rhs) : Any))
573
                            else
574
                                kwarg.args[1] = :($lhs::typeof($rhs))
575
                            end
576
                        end
577
                    end
578
                    kwarg
579
                end
580
            elseif isexpr(arg, :kw)
×
581
                if kwargs === nothing
×
582
                    kwargs = Any[]
×
583
                end
584
                lhs = arg.args[1]
×
585
                rhs = arg.args[2]
×
586
                if lhs isa Symbol
×
587
                    if rhs isa Symbol
×
588
                        arg.args[1] = :($lhs::(@isdefined($rhs) ? typeof($rhs) : Any))
×
589
                    else
590
                        arg.args[1] = :($lhs::typeof($rhs))
×
591
                    end
592
                end
593
                push!(kwargs, arg)
×
594
            else
595
                if arg isa Symbol
×
596
                    arg = :($arg::(@isdefined($arg) ? typeof($arg) : Any))
×
597
                elseif !isexpr(arg, :(::))
×
598
                    arg = :(::typeof($arg))
×
599
                end
600
                push!(pargs, arg)
×
601
            end
602
        end
×
603
        if kwargs === nothing
×
604
            x.args = Any[x.args[1], pargs...]
×
605
        else
606
            x.args = Any[x.args[1], Expr(:parameters, kwargs...), pargs...]
×
607
        end
608
    end
609
    #docs = lookup_doc(x) # TODO
610
    docs = esc(:(@doc $x))
53✔
611
    docs = if isfield(x)
36✔
612
        quote
15✔
613
            if isa($(esc(x.args[1])), DataType)
614
                fielddoc($(esc(x.args[1])), $(esc(x.args[2])))
615
            else
616
                $docs
617
            end
618
        end
619
    else
620
        docs
41✔
621
    end
622
    docs = log_nonpublic_access(macroexpand(mod, docs), mod, internal_accesses)
43✔
623
    :(REPL.trimdocs($docs, $brief))
43✔
624
end
625

626
"""
627
    fielddoc(binding, field)
628

629
Return documentation for a particular `field` of a type if it exists.
630
"""
631
function fielddoc(binding::Binding, field::Symbol)
×
632
    for mod in modules
×
633
        dict = meta(mod; autoinit=false)
×
634
        isnothing(dict) && continue
×
635
        if haskey(dict, binding)
×
636
            multidoc = dict[binding]
×
637
            if haskey(multidoc.docs, Union{})
×
638
                fields = multidoc.docs[Union{}].data[:fields]
×
639
                if haskey(fields, field)
×
640
                    doc = fields[field]
×
641
                    return isa(doc, Markdown.MD) ? doc : Markdown.parse(doc)
×
642
                end
643
            end
644
        end
645
    end
×
646
    fields = join(["`$f`" for f in fieldnames(resolve(binding))], ", ", ", and ")
×
647
    fields = isempty(fields) ? "no fields" : "fields $fields"
×
648
    Markdown.parse("`$(resolve(binding))` has $fields.")
×
649
end
650

651
# As with the additional `doc` methods, this converts an object to a `Binding` first.
652
fielddoc(object, field::Symbol) = fielddoc(aliasof(object, typeof(object)), field)
×
653

654

655
# Search & Rescue
656
# Utilities for correcting user mistakes and (eventually)
657
# doing full documentation searches from the repl.
658

659
# Fuzzy Search Algorithm
660

661
function matchinds(needle, haystack; acronym::Bool = false)
474✔
662
    chars = collect(needle)
237✔
663
    is = Int[]
237✔
664
    lastc = '\0'
×
665
    for (i, char) in enumerate(haystack)
474✔
666
        while !isempty(chars) && isspace(first(chars))
2,056✔
667
            popfirst!(chars) # skip spaces
4✔
668
        end
4✔
669
        isempty(chars) && break
1,039✔
670
        if lowercase(char) == lowercase(chars[1]) &&
1,142✔
671
           (!acronym || !isletter(lastc))
672
            push!(is, i)
311✔
673
            popfirst!(chars)
311✔
674
        end
675
        lastc = char
×
676
    end
1,013✔
677
    return is
237✔
678
end
679

680
longer(x, y) = length(x) ≥ length(y) ? (x, true) : (y, false)
174✔
681

682
bestmatch(needle, haystack) =
174✔
683
    longer(matchinds(needle, haystack, acronym = true),
684
           matchinds(needle, haystack))
685

686
# Optimal string distance: Counts the minimum number of insertions, deletions,
687
# transpositions or substitutions to go from one string to the other.
688
function string_distance(a::AbstractString, lena::Integer, b::AbstractString, lenb::Integer)
98,265✔
689
    if lena > lenb
98,265✔
690
        a, b = b, a
×
691
        lena, lenb = lenb, lena
×
692
    end
693
    start = 0
98,265✔
694
    for (i, j) in zip(a, b)
195,160✔
695
        if a == b
97,030✔
696
            start += 1
183✔
697
        else
698
            break
×
699
        end
700
    end
318✔
701
    start == lena && return lenb - start
98,265✔
702
    vzero = collect(1:(lenb - start))
967,580✔
703
    vone = similar(vzero)
96,847✔
704
    prev_a, prev_b = first(a), first(b)
96,847✔
705
    current = 0
×
706
    for (i, ai) in enumerate(a)
193,694✔
707
        i > start || (prev_a = ai; continue)
304,903✔
708
        left = i - start - 1
304,903✔
709
        current = i - start
304,903✔
710
        transition_next = 0
×
711
        for (j, bj) in enumerate(b)
609,806✔
712
            j > start || (prev_b = bj; continue)
3,241,900✔
713
            # No need to look beyond window of lower right diagonal
714
            above = current
×
715
            this_transition = transition_next
×
716
            transition_next = vone[j - start]
3,241,900✔
717
            vone[j - start] = current = left
3,241,900✔
718
            left = vzero[j - start]
3,241,900✔
719
            if ai != bj
3,241,900✔
720
                # Minimum between substitution, deletion and insertion
721
                current = min(current + 1, above + 1, left + 1)
3,140,844✔
722
                if i > start + 1 && j > start + 1 && ai == prev_b && prev_a == bj
3,140,844✔
723
                    current = min(current, (this_transition += 1))
3,978✔
724
                end
725
            end
726
            vzero[j - start] = current
3,241,900✔
727
            prev_b = bj
×
728
        end
6,178,897✔
729
        prev_a = ai
304,903✔
730
    end
512,959✔
731
    current
96,847✔
732
end
733

734
function fuzzyscore(needle::AbstractString, haystack::AbstractString)
12,259✔
735
    lena, lenb = length(needle), length(haystack)
98,265✔
736
    1 - (string_distance(needle, lena, haystack, lenb) / max(lena, lenb))
98,265✔
737
end
738

739
function fuzzysort(search::String, candidates::Vector{String})
43✔
740
    scores = map(cand -> fuzzyscore(search, cand), candidates)
86,049✔
741
    candidates[sortperm(scores)] |> reverse
43✔
742
end
743

744
# Levenshtein Distance
745

746
function levenshtein(s1, s2)
12,561✔
747
    a, b = collect(s1), collect(s2)
12,561✔
748
    m = length(a)
12,561✔
749
    n = length(b)
12,561✔
750
    d = Matrix{Int}(undef, m+1, n+1)
12,561✔
751

752
    d[1:m+1, 1] = 0:m
72,042✔
753
    d[1, 1:n+1] = 0:n
129,980✔
754

755
    for i = 1:m, j = 1:n
59,452✔
756
        d[i+1,j+1] = min(d[i  , j+1] + 1,
415,518✔
757
                         d[i+1, j  ] + 1,
758
                         d[i  , j  ] + (a[i] != b[j]))
759
    end
449,877✔
760

761
    return d[m+1, n+1]
12,561✔
762
end
763

764
function levsort(search::String, candidates::Vector{String})
6✔
765
    scores = map(cand -> (Float64(levenshtein(search, cand)), -fuzzyscore(search, cand)), candidates)
12,126✔
766
    candidates = candidates[sortperm(scores)]
6✔
767
    i = 0
6✔
768
    for outer i = 1:length(candidates)
12✔
769
        levenshtein(search, candidates[i]) > 3 && break
441✔
770
    end
435✔
771
    return candidates[1:i]
6✔
772
end
773

774
# Result printing
775

776
function printmatch(io::IO, word, match)
118✔
777
    is, _ = bestmatch(word, match)
174✔
778
    for (i, char) = enumerate(match)
236✔
779
        if i in is
1,499✔
780
            printstyled(io, char, bold=true)
244✔
781
        else
782
            print(io, char)
288✔
783
        end
784
    end
532✔
785
end
786

787
function printmatches(io::IO, word, matches; cols::Int = _displaysize(io)[2])
46✔
788
    total = 0
23✔
789
    for match in matches
23✔
790
        total + length(match) + 1 > cols && break
141✔
791
        fuzzyscore(word, match) < 0.5 && break
135✔
792
        print(io, " ")
118✔
793
        printmatch(io, word, match)
118✔
794
        total += length(match) + 1
118✔
795
    end
141✔
796
end
797

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

800
function print_joined_cols(io::IO, ss::Vector{String}, delim = "", last = delim; cols::Int = _displaysize(io)[2])
12✔
801
    i = 0
6✔
802
    total = 0
6✔
803
    for outer i = 1:length(ss)
12✔
804
        total += length(ss[i])
64✔
805
        total + max(i-2,0)*length(delim) + (i>1 ? 1 : 0)*length(last) > cols && (i-=1; break)
68✔
806
    end
60✔
807
    join(io, ss[1:i], delim, last)
6✔
808
end
809

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

812
function print_correction(io::IO, word::String, mod::Module)
6✔
813
    cors = map(quote_spaces, levsort(word, accessible(mod)))
6✔
814
    pre = "Perhaps you meant "
6✔
815
    print(io, pre)
6✔
816
    print_joined_cols(io, cors, ", ", " or "; cols = _displaysize(io)[2] - length(pre))
6✔
817
    println(io)
6✔
818
    return
6✔
819
end
820

821
# TODO: document where this is used
822
print_correction(word, mod::Module) = print_correction(stdout, word, mod)
×
823

824
# Completion data
825

826

827
moduleusings(mod) = ccall(:jl_module_usings, Any, (Any,), mod)
49✔
828

829
filtervalid(names) = filter(x->!occursin(r"#", x), map(string, names))
149,252✔
830

831
accessible(mod::Module) =
49✔
832
    Symbol[filter!(s -> !Base.isdeprecated(mod, s), names(mod, all=true, imported=true));
100,891✔
833
           map(names, moduleusings(mod))...;
834
           collect(keys(Base.Docs.keywords))] |> unique |> filtervalid
835

836
function doc_completions(name, mod::Module=Main)
46✔
837
    res = fuzzysort(name, accessible(mod))
46✔
838

839
    # to insert an entry like `raw""` for `"@raw_str"` in `res`
840
    ms = match.(r"^@(.*?)_str$", res)
86✔
841
    idxs = findall(!isnothing, ms)
86✔
842

843
    # avoid messing up the order while inserting
844
    for i in reverse!(idxs)
43✔
845
        c = only((ms[i]::AbstractMatch).captures)
1,122✔
846
        insert!(res, i, "$(c)\"\"")
1,122✔
847
    end
604✔
848
    res
43✔
849
end
850
doc_completions(name::Symbol) = doc_completions(string(name), mod)
×
851

852

853
# Searching and apropos
854

855
# Docsearch simply returns true or false if an object contains the given needle
856
docsearch(haystack::AbstractString, needle) = occursin(needle, haystack)
6,479✔
857
docsearch(haystack::Symbol, needle) = docsearch(string(haystack), needle)
×
858
docsearch(::Nothing, needle) = false
×
859
function docsearch(haystack::Array, needle)
×
860
    for elt in haystack
×
861
        docsearch(elt, needle) && return true
×
862
    end
×
863
    false
×
864
end
865
function docsearch(haystack, needle)
×
866
    @warn "Unable to search documentation of type $(typeof(haystack))" maxlog=1
×
867
    false
×
868
end
869

870
## Searching specific documentation objects
871
function docsearch(haystack::MultiDoc, needle)
5,862✔
872
    for v in values(haystack.docs)
11,724✔
873
        docsearch(v, needle) && return true
6,289✔
874
    end
3,435✔
875
    false
2,581✔
876
end
877

878
function docsearch(haystack::DocStr, needle)
6,289✔
879
    docsearch(parsedoc(haystack), needle) && return true
6,289✔
880
    if haskey(haystack.data, :fields)
3,008✔
881
        for doc in values(haystack.data[:fields])
274✔
882
            docsearch(doc, needle) && return true
10✔
883
        end
10✔
884
    end
885
    false
3,008✔
886
end
887

888
## doc search
889

890
## Markdown search simply strips all markup and searches plain text version
891
docsearch(haystack::Markdown.MD, needle) = docsearch(stripmd(haystack.content), needle)
6,469✔
892

893
"""
894
    stripmd(x)
895

896
Strip all Markdown markup from x, leaving the result in plain text. Used
897
internally by apropos to make docstrings containing more than one markdown
898
element searchable.
899
"""
900
stripmd(@nospecialize x) = string(x) # for random objects interpolated into the docstring
×
901
stripmd(x::AbstractString) = x  # base case
93,033✔
902
stripmd(x::Nothing) = " "
42✔
903
stripmd(x::Vector) = string(map(stripmd, x)...)
33,266✔
904

905
stripmd(x::Markdown.BlockQuote) = "$(stripmd(x.content))"
×
906
stripmd(x::Markdown.Admonition) = "$(stripmd(x.content))"
1,009✔
907
stripmd(x::Markdown.Bold) = "$(stripmd(x.text))"
41✔
908
stripmd(x::Markdown.Code) = "$(stripmd(x.code))"
43,063✔
909
stripmd(x::Markdown.Header) = stripmd(x.text)
2,467✔
910
stripmd(x::Markdown.HorizontalRule) = " "
68✔
911
stripmd(x::Markdown.Image) = "$(stripmd(x.alt)) $(x.url)"
12✔
912
stripmd(x::Markdown.Italic) = "$(stripmd(x.text))"
299✔
913
stripmd(x::Markdown.LaTeX) = "$(x.formula)"
369✔
914
stripmd(x::Markdown.LineBreak) = " "
34✔
915
stripmd(x::Markdown.Link) = "$(stripmd(x.text)) $(x.url)"
5,800✔
916
stripmd(x::Markdown.List) = join(map(stripmd, x.items), " ")
634✔
917
stripmd(x::Markdown.MD) = join(map(stripmd, x.content), " ")
6,445✔
918
stripmd(x::Markdown.Paragraph) = stripmd(x.content)
14,308✔
919
stripmd(x::Markdown.Footnote) = "$(stripmd(x.id)) $(stripmd(x.text))"
82✔
920
stripmd(x::Markdown.Table) =
34✔
921
    join([join(map(stripmd, r), " ") for r in x.rows], " ")
922

923
"""
924
    apropos([io::IO=stdout], pattern::Union{AbstractString,Regex})
925

926
Search available docstrings for entries containing `pattern`.
927

928
When `pattern` is a string, case is ignored. Results are printed to `io`.
929

930
`apropos` can be called from the help mode in the REPL by wrapping the query in double quotes:
931
```
932
help?> "pattern"
933
```
934
"""
935
apropos(string) = apropos(stdout, string)
×
936
apropos(io::IO, string) = apropos(io, Regex("\\Q$string", "i"))
1✔
937

938
function apropos(io::IO, needle::Regex)
2✔
939
    for mod in modules
2✔
940
        # Module doc might be in README.md instead of the META dict
941
        docsearch(doc(mod), needle) && println(io, mod)
180✔
942
        dict = meta(mod; autoinit=false)
360✔
943
        isnothing(dict) && continue
360✔
944
        for (k, v) in dict
360✔
945
            docsearch(v, needle) && println(io, k)
5,862✔
946
        end
5,862✔
947
    end
180✔
948
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