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

JuliaLang / julia / #37433

pending completion
#37433

push

local

web-flow
Merge pull request #48513 from JuliaLang/jn/extend-once

ensure extension triggers are only run by the package that satified them

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

72324 of 82360 relevant lines covered (87.81%)

31376331.4 hits per line

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

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

26
const extended_help_on = Ref{Any}(nothing)
27

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

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

85
function formatdoc(d::DocStr)
6,747✔
86
    buffer = IOBuffer()
6,747✔
87
    for part in d.text
13,494✔
88
        formatdoc(buffer, d, part)
8,854✔
89
    end
10,961✔
90
    Markdown.MD(Any[Markdown.parse(seekstart(buffer))])
6,747✔
91
end
92
@noinline formatdoc(buffer, d, part) = print(buffer, part)
8,852✔
93

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

104
## Trimming long help ("# Extended help")
105

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

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

116
trimdocs(doc, brief::Bool) = doc
2✔
117

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

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

149
_trimdocs(md, brief::Bool) = md, false
127✔
150

151
"""
152
    Docs.doc(binding, sig)
153

154
Return all documentation that matches both `binding` and `sig`.
155

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

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

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

246
# Object Summaries.
247
# =================
248

249
function summarize(binding::Binding, sig)
252✔
250
    io = IOBuffer()
252✔
251
    if defined(binding)
252✔
252
        binding_res = resolve(binding)
242✔
253
        !isa(binding_res, Module) && println(io, "No documentation found.\n")
242✔
254
        summarize(io, binding_res, binding)
242✔
255
    else
256
        println(io, "No documentation found.\n")
10✔
257
        quot = any(isspace, sprint(print, binding)) ? "'" : ""
10✔
258
        println(io, "Binding ", quot, "`", binding, "`", quot, " does not exist.")
10✔
259
    end
260
    md = Markdown.parse(seekstart(io))
252✔
261
    # Save metadata in the generated markdown.
262
    md.meta[:results] = DocStr[]
252✔
263
    md.meta[:binding] = binding
252✔
264
    md.meta[:typesig] = sig
252✔
265
    return md
252✔
266
end
267

268
function summarize(io::IO, λ::Function, binding::Binding)
4✔
269
    kind = startswith(string(binding.var), '@') ? "macro" : "`Function`"
4✔
270
    println(io, "`", binding, "` is a ", kind, ".")
4✔
271
    println(io, "```\n", methods(λ), "\n```")
4✔
272
end
273

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

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

366
function summarize(io::IO, @nospecialize(T), binding::Binding)
2✔
367
    T = typeof(T)
2✔
368
    println(io, "`", binding, "` is of type `", T, "`.\n")
2✔
369
    summarize(io, T, binding)
2✔
370
end
371

372
# repl search and completions for help
373

374

375
quote_spaces(x) = any(isspace, x) ? "'" * x * "'" : x
53,269✔
376

377
function repl_search(io::IO, s::Union{Symbol,String}, mod::Module)
21✔
378
    pre = "search:"
21✔
379
    print(io, pre)
21✔
380
    printmatches(io, s, map(quote_spaces, doc_completions(s, mod)), cols = _displaysize(io)[2] - length(pre))
21✔
381
    println(io, "\n")
21✔
382
end
383

384
# TODO: document where this is used
385
repl_search(s, mod::Module) = repl_search(stdout, s, mod)
×
386

387
function repl_corrections(io::IO, s, mod::Module)
7✔
388
    print(io, "Couldn't find ")
7✔
389
    quot = any(isspace, s) ? "'" : ""
7✔
390
    print(io, quot)
7✔
391
    printstyled(io, s, color=:cyan)
7✔
392
    print(io, quot, '\n')
7✔
393
    print_correction(io, s, mod)
7✔
394
end
395
repl_corrections(s) = repl_corrections(stdout, s)
×
396

397
# inverse of latex_symbols Dict, lazily created as needed
398
const symbols_latex = Dict{String,String}()
399
function symbol_latex(s::String)
45✔
400
    if isempty(symbols_latex) && isassigned(Base.REPL_MODULE_REF)
45✔
401
        for (k,v) in Iterators.flatten((REPLCompletions.latex_symbols,
2✔
402
                                        REPLCompletions.emoji_symbols))
403
            symbols_latex[v] = k
7,388✔
404
        end
7,392✔
405

406
        # Overwrite with canonical mapping when a symbol has several completions (#39148)
407
        merge!(symbols_latex, REPLCompletions.symbols_latex_canonical)
2✔
408
    end
409

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

470
macro repl(ex, brief::Bool=false, mod::Module=Main) repl(ex; brief, mod) end
12✔
471
macro repl(io, ex, brief, mod) repl(io, ex; brief, mod) end
35✔
472

473
function repl(io::IO, s::Symbol; brief::Bool=true, mod::Module=Main)
42✔
474
    str = string(s)
21✔
475
    quote
28✔
476
        repl_latex($io, $str)
477
        repl_search($io, $str, $mod)
478
        $(if !isdefined(mod, s) && !haskey(keywords, s) && !Base.isoperator(s)
479
               :(repl_corrections($io, $str, $mod))
7✔
480
          end)
481
        $(_repl(s, brief))
482
    end
483
end
484
isregex(x) = isexpr(x, :macrocall, 3) && x.args[1] === Symbol("@r_str") && !isempty(x.args[3])
17✔
485

486
repl(io::IO, ex::Expr; brief::Bool=true, mod::Module=Main) = isregex(ex) ? :(apropos($io, $ex)) : _repl(ex, brief)
34✔
487
repl(io::IO, str::AbstractString; brief::Bool=true, mod::Module=Main) = :(apropos($io, $str))
2✔
488
repl(io::IO, other; brief::Bool=true, mod::Module=Main) = esc(:(@doc $other))
4✔
489
#repl(io::IO, other) = lookup_doc(other) # TODO
490

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

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

562
"""
563
    fielddoc(binding, field)
