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

JuliaLang / julia / #37599

16 Aug 2023 01:53AM UTC coverage: 86.427% (-1.1%) from 87.526%
#37599

push

local

web-flow
Change heap-size-hint in test processes to total memory (#50922)

It seems this is causing macos to hang because the shown free memory is
generally very small.

xref: JuliaLang/julia#50673

73086 of 84564 relevant lines covered (86.43%)

32342958.75 hits per line

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

90.98
/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)
80✔
29
    line = strip(line)
80✔
30
    ternary_operator_help = (line == "?" || line == "?:")
100✔
31
    if startswith(line, '?') && !ternary_operator_help
50✔
32
        line = line[2:end]
1✔
33
        extended_help_on[] = line
1✔
34
        brief = false
1✔
35
    else
36
        extended_help_on[] = nothing
49✔
37
        brief = true
49✔
38
    end
39
    # interpret anything starting with # or #= as asking for help on comments
40
    if startswith(line, "#")
100✔
41
        if startswith(line, "#=")
×
42
            line = "#="
×
43
        else
44
            line = "#"
×
45
        end
46
    end
47
    x = Meta.parse(line, raise = false, depwarn = false)
50✔
48
    assym = Symbol(line)
50✔
49
    expr =
108✔
50
        if haskey(keywords, assym) || 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))
51✔
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
87✔
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)
50✔
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])
13✔
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,761✔
86
    buffer = IOBuffer()
6,761✔
87
    for part in d.text
13,522✔
88
        formatdoc(buffer, d, part)
8,880✔
89
    end
10,999✔
90
    Markdown.MD(Any[Markdown.parse(seekstart(buffer))])
6,761✔
91
end
92
@noinline formatdoc(buffer, d, part) = print(buffer, part)
8,878✔
93

94
function parsedoc(d::DocStr)
13,408✔
95
    if d.object === nothing
13,408✔
96
        md = formatdoc(d)
6,761✔
97
        md.meta[:module] = d.data[:module]
6,761✔
98
        md.meta[:path]   = d.data[:path]
6,761✔
99
        d.object = md
6,761✔
100
    end
101
    d.object
13,408✔
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)
42✔
119
    brief || return md
51✔
120
    md, trimmed = _trimdocs(md, brief)
33✔
121
    if trimmed
33✔
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
33✔
127
end
128

129
function _trimdocs(md::Markdown.MD, brief::Bool)
67✔
130
    content, trimmed = [], false
67✔
131
    for c in md.content
67✔
132
        if isa(c, Markdown.Header{1}) && isa(c.text, AbstractArray) && !isempty(c.text)
165✔
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)
163✔
143
        trimmed |= trm
163✔
144
        push!(content, c)
163✔
145
    end
163✔
146
    return Markdown.MD(content, md.meta), trimmed
67✔
147
end
148

149
_trimdocs(md, brief::Bool) = md, false
129✔
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{})
586✔
160
    if defined(binding)
586✔
161
        result = getdoc(resolve(binding), sig)
495✔
162
        result === nothing || return result
15✔
163
    end
164
    results, groups = DocStr[], MultiDoc[]
502✔
165
    # Lookup `binding` and `sig` for matches in all modules of the docsystem.
166
    for mod in modules
502✔
167
        dict = meta(mod; autoinit=false)
95,006✔
168
        isnothing(dict) && continue
95,006✔
169
        if haskey(dict, binding)
47,763✔
170
            multidoc = dict[binding]
260✔
171
            push!(groups, multidoc)
260✔
172
            for msig in multidoc.order
260✔
173
                sig <: msig && push!(results, multidoc.docs[msig])
307✔
174
            end
307✔
175
        end
176
    end
48,005✔
177
    if isempty(groups)
502✔
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)
250✔
183
        alias == binding ? summarize(alias, sig) : doc(alias, sig)
250✔
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)
252✔
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)...)
252✔
194
        # Save metadata in the generated markdown.
