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

JuliaLang / julia / #37524

pending completion
#37524

push

local

web-flow
Fix dyld lock not getting unlocked on invalid threads. (#49446)

70720 of 81817 relevant lines covered (86.44%)

34557294.34 hits per line

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

57.74
/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))))
×
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)
×
29
    line = strip(line)
×
30
    ternary_operator_help = (line == "?" || line == "?:")
×
31
    if startswith(line, '?') && !ternary_operator_help
×
32
        line = line[2:end]
×
33
        extended_help_on[] = line
×
34
        brief = false
×
35
    else
36
        extended_help_on[] = nothing
×
37
        brief = true
×
38
    end
39
    # interpret anything starting with # or #= as asking for help on comments
40
    if startswith(line, "#")
×
41
        if startswith(line, "#=")
×
42
            line = "#="
×
43
        else
44
            line = "#"
×
45
        end
46
    end
47
    x = Meta.parse(line, raise = false, depwarn = false)
×
48
    assym = Symbol(line)
×
49
    expr =
×
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
×
55
        elseif isexpr(x, (:using, :import))
×
56
            (x::Expr).head
×
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
×
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)
×
68
end
69
_helpmode(line::AbstractString, mod::Module=Main) = _helpmode(stdout, line, mod)
×
70

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

85
function formatdoc(d::DocStr)
3,365✔
86
    buffer = IOBuffer()
3,365✔
87
    for part in d.text
6,730✔
88
        formatdoc(buffer, d, part)
4,427✔
89
    end
5,489✔
90
    Markdown.MD(Any[Markdown.parse(seekstart(buffer))])
3,365✔
91
end
92
@noinline formatdoc(buffer, d, part) = print(buffer, part)
4,425✔
93

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

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

106
struct Message  # For direct messages to the terminal
107
    msg    # AbstractString
×
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
×
117

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

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

149
_trimdocs(md, brief::Bool) = md, false
×
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{})
360✔
160
    if defined(binding)
360✔
161
        result = getdoc(resolve(binding), sig)
299✔
162
        result === nothing || return result
15✔
163
    end
164
    results, groups = DocStr[], MultiDoc[]
297✔
165
    # Lookup `binding` and `sig` for matches in all modules of the docsystem.
166
    for mod in modules
297✔
167
        dict = meta(mod; autoinit=false)
58,268✔
168
        isnothing(dict) && continue
58,268✔
169
        if haskey(dict, binding)
29,302✔
170
            multidoc = dict[binding]
168✔
171
            push!(groups, multidoc)
168✔
172
            for msig in multidoc.order
168✔
173
                sig <: msig && push!(results, multidoc.docs[msig])
215✔
174
            end
215✔
175
        end
176
    end
29,431✔
177
    if isempty(groups)
297✔
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)
135✔
183
        alias == binding ? summarize(alias, sig) : doc(alias, sig)
135✔
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)
162✔
188
            for group in groups, each in group.order
10✔
189
                push!(results, group.docs[each])
14✔
190
            end
24✔
191
        end
192
        # Get parsed docs and concatenate them.
193
        md = catdoc(mapany(parsedoc, results)...)
162✔
194
        # Save metadata in the generated markdown.
195
        if isa(md, Markdown.MD)
162✔
196
            md.meta[:results] = results
161✔
197
            md.meta[:binding] = binding
161✔
198
            md.meta[:typesig] = sig
161✔
199
        end
200
        return md
162✔
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)
413✔
207
doc(object, sig...)              = doc(object, Tuple{sig...})
×
208

209
function lookup_doc(ex)
94✔
210
    if isa(ex, Expr) && ex.head !== :(.) && Base.isoperator(ex.head)
118✔
211
        # handle syntactic operators, e.g. +=, ::, .=
212
        ex = ex.head
×
213
    end
214
    if haskey(keywords, ex)
94✔
215
        return parsedoc(keywords[ex])
×
216
    elseif Meta.isexpr(ex, :incomplete)
80✔
217
        return :($(Markdown.md"No documentation found."))
1✔
218
    elseif !isa(ex, Expr) && !isa(ex, Symbol)
93✔
219
        return :($(doc)($(typeof)($(esc(ex)))))
1✔
220
    end
221
    if isa(ex, Symbol) && Base.isoperator(ex)
106✔
222
        str = string(ex)
×
223
        isdotted = startswith(str, ".")
×
224
        if endswith(str, "=") && Base.operator_precedence(ex) == Base.prec_assignment && ex !== :(:=)
×
225
            op = chop(str)
×
226
            eq = isdotted ? ".=" : "="
×
227
            return Markdown.parse("`x $op= y` is a synonym for `x $eq x $op y`")
×
228
        elseif isdotted && ex !== :(..)
×
229
            op = str[2:end]
×
230
            if op in ("&&", "||")
×
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).")
×
234
            end
235
        end
236
    end
237
    binding = esc(bindingexpr(namify(ex)))
111✔
238
    if isexpr(ex, :call) || isexpr(ex, :macrocall) || isexpr(ex, :where)