564

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

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

590

591
# Search & Rescue
592
# Utilities for correcting user mistakes and (eventually)
593
# doing full documentation searches from the repl.
594

595
# Fuzzy Search Algorithm
596

597
function matchinds(needle, haystack; acronym::Bool = false)
238,194✔
598
    chars = collect(needle)
119,097✔
599
    is = Int[]
119,097✔
600
    lastc = '\0'
×
601
    for (i, char) in enumerate(haystack)
238,098✔
602
        while !isempty(chars) && isspace(first(chars))
1,755,744✔
603
            popfirst!(chars) # skip spaces
9✔
604
        end
9✔
605
        isempty(chars) && break
879,230✔
606
        if lowercase(char) == lowercase(chars[1]) &&
883,821✔
607
           (!acronym || !isletter(lastc))
608
            push!(is, i)
9,869✔
609
            popfirst!(chars)
9,869✔
610
        end
611
        lastc = char
×
612
    end
876,505✔
613
    return is
119,097✔
614
end
615

616
longer(x, y) = length(x) ≥ length(y) ? (x, true) : (y, false)
64,153✔
617

618
bestmatch(needle, haystack) =
64,153✔
619
    longer(matchinds(needle, haystack, acronym = true),
620
           matchinds(needle, haystack))
621

622
avgdistance(xs) =
9,094✔
623
    isempty(xs) ? 0 :
624
    (xs[end] - xs[1] - length(xs)+1)/length(xs)
625

626
function fuzzyscore(needle, haystack)
59,483✔
627
    score = 0.
×
628
    is, acro = bestmatch(needle, haystack)
64,030✔
629
    score += (acro ? 2 : 1)*length(is) # Matched characters
59,483✔
630
    score -= 2(length(needle)-length(is)) # Missing characters
59,483✔
631
    !acro && (score -= avgdistance(is)/10) # Contiguous
59,483✔
632
    !isempty(is) && (score -= sum(is)/length(is)/100) # Closer to beginning
65,157✔
633
    return score
59,483✔
634
end
635

636
function fuzzysort(search::String, candidates::Vector{String})
41✔
637
    scores = map(cand -> (fuzzyscore(search, cand), -Float64(levenshtein(search, cand))), candidates)
50,779✔
638
    candidates[sortperm(scores)] |> reverse
41✔
639
end
640

641
# Levenshtein Distance
642

643
function levenshtein(s1, s2)
59,850✔
644
    a, b = collect(s1), collect(s2)
59,850✔
645
    m = length(a)
59,850✔
646
    n = length(b)
59,850✔
647
    d = Matrix{Int}(undef, m+1, n+1)
59,850✔
648

649
    d[1:m+1, 1] = 0:m
323,603✔
650
    d[1, 1:n+1] = 0:n
507,928✔
651

652
    for i = 1:m, j = 1:n