195
        if isa(md, Markdown.MD)
252✔
196
            md.meta[:results] = results
249✔
197
            md.meta[:binding] = binding
249✔
198
            md.meta[:typesig] = sig
249✔
199
        end
200
        return md
252✔
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)
771✔
207
doc(object, sig...)              = doc(object, Tuple{sig...})
×
208

209
function lookup_doc(ex)
129✔
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)
129✔
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)
122✔
219
        return :($(doc)($(typeof)($(esc(ex)))))
2✔
220
    end
221
    if isa(ex, Symbol) && Base.isoperator(ex)
145✔
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)))
140✔
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))
82✔
243
    end
244
end
245

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

249
function summarize(binding::Binding, sig)
250✔
250
    io = IOBuffer()
250✔
251
    if defined(binding)
250✔
252
        binding_res = resolve(binding)
239✔
253
        !isa(binding_res, Module) && println(io, "No documentation found.\n")
239✔
254
        summarize(io, binding_res, binding)
239✔
255
    else
256
        println(io, "No documentation found.\n")
11✔
257
        quot = any(isspace, sprint(print, binding)) ? "'" : ""
11✔
258
        if Base.isbindingresolved(binding.mod, binding.var)
11✔
259
            println(io, "Binding ", quot, "`", binding, "`", quot, " exists, but has not been assigned a value.")
1✔
260
        else
261
            println(io, "Binding ", quot, "`", binding, "`", quot, " does not exist.")
10✔
262
        end
263
    end
264
    md = Markdown.parse(seekstart(io))
250✔
265
    # Save metadata in the generated markdown.
266
    md.meta[:results] = DocStr[]
250✔
267
    md.meta[:binding] = binding
250✔
268
    md.meta[:typesig] = sig
250✔
269
    return md
250✔
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)
17✔
279
    println(io, "# Summary")
17✔
280
    T = Base.unwrap_unionall(TT)
17✔
281
    if T isa DataType
17✔
282
        println(io, "```")
14✔
283
        print(io,
23✔
284
            Base.isabstracttype(T) ? "abstract type " :
285
            Base.ismutabletype(T)  ? "mutable struct " :
286
            Base.isstructtype(T) ? "struct " :
287
            "primitive type ")
288
        supert = supertype(T)
14✔
289
        println(io, T)
14✔
290
        println(io, "```")
14✔
291
        if !Base.isabstracttype(T) && T.name !== Tuple.name && !isempty(fieldnames(T))
14✔
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)
14✔
301
        if !isempty(subt)
19✔
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
14✔
310
            println(io, "# Supertype Hierarchy")
10✔
311
            println(io, "```")
10✔
312
            Base.show_supertypes(io, T)
10✔
313
            println(io)
10✔
314
            println(io, "```")
10✔
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}
218✔
328
    mpath = pathof(m)
218✔
329
    isnothing(mpath) && return nothing
254✔
330
    !isfile(mpath) && return nothing # modules in sysimage, where src files are omitted
36✔
331
    path = dirname(mpath)
36✔
332
    top_path = pkgdir(m)
72✔
333
    while true
72✔
334
        for file in readdir(path; join=true, sort=true)
72✔
335
            isfile(file) && (basename(lowercase(file)) in ["readme.md", "readme"]) || continue
862✔
336
            return file
16✔
337
        end
410✔
338
        path == top_path && break # go no further than pkgdir
112✔
339
        path = dirname(path) # work up through nested modules
36✔
340
    end
36✔
341
    return nothing
20✔
342
end
343
function summarize(io::IO, m::Module, binding::Binding; nlines::Int = 200)
436✔
344
    readme_path = find_readme(m)
218✔
345
    if isnothing(readme_path)
234✔
346
        println(io, "No docstring or readme file found for module `$m`.\n")
202✔
347
    else
348
        println(io, "No docstring found for module `$m`.")
16✔
349
    end