144✔
239
        sig = esc(signature(ex))
31✔
240
        :($(doc)($binding, $sig))
31✔
241
    else
242
        :($(doc)($binding))
61✔
243
    end
244
end
245

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

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

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

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

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

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

376
# repl search and completions for help
377

378

379
quote_spaces(x) = any(isspace, x) ? "'" * x * "'" : x
×
380

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

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

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

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

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

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

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

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

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

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

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

567
"""
568
    fielddoc(binding, field)
569

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

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

595

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

600
# Fuzzy Search Algorithm
601

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

621
longer(x, y) = length(x) ≥ length(y) ? (x, true) : (y, false)
×
622

623
bestmatch(needle, haystack) =
×
624
    longer(matchinds(needle, haystack, acronym = true),
625
           matchinds(needle, haystack))
626

627
avgdistance(xs) =
×
628
    isempty(xs) ? 0 :
629
    (xs[end] - xs[1] - length(xs)+1)/length(xs)
630

631
function fuzzyscore(needle, haystack)
×
632
    score = 0.
×
633
    is, acro = bestmatch(needle, haystack)
×
634
    score += (acro ? 2 : 1)*length(is) # Matched characters
×
635
    score -= 2(length(needle)-length(is)) # Missing characters
×
636
    !acro && (score -= avgdistance(is)/10) # Contiguous
×
637
    !isempty(is) && (score -= sum(is)/length(is)/100) # Closer to beginning
×
638
    return score
×
639
end
640

641
function fuzzysort(search::String, candidates::Vector{String})
×
642
    scores = map(cand -> (fuzzyscore(search, cand), -Float64(levenshtein(search, cand))), candidates)
×
643
    candidates[sortperm(scores)] |> reverse
×
644
end
645

646
# Levenshtein Distance
647

648
function levenshtein(s1, s2)
×
649
    a, b = collect(s1), collect(s2)
×
650
    m = length(a)
×
651
    n = length(b)
×
652
    d = Matrix{Int}(undef, m+1, n+1)
×
653

654
    d[1:m+1, 1] = 0:m
×
655
    d[1, 1:n+1] = 0:n
×
656

657
    for i = 1:m, j = 1:n
×
658
        d[i+1,j+1] = min(d[i  , j+1] + 1,
×
659
                         d[i+1, j  ] + 1,
660
                         d[i  , j  ] + (a[i] != b[j]))
661
    end
×
662

663
    return d[m+1, n+1]
×
664
end
665

666
function levsort(search::String, candidates::Vector{String})
×
667
    scores = map(cand -> (Float64(levenshtein(search, cand)), -fuzzyscore(search, cand)), candidates)
×
668
    candidates = candidates[sortperm(scores)]
×
669
    i = 0
×
670
    for outer i = 1:length(candidates)
×
671
        levenshtein(search, candidates[i]) > 3 && break
×
672
    end
×
673
    return candidates[1:i]
×
674
end
675

676
# Result printing
677

678
function printmatch(io::IO, word, match)
×
679
    is, _ = bestmatch(word, match)
×
680
    for (i, char) = enumerate(match)
×
681
        if i in is
×
682
            printstyled(io, char, bold=true)
×
683
        else
684
            print(io, char)
×
685
        end
686
    end
×
687
end
688

689
function printmatches(io::IO, word, matches; cols::Int = _displaysize(io)[2])
×
690
    total = 0
×
691
    for match in matches
×
692
        total + length(match) + 1 > cols && break
×
693
        fuzzyscore(word, match) < 0 && break
×
694
        print(io, " ")
×
695
        printmatch(io, word, match)
×
696
        total += length(match) + 1
×
697
    end
×
698
end
699

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

702
function print_joined_cols(io::IO, ss::Vector{String}, delim = "", last = delim; cols::Int = _displaysize(io)[2])
×
703
    i = 0
×
704
    total = 0
×
705
    for outer i = 1:length(ss)
×
706
        total += length(ss[i])
×
707
        total + max(i-2,0)*length(delim) + (i>1 ? 1 : 0)*length(last) > cols && (i-=1; break)
×
708
    end
×
709
    join(io, ss[1:i], delim, last)
×
710
end
711

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

714
function print_correction(io::IO, word::String, mod::Module)
×
715
    cors = map(quote_spaces, levsort(word, accessible(mod)))
×
716
    pre = "Perhaps you meant "
×
717
    print(io, pre)
×
718
    print_joined_cols(io, cors, ", ", " or "; cols = _displaysize(io)[2] - length(pre))
×
719
    println(io)
×
720
    return
×
721
end
722

723
# TODO: document where this is used
724
print_correction(word, mod::Module) = print_correction(stdout, word, mod)
×
725

726
# Completion data
727

728

729
moduleusings(mod) = ccall(:jl_module_usings, Any, (Any,), mod)
1✔
730

731
filtervalid(names) = filter(x->!occursin(r"#", x), map(string, names))
1,272✔
732

733
accessible(mod::Module) =
1✔
734
    Symbol[filter!(s -> !Base.isdeprecated(mod, s), names(mod, all=true, imported=true));
83✔
735
           map(names, moduleusings(mod))...;
736
           collect(keys(Base.Docs.keywords))] |> unique |> filtervalid
737

738
function doc_completions(name, mod::Module=Main)
×
739
    res = fuzzysort(name, accessible(mod))
×
740

741
    # to insert an entry like `raw""` for `"@raw_str"` in `res`
742
    ms = match.(r"^@(.*?)_str$", res)
×
743
    idxs = findall(!isnothing, ms)
×
744

745
    # avoid messing up the order while inserting
746
    for i in reverse(idxs)
×
747
        c = only((ms[i]::AbstractMatch).captures)
×
748
        insert!(res, i, "$(c)\"\"")
×
749
    end
×
750
    res
×
751
end
752
doc_completions(name::Symbol) = doc_completions(string(name), mod)
×
753

754

755
# Searching and apropos
756

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

772
## Searching specific documentation objects
773
function docsearch(haystack::MultiDoc, needle)
5,771✔
774
    for v in values(haystack.docs)
11,542✔
775
        docsearch(v, needle) && return true
6,697✔
776
    end
7,401✔
777
    false
5,549✔
778
end
779

780
function docsearch(haystack::DocStr, needle)
6,697✔
781
    docsearch(parsedoc(haystack), needle) && return true
6,697✔
782
    if haskey(haystack.data, :fields)
6,475✔
783
        for doc in values(haystack.data[:fields])
555✔
784
            docsearch(doc, needle) && return true
44✔
785
        end
44✔
786
    end
787
    false
6,475✔
788
end
789

790
## doc search
791

792
## Markdown search simply strips all markup and searches plain text version
793
docsearch(haystack::Markdown.MD, needle) = docsearch(stripmd(haystack.content), needle)
6,893✔
794

795
"""
796
    stripmd(x)
