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

JuliaLang / julia / #37616

10 Sep 2023 01:51AM UTC coverage: 86.489% (+0.3%) from 86.196%
#37616

push

local

web-flow
Make _global_logstate a typed global (#51257)

73902 of 85447 relevant lines covered (86.49%)

13068259.04 hits per line

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

90.79
/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
helpmode(io::IO, line::AbstractString, mod::Module=Main) = :($REPL.insert_hlines($(REPL._helpmode(io, line, mod))))
10✔
24
helpmode(line::AbstractString, mod::Module=Main) = helpmode(stdout, line, mod)
×
25

26
# A hack to make the line entered at the REPL available at trimdocs without
27
# passing the string through the entire mechanism.
28
const extended_help_on = Ref{Any}(nothing)
29

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

73
# Print vertical lines along each docstring if there are multiple docs
74
function insert_hlines(docs)
8✔
75
    if !isa(docs, Markdown.MD) || !haskey(docs.meta, :results) || isempty(docs.meta[:results])
13✔
76
        return docs
7✔
77
    end
78
    docs = docs::Markdown.MD
×
79
    v = Any[]
1✔
80
    for (n, doc) in enumerate(docs.content)
2✔
81
        push!(v, doc)
1✔
82
        n == length(docs.content) || push!(v, Markdown.HorizontalRule())
1✔
83
    end
1✔
84
    return Markdown.MD(v)
1✔
85
end
86

87
function formatdoc(d::DocStr)
6,806✔
88
    buffer = IOBuffer()
6,806✔
89
    for part in d.text
13,612✔
90
        formatdoc(buffer, d, part)
8,925✔
91
    end
11,044✔
92
    Markdown.MD(Any[Markdown.parse(seekstart(buffer))])
6,806✔
93
end
94
@noinline formatdoc(buffer, d, part) = print(buffer, part)
8,923✔
95

96
function parsedoc(d::DocStr)
13,509✔
97
    if d.object === nothing
13,509✔
98
        md = formatdoc(d)
6,806✔
99
        md.meta[:module] = d.data[:module]
6,806✔
100
        md.meta[:path]   = d.data[:path]
6,806✔
101
        d.object = md
6,806✔
102
    end
103
    d.object
13,509✔
104
end
105

106
## Trimming long help ("# Extended help")
107

108
struct Message  # For direct messages to the terminal
109
    msg    # AbstractString
2✔
110
    fmt    # keywords to `printstyled`
111
end
112
Message(msg) = Message(msg, ())
×
113

114
function Markdown.term(io::IO, msg::Message, columns)
×
115
    printstyled(io, msg.msg; msg.fmt...)
×
116
end
117

118
trimdocs(doc, brief::Bool) = doc
2✔
119

120
function trimdocs(md::Markdown.MD, brief::Bool)
42✔
121
    brief || return md
51✔
122
    md, trimmed = _trimdocs(md, brief)
33✔
123
    if trimmed
33✔
124
        line = extended_help_on[]
2✔
125
        line = isa(line, AbstractString) ? line : ""
2✔
126
        push!(md.content, Message("Extended help is available with `??$line`", (color=Base.info_color(), bold=true)))
2✔
127
    end
128
    return md
33✔
129
end
130

131
function _trimdocs(md::Markdown.MD, brief::Bool)
67✔
132
    content, trimmed = [], false
67✔
133
    for c in md.content
67✔
134
        if isa(c, Markdown.Header{1}) && isa(c.text, AbstractArray) && !isempty(c.text)
165✔
135
            item = c.text[1]
13✔
136
            if isa(item, AbstractString) &&
43✔
137
                lowercase(item) ∈ ("extended help",
138
                                   "extended documentation",
139
                                   "extended docs")
140
                trimmed = true
×
141
                break
2✔
142
            end
143
        end
144
        c, trm = _trimdocs(c, brief)
163✔
145
        trimmed |= trm
163✔
146
        push!(content, c)
163✔
147
    end
163✔
148
    return Markdown.MD(content, md.meta), trimmed
67✔
149
end
150

151
_trimdocs(md, brief::Bool) = md, false
129✔
152

153
"""
154
    Docs.doc(binding, sig)
155

156
Return all documentation that matches both `binding` and `sig`.
157

158
If `getdoc` returns a non-`nothing` result on the value of the binding, then a
159
dynamic docstring is returned instead of one based on the binding itself.
160
"""
161
function doc(binding::Binding, sig::Type = Union{})
590✔
162
    if defined(binding)
590✔
163
        result = getdoc(resolve(binding), sig)
499✔
164
        result === nothing || return result
15✔
165
    end
166
    results, groups = DocStr[], MultiDoc[]
506✔
167
    # Lookup `binding` and `sig` for matches in all modules of the docsystem.
168
    for mod in modules
506✔
169
        dict = meta(mod; autoinit=false)
96,770✔
170
        isnothing(dict) && continue
96,770✔
171
        if haskey(dict, binding)
48,645✔
172
            multidoc = dict[binding]
260✔
173
            push!(groups, multidoc)
260✔
174
            for msig in multidoc.order
260✔
175
                sig <: msig && push!(results, multidoc.docs[msig])
307✔
176
            end
307✔
177
        end
178
    end
48,891✔
179
    if isempty(groups)
506✔
180
        # When no `MultiDoc`s are found that match `binding` then we check whether `binding`
181
        # is an alias of some other `Binding`. When it is we then re-run `doc` with that
182
        # `Binding`, otherwise if it's not an alias then we generate a summary for the
183
        # `binding` and display that to the user instead.
184
        alias = aliasof(binding)
254✔
185
        alias == binding ? summarize(alias, sig) : doc(alias, sig)
254✔
186
    else
187
        # There was at least one match for `binding` while searching. If there weren't any
188
        # matches for `sig` then we concatenate *all* the docs from the matching `Binding`s.
189
        if isempty(results)
252✔
190
            for group in groups, each in group.order
13✔
191
                push!(results, group.docs[each])
17✔
192
            end
30✔
193
        end
194
        # Get parsed docs and concatenate them.
195
        md = catdoc(mapany(parsedoc, results)...)
252✔
196
        # Save metadata in the generated markdown.
197
        if isa(md, Markdown.MD)
252✔
198
            md.meta[:results] = results
249✔
199
            md.meta[:binding] = binding
249✔
200
            md.meta[:typesig] = sig
249✔
201
        end
202
        return md
252✔
203
    end
204
end
205

206
# Some additional convenience `doc` methods that take objects rather than `Binding`s.
207
doc(obj::UnionAll) = doc(Base.unwrap_unionall(obj))
2✔
208
doc(object, sig::Type = Union{}) = doc(aliasof(object, typeof(object)), sig)
779✔
209
doc(object, sig...)              = doc(object, Tuple{sig...})
×
210

211
function lookup_doc(ex)
129✔
212
    if isa(ex, Expr) && ex.head !== :(.) && Base.isoperator(ex.head)
135✔
213
        # handle syntactic operators, e.g. +=, ::, .=
214
        ex = ex.head
×
215
    end
216
    if haskey(keywords, ex)
129✔
217
        return parsedoc(keywords[ex])
6✔
218
    elseif Meta.isexpr(ex, :incomplete)
92✔
219
        return :($(Markdown.md"No documentation found."))
1✔
220
    elseif !isa(ex, Expr) && !isa(ex, Symbol)
91✔
221
        return :($(doc)($(typeof)($(esc(ex)))))
2✔
222
    end
223
    if isa(ex, Symbol) && Base.isoperator(ex)
145✔
224
        str = string(ex)
6✔
225
        isdotted = startswith(str, ".")
12✔
226
        if endswith(str, "=") && Base.operator_precedence(ex) == Base.prec_assignment && ex !== :(:=)
6✔
227
            op = chop(str)
2✔
228
            eq = isdotted ? ".=" : "="
2✔
229
            return Markdown.parse("`x $op= y` is a synonym for `x $eq x $op y`")
2✔
230
        elseif isdotted && ex !== :(..)
4✔
231
            op = str[2:end]
2✔
232
            if op in ("&&", "||")
3✔
233
                return Markdown.parse("`x $ex y` broadcasts the boolean operator `$op` to `x` and `y`. See [`broadcast`](@ref).")
×
234
            else
235
                return Markdown.parse("`x $ex y` is akin to `broadcast($op, x, y)`. See [`broadcast`](@ref).")
1✔
236
            end
237
        end
238
    end
239
    binding = esc(bindingexpr(namify(ex)))
140✔
240
    if isexpr(ex, :call) || isexpr(ex, :macrocall) || isexpr(ex, :where)
166✔
241
        sig = esc(signature(ex))
35✔
242
        :($(doc)($binding, $sig))
35✔
243
    else
244
        :($(doc)($binding))
82✔
245
    end
246
end
247

248
# Object Summaries.
249
# =================
250

251
function summarize(binding::Binding, sig)
254✔
252
    io = IOBuffer()
254✔
253
    if defined(binding)
254✔
254
        binding_res = resolve(binding)
243✔
255
        !isa(binding_res, Module) && println(io, "No documentation found.\n")
243✔
256
        summarize(io, binding_res, binding)
243✔
257
    else
258
        println(io, "No documentation found.\n")
11✔
259
        quot = any(isspace, sprint(print, binding)) ? "'" : ""
11✔
260
        if Base.isbindingresolved(binding.mod, binding.var)
11✔
261
            println(io, "Binding ", quot, "`", binding, "`", quot, " exists, but has not been assigned a value.")
1✔
262
        else
263
            println(io, "Binding ", quot, "`", binding, "`", quot, " does not exist.")
10✔
264
        end
265
    end
266
    md = Markdown.parse(seekstart(io))
254✔
267
    # Save metadata in the generated markdown.
268
    md.meta[:results] = DocStr[]
254✔
269
    md.meta[:binding] = binding
254✔
270
    md.meta[:typesig] = sig
254✔
271
    return md
254✔
272
end
273

274
function summarize(io::IO, λ::Function, binding::Binding)
4✔
275
    kind = startswith(string(binding.var), '@') ? "macro" : "`Function`"
4✔
276
    println(io, "`", binding, "` is a ", kind, ".")
4✔
277
    println(io, "```\n", methods(λ), "\n```")
4✔
278
end
279

280
function summarize(io::IO, TT::Type, binding::Binding)
17✔
281
    println(io, "# Summary")
17✔
282
    T = Base.unwrap_unionall(TT)
17✔
283
    if T isa DataType
17✔
284
        println(io, "```")
14✔
285
        print(io,
23✔
286
            Base.isabstracttype(T) ? "abstract type " :
287
            Base.ismutabletype(T)  ? "mutable struct " :
288
            Base.isstructtype(T) ? "struct " :
289
            "primitive type ")
290
        supert = supertype(T)
14✔
291
        println(io, T)
14✔
292
        println(io, "```")
14✔
293
        if !Base.isabstracttype(T) && T.name !== Tuple.name && !isempty(fieldnames(T))
14✔
294
            println(io, "# Fields")
6✔
295
            println(io, "```")
6✔
296
            pad = maximum(length(string(f)) for f in fieldnames(T))
6✔
297
            for (f, t) in zip(fieldnames(T), fieldtypes(T))
6✔
298
                println(io, rpad(f, pad), " :: ", t)
11✔
299
            end
11✔
300
            println(io, "```")
6✔
301
        end
302
        subt = subtypes(TT)
14✔
303
        if !isempty(subt)
19✔
304
            println(io, "# Subtypes")
5✔
305
            println(io, "```")
5✔
306
            for t in subt
5✔
307
                println(io, Base.unwrap_unionall(t))
12✔
308
            end
12✔
309
            println(io, "```")
5✔
310
        end
311
        if supert != Any
14✔
312
            println(io, "# Supertype Hierarchy")
10✔
313
            println(io, "```")
10✔
314
            Base.show_supertypes(io, T)
10✔
315
            println(io)
10✔
316
            println(io, "```")
10✔
317
        end
318
    elseif T isa Union
3✔
319
        println(io, "`", binding, "` is of type `", typeof(TT), "`.\n")
3✔
320
        println(io, "# Union Composed of Types")
3✔
321
        for T1 in Base.uniontypes(T)
3✔
322
            println(io, " - `", Base.rewrap_unionall(T1, TT), "`")
10✔
323
        end
10✔
324
    else # unreachable?
325
        println(io, "`", binding, "` is of type `", typeof(TT), "`.\n")
×
326
    end
327
end
328

329
function find_readme(m::Module)::Union{String, Nothing}
222✔
330
    mpath = pathof(m)
222✔
331
    isnothing(mpath) && return nothing
258✔
332
    !isfile(mpath) && return nothing # modules in sysimage, where src files are omitted
36✔
333
    path = dirname(mpath)
36✔
334
    top_path = pkgdir(m)
72✔
335
    while true
72✔
336
        for file in readdir(path; join=true, sort=true)
72✔
337
            isfile(file) && (basename(lowercase(file)) in ["readme.md", "readme"]) || continue
868✔
338
            return file
16✔
339
        end
412✔
340
        path == top_path && break # go no further than pkgdir
112✔
341
        path = dirname(path) # work up through nested modules
36✔
342
    end
36✔
343
    return nothing
20✔
344
end
345
function summarize(io::IO, m::Module, binding::Binding; nlines::Int = 200)
444✔
346
    readme_path = find_readme(m)
222✔
347
    if isnothing(readme_path)
238✔
348
        println(io, "No docstring or readme file found for module `$m`.\n")
206✔
349
    else
350
        println(io, "No docstring found for module `$m`.")
16✔
351
    end
352
    exports = filter!(!=(nameof(m)), names(m))
222✔
353
    if isempty(exports)
222✔
354
        println(io, "Module does not export any names.")
65✔
355
    else
356
        println(io, "# Exported names")
157✔
357
        print(io, "  `")
157✔
358
        join(io, exports, "`, `")
157✔
359
        println(io, "`\n")
157✔
360
    end
361
    if !isnothing(readme_path)
238✔
362
        readme_lines = readlines(readme_path)
16✔
363
        isempty(readme_lines) && return  # don't say we are going to print empty file
16✔
364
        println(io, "# Displaying contents of readme found at `$(readme_path)`")
16✔
365
        for line in first(readme_lines, nlines)
16✔
366
            println(io, line)
2,560✔
367
        end
2,576✔
368
        length(readme_lines) > nlines && println(io, "\n[output truncated to first $nlines lines]")
222✔
369
    end
370
end
371

372
function summarize(io::IO, @nospecialize(T), binding::Binding)
2✔
373
    T = typeof(T)
2✔
374
    println(io, "`", binding, "` is of type `", T, "`.\n")
2✔
375
    summarize(io, T, binding)
2✔
376
end
377

378
# repl search and completions for help
379

380

381
quote_spaces(x) = any(isspace, x) ? "'" * x * "'" : x
56,216✔
382

383
function repl_search(io::IO, s::Union{Symbol,String}, mod::Module)
22✔
384
    pre = "search:"
22✔
385
    print(io, pre)
22✔
386
    printmatches(io, s, map(quote_spaces, doc_completions(s, mod)), cols = _displaysize(io)[2] - length(pre))
22✔
387
    println(io, "\n")
22✔
388
end
389

390
# TODO: document where this is used
391
repl_search(s, mod::Module) = repl_search(stdout, s, mod)
×
392

393
function repl_corrections(io::IO, s, mod::Module)
8✔
394
    print(io, "Couldn't find ")
8✔
395
    quot = any(isspace, s) ? "'" : ""
8✔
396
    print(io, quot)
8✔
397
    printstyled(io, s, color=:cyan)
8✔
398
    print(io, quot, '\n')
8✔
399
    print_correction(io, s, mod)
8✔
400
end
401
repl_corrections(s) = repl_corrections(stdout, s)
×
402

403
# inverse of latex_symbols Dict, lazily created as needed
404
const symbols_latex = Dict{String,String}()
405
function symbol_latex(s::String)
47✔
406
    if isempty(symbols_latex) && isassigned(Base.REPL_MODULE_REF)
47✔
407
        for (k,v) in Iterators.flatten((REPLCompletions.latex_symbols,
2✔
408
                                        REPLCompletions.emoji_symbols))
409
            symbols_latex[v] = k
7,398✔
410
        end
7,402✔
411

412
        # Overwrite with canonical mapping when a symbol has several completions (#39148)
413
        merge!(symbols_latex, REPLCompletions.symbols_latex_canonical)
2✔
414
    end
415

416
    return get(symbols_latex, s, "")
47✔
417
end
418
function repl_latex(io::IO, s0::String)
26✔
419
    # This has rampant `Core.Box` problems (#15276). Use the tricks of
420
    # https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-captured
421
    # We're changing some of the values so the `let` trick isn't applicable.
422
    s::String = s0
26✔
423
    latex::String = symbol_latex(s)
26✔
424
    if isempty(latex)
26✔
425
        # Decompose NFC-normalized identifier to match tab-completion
426
        # input if the first search came up empty.
427
        s = normalize(s, :NFD)
21✔
428
        latex = symbol_latex(s)
21✔
429
    end
430
    if !isempty(latex)
26✔
431
        print(io, "\"")
5✔
432
        printstyled(io, s, color=:cyan)
5✔
433
        print(io, "\" can be typed by ")
5✔
434
        printstyled(io, latex, "<tab>", color=:cyan)
5✔
435
        println(io, '\n')
5✔
436
    elseif any(c -> haskey(symbols_latex, string(c)), s)
106✔
437
        print(io, "\"")
3✔
438
        printstyled(io, s, color=:cyan)
3✔
439
        print(io, "\" can be typed by ")
3✔
440
        state::Char = '\0'
3✔
441
        with_output_color(:cyan, io) do io
3✔
442
            for c in s
5✔
443
                cstr = string(c)
13✔
444
                if haskey(symbols_latex, cstr)
13✔
445
                    latex = symbols_latex[cstr]
11✔
446
                    if length(latex) == 3 && latex[2] in ('^','_')
15✔
447
                        # coalesce runs of sub/superscripts
448
                        if state != latex[2]
14✔
449
                            '\0' != state && print(io, "<tab>")
3✔
450
                            print(io, latex[1:2])
3✔
451
                            state = latex[2]
6✔
452
                        end
453
                        print(io, latex[3])
7✔
454
                    else
455
                        if '\0' != state
4✔
456
                            print(io, "<tab>")
1✔
457
                            state = '\0'
1✔
458
                        end
459
                        print(io, latex, "<tab>")
15✔
460
                    end
461
                else
462
                    if '\0' != state
2✔
463
                        print(io, "<tab>")
×
464
                        state = '\0'
×
465
                    end
466
                    print(io, c)
2✔
467
                end
468
            end
13✔
469
            '\0' != state && print(io, "<tab>")
3✔
470
        end
471
        println(io, '\n')
21✔
472
    end
473
end
474
repl_latex(s::String) = repl_latex(stdout, s)
×
475

476
macro repl(ex, brief::Bool=false, mod::Module=Main) repl(ex; brief, mod) end
12✔
477
macro repl(io, ex, brief, mod) repl(io, ex; brief, mod) end
36✔
478

479
function repl(io::IO, s::Symbol; brief::Bool=true, mod::Module=Main)
44✔
480
    str = string(s)
22✔
481
    quote
30✔
482
        repl_latex($io, $str)
483
        repl_search($io, $str, $mod)
484
        $(if !isdefined(mod, s) && !Base.isbindingresolved(mod, s) && !haskey(keywords, s) && !Base.isoperator(s)
485
               # n.b. we call isdefined for the side-effect of resolving the binding, if possible
486
               :(repl_corrections($io, $str, $mod))
8✔
487
          end)
488
        $(_repl(s, brief))
489
    end
490
end
491
isregex(x) = isexpr(x, :macrocall, 3) && x.args[1] === Symbol("@r_str") && !isempty(x.args[3])
17✔
492

493
repl(io::IO, ex::Expr; brief::Bool=true, mod::Module=Main) = isregex(ex) ? :(apropos($io, $ex)) : _repl(ex, brief)
34✔
494
repl(io::IO, str::AbstractString; brief::Bool=true, mod::Module=Main) = :(apropos($io, $str))
2✔
495
repl(io::IO, other; brief::Bool=true, mod::Module=Main) = esc(:(@doc $other))
4✔
496
#repl(io::IO, other) = lookup_doc(other) # TODO
497

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

500
function _repl(x, brief::Bool=true)
46✔
501
    if isexpr(x, :call)
24✔
502
        x = x::Expr
6✔
503
        # determine the types of the values
504
        kwargs = nothing
6✔
505
        pargs = Any[]
6✔
506
        for arg in x.args[2:end]
6✔
507
            if isexpr(arg, :parameters)
14✔
508
                kwargs = mapany(arg.args) do kwarg
×
509
                    if kwarg isa Symbol
510
                        kwarg = :($kwarg::Any)
511
                    elseif isexpr(kwarg, :kw)
512
                        lhs = kwarg.args[1]
513
                        rhs = kwarg.args[2]
514
                        if lhs isa Symbol
515
                            if rhs isa Symbol
516
                                kwarg.args[1] = :($lhs::(@isdefined($rhs) ? typeof($rhs) : Any))
517
                            else
518
                                kwarg.args[1] = :($lhs::typeof($rhs))
519
                            end
520
                        end
521
                    end
522
                    kwarg
523
                end
524
            elseif isexpr(arg, :kw)
14✔
525
                if kwargs === nothing
1✔
526
                    kwargs = Any[]
1✔
527
                end
528
                lhs = arg.args[1]
1✔
529
                rhs = arg.args[2]
1✔
530
                if lhs isa Symbol
1✔
531
                    if rhs isa Symbol
1✔
532
                        arg.args[1] = :($lhs::(@isdefined($rhs) ? typeof($rhs) : Any))
×
533
                    else
534
                        arg.args[1] = :($lhs::typeof($rhs))
1✔
535
                    end
536
                end
537
                push!(kwargs, arg)
1✔
538
            else
539
                if arg isa Symbol
8✔
540
                    arg = :($arg::(@isdefined($arg) ? typeof($arg) : Any))
1✔
541
                elseif !isexpr(arg, :(::))
11✔
542
                    arg = :(::typeof($arg))
4✔
543
                end
544
                push!(pargs, arg)
8✔
545
            end
546
        end
15✔
547
        if kwargs === nothing
6✔
548
            x.args = Any[x.args[1], pargs...]
5✔
549
        else
550
            x.args = Any[x.args[1], Expr(:parameters, kwargs...), pargs...]
1✔
551
        end
552
    end
553
    #docs = lookup_doc(x) # TODO
554
    docs = esc(:(@doc $x))
46✔
555
    docs = if isfield(x)
20✔
556
        quote
7✔
557
            if isa($(esc(x.args[1])), DataType)
558
                fielddoc($(esc(x.args[1])), $(esc(x.args[2])))
559
            else
560
                $docs
561
            end
562
        end
563
    else
564
        docs
33✔
565
    end
566
    :(REPL.trimdocs($docs, $brief))
42✔
567
end
568

569
"""
570
    fielddoc(binding, field)
571

572
Return documentation for a particular `field` of a type if it exists.
573
"""
574
function fielddoc(binding::Binding, field::Symbol)
1✔
575
    for mod in modules
1✔
576
        dict = meta(mod; autoinit=false)
180✔
577
        isnothing(dict) && continue
180✔
578
        if haskey(dict, binding)
91✔
579
            multidoc = dict[binding]
1✔
580
            if haskey(multidoc.docs, Union{})
1✔
581
                fields = multidoc.docs[Union{}].data[:fields]
1✔
582
                if haskey(fields, field)
1✔
583
                    doc = fields[field]
1✔
584
                    return isa(doc, Markdown.MD) ? doc : Markdown.parse(doc)
1✔
585
                end
586
            end
587
        end
588
    end
89✔
589
    fields = join(["`$f`" for f in fieldnames(resolve(binding))], ", ", ", and ")
×
590
    fields = isempty(fields) ? "no fields" : "fields $fields"
×
591
    Markdown.parse("`$(resolve(binding))` has $fields.")
×
592
end
593

594
# As with the additional `doc` methods, this converts an object to a `Binding` first.
595
fielddoc(object, field::Symbol) = fielddoc(aliasof(object, typeof(object)), field)
1✔
596

597

598
# Search & Rescue
599
# Utilities for correcting user mistakes and (eventually)
600
# doing full documentation searches from the repl.
601

602
# Fuzzy Search Algorithm
603

604
function matchinds(needle, haystack; acronym::Bool = false)
414✔
605
    chars = collect(needle)
207✔
606
    is = Int[]
207✔
607
    lastc = '\0'
×
608
    for (i, char) in enumerate(haystack)
414✔
609
        while !isempty(chars) && isspace(first(chars))
1,676✔
610
            popfirst!(chars) # skip spaces
3✔
611
        end
3✔
612
        isempty(chars) && break
849✔
613
        if lowercase(char) == lowercase(chars[1]) &&
927✔
614
           (!acronym || !isletter(lastc))
615
            push!(is, i)
247✔
616
            popfirst!(chars)
247✔
617
        end
618
        lastc = char
×
619
    end
824✔
620
    return is
207✔
621
end
622

623
longer(x, y) = length(x) ≥ length(y) ? (x, true) : (y, false)
147✔
624

625
bestmatch(needle, haystack) =
147✔
626
    longer(matchinds(needle, haystack, acronym = true),
627
           matchinds(needle, haystack))
628

629
# Optimal string distance: Counts the minimum number of insertions, deletions,
630
# transpositions or substitutions to go from one string to the other.
631
function string_distance(a::AbstractString, lena::Integer, b::AbstractString, lenb::Integer)
62,580✔
632
    if lena > lenb
62,580✔
633
        a, b = b, a
×
634
        lena, lenb = lenb, lena
×
635
    end
636
    start = 0
62,580✔
637
    for (i, j) in zip(a, b)
123,857✔
638
        if a == b
61,365✔
639
            start += 1
127✔
640
        else
641
            break
×
642
        end
643
    end
215✔
644
    start == lena && return lenb - start
62,580✔
645
    vzero = collect(1:(lenb - start))
508,790✔
646
    vone = similar(vzero)
61,238✔
647
    prev_a, prev_b = first(a), first(b)
61,238✔
648
    current = 0
×
649
    for (i, ai) in enumerate(a)
122,476✔
650
        i > start || (prev_a = ai; continue)
197,476✔
651
        left = i - start - 1
197,476✔
652
        current = i - start
197,476✔
653
        transition_next = 0
×
654
        for (j, bj) in enumerate(b)
394,952✔
655
            j > start || (prev_b = bj; continue)
1,856,185✔
656
            # No need to look beyond window of lower right diagonal
657
            above = current
×
658
            this_transition = transition_next
×
659
            transition_next = vone[j - start]
1,856,185✔
660
            vone[j - start] = current = left
1,856,185✔
661
            left = vzero[j - start]
1,856,185✔
662
            if ai != bj
1,856,185✔
663
                # Minimum between substitution, deletion and insertion
664
                current = min(current + 1, above + 1, left + 1)
1,794,978✔
665
                if i > start + 1 && j > start + 1 && ai == prev_b && prev_a == bj
1,794,978✔
666
                    current = min(current, (this_transition += 1))
2,420✔
667
                end
668
            end
669
            vzero[j - start] = current
1,856,185✔
670
            prev_b = bj
×
671
        end
3,514,894✔
672
        prev_a = ai
197,476✔
673
    end
333,714✔
674
    current
61,238✔
675
end
676

677
function fuzzyscore(needle::AbstractString, haystack::AbstractString)
10,110✔
678
    lena, lenb = length(needle), length(haystack)
62,580✔
679
    1 - (string_distance(needle, lena, haystack, lenb) / max(lena, lenb))
62,580✔
680
end
681

682
function fuzzysort(search::String, candidates::Vector{String})
42✔
683
    scores = map(cand -> fuzzyscore(search, cand), candidates)
52,512✔
684
    candidates[sortperm(scores)] |> reverse
42✔
685
end
686

687
# Levenshtein Distance
688

689
function levenshtein(s1, s2)
10,443✔
690
    a, b = collect(s1), collect(s2)
10,443✔
691
    m = length(a)
10,443✔
692
    n = length(b)
10,443✔
693
    d = Matrix{Int}(undef, m+1, n+1)
10,443✔
694

695
    d[1:m+1, 1] = 0:m
81,656✔
696
    d[1, 1:n+1] = 0:n
86,498✔
697

698
    for i = 1:m, j = 1:n
71,163✔
699
        d[i+1,j+1] = min(d[i  , j+1] + 1,
452,182✔
700
                         d[i+1, j  ] + 1,
701
                         d[i  , j  ] + (a[i] != b[j]))
702
    end
502,509✔
703

704
    return d[m+1, n+1]
10,443✔
705
end
706

707
function levsort(search::String, candidates::Vector{String})
8✔
708
    scores = map(cand -> (Float64(levenshtein(search, cand)), -fuzzyscore(search, cand)), candidates)
9,994✔
709
    candidates = candidates[sortperm(scores)]
8✔
710
    i = 0
8✔
711
    for outer i = 1:length(candidates)
16✔
712
        levenshtein(search, candidates[i]) > 3 && break
457✔
713
    end
449✔
714
    return candidates[1:i]
8✔
715
end
716

717
# Result printing
718

719
function printmatch(io::IO, word, match)
103✔
720
    is, _ = bestmatch(word, match)
147✔
721
    for (i, char) = enumerate(match)
206✔
722
        if i in is
1,074✔
723
            printstyled(io, char, bold=true)
190✔
724
        else
725
            print(io, char)
243✔
726
        end
727
    end
433✔
728
end
729

730
function printmatches(io::IO, word, matches; cols::Int = _displaysize(io)[2])
44✔
731
    total = 0
22✔
732
    for match in matches
22✔
733
        total + length(match) + 1 > cols && break
125✔
734
        fuzzyscore(word, match) < 0.5 && break
120✔
735
        print(io, " ")
103✔
736
        printmatch(io, word, match)
103✔
737
        total += length(match) + 1
103✔
738
    end
125✔
739
end
740

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

743
function print_joined_cols(io::IO, ss::Vector{String}, delim = "", last = delim; cols::Int = _displaysize(io)[2])
16✔
744
    i = 0
8✔
745
    total = 0
8✔
746
    for outer i = 1:length(ss)
16✔
747
        total += length(ss[i])
76✔
748
        total + max(i-2,0)*length(delim) + (i>1 ? 1 : 0)*length(last) > cols && (i-=1; break)
81✔
749
    end
71✔
750
    join(io, ss[1:i], delim, last)
8✔
751
end
752

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

755
function print_correction(io::IO, word::String, mod::Module)
8✔
756
    cors = map(quote_spaces, levsort(word, accessible(mod)))
8✔
757
    pre = "Perhaps you meant "
8✔
758
    print(io, pre)
8✔
759
    print_joined_cols(io, cors, ", ", " or "; cols = _displaysize(io)[2] - length(pre))
8✔
760
    println(io)
8✔
761
    return
8✔
762
end
763

764
# TODO: document where this is used
765
print_correction(word, mod::Module) = print_correction(stdout, word, mod)
×
766

767
# Completion data
768

769

770
moduleusings(mod) = ccall(:jl_module_usings, Any, (Any,), mod)
51✔
771

772
filtervalid(names) = filter(x->!occursin(r"#", x), map(string, names))
64,439✔
773

774
accessible(mod::Module) =
51✔
775
    Symbol[filter!(s -> !Base.isdeprecated(mod, s), names(mod, all=true, imported=true));
2,067✔
776
           map(names, moduleusings(mod))...;
777
           collect(keys(Base.Docs.keywords))] |> unique |> filtervalid
778

779
function doc_completions(name, mod::Module=Main)
45✔
780
    res = fuzzysort(name, accessible(mod))
45✔
781

782
    # to insert an entry like `raw""` for `"@raw_str"` in `res`
783
    ms = match.(r"^@(.*?)_str$", res)
84✔
784
    idxs = findall(!isnothing, ms)
84✔
785

786
    # avoid messing up the order while inserting
787
    for i in reverse!(idxs)
42✔
788
        c = only((ms[i]::AbstractMatch).captures)
1,092✔
789
        insert!(res, i, "$(c)\"\"")
1,092✔
790
    end
588✔
791
    res
42✔
792
end
793
doc_completions(name::Symbol) = doc_completions(string(name), mod)
×
794

795

796
# Searching and apropos
797

798
# Docsearch simply returns true or false if an object contains the given needle
799
docsearch(haystack::AbstractString, needle) = occursin(needle, haystack)
13,646✔
800
docsearch(haystack::Symbol, needle) = docsearch(string(haystack), needle)
×
801
docsearch(::Nothing, needle) = false
×
802
function docsearch(haystack::Array, needle)
×
803
    for elt in haystack
×
804
        docsearch(elt, needle) && return true
×
805
    end
×
806
    false
×
807
end
808
function docsearch(haystack, needle)
×
809
    @warn "Unable to search documentation of type $(typeof(haystack))" maxlog=1
×
810
    false
×
811
end
812

813
## Searching specific documentation objects
814
function docsearch(haystack::MultiDoc, needle)
11,829✔
815
    for v in values(haystack.docs)
23,658✔
816
        docsearch(v, needle) && return true
13,212✔
817
    end
11,092✔
818
    false
8,326✔
819
end
820

821
function docsearch(haystack::DocStr, needle)
13,212✔
822
    docsearch(parsedoc(haystack), needle) && return true
13,212✔
823
    if haskey(haystack.data, :fields)
9,709✔
824
        for doc in values(haystack.data[:fields])
859✔
825
            docsearch(doc, needle) && return true
54✔
826
        end
54✔
827
    end
828
    false
9,709✔
829
end
830

831
## doc search
832

833
## Markdown search simply strips all markup and searches plain text version
834
docsearch(haystack::Markdown.MD, needle) = docsearch(stripmd(haystack.content), needle)
13,590✔
835

836
"""
837
    stripmd(x)
838

839
Strip all Markdown markup from x, leaving the result in plain text. Used
840
internally by apropos to make docstrings containing more than one markdown
841
element searchable.
842
"""
843
stripmd(@nospecialize x) = string(x) # for random objects interpolated into the docstring
6✔
844
stripmd(x::AbstractString) = x  # base case
193,433✔
845
stripmd(x::Nothing) = " "
84✔
846
stripmd(x::Vector) = string(map(stripmd, x)...)
69,303✔
847

848
stripmd(x::Markdown.BlockQuote) = "$(stripmd(x.content))"
×
849
stripmd(x::Markdown.Admonition) = "$(stripmd(x.content))"
2,114✔
850
stripmd(x::Markdown.Bold) = "$(stripmd(x.text))"
81✔
851
stripmd(x::Markdown.Code) = "$(stripmd(x.code))"
89,490✔
852
stripmd(x::Markdown.Header) = stripmd(x.text)
5,163✔
853
stripmd(x::Markdown.HorizontalRule) = " "
136✔
854
stripmd(x::Markdown.Image) = "$(stripmd(x.alt)) $(x.url)"
24✔
855
stripmd(x::Markdown.Italic) = "$(stripmd(x.text))"
610✔
856
stripmd(x::Markdown.LaTeX) = "$(x.formula)"
769✔
857
stripmd(x::Markdown.LineBreak) = " "
68✔
858
stripmd(x::Markdown.Link) = "$(stripmd(x.text)) $(x.url)"
12,040✔
859
stripmd(x::Markdown.List) = join(map(stripmd, x.items), " ")
1,287✔
860
stripmd(x::Markdown.MD) = join(map(stripmd, x.content), " ")
13,524✔
861
stripmd(x::Markdown.Paragraph) = stripmd(x.content)
29,749✔
862
stripmd(x::Markdown.Footnote) = "$(stripmd(x.id)) $(stripmd(x.text))"
164✔
863
stripmd(x::Markdown.Table) =
74✔
864
    join([join(map(stripmd, r), " ") for r in x.rows], " ")
865

866
"""
867
    apropos([io::IO=stdout], pattern::Union{AbstractString,Regex})
868

869
Search available docstrings for entries containing `pattern`.
870

871
When `pattern` is a string, case is ignored. Results are printed to `io`.
872

873
`apropos` can be called from the help mode in the REPL by wrapping the query in double quotes:
874
```
875
help?> "pattern"
876
```
877
"""
878
apropos(string) = apropos(stdout, string)
×
879
apropos(io::IO, string) = apropos(io, Regex("\\Q$string", "i"))
2✔
880

881
function apropos(io::IO, needle::Regex)
4✔
882
    for mod in modules
4✔
883
        # Module doc might be in README.md instead of the META dict
884
        docsearch(doc(mod), needle) && println(io, mod)
380✔
885
        dict = meta(mod; autoinit=false)
760✔
886
        isnothing(dict) && continue
760✔
887
        for (k, v) in dict
758✔
888
            docsearch(v, needle) && println(io, k)
11,829✔
889
        end
11,829✔
890
    end
380✔
891
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

© 2025 Coveralls, Inc