350
    exports = filter!(!=(nameof(m)), names(m))
218✔
351
    if isempty(exports)
218✔
352
        println(io, "Module does not export any names.")
65✔
353
    else
354
        println(io, "# Exported names")
153✔
355
        print(io, "  `")
153✔
356
        join(io, exports, "`, `")
153✔
357
        println(io, "`\n")
153✔
358
    end
359
    if !isnothing(readme_path)
234✔
360
        readme_lines = readlines(readme_path)
16✔
361
        isempty(readme_lines) && return  # don't say we are going to print empty file
16✔
362
        println(io, "# Displaying contents of readme found at `$(readme_path)`")
16✔
363
        for line in first(readme_lines, nlines)
16✔
364
            println(io, line)
2,560✔
365
        end
2,576✔
366
        length(readme_lines) > nlines && println(io, "\n[output truncated to first $nlines lines]")
218✔
367
    end
368
end
369

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

376
# repl search and completions for help
377

378

379
quote_spaces(x) = any(isspace, x) ? "'" * x * "'" : x
56,392✔
380

381
function repl_search(io::IO, s::Union{Symbol,String}, mod::Module)
22✔
382
    pre = "search:"
22✔
383
    print(io, pre)
22✔
384
    printmatches(io, s, map(quote_spaces, doc_completions(s, mod)), cols = _displaysize(io)[2] - length(pre))
22✔
385
    println(io, "\n")
22✔
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)
8✔
392
    print(io, "Couldn't find ")
8✔
393
    quot = any(isspace, s) ? "'" : ""
8✔
394
    print(io, quot)
8✔
395
    printstyled(io, s, color=:cyan)
8✔
396
    print(io, quot, '\n')
8✔
397
    print_correction(io, s, mod)
8✔
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)
47✔
404
    if isempty(symbols_latex) && isassigned(Base.REPL_MODULE_REF)
47✔
405
        for (k,v) in Iterators.flatten((REPLCompletions.latex_symbols,
2✔
406
                                        REPLCompletions.emoji_symbols))
407
            symbols_latex[v] = k
7,398✔
408
        end
7,402✔
409

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

414
    return get(symbols_latex, s, "")
47✔
415
end
416
function repl_latex(io::IO, s0::String)
26✔
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
26✔
421
    latex::String = symbol_latex(s)
26✔
422
    if isempty(latex)
26✔
423
        # Decompose NFC-normalized identifier to match tab-completion
424
        # input if the first search came up empty.
425
        s = normalize(s, :NFD)
21✔
426
        latex = symbol_latex(s)
21✔
427
    end
428
    if !isempty(latex)
26✔
429
        print(io, "\"")
5✔
430
        printstyled(io, s, color=:cyan)
5✔
431
        print(io, "\" can be typed by ")
5✔
432
        printstyled(io, latex, "<tab>", color=:cyan)
5✔
433
        println(io, '\n')
5✔
434
    elseif any(c -> haskey(symbols_latex, string(c)), s)
106✔
435
        print(io, "\"")
3✔
436
        printstyled(io, s, color=:cyan)
3✔
437
        print(io, "\" can be typed by ")
3✔
438
        state::Char = '\0'
3✔
439
        with_output_color(:cyan, io) do io
3✔
440
            for c in s
5✔
441
                cstr = string(c)
13✔
442
                if haskey(symbols_latex, cstr)
13✔
443
                    latex = symbols_latex[cstr]
11✔
444
                    if length(latex) == 3 && latex[2] in ('^','_')
15✔
445
                        # coalesce runs of sub/superscripts
446
                        if state != latex[2]
14✔
447
                            '\0' != state && print(io, "<tab>")
3✔
448
                            print(io, latex[1:2])
3✔
449
                            state = latex[2]
6✔
450
                        end
451
                        print(io, latex[3])
7✔
452
                    else
453
                        if '\0' != state
4✔
454
                            print(io, "<tab>")
1✔
455
                            state = '\0'
