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

JuliaLang / julia / #37815

21 Jun 2024 05:00PM UTC coverage: 87.504% (+1.8%) from 85.685%
#37815

push

local

web-flow
Add docstring for inference barrier. (#54844)

`compilerbarrier` actually has a great docstring.
But it's most common form is via `inferencebarrier` which had none.

77230 of 88259 relevant lines covered (87.5%)

16071294.25 hits per line

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

90.82
/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, isdeprecated, isexported
13

14
using Base.Filesystem: _readdirx
15

16
import REPL
17

18
using InteractiveUtils: subtypes
19

20
using Unicode: normalize
21

22
## Help mode ##
23

24
# This is split into helpmode and _helpmode to easier unittest _helpmode
25
function helpmode(io::IO, line::AbstractString, mod::Module=Main)
23✔
26
    internal_accesses = Set{Pair{Module,Symbol}}()
27✔
27
    quote
23✔
28
        docs = $REPL.insert_hlines($(REPL._helpmode(io, line, mod, internal_accesses)))
29
        $REPL.insert_internal_warning(docs, $internal_accesses)
30
    end
31
end
32
helpmode(line::AbstractString, mod::Module=Main) = helpmode(stdout, line, mod)
2✔
33

34
# A hack to make the line entered at the REPL available at trimdocs without
35
# passing the string through the entire mechanism.
36
const extended_help_on = Ref{Any}(nothing)
37

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

81
# Print horizontal lines between each docstring if there are multiple docs
82
function insert_hlines(docs)
25✔
83
    if !isa(docs, Markdown.MD) || !haskey(docs.meta, :results) || isempty(docs.meta[:results])
66✔
84
        return docs
21✔
85
    end
86
    docs = docs::Markdown.MD
×
87
    v = Any[]
4✔
88
    for (n, doc) in enumerate(docs.content)
4✔
89
        push!(v, doc)
4✔
90
        n == length(docs.content) || push!(v, Markdown.HorizontalRule())
4✔
91
    end
4✔
92
    return Markdown.MD(v)
4✔
93
end
94

95
function formatdoc(d::DocStr)
6,240✔
96
    buffer = IOBuffer()
6,240✔
97
    for part in d.text
12,480✔
98
        formatdoc(buffer, d, part)
7,684✔
99
    end
9,128✔
100
    Markdown.MD(Any[Markdown.parse(seekstart(buffer))])
6,240✔
101
end
102
@noinline formatdoc(buffer, d, part) = print(buffer, part)
7,682✔
103

104
function parsedoc(d::DocStr)
12,516✔
105
    if d.object === nothing
12,516✔
106
        md = formatdoc(d)
6,240✔
107
        md.meta[:module] = d.data[:module]
6,240✔
108
        md.meta[:path]   = d.data[:path]
6,240✔
109
        d.object = md
6,240✔
110
    end
111
    d.object
12,516✔
112
end
113

114
## Trimming long help ("# Extended help")
115

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

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

126
trimdocs(doc, brief::Bool) = doc
2✔
127

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

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

159
_trimdocs(md, brief::Bool) = md, false
179✔
160

161

162
is_tuple(expr) = false
×
163
is_tuple(expr::Expr) = expr.head == :tuple
×
164

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

176
function log_nonpublic_access(expr::Expr, mod::Module, internal_access::Set{Pair{Module,Symbol}})
217✔
177
    if expr.head === :. && length(expr.args) == 2 && !is_tuple(expr.args[2])
217✔
178
        Expr(:call, Logged(getproperty, mod, internal_access), log_nonpublic_access.(expr.args, (mod,), (internal_access,))...)
24✔
179
    elseif expr.head === :call && expr.args[1] === Base.Docs.Binding
193✔
180
        Expr(:call, Logged(Base.Docs.Binding, mod, internal_access), log_nonpublic_access.(expr.args[2:end], (mod,), (internal_access,))...)
22✔
181
    else
182
        Expr(expr.head, log_nonpublic_access.(expr.args, (mod,), (internal_access,))...)
171✔
183
    end
184
end
185
log_nonpublic_access(expr, ::Module, _) = expr
36✔
186

187
function insert_internal_warning(md::Markdown.MD, internal_access::Set{Pair{Module,Symbol}})
23✔
188
    if !isempty(internal_access)
23✔
189
        items = Any[Any[Markdown.Paragraph(Any[Markdown.Code("", s)])] for s in sort!(["$mod.$sym" for (mod, sym) in internal_access])]
6✔
190
        admonition = Markdown.Admonition("warning", "Warning", Any[
12✔
191
            Markdown.Paragraph(Any["The following bindings may be internal; they may change or be removed in future versions:"]),
192
            Markdown.List(items, -1, false)])
193
        pushfirst!(md.content, admonition)
6✔
194
    end
195
    md
×
196
end
197
function insert_internal_warning(other, internal_access::Set{Pair{Module,Symbol}})
×
198
    # We don't know how to insert an internal symbol warning into non-markdown
199
    # content, so we don't.
200
    other
×
201
end
202

203
function doc(binding::Binding, sig::Type = Union{})
563✔
204
    if defined(binding)
563✔
205
        result = getdoc(resolve(binding), sig)
457✔
206
        result === nothing || return result
17✔
207
    end
208
    results, groups = DocStr[], MultiDoc[]
466✔
209
    # Lookup `binding` and `sig` for matches in all modules of the docsystem.
210
    for mod in modules
466✔
211
        dict = meta(mod; autoinit=false)
77,510✔
212
        isnothing(dict) && continue
77,510✔
213
        if haskey(dict, binding)
38,755✔
214
            multidoc = dict[binding]
268✔
215
            push!(groups, multidoc)
268✔
216
            for msig in multidoc.order
268✔
217
                sig <: msig && push!(results, multidoc.docs[msig])
315✔
218
            end
315✔
219
        end
220
    end
38,755✔
221
    if isempty(groups)
466✔
222
        # When no `MultiDoc`s are found that match `binding` then we check whether `binding`
223
        # is an alias of some other `Binding`. When it is we then re-run `doc` with that
224
        # `Binding`, otherwise if it's not an alias then we generate a summary for the
225
        # `binding` and display that to the user instead.
226
        alias = aliasof(binding)
204✔
227
        alias == binding ? summarize(alias, sig) : doc(alias, sig)
204✔
228
    else
229
        # There was at least one match for `binding` while searching. If there weren't any
230
        # matches for `sig` then we concatenate *all* the docs from the matching `Binding`s.
231
        if isempty(results)
262✔
232
            for group in groups, each in group.order
13✔
233
                push!(results, group.docs[each])
16✔
234
            end
16✔
235
        end
236
        # Get parsed docs and concatenate them.
237
        md = catdoc(mapany(parsedoc, results)...)
279✔
238
        # Save metadata in the generated markdown.
239
        if isa(md, Markdown.MD)
262✔
240
            md.meta[:results] = results
259✔
241
            md.meta[:binding] = binding
259✔
242
            md.meta[:typesig] = sig
259✔
243
        end
244
        return md
262✔
245
    end
246
end
247

248
# Some additional convenience `doc` methods that take objects rather than `Binding`s.
249
doc(obj::UnionAll) = doc(Base.unwrap_unionall(obj))
2✔
250
doc(object, sig::Type = Union{}) = doc(aliasof(object, typeof(object)), sig)
671✔
251
doc(object, sig...)              = doc(object, Tuple{sig...})
×
252

253
function lookup_doc(ex)
152✔
254
    if isa(ex, Expr) && ex.head !== :(.) && Base.isoperator(ex.head)
161✔
255
        # handle syntactic operators, e.g. +=, ::, .=
256
        ex = ex.head
×
257
    end
258
    if haskey(keywords, ex)
194✔
259
        return parsedoc(keywords[ex])
6✔
260
    elseif Meta.isexpr(ex, :incomplete)
112✔
261
        return :($(Markdown.md"No documentation found."))
1✔
262
    elseif !isa(ex, Expr) && !isa(ex, Symbol)
111✔
263
        return :($(doc)($(typeof)($(esc(ex)))))
2✔
264
    end
265
    if isa(ex, Symbol) && Base.isoperator(ex)
171✔
266
        str = string(ex)
6✔
267
        isdotted = startswith(str, ".")
6✔
268
        if endswith(str, "=") && Base.operator_precedence(ex) == Base.prec_assignment && ex !== :(:=)
6✔
269
            op = chop(str)
2✔
270
            eq = isdotted ? ".=" : "="
2✔
271
            return Markdown.parse("`x $op= y` is a synonym for `x $eq x $op y`")
2✔
272
        elseif isdotted && ex !== :(..)
4✔
273
            op = str[2:end]
2✔
274
            if op in ("&&", "||")
2✔
275
                return Markdown.parse("`x $ex y` broadcasts the boolean operator `$op` to `x` and `y`. See [`broadcast`](@ref).")
×
276
            else
277
                return Markdown.parse("`x $ex y` is akin to `broadcast($op, x, y)`. See [`broadcast`](@ref).")
1✔
278
            end
279
        end
280
    end
281
    binding = esc(bindingexpr(namify(ex)))
169✔
282
    if isexpr(ex, :call) || isexpr(ex, :macrocall) || isexpr(ex, :where)
200✔
283
        sig = esc(signature(ex))
41✔
284
        :($(doc)($binding, $sig))
41✔
285
    else
286
        :($(doc)($binding))
99✔
287
    end
288
end
289

290
# Object Summaries.
291
# =================
292

293
function summarize(binding::Binding, sig)
205✔
294
    io = IOBuffer()
205✔
295
    if defined(binding)
205✔
296
        binding_res = resolve(binding)
192✔
297
        if !isa(binding_res, Module)
192✔
298
            if Base.ispublic(binding.mod, binding.var)
24✔
299
                println(io, "No documentation found for public symbol.\n")
5✔
300
            else
301
                println(io, "No documentation found for private symbol.\n")
19✔
302
            end
303
        end
304
        summarize(io, binding_res, binding)
192✔
305
    else
306
        println(io, "No documentation found.\n")
13✔
307
        quot = any(isspace, sprint(print, binding)) ? "'" : ""
13✔
308
        if Base.isbindingresolved(binding.mod, binding.var)
13✔
309
            println(io, "Binding ", quot, "`", binding, "`", quot, " exists, but has not been assigned a value.")
1✔
310
        else
311
            println(io, "Binding ", quot, "`", binding, "`", quot, " does not exist.")
12✔
312
        end
313
    end
314
    md = Markdown.parse(seekstart(io))
410✔
315
    # Save metadata in the generated markdown.
316
    md.meta[:results] = DocStr[]
205✔
317
    md.meta[:binding] = binding
205✔
318
    md.meta[:typesig] = sig
205✔
319
    return md
205✔
320
end
321

322
function summarize(io::IO, λ::Function, binding::Binding)
5✔
323
    kind = startswith(string(binding.var), '@') ? "macro" : "`Function`"
10✔
324
    println(io, "`", binding, "` is a ", kind, ".")
5✔
325
    println(io, "```\n", methods(λ), "\n```")
5✔
326
end
327

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

377
function find_readme(m::Module)::Union{String, Nothing}
168✔
378
    mpath = pathof(m)
168✔
379
    isnothing(mpath) && return nothing
176✔
380
    !isfile(mpath) && return nothing # modules in sysimage, where src files are omitted
8✔
381
    path = dirname(mpath)
8✔
382
    top_path = pkgdir(m)
8✔
383
    while true
16✔
384
        for entry in _readdirx(path; sort=true)
16✔
385
            isfile(entry) && (lowercase(entry.name) in ["readme.md", "readme"]) || continue
180✔
386
            return entry.path
8✔
387
        end
60✔
388
        path == top_path && break # go no further than pkgdir
16✔
389
        path = dirname(path) # work up through nested modules
8✔
390
    end
8✔
391
    return nothing
×
392
end
393
function summarize(io::IO, m::Module, binding::Binding; nlines::Int = 200)
336✔
394
    readme_path = find_readme(m)
168✔
395
    public = Base.ispublic(binding.mod, binding.var) ? "public" : "internal"
168✔
396
    if isnothing(readme_path)
176✔
397
        println(io, "No docstring or readme file found for $public module `$m`.\n")
160✔
398
    else
399
        println(io, "No docstring found for $public module `$m`.")
8✔
400
    end
401
    exports = filter!(!=(nameof(m)), names(m))
336✔
402
    if isempty(exports)
168✔
403
        println(io, "Module does not have any public names.")
47✔
404
    else
405
        println(io, "# Public names")
121✔
406
        print(io, "  `")
121✔
407
        join(io, exports, "`, `")
121✔
408
        println(io, "`\n")
121✔
409
    end
410
    if !isnothing(readme_path)
176✔
411
        readme_lines = readlines(readme_path)
8✔
412
        isempty(readme_lines) && return  # don't say we are going to print empty file
8✔
413
        println(io, "# Displaying contents of readme found at `$(readme_path)`")
8✔
414
        for line in first(readme_lines, nlines)
8✔
415
            println(io, line)
80✔
416
        end
80✔
417
        length(readme_lines) > nlines && println(io, "\n[output truncated to first $nlines lines]")
8✔
418
    end
419
end
420

421
function summarize(io::IO, @nospecialize(T), binding::Binding)
4✔
422
    T = typeof(T)
4✔
423
    println(io, "`", binding, "` is of type `", T, "`.\n")
4✔
424
    summarize(io, T, binding)
4✔
425
end
426

427
# repl search and completions for help
428

429
# This type is returned from `accessible` and denotes a binding that is accessible within
430
# some context. It differs from `Base.Docs.Binding`, which is also used by the REPL, in
431
# that it doesn't track the defining module for a symbol unless the symbol is public but
432
# not exported, i.e. it's accessible but requires qualification. Using this type rather
433
# than `Base.Docs.Binding` simplifies things considerably, partially because REPL searching
434
# is based on `String`s, which this type stores, but `Base.Docs.Binding` stores a module
435
# and symbol and does not have any notion of the context from which the binding is accessed.
436
struct AccessibleBinding
437
    source::Union{String,Nothing}
164,180✔
438
    name::String
439
end
440

441
function AccessibleBinding(mod::Module, name::Symbol)
47,500✔
442
    m = isexported(mod, name) ? nothing : String(nameof(mod))
50,493✔
443
    return AccessibleBinding(m, String(name))
47,500✔
444
end
445
AccessibleBinding(name::Symbol) = AccessibleBinding(nothing, String(name))
53,271✔
446

447
function Base.show(io::IO, b::AccessibleBinding)
448
    b.source === nothing || print(io, b.source, '.')
26,575✔
449
    print(io, b.name)
24,879✔
450
end
451

452
quote_spaces(x) = any(isspace, x) ? "'" * x * "'" : x
63,006✔
453
quote_spaces(x::AccessibleBinding) = AccessibleBinding(x.source, quote_spaces(x.name))
63,908✔
454

455
function repl_search(io::IO, s::Union{Symbol,String}, mod::Module)
25✔
456
    pre = "search:"
25✔
457
    print(io, pre)
25✔
458
    printmatches(io, s, map(quote_spaces, doc_completions(s, mod)), cols = _displaysize(io)[2] - length(pre))
25✔
459
    println(io, "\n")
25✔
460
end
461

462
# TODO: document where this is used
463
repl_search(s, mod::Module) = repl_search(stdout, s, mod)
×
464

465
function repl_corrections(io::IO, s, mod::Module)
7✔
466
    print(io, "Couldn't find ")
7✔
467
    quot = any(isspace, s) ? "'" : ""
7✔
468
    print(io, quot)
7✔
469
    printstyled(io, s, color=:cyan)
7✔
470
    print(io, quot, '\n')
7✔
471
    print_correction(io, s, mod)
7✔
472
end
473
repl_corrections(s) = repl_corrections(stdout, s)
×
474

475
# inverse of latex_symbols Dict, lazily created as needed
476
const symbols_latex = Dict{String,String}()
477
function symbol_latex(s::String)
53✔
478
    if isempty(symbols_latex) && isassigned(Base.REPL_MODULE_REF)
53✔
479
        for (k,v) in Iterators.flatten((REPLCompletions.latex_symbols,
2✔
480
                                        REPLCompletions.emoji_symbols))
481
            symbols_latex[v] = k
7,580✔
482
        end
7,584✔
483

484
        # Overwrite with canonical mapping when a symbol has several completions (#39148)
485
        merge!(symbols_latex, REPLCompletions.symbols_latex_canonical)
2✔
486
    end
487

488
    return get(symbols_latex, s, "")
53✔
489
end
490
function repl_latex(io::IO, s0::String)
29✔
491
    # This has rampant `Core.Box` problems (#15276). Use the tricks of
492
    # https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-captured
493
    # We're changing some of the values so the `let` trick isn't applicable.
494
    s::String = s0
29✔
495
    latex::String = symbol_latex(s)
29✔
496
    if isempty(latex)
29✔
497
        # Decompose NFC-normalized identifier to match tab-completion
498
        # input if the first search came up empty.
499
        s = normalize(s, :NFD)
24✔
500
        latex = symbol_latex(s)
24✔
501
    end
502
    if !isempty(latex)
29✔
503
        print(io, "\"")
5✔
504
        printstyled(io, s, color=:cyan)
5✔
505
        print(io, "\" can be typed by ")
5✔
506
        printstyled(io, latex, "<tab>", color=:cyan)
5✔
507
        println(io, '\n')
5✔
508
    elseif any(c -> haskey(symbols_latex, string(c)), s)
120✔
509
        print(io, "\"")
3✔
510
        printstyled(io, s, color=:cyan)
3✔
511
        print(io, "\" can be typed by ")
3✔
512
        state::Char = '\0'
3✔
513
        with_output_color(:cyan, io) do io
3✔
514
            for c in s
5✔
515
                cstr = string(c)
13✔
516
                if haskey(symbols_latex, cstr)
13✔
517
                    latex = symbols_latex[cstr]
11✔
518
                    if length(latex) == 3 && latex[2] in ('^','_')
15✔
519
                        # coalesce runs of sub/superscripts
520
                        if state != latex[2]
14✔
521
                            '\0' != state && print(io, "<tab>")
3✔
522
                            print(io, latex[1:2])
6✔
523
                            state = latex[2]
6✔
524
                        end
525
                        print(io, latex[3])
14✔
526
                    else
527
                        if '\0' != state
4✔
528
                            print(io, "<tab>")
1✔
529
                            state = '\0'
1✔
530
                        end
531
                        print(io, latex, "<tab>")
4✔
532
                    end
533
                else
534
                    if '\0' != state
2✔
535
                        print(io, "<tab>")
×
536
                        state = '\0'
×
537
                    end
538
                    print(io, c)
2✔
539
                end
540
            end
13✔
541
            '\0' != state && print(io, "<tab>")
3✔
542
        end
543
        println(io, '\n')
3✔
544
    end
545
end
546
repl_latex(s::String) = repl_latex(stdout, s)
×
547

548
macro repl(ex, brief::Bool=false, mod::Module=Main) repl(ex; brief, mod) end
12✔
549
macro repl(io, ex, brief, mod, internal_accesses) repl(io, ex; brief, mod, internal_accesses) end
53✔
550

551
function repl(io::IO, s::Symbol; brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing)
50✔
552
    str = string(s)
25✔
553
    quote
32✔
554
        repl_latex($io, $str)
555
        repl_search($io, $str, $mod)
556
        $(if !isdefined(mod, s) && !Base.isbindingresolved(mod, s) && !haskey(keywords, s) && !Base.isoperator(s)
557
               # n.b. we call isdefined for the side-effect of resolving the binding, if possible
558
               :(repl_corrections($io, $str, $mod))
7✔
559
          end)
560
        $(_repl(s, brief, mod, internal_accesses))
561
    end
562
end
563
isregex(x) = isexpr(x, :macrocall, 3) && x.args[1] === Symbol("@r_str") && !isempty(x.args[3])
31✔
564

565
repl(io::IO, ex::Expr; brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing) = isregex(ex) ? :(apropos($io, $ex)) : _repl(ex, brief, mod, internal_accesses)
62✔
566
repl(io::IO, str::AbstractString; brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing) = :(apropos($io, $str))
2✔
567
repl(io::IO, other; brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing) = esc(:(@doc $other)) # TODO: track internal_accesses
4✔
568
#repl(io::IO, other) = lookup_doc(other) # TODO
569

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

572
function _repl(x, brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing)
63✔
573
    if isexpr(x, :call)
54✔
574
        x = x::Expr
6✔
575
        # determine the types of the values
576
        kwargs = nothing
6✔
577
        pargs = Any[]
6✔
578
        for arg in x.args[2:end]
6✔
579
            if isexpr(arg, :parameters)
9✔
580
                kwargs = mapany(arg.args) do kwarg
×
581
                    if kwarg isa Symbol
582
                        kwarg = :($kwarg::Any)
583
                    elseif isexpr(kwarg, :kw)
584
                        lhs = kwarg.args[1]
585
                        rhs = kwarg.args[2]
586
                        if lhs isa Symbol
587
                            if rhs isa Symbol
588
                                kwarg.args[1] = :($lhs::(@isdefined($rhs) ? typeof($rhs) : Any))
589
                            else
590
                                kwarg.args[1] = :($lhs::typeof($rhs))
591
                            end
592
                        end
593
                    end
594
                    kwarg
595
                end
596
            elseif isexpr(arg, :kw)
9✔
597
                if kwargs === nothing
1✔
598
                    kwargs = Any[]
1✔
599
                end
600
                lhs = arg.args[1]
1✔
601
                rhs = arg.args[2]
1✔
602
                if lhs isa Symbol
1✔
603
                    if rhs isa Symbol
1✔
604
                        arg.args[1] = :($lhs::(@isdefined($rhs) ? typeof($rhs) : Any))
×
605
                    else
606
                        arg.args[1] = :($lhs::typeof($rhs))
1✔
607
                    end
608
                end
609
                push!(kwargs, arg)
1✔
610
            else
611
                if arg isa Symbol
8✔
612
                    arg = :($arg::(@isdefined($arg) ? typeof($arg) : Any))
1✔
613
                elseif !isexpr(arg, :(::))
7✔
614
                    arg = :(::typeof($arg))
4✔
615
                end
616
                push!(pargs, arg)
8✔
617
            end
618
        end
9✔
619
        if kwargs === nothing
6✔
620
            x.args = Any[x.args[1], pargs...]
5✔
621
        else
622
            x.args = Any[x.args[1], Expr(:parameters, kwargs...), pargs...]
1✔
623
        end
624
    end
625
    #docs = lookup_doc(x) # TODO
626
    docs = esc(:(@doc $x))
59✔
627
    docs = if isfield(x)
50✔
628
        quote
21✔
629
            if isa($(esc(x.args[1])), DataType)
630
                fielddoc($(esc(x.args[1])), $(esc(x.args[2])))
631
            else
632
                $docs
633
            end
634
        end
635
    else
636
        docs
63✔
637
    end
638
    docs = log_nonpublic_access(macroexpand(mod, docs), mod, internal_accesses)
59✔
639
    :(REPL.trimdocs($docs, $brief))
59✔
640
end
641

642
"""
643
    fielddoc(binding, field)
644

645
Return documentation for a particular `field` of a type if it exists.
646
"""
647
function fielddoc(binding::Binding, field::Symbol)
5✔
648
    for mod in modules
5✔
649
        dict = meta(mod; autoinit=false)
760✔
650
        isnothing(dict) && continue
760✔
651
        if haskey(dict, binding)
380✔
652
            multidoc = dict[binding]
1✔
653
            if haskey(multidoc.docs, Union{})
1✔
654
                fields = multidoc.docs[Union{}].data[:fields]
1✔
655
                if haskey(fields, field)
1✔
656
                    doc = fields[field]
1✔
657
                    return isa(doc, Markdown.MD) ? doc : Markdown.parse(doc)
1✔
658
                end
659
            end
660
        end
661
    end
379✔
662
    fs = fieldnames(resolve(binding))
4✔
663
    fields = isempty(fs) ? "no fields" : (length(fs) == 1 ? "field " : "fields ") *
7✔
664
                                          join(("`$f`" for f in fs), ", ", ", and ")
665
    Markdown.parse("`$(resolve(binding))` has $fields.")
4✔
666
end
667

668
# As with the additional `doc` methods, this converts an object to a `Binding` first.
669
fielddoc(object, field::Symbol) = fielddoc(aliasof(object, typeof(object)), field)
5✔
670

671

672
# Search & Rescue
673
# Utilities for correcting user mistakes and (eventually)
674
# doing full documentation searches from the repl.
675

676
# Fuzzy Search Algorithm
677

678
function matchinds(needle, haystack; acronym::Bool = false)
506✔
679
    chars = collect(needle)
506✔
680
    is = Int[]
253✔
681
    lastc = '\0'
×
682
    for (i, char) in enumerate(haystack)
506✔
683
        while !isempty(chars) && isspace(first(chars))
2,204✔
684
            popfirst!(chars) # skip spaces
4✔
685
        end
4✔
686
        isempty(chars) && break
1,111✔
687
        if lowercase(char) == lowercase(chars[1]) &&
1,221✔
688
           (!acronym || !isletter(lastc))
689
            push!(is, i)
323✔
690
            popfirst!(chars)
323✔
691
        end
692
        lastc = char
×
693
    end
1,089✔
694
    return is
253✔
695
end
696

697
matchinds(needle, (; name)::AccessibleBinding; acronym::Bool=false) =
×
698
    matchinds(needle, name; acronym)
699

700
longer(x, y) = length(x) ≥ length(y) ? (x, true) : (y, false)
126✔
701

702
bestmatch(needle, haystack) =
126✔
703
    longer(matchinds(needle, haystack, acronym = true),
704
           matchinds(needle, haystack))
705

706
# Optimal string distance: Counts the minimum number of insertions, deletions,
707
# transpositions or substitutions to go from one string to the other.
708
function string_distance(a::AbstractString, lena::Integer, b::AbstractString, lenb::Integer)
98,847✔
709
    if lena > lenb
98,847✔
710
        a, b = b, a
×
711
        lena, lenb = lenb, lena
×
712
    end
713
    start = 0
98,847✔
714
    for (i, j) in zip(a, b)
196,468✔
715
        if a == b
97,792✔
716
            start += 1
228✔
717
        else
718
            break
×
719
        end
720
    end
399✔
721
    start == lena && return lenb - start
98,847✔
722
    vzero = collect(1:(lenb - start))
998,048✔
723
    vone = similar(vzero)
195,128✔
724
    prev_a, prev_b = first(a), first(b)
193,461✔
725
    current = 0
×
726
    for (i, ai) in enumerate(a)
195,128✔
727
        i > start || (prev_a = ai; continue)
312,375✔
728
        left = i - start - 1
312,375✔
729
        current = i - start
312,375✔
730
        transition_next = 0
×
731
        for (j, bj) in enumerate(b)
624,750✔
732
            j > start || (prev_b = bj; continue)
3,373,256✔
733
            # No need to look beyond window of lower right diagonal
734
            above = current
×
735
            this_transition = transition_next
×
736
            transition_next = vone[j - start]
3,373,256✔
737
            vone[j - start] = current = left
3,373,256✔
738
            left = vzero[j - start]
3,373,256✔
739
            if ai != bj
3,373,256✔
740
                # Minimum between substitution, deletion and insertion
741
                current = min(current + 1, above + 1, left + 1)
3,266,078✔
742
                if i > start + 1 && j > start + 1 && ai == prev_b && prev_a == bj
3,266,078✔
743
                    current = min(current, (this_transition += 1))
4,194✔
744
                end
745
            end
746
            vzero[j - start] = current
3,373,256✔
747
            prev_b = bj
×
748
        end
6,434,137✔
749
        prev_a = ai
312,375✔
750
    end
527,186✔
751
    current
97,564✔
752
end
753

754
function fuzzyscore(needle::AbstractString, haystack::AbstractString)
4✔
755
    lena, lenb = length(needle), length(haystack)
98,847✔
756
    1 - (string_distance(needle, lena, haystack, lenb) / max(lena, lenb))
98,847✔
757
end
758

759
function fuzzyscore(needle::AbstractString, haystack::AccessibleBinding)
98,843✔
760
    score = fuzzyscore(needle, haystack.name)
98,843✔
761
    haystack.source === nothing && return score
98,843✔
762
    # Apply a "penalty" of half an edit if the comparator binding is public but not
763
    # exported so that exported/local names that exactly match the search query are
764
    # listed first
765
    penalty = 1 / (2 * max(length(needle), length(haystack.name)))
2,907✔
766
    return max(score - penalty, 0)
2,907✔
767
end
768

769
function fuzzysort(search::String, candidates::Vector{AccessibleBinding})
45✔
770
    scores = map(cand -> fuzzyscore(search, cand), candidates)
85,848✔
771
    candidates[sortperm(scores)] |> reverse
45✔
772
end
773

774
# Levenshtein Distance
775

776
function levenshtein(s1, s2)
13,325✔
777
    a, b = collect(s1), collect(s2)
26,641✔
778
    m = length(a)
13,325✔
779
    n = length(b)
13,325✔
780
    d = Matrix{Int}(undef, m+1, n+1)
26,650✔
781

782
    d[1:m+1, 1] = 0:m
63,167✔
783
    d[1, 1:n+1] = 0:n
140,137✔
784

785
    for i = 1:m, j = 1:n
13,325✔
786
        d[i+1,j+1] = min(d[i  , j+1] + 1,
448,177✔
787
                         d[i+1, j  ] + 1,
788
                         d[i  , j  ] + (a[i] != b[j]))
789
    end
484,694✔
790

791
    return d[m+1, n+1]
13,325✔
792
end
793

794
function levsort(search::String, candidates::Vector{AccessibleBinding})
7✔
795
    scores = map(candidates) do cand
7✔
796
        (Float64(levenshtein(search, cand.name)), -fuzzyscore(search, cand))
12,891✔
797
    end
798
    candidates = candidates[sortperm(scores)]
7✔
799
    i = 0
7✔
800
    for outer i = 1:length(candidates)
7✔
801
        levenshtein(search, candidates[i].name) > 3 && break
434✔
802
    end
427✔
803
    return candidates[1:i]
7✔
804
end
805

806
# Result printing
807

808
function printmatch(io::IO, word, match)
126✔
809
    is, _ = bestmatch(word, match)
126✔
810
    for (i, char) = enumerate(match)
252✔
811
        if i in is
1,523✔
812
            printstyled(io, char, bold=true)
255✔
813
        else
814
            print(io, char)
309✔
815
        end
816
    end
564✔
817
end
818

819
function printmatch(io::IO, word, match::AccessibleBinding)
820
    match.source === nothing || print(io, match.source, '.')
131✔
821
    printmatch(io, word, match.name)
126✔
822
end
823

824
function matchlength(x::AccessibleBinding)
223✔
825
    n = length(x.name)
223✔
826
    if x.source !== nothing
223✔
827
        n += length(x.source) + 1  # the +1 is for the `.` separator
8✔
828
    end
829
    return n
223✔
830
end
831
matchlength(x) = length(x)
×
832

833
function printmatches(io::IO, word, matches; cols::Int = _displaysize(io)[2])
50✔
834
    total = 0
25✔
835
    for match in matches
25✔
836
        ml = matchlength(match)
151✔
837
        total + ml + 1 > cols && break
151✔
838
        fuzzyscore(word, match) < 0.5 && break
145✔
839
        print(io, " ")
126✔
840
        printmatch(io, word, match)
131✔
841
        total += ml + 1
126✔
842
    end
126✔
843
end
844

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

847
function print_joined_cols(io::IO, ss::Vector{AccessibleBinding}, delim = "", last = delim; cols::Int = _displaysize(io)[2])
14✔
848
    i = 0
7✔
849
    total = 0
7✔
850
    for outer i = 1:length(ss)
7✔
851
        total += matchlength(ss[i])
72✔
852
        total + max(i-2,0)*length(delim) + (i>1 ? 1 : 0)*length(last) > cols && (i-=1; break)
72✔
853
    end
67✔
854
    join(io, ss[1:i], delim, last)
7✔
855
end
856

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

859
function print_correction(io::IO, word::String, mod::Module)
7✔
860
    cors = map(quote_spaces, levsort(word, accessible(mod)))
7✔
861
    pre = "Perhaps you meant "
7✔
862
    print(io, pre)
7✔
863
    print_joined_cols(io, cors, ", ", " or "; cols = _displaysize(io)[2] - length(pre))
7✔
864
    println(io)
7✔
865
    return
7✔
866
end
867

868
# TODO: document where this is used
869
print_correction(word, mod::Module) = print_correction(stdout, word, mod)
×
870

871
# Completion data
872

873
moduleusings(mod) = ccall(:jl_module_usings, Any, (Any,), mod)
53✔
874

875
function accessible(mod::Module)
53✔
876
    bindings = Set(AccessibleBinding(s) for s in names(mod; non_public=true, imported=true))
53✔
877
    for used in moduleusings(mod)
53✔
878
        union!(bindings, (AccessibleBinding(used, s) for s in names(used)))
661✔
879
    end
661✔
880
    union!(bindings, (AccessibleBinding(k) for k in keys(Base.Docs.keywords)))
53✔
881
    filter!(b -> !occursin('#', b.name), bindings)
100,245✔
882
    return collect(bindings)
53✔
883
end
884

885
function doc_completions(name, mod::Module=Main)
45✔
886
    res = fuzzysort(name, accessible(mod))
48✔
887

888
    # to insert an entry like `raw""` for `"@raw_str"` in `res`
889
    ms = map(c -> match(r"^@(.*?)_str$", c.name), res)
85,848✔
890
    idxs = findall(!isnothing, ms)
90✔
891

892
    # avoid messing up the order while inserting
893
    for i in reverse!(idxs)
45✔
894
        c = only((ms[i]::AbstractMatch).captures)
401✔
895
        insert!(res, i, AccessibleBinding(res[i].source, "$(c)\"\""))
401✔
896
    end
401✔
897
    res
×
898
end
899
doc_completions(name::Symbol) = doc_completions(string(name), mod)
×
900

901

902
# Searching and apropos
903

904
# Docsearch simply returns true or false if an object contains the given needle
905
docsearch(haystack::AbstractString, needle) = occursin(needle, haystack)
12,593✔
906
docsearch(haystack::Symbol, needle) = docsearch(string(haystack), needle)
×
907
docsearch(::Nothing, needle) = false
×
908
function docsearch(haystack::Array, needle)
×
909
    for elt in haystack
×
910
        docsearch(elt, needle) && return true
×
911
    end
×
912
    false
×
913
end
914
function docsearch(haystack, needle)
×
915
    @warn "Unable to search documentation of type $(typeof(haystack))" maxlog=1
×
916
    false
×
917
end
918

919
## Searching specific documentation objects
920
function docsearch(haystack::MultiDoc, needle)
10,974✔
921
    for v in values(haystack.docs)
21,948✔
922
        docsearch(v, needle) && return true
12,210✔
923
    end
8,931✔
924
    false
925
end
926

927
function docsearch(haystack::DocStr, needle)
12,210✔
928
    docsearch(parsedoc(haystack), needle) && return true
12,210✔
929
    if haskey(haystack.data, :fields)
17,862✔
930
        for doc in values(haystack.data[:fields])
796✔
931
            docsearch(doc, needle) && return true
54✔
932
        end
54✔
933
    end
934
    false
935
end
936

937
## doc search
938

939
## Markdown search simply strips all markup and searches plain text version
940
docsearch(haystack::Markdown.MD, needle) = docsearch(stripmd(haystack.content), needle)
12,537✔
941

942
"""
943
    stripmd(x)
944

945
Strip all Markdown markup from x, leaving the result in plain text. Used
946
internally by apropos to make docstrings containing more than one markdown
947
element searchable.
948
"""
949
stripmd(@nospecialize x) = string(x) # for random objects interpolated into the docstring
6✔
950
stripmd(x::AbstractString) = x  # base case
185,426✔
951
stripmd(x::Nothing) = " "
100✔
952
stripmd(x::Vector) = string(map(stripmd, x)...)
66,130✔
953

954
stripmd(x::Markdown.BlockQuote) = "$(stripmd(x.content))"
4✔
955
stripmd(x::Markdown.Admonition) = "$(stripmd(x.content))"
2,590✔
956
stripmd(x::Markdown.Bold) = "$(stripmd(x.text))"
69✔
957
stripmd(x::Markdown.Code) = "$(stripmd(x.code))"
85,315✔
958
stripmd(x::Markdown.Header) = stripmd(x.text)
4,763✔
959
stripmd(x::Markdown.HorizontalRule) = " "
140✔
960
stripmd(x::Markdown.Image) = "$(stripmd(x.alt)) $(x.url)"
32✔
961
stripmd(x::Markdown.Italic) = "$(stripmd(x.text))"
568✔
962
stripmd(x::Markdown.LaTeX) = "$(x.formula)"
815✔
963
stripmd(x::Markdown.LineBreak) = " "
84✔
964
stripmd(x::Markdown.Link) = "$(stripmd(x.text)) $(x.url)"
12,732✔
965
stripmd(x::Markdown.List) = join(map(stripmd, x.items), " ")
1,078✔
966
stripmd(x::Markdown.MD) = join(map(stripmd, x.content), " ")
12,538✔
967
stripmd(x::Markdown.Paragraph) = stripmd(x.content)
28,272✔
968
stripmd(x::Markdown.Footnote) = "$(stripmd(x.id)) $(stripmd(x.text))"
196✔
969
stripmd(x::Markdown.Table) =
50✔
970
    join([join(map(stripmd, r), " ") for r in x.rows], " ")
971

972
apropos(string) = apropos(stdout, string)
×
973
apropos(io::IO, string) = apropos(io, Regex("\\Q$string", "i"))
2✔
974

975
function apropos(io::IO, needle::Regex)
4✔
976
    for mod in modules
4✔
977
        # Module doc might be in README.md instead of the META dict
978
        docsearch(doc(mod), needle) && println(io, mod)
329✔
979
        dict = meta(mod; autoinit=false)
658✔
980
        isnothing(dict) && continue
658✔
981
        for (k, v) in dict
656✔
982
            docsearch(v, needle) && println(io, k)
10,974✔
983
        end
10,974✔
984
    end
329✔
985
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