797

798
Strip all Markdown markup from x, leaving the result in plain text. Used
799
internally by apropos to make docstrings containing more than one markdown
800
element searchable.
801
"""
802
stripmd(@nospecialize x) = string(x) # for random objects interpolated into the docstring
6✔
803
stripmd(x::AbstractString) = x  # base case
96,920✔
804
stripmd(x::Nothing) = " "
42✔
805
stripmd(x::Vector) = string(map(stripmd, x)...)
34,633✔
806

807
stripmd(x::Markdown.BlockQuote) = "$(stripmd(x.content))"
×
808
stripmd(x::Markdown.Admonition) = "$(stripmd(x.content))"
1,035✔
809
stripmd(x::Markdown.Bold) = "$(stripmd(x.text))"
40✔
810
stripmd(x::Markdown.Code) = "$(stripmd(x.code))"
44,902✔
811
stripmd(x::Markdown.Header) = stripmd(x.text)
2,657✔
812
stripmd(x::Markdown.HorizontalRule) = " "
68✔
813
stripmd(x::Markdown.Image) = "$(stripmd(x.alt)) $(x.url)"
12✔
814
stripmd(x::Markdown.Italic) = "$(stripmd(x.text))"
299✔
815
stripmd(x::Markdown.LaTeX) = "$(x.formula)"
394✔
816
stripmd(x::Markdown.LineBreak) = " "
36✔
817
stripmd(x::Markdown.Link) = "$(stripmd(x.text)) $(x.url)"
6,133✔
818
stripmd(x::Markdown.List) = join(map(stripmd, x.items), " ")
605✔
819
stripmd(x::Markdown.MD) = join(map(stripmd, x.content), " ")
6,861✔
820
stripmd(x::Markdown.Paragraph) = stripmd(x.content)
14,719✔
821
stripmd(x::Markdown.Footnote) = "$(stripmd(x.id)) $(stripmd(x.text))"
82✔
822
stripmd(x::Markdown.Table) =
38✔
823
    join([join(map(stripmd, r), " ") for r in x.rows], " ")
824

825
"""
826
    apropos([io::IO=stdout], pattern::Union{AbstractString,Regex})
827

828
Search available docstrings for entries containing `pattern`.
829

830
When `pattern` is a string, case is ignored. Results are printed to `io`.
831

832
`apropos` can be called from the help mode in the REPL by wrapping the query in double quotes:
833
```
834
help?> "pattern"
835
```
836
"""
837
apropos(string) = apropos(stdout, string)
×
838
apropos(io::IO, string) = apropos(io, Regex("\\Q$string", "i"))
1✔
839

840
function apropos(io::IO, needle::Regex)
2✔
841
    for mod in modules
2✔
842
        # Module doc might be in README.md instead of the META dict
843
        docsearch(doc(mod), needle) && println(io, mod)
198✔
844
        dict = meta(mod; autoinit=false)
396✔
845
        isnothing(dict) && continue
396✔
846
        for (k, v) in dict
394✔
847
            docsearch(v, needle) && println(io, k)
5,771✔
848
        end
5,771✔
849
    end
198✔
850
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