1✔
456
                        end
457
                        print(io, latex, "<tab>")
15✔
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
13✔
467
            '\0' != state && print(io, "<tab>")
3✔
468
        end
469
        println(io, '\n')
21✔
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
36✔
476

477
function repl(io::IO, s::Symbol; brief::Bool=true, mod::Module=Main)
44✔
478
    str = string(s)
22✔
479
    quote
30✔
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))
8✔
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])
17✔
490

491
repl(io::IO, ex::Expr; brief::Bool=true, mod::Module=Main) = isregex(ex) ? :(apropos($io, $ex)) : _repl(ex, brief)
34✔
492
repl(io::IO, str::AbstractString; brief::Bool=true, mod::Module=Main) = :(apropos($io, $str))
2✔
493
repl(io::IO, other; brief::Bool=true, mod::Module=Main) = esc(:(@doc $other))
4✔
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)
46✔
499
    if isexpr(x, :call)
24✔
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))
46✔
553
    docs = if isfield(x)
20✔
554
        quote
7✔
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
33✔
563
    end
564
    :(REPL.trimdocs($docs, $brief))
42✔
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)
178✔
575
        isnothing(dict) && continue
178✔
576
        if haskey(dict, binding)
90✔
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
88✔
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)
414✔
603
    chars = collect(needle)
207✔
604
    is = Int[]
207✔
605
    lastc = '\0'
×
606
    for (i, char) in enumerate(haystack)
414✔
607
        while !isempty(chars) && isspace(first(chars))
1,676✔
608
            popfirst!(chars) # skip spaces
3✔
609
        end
3✔
610
        isempty(chars) && break
849✔
611
        if lowercase(char) == lowercase(chars[1]) &&
927✔
612
           (!acronym || !isletter(lastc))
613
            push!(is, i)
247✔
614
            popfirst!(chars)
247✔
615
        end
616
        lastc = char
×
617
    end
824✔
618
    return is
207✔
619
end
620

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

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

627
# Optimal string distance: Counts the minimum number of insertions, deletions,
628
# transpositions or substitutions to go from one string to the other.
629
function string_distance(a::AbstractString, lena::Integer, b::AbstractString, lenb::Integer)
62,780✔
630
    if lena > lenb
62,780✔
631
        a, b = b, a
×
632
        lena, lenb = lenb, lena
×
633
    end
634
    start = 0
62,780✔
635
    for (i, j) in zip(a, b)
124,253✔
636
        if a == b
61,561✔
637
            start += 1
127✔
638
        else
639
            break
×
640
        end
641
    end
215✔
642
    start == lena && return lenb - start
62,780✔
643
    vzero = collect(1:(lenb - start))
512,906✔
644
    vone = similar(vzero)
61,434✔
645
    prev_a, prev_b = first(a), first(b)
61,434✔
646
    current = 0
×
647
    for (i, ai) in enumerate(a)
122,868✔
648
        i > start || (prev_a = ai; continue)
198,268✔
649
        left = i - start - 1
198,268✔
650
        current = i - start
198,268✔
651
        transition_next = 0
×
652
        for (j, bj) in enumerate(b)
396,536✔
653
            j > start || (prev_b = bj; continue)
1,872,817✔
654
            # No need to look beyond window of lower right diagonal
655
            above = current
×
656
            this_transition = transition_next
×
657
            transition_next = vone[j - start]
1,872,817✔
658
            vone[j - start] = current = left
1,872,817✔
659
            left = vzero[j - start]
1,872,817✔
660
            if ai != bj
1,872,817✔
661
                # Minimum between substitution, deletion and insertion
662
                current = min(current + 1, above + 1, left + 1)
1,811,126✔
663
                if i > start + 1 && j > start + 1 && ai == prev_b && prev_a == bj
1,811,126✔
664
                    current = min(current, (this_transition += 1))
2,436✔
665
                end
666
            end
667
            vzero[j - start] = current