263,587✔
653
        d[i+1,j+1] = min(d[i  , j+1] + 1,
1,531,440✔
654
                         d[i+1, j  ] + 1,
655
                         d[i  , j  ] + (a[i] != b[j]))
656
    end
1,676,732✔
657

658
    return d[m+1, n+1]
59,850✔
659
end
660

661
function levsort(search::String, candidates::Vector{String})
7✔
662
    scores = map(cand -> (Float64(levenshtein(search, cand)), -fuzzyscore(search, cand)), candidates)
8,672✔
663
    candidates = candidates[sortperm(scores)]
7✔
664
    i = 0
7✔
665
    for outer i = 1:length(candidates)
14✔
666
        levenshtein(search, candidates[i]) > 3 && break
447✔
667
    end
440✔
668
    return candidates[1:i]
7✔
669
end
670

671
# Result printing
672

673
function printmatch(io::IO, word, match)
65✔
674
    is, _ = bestmatch(word, match)
123✔
675
    for (i, char) = enumerate(match)
130✔
676
        if i in is
1,806✔
677
            printstyled(io, char, bold=true)
234✔
678
        else
679
            print(io, char)
252✔
680
        end
681
    end
486✔
682
end
683

684
function printmatches(io::IO, word, matches; cols::Int = _displaysize(io)[2])
42✔
685
    total = 0
21✔
686
    for match in matches
21✔
687
        total + length(match) + 1 > cols && break
86✔
688
        fuzzyscore(word, match) < 0 && break
80✔
689
        print(io, " ")
65✔
690
        printmatch(io, word, match)
65✔
691
        total += length(match) + 1
65✔
692
    end
86✔
693
end
694

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

697
function print_joined_cols(io::IO, ss::Vector{String}, delim = "", last = delim; cols::Int = _displaysize(io)[2])
14✔
698
    i = 0
7✔
699
    total = 0
7✔
700
    for outer i = 1:length(ss)
14✔
701
        total += length(ss[i])
75✔
702
        total + max(i-2,0)*length(delim) + (i>1 ? 1 : 0)*length(last) > cols && (i-=1; break)
80✔
703
    end
70✔
704
    join(io, ss[1:i], delim, last)
7✔
705
end
706

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

709
function print_correction(io::IO, word::String, mod::Module)
7✔
710
    cors = map(quote_spaces, levsort(word, accessible(mod)))
7✔
711
    pre = "Perhaps you meant "
7✔
712
    print(io, pre)
7✔
713
    print_joined_cols(io, cors, ", ", " or "; cols = _displaysize(io)[2] - length(pre))
7✔
714
    println(io)
7✔
715
    return
7✔
716
end
717

718
# TODO: document where this is used
719
print_correction(word, mod::Module) = print_correction(stdout, word, mod)
×
720

721
# Completion data
722

723

724
moduleusings(mod) = ccall(:jl_module_usings, Any, (Any,), mod)
49✔
725

726
filtervalid(names) = filter(x->!occursin(r"#", x), map(string, names))
61,345✔
727

728
accessible(mod::Module) =
49✔
729
    Symbol[filter!(s -> !Base.isdeprecated(mod, s), names(mod, all=true, imported=true));
2,127✔
730
           map(names, moduleusings(mod))...;
731
           collect(keys(Base.Docs.keywords))] |> unique |> filtervalid
732

733
function doc_completions(name, mod::Module=Main)
44✔
734
    res = fuzzysort(name, accessible(mod))
44✔
735

736
    # to insert an entry like `raw""` for `"@raw_str"` in `res`
737
    ms = match.(r"^@(.*?)_str$", res)
82✔
738
    idxs = findall(!isnothing, ms)
82✔
739

740
    # avoid messing up the order while inserting
741
    for i in reverse(idxs)
41✔
742
        c = only((ms[i]::AbstractMatch).captures)
1,066✔
743
        insert!(res, i, "$(c)\"\"")
1,066✔
744
    end
574✔
745
    res
41✔
746
end
747
doc_completions(name::Symbol) = doc_completions(string(name), mod)
×
748

749

750
# Searching and apropos
751

752
# Docsearch simply returns true or false if an object contains the given needle
753
docsearch(haystack::AbstractString, needle) = occursin(needle, haystack)
13,545✔
754
docsearch(haystack::Symbol, needle) = docsearch(string(haystack), needle)
×
755
docsearch(::Nothing, needle) = false
×
756
function docsearch(haystack::Array, needle)
×
757
    for elt in haystack