1,872,817✔
668
            prev_b = bj
×
669
        end
3,547,366✔
670
        prev_a = ai
198,268✔
671
    end
335,102✔
672
    current
61,434✔
673
end
674

675
function fuzzyscore(needle::AbstractString, haystack::AbstractString)
10,142✔
676
    lena, lenb = length(needle), length(haystack)
62,780✔
677
    1 - (string_distance(needle, lena, haystack, lenb) / max(lena, lenb))
62,780✔
678
end
679

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

685
# Levenshtein Distance
686

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

693
    d[1:m+1, 1] = 0:m
81,912✔
694
    d[1, 1:n+1] = 0:n
87,202✔
695

696
    for i = 1:m, j = 1:n
71,387✔
697
        d[i+1,j+1] = min(d[i  , j+1] + 1,
456,214✔
698
                         d[i+1, j  ] + 1,
699
                         d[i  , j  ] + (a[i] != b[j]))
700
    end
506,701✔
701

702
    return d[m+1, n+1]
10,475✔
703
end
704

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

715
# Result printing
716

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

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

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

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

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

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

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

765
# Completion data
766

767

768
moduleusings(mod) = ccall(:jl_module_usings, Any, (Any,), mod)
51✔
769

770
filtervalid(names) = filter(x->!occursin(r"#", x), map(string, names))
64,639✔
771

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

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

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

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

793

794
# Searching and apropos
795

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

811
## Searching specific documentation objects
812
function docsearch(haystack::MultiDoc, needle)
11,741✔
813
    for v in values(haystack.docs)
23,482✔
814
        docsearch(v, needle) && return true
13,111✔
815
    end
11,001✔
816
    false
8,261✔
817
end
818

819
function docsearch(haystack::DocStr, needle)
13,111✔
820
    docsearch(parsedoc(haystack), needle) && return true
13,111✔
821
    if haskey(haystack.data, :fields)
9,631✔
822
        for doc in values(haystack.data[:fields])
845✔
823
            docsearch(doc, needle) && return true
54✔
824
        end
54✔
825
    end
826
    false
9,631✔
827
end
828

829
## doc search
830

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

834
"""
835
    stripmd(x)
836

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

846
stripmd(x::Markdown.BlockQuote) = "$(stripmd(x.content))"
×
847
stripmd(x::Markdown.Admonition) = "$(stripmd(x.content))"
2,088✔
848
stripmd(x::Markdown.Bold) = "$(stripmd(x.text))"
77✔
849
stripmd(x::Markdown.Code) = "$(stripmd(x.code))"
88,974✔
850
stripmd(x::Markdown.Header) = stripmd(x.text)
5,138✔
851
stripmd(x::Markdown.HorizontalRule) = " "
132✔
852
stripmd(x::Markdown.Image) = "$(stripmd(x.alt)) $(x.url)"
24✔
853
stripmd(x::Markdown.Italic) = "$(stripmd(x.text))"
598✔
854
stripmd(x::Markdown.LaTeX) = "$(x.formula)"
763✔
855
stripmd(x::Markdown.LineBreak) = " "
68✔
856
stripmd(x::Markdown.Link) = "$(stripmd(x.text)) $(x.url)"
12,041✔
857
stripmd(x::Markdown.List) = join(map(stripmd, x.items), " ")
1,271✔
858
stripmd(x::Markdown.MD) = join(map(stripmd, x.content), " ")
13,423✔
859
stripmd(x::Markdown.Paragraph) = stripmd(x.content)
29,444✔
860
stripmd(x::Markdown.Footnote) = "$(stripmd(x.id)) $(stripmd(x.text))"
164✔
861
stripmd(x::Markdown.Table) =
74✔
862
    join([join(map(stripmd, r), " ") for r in x.rows], " ")
863

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

867
Search available docstrings for entries containing `pattern`.
868

869
When `pattern` is a string, case is ignored. Results are printed to `io`.
870

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

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