×
758
        docsearch(elt, needle) && return true
×
759
    end
×
760
    false
×
761
end
762
function docsearch(haystack, needle)
×
763
    @warn "Unable to search documentation of type $(typeof(haystack))" maxlog=1
×
764
    false
×
765
end
766

767
## Searching specific documentation objects
768
function docsearch(haystack::MultiDoc, needle)
11,722✔
769
    for v in values(haystack.docs)
23,444✔
770
        docsearch(v, needle) && return true
13,097✔
771
    end
10,990✔
772
    false
8,240✔
773
end
774

775
function docsearch(haystack::DocStr, needle)
13,097✔
776
    docsearch(parsedoc(haystack), needle) && return true
13,097✔
777
    if haskey(haystack.data, :fields)
19,230✔
778
        for doc in values(haystack.data[:fields])
822✔
779
            docsearch(doc, needle) && return true
54✔
780
        end
54✔
781
    end
782
    false
9,615✔
783
end
784

785
## doc search
786

787
## Markdown search simply strips all markup and searches plain text version
788
docsearch(haystack::Markdown.MD, needle) = docsearch(stripmd(haystack.content), needle)
13,489✔
789

790
"""
791
    stripmd(x)
792

793
Strip all Markdown markup from x, leaving the result in plain text. Used
794
internally by apropos to make docstrings containing more than one markdown
795
element searchable.
796
"""
797
stripmd(@nospecialize x) = string(x) # for random objects interpolated into the docstring
6✔
798
stripmd(x::AbstractString) = x  # base case
192,465✔
799
stripmd(x::Nothing) = " "
108✔
800
stripmd(x::Vector) = string(map(stripmd, x)...)
68,137✔
801

802
stripmd(x::Markdown.BlockQuote) = "$(stripmd(x.content))"
×
803
stripmd(x::Markdown.Admonition) = "$(stripmd(x.content))"
1,986✔
804
stripmd(x::Markdown.Bold) = "$(stripmd(x.text))"
77✔
805
stripmd(x::Markdown.Code) = "$(stripmd(x.code))"
89,127✔
806
stripmd(x::Markdown.Header) = stripmd(x.text)
5,187✔
807
stripmd(x::Markdown.HorizontalRule) = " "
140✔
808
stripmd(x::Markdown.Image) = "$(stripmd(x.alt)) $(x.url)"
24✔
809
stripmd(x::Markdown.Italic) = "$(stripmd(x.text))"
571✔
810
stripmd(x::Markdown.LaTeX) = "$(x.formula)"
783✔
811
stripmd(x::Markdown.LineBreak) = " "
72✔
812
stripmd(x::Markdown.Link) = "$(stripmd(x.text)) $(x.url)"
12,004✔
813
stripmd(x::Markdown.List) = join(map(stripmd, x.items), " ")
1,198✔
814
stripmd(x::Markdown.MD) = join(map(stripmd, x.content), " ")
13,439✔
815
stripmd(x::Markdown.Paragraph) = stripmd(x.content)
29,100✔
816
stripmd(x::Markdown.Footnote) = "$(stripmd(x.id)) $(stripmd(x.text))"
204✔
817
stripmd(x::Markdown.Table) =
77✔
818
    join([join(map(stripmd, r), " ") for r in x.rows], " ")
819

820
"""
821
    apropos([io::IO=stdout], pattern::Union{AbstractString,Regex})
822

823
Search available docstrings for entries containing `pattern`.
824

825
When `pattern` is a string, case is ignored. Results are printed to `io`.
826

827
`apropos` can be called from the help mode in the REPL by wrapping the query in double quotes:
828
```
829
help?> "pattern"
830
```
831
"""
832
apropos(string) = apropos(stdout, string)
×
833
apropos(io::IO, string) = apropos(io, Regex("\\Q$string", "i"))
2✔
834

835
function apropos(io::IO, needle::Regex)
4✔
836
    for mod in modules
4✔
837
        # Module doc might be in README.md instead of the META dict
838
        docsearch(doc(mod), needle) && println(io, mod)
394✔
839
        dict = meta(mod; autoinit=false)
788✔
840
        isnothing(dict) && continue
788✔
841
        for (k, v) in dict
786✔
842
            docsearch(v, needle) && println(io, k)
11,722✔
843
        end
11,722✔
844
    end
394✔
845
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