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

JuliaLang / julia / #37836

11 Jul 2024 11:31AM UTC coverage: 85.36% (-2.2%) from 87.516%
#37836

push

local

web-flow
fix loading of repeated/concurrent modules (#55066)

More followup to fix issues with require. There was an accidental
variable reuse (build_id) that caused it to be unable to load cache
files in many cases. There was also missing check for a dependency
already being loaded, resulting in trying to load it twice. Finally, the
start_loading code may drop the require_lock, but the surrounding code
was not prepared for that. Now integrate the necessary checks into
start_loading, instead of needing to duplicate them before and
afterwards.

Fixes #53983
Fixes #54940
Closes #55064

74 of 89 new or added lines in 1 file covered. (83.15%)

2133 existing lines in 46 files now uncovered.

73916 of 86593 relevant lines covered (85.36%)

15331627.57 hits per line

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

51.77
/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
UNCOV
25
function helpmode(io::IO, line::AbstractString, mod::Module=Main)
×
UNCOV
26
    internal_accesses = Set{Pair{Module,Symbol}}()
×
UNCOV
27
    quote
×
UNCOV
28
        docs = $REPL.insert_hlines($(REPL._helpmode(io, line, mod, internal_accesses)))
×
UNCOV
29
        $REPL.insert_internal_warning(docs, $internal_accesses)
×
30
    end
31
end
UNCOV
32
helpmode(line::AbstractString, mod::Module=Main) = helpmode(stdout, line, mod)
×
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

UNCOV
38
function _helpmode(io::IO, line::AbstractString, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing)
×
UNCOV
39
    line = strip(line)
×
UNCOV
40
    ternary_operator_help = (line == "?" || line == "?:")
×
UNCOV
41
    if startswith(line, '?') && !ternary_operator_help
×
UNCOV
42
        line = line[2:end]
×
UNCOV
43
        extended_help_on[] = nothing
×
UNCOV
44
        brief = false
×
45
    else
UNCOV
46
        extended_help_on[] = line
×
UNCOV
47
        brief = true
×
48
    end
49
    # interpret anything starting with # or #= as asking for help on comments
UNCOV
50
    if startswith(line, "#")
×
51
        if startswith(line, "#=")
×
52
            line = "#="
×
53
        else
54
            line = "#"
×
55
        end
56
    end
UNCOV
57
    x = Meta.parse(line, raise = false, depwarn = false)
×
UNCOV
58
    assym = Symbol(line)
×
UNCOV
59
    expr =
×
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`.
UNCOV
64
            assym
×
UNCOV
65
        elseif isexpr(x, (:using, :import))
×
UNCOV
66
            (x::Expr).head
×
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.
UNCOV
73
            (isexpr(x, :macrocall, 1) && !endswith(line, "()")) ? quot(x) : x
×
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
UNCOV
77
    :($REPL.@repl $io $expr $brief $mod $internal_accesses)
×
78
end
UNCOV
79
_helpmode(line::AbstractString, mod::Module=Main) = _helpmode(stdout, line, mod)
×
80

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

95
function formatdoc(d::DocStr)
3,188✔
96
    buffer = IOBuffer()
3,188✔
97
    for part in d.text
6,376✔
98
        formatdoc(buffer, d, part)
3,918✔
99
    end
4,648✔
100
    Markdown.MD(Any[Markdown.parse(seekstart(buffer))])
3,188✔
101
end
102
@noinline formatdoc(buffer, d, part) = print(buffer, part)
3,916✔
103

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

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

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

UNCOV
126
trimdocs(doc, brief::Bool) = doc
×
127

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

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

UNCOV
159
_trimdocs(md, brief::Bool) = md, false
×
160

161

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

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

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

UNCOV
187
function insert_internal_warning(md::Markdown.MD, internal_access::Set{Pair{Module,Symbol}})
×
UNCOV
188
    if !isempty(internal_access)
×
UNCOV
189
        items = Any[Any[Markdown.Paragraph(Any[Markdown.Code("", s)])] for s in sort!(["$mod.$sym" for (mod, sym) in internal_access])]
×
UNCOV
190
        admonition = Markdown.Admonition("warning", "Warning", Any[
×
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)])
UNCOV
193
        pushfirst!(md.content, admonition)
×
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{})
340✔
204
    if defined(binding)
340✔
205
        result = getdoc(resolve(binding), sig)
279✔
206
        result === nothing || return result
17✔
207
    end
208
    results, groups = DocStr[], MultiDoc[]
277✔
209
    # Lookup `binding` and `sig` for matches in all modules of the docsystem.
210
    for mod in modules
277✔
211
        dict = meta(mod; autoinit=false)
49,066✔
212
        isnothing(dict) && continue
49,066✔
213
        if haskey(dict, binding)
24,533✔
214
            multidoc = dict[binding]
169✔
215
            push!(groups, multidoc)
169✔
216
            for msig in multidoc.order
169✔
217
                sig <: msig && push!(results, multidoc.docs[msig])
216✔
218
            end
216✔
219
        end
220
    end
24,533✔
221
    if isempty(groups)
277✔
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)
112✔
227
        alias == binding ? summarize(alias, sig) : doc(alias, sig)
112✔
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)
165✔
232
            for group in groups, each in group.order
10✔
233
                push!(results, group.docs[each])
13✔
234
            end
13✔
235
        end
236
        # Get parsed docs and concatenate them.
237
        md = catdoc(mapany(parsedoc, results)...)
180✔
238
        # Save metadata in the generated markdown.
239
        if isa(md, Markdown.MD)
165✔
240
            md.meta[:results] = results
164✔
241
            md.meta[:binding] = binding
164✔
242
            md.meta[:typesig] = sig
164✔
243
        end
244
        return md
165✔
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)
369✔
251
doc(object, sig...)              = doc(object, Tuple{sig...})
×
252

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

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

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

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

328
function summarize(io::IO, TT::Type, binding::Binding)
16✔
329
    println(io, "# Summary")
16✔
330
    T = Base.unwrap_unionall(TT)
16✔
331
    if T isa DataType
16✔
332
        println(io, "```")
13✔
333
        print(io,
21✔
334
            Base.isabstracttype(T) ? "abstract type " :
335
            Base.ismutabletype(T)  ? "mutable struct " :
336
            Base.isstructtype(T) ? "struct " :
337
            "primitive type ")
338
        supert = supertype(T)
13✔
339
        println(io, T)
13✔
340
        println(io, "```")
13✔
341
        if !Base.isabstracttype(T) && T.name !== Tuple.name && !isempty(fieldnames(T))
13✔
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)
13✔
351
        if !isempty(subt)
18✔
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
13✔
360
            println(io, "# Supertype Hierarchy")
9✔
361
            println(io, "```")
9✔
362
            Base.show_supertypes(io, T)
9✔
363
            println(io)
9✔
364
            println(io, "```")
9✔
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}
91✔
378
    mpath = pathof(m)
91✔
379
    isnothing(mpath) && return nothing
95✔
380
    !isfile(mpath) && return nothing # modules in sysimage, where src files are omitted
4✔
381
    path = dirname(mpath)
4✔
382
    top_path = pkgdir(m)
4✔
383
    while true
8✔
384
        for entry in _readdirx(path; sort=true)
8✔
385
            isfile(entry) && (lowercase(entry.name) in ["readme.md", "readme"]) || continue
90✔
386
            return entry.path
4✔
387
        end
30✔
388
        path == top_path && break # go no further than pkgdir
8✔
389
        path = dirname(path) # work up through nested modules
4✔
390
    end
4✔
391
    return nothing
×
392
end
393
function summarize(io::IO, m::Module, binding::Binding; nlines::Int = 200)
182✔
394
    readme_path = find_readme(m)
91✔
395
    public = Base.ispublic(binding.mod, binding.var) ? "public" : "internal"
91✔
396
    if isnothing(readme_path)
95✔
397
        println(io, "No docstring or readme file found for $public module `$m`.\n")
87✔
398
    else
399
        println(io, "No docstring found for $public module `$m`.")
4✔
400
    end
401
    exports = filter!(!=(nameof(m)), names(m))
182✔
402
    if isempty(exports)
91✔
403
        println(io, "Module does not have any public names.")
31✔
404
    else
405
        println(io, "# Public names")
60✔
406
        print(io, "  `")
60✔
407
        join(io, exports, "`, `")
60✔
408
        println(io, "`\n")
60✔
409
    end
410
    if !isnothing(readme_path)
95✔
411
        readme_lines = readlines(readme_path)
4✔
412
        isempty(readme_lines) && return  # don't say we are going to print empty file
4✔
413
        println(io, "# Displaying contents of readme found at `$(readme_path)`")
4✔
414
        for line in first(readme_lines, nlines)
4✔
415
            println(io, line)
40✔
416
        end
40✔
417
        length(readme_lines) > nlines && println(io, "\n[output truncated to first $nlines lines]")
4✔
418
    end
419
end
420

421
function summarize(io::IO, @nospecialize(T), binding::Binding)
1✔
422
    T = typeof(T)
1✔
423
    println(io, "`", binding, "` is of type `", T, "`.\n")
1✔
424
    summarize(io, T, binding)
1✔
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}
1,411✔
438
    name::String
439
end
440

441
function AccessibleBinding(mod::Module, name::Symbol)
1,249✔
442
    m = isexported(mod, name) ? nothing : String(nameof(mod))
1,343✔
443
    return AccessibleBinding(m, String(name))
1,249✔
444
end
445
AccessibleBinding(name::Symbol) = AccessibleBinding(nothing, String(name))
162✔
446

447
function Base.show(io::IO, b::AccessibleBinding)
448
    b.source === nothing || print(io, b.source, '.')
1,470✔
449
    print(io, b.name)
1,376✔
450
end
451

UNCOV
452
quote_spaces(x) = any(isspace, x) ? "'" * x * "'" : x
×
UNCOV
453
quote_spaces(x::AccessibleBinding) = AccessibleBinding(x.source, quote_spaces(x.name))
×
454

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

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

UNCOV
465
function repl_corrections(io::IO, s, mod::Module)
×
UNCOV
466
    print(io, "Couldn't find ")
×
UNCOV
467
    quot = any(isspace, s) ? "'" : ""
×
UNCOV
468
    print(io, quot)
×
UNCOV
469
    printstyled(io, s, color=:cyan)
×
UNCOV
470
    print(io, quot, '\n')
×
UNCOV
471
    print_correction(io, s, mod)
×
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)
6✔
478
    if isempty(symbols_latex) && isassigned(Base.REPL_MODULE_REF)
6✔
479
        for (k,v) in Iterators.flatten((REPLCompletions.latex_symbols,
1✔
480
                                        REPLCompletions.emoji_symbols))
481
            symbols_latex[v] = k
3,790✔
482
        end
3,792✔
483

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

488
    return get(symbols_latex, s, "")
6✔
489
end
490
function repl_latex(io::IO, s0::String)
4✔
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
4✔
495
    latex::String = symbol_latex(s)
4✔
496
    if isempty(latex)
4✔
497
        # Decompose NFC-normalized identifier to match tab-completion
498
        # input if the first search came up empty.
499
        s = normalize(s, :NFD)
2✔
500
        latex = symbol_latex(s)
2✔
501
    end
502
    if !isempty(latex)
4✔
503
        print(io, "\"")
2✔
504
        printstyled(io, s, color=:cyan)
2✔
505
        print(io, "\" can be typed by ")
2✔
506
        printstyled(io, latex, "<tab>", color=:cyan)
2✔
507
        println(io, '\n')
2✔
508
    elseif any(c -> haskey(symbols_latex, string(c)), s)
6✔
509
        print(io, "\"")
2✔
510
        printstyled(io, s, color=:cyan)
2✔
511
        print(io, "\" can be typed by ")
2✔
512
        state::Char = '\0'
2✔
513
        with_output_color(:cyan, io) do io
2✔
514
            for c in s
4✔
515
                cstr = string(c)
5✔
516
                if haskey(symbols_latex, cstr)
5✔
517
                    latex = symbols_latex[cstr]
3✔
518
                    if length(latex) == 3 && latex[2] in ('^','_')
4✔
519
                        # coalesce runs of sub/superscripts
520
                        if state != latex[2]
2✔
521
                            '\0' != state && print(io, "<tab>")
1✔
522
                            print(io, latex[1:2])
2✔
523
                            state = latex[2]
2✔
524
                        end
525
                        print(io, latex[3])
2✔
526
                    else
527
                        if '\0' != state
2✔
UNCOV
528
                            print(io, "<tab>")
×
UNCOV
529
                            state = '\0'
×
530
                        end
531
                        print(io, latex, "<tab>")
2✔
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
5✔
541
            '\0' != state && print(io, "<tab>")
2✔
542
        end
543
        println(io, '\n')
2✔
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
550

UNCOV
551
function repl(io::IO, s::Symbol; brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing)
×
UNCOV
552
    str = string(s)
×
UNCOV
553
    quote
×
UNCOV
554
        repl_latex($io, $str)
×
UNCOV
555
        repl_search($io, $str, $mod)
×
UNCOV
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
UNCOV
558
               :(repl_corrections($io, $str, $mod))
×
559
          end)
UNCOV
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])
5✔
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)
10✔
UNCOV
566
repl(io::IO, str::AbstractString; brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing) = :(apropos($io, $str))
×
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
2✔
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)
13✔
573
    if isexpr(x, :call)
13✔
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))
9✔
627
    docs = if isfield(x)
9✔
628
        quote
1✔
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
17✔
637
    end
638
    docs = log_nonpublic_access(macroexpand(mod, docs), mod, internal_accesses)
9✔
639
    :(REPL.trimdocs($docs, $brief))
9✔
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)
1✔
648
    for mod in modules
1✔
649
        dict = meta(mod; autoinit=false)
152✔
650
        isnothing(dict) && continue
152✔
651
        if haskey(dict, binding)
76✔
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
75✔
UNCOV
662
    fs = fieldnames(resolve(binding))
×
UNCOV
663
    fields = isempty(fs) ? "no fields" : (length(fs) == 1 ? "field " : "fields ") *
×
664
                                          join(("`$f`" for f in fs), ", ", ", and ")
UNCOV
665
    Markdown.parse("`$(resolve(binding))` has $fields.")
×
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)
1✔
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

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

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

UNCOV
700
longer(x, y) = length(x) ≥ length(y) ? (x, true) : (y, false)
×
701

UNCOV
702
bestmatch(needle, haystack) =
×
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.
UNCOV
708
function string_distance(a::AbstractString, lena::Integer, b::AbstractString, lenb::Integer)
×
UNCOV
709
    if lena > lenb
×
710
        a, b = b, a
×
711
        lena, lenb = lenb, lena
×
712
    end
UNCOV
713
    start = 0
×
UNCOV
714
    for (i, j) in zip(a, b)
×
UNCOV
715
        if a == b
×
UNCOV
716
            start += 1
×
717
        else
718
            break
×
719
        end
UNCOV
720
    end
×
UNCOV
721
    start == lena && return lenb - start
×
UNCOV
722
    vzero = collect(1:(lenb - start))
×
UNCOV
723
    vone = similar(vzero)
×
UNCOV
724
    prev_a, prev_b = first(a), first(b)
×
725
    current = 0
×
UNCOV
726
    for (i, ai) in enumerate(a)
×
UNCOV
727
        i > start || (prev_a = ai; continue)
×
UNCOV
728
        left = i - start - 1
×
UNCOV
729
        current = i - start
×
730
        transition_next = 0
×
UNCOV
731
        for (j, bj) in enumerate(b)
×
UNCOV
732
            j > start || (prev_b = bj; continue)
×
733
            # No need to look beyond window of lower right diagonal
734
            above = current
×
735
            this_transition = transition_next
×
UNCOV
736
            transition_next = vone[j - start]
×
UNCOV
737
            vone[j - start] = current = left
×
UNCOV
738
            left = vzero[j - start]
×
UNCOV
739
            if ai != bj
×
740
                # Minimum between substitution, deletion and insertion
UNCOV
741
                current = min(current + 1, above + 1, left + 1)
×
UNCOV
742
                if i > start + 1 && j > start + 1 && ai == prev_b && prev_a == bj
×
UNCOV
743
                    current = min(current, (this_transition += 1))
×
744
                end
745
            end
UNCOV
746
            vzero[j - start] = current
×
747
            prev_b = bj
×
UNCOV
748
        end
×
UNCOV
749
        prev_a = ai
×
UNCOV
750
    end
×
UNCOV
751
    current
×
752
end
753

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

UNCOV
759
function fuzzyscore(needle::AbstractString, haystack::AccessibleBinding)
×
UNCOV
760
    score = fuzzyscore(needle, haystack.name)
×
UNCOV
761
    haystack.source === nothing && return score
×
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
UNCOV
765
    penalty = 1 / (2 * max(length(needle), length(haystack.name)))
×
UNCOV
766
    return max(score - penalty, 0)
×
767
end
768

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

774
# Levenshtein Distance
775

UNCOV
776
function levenshtein(s1, s2)
×
UNCOV
777
    a, b = collect(s1), collect(s2)
×
UNCOV
778
    m = length(a)
×
UNCOV
779
    n = length(b)
×
UNCOV
780
    d = Matrix{Int}(undef, m+1, n+1)
×
781

UNCOV
782
    d[1:m+1, 1] = 0:m
×
UNCOV
783
    d[1, 1:n+1] = 0:n
×
784

UNCOV
785
    for i = 1:m, j = 1:n
×
UNCOV
786
        d[i+1,j+1] = min(d[i  , j+1] + 1,
×
787
                         d[i+1, j  ] + 1,
788
                         d[i  , j  ] + (a[i] != b[j]))
UNCOV
789
    end
×
790

UNCOV
791
    return d[m+1, n+1]
×
792
end
793

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

806
# Result printing
807

UNCOV
808
function printmatch(io::IO, word, match)
×
UNCOV
809
    is, _ = bestmatch(word, match)
×
UNCOV
810
    for (i, char) = enumerate(match)
×
UNCOV
811
        if i in is
×
UNCOV
812
            printstyled(io, char, bold=true)
×
813
        else
UNCOV
814
            print(io, char)
×
815
        end
UNCOV
816
    end
×
817
end
818

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

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

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

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

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

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

UNCOV
859
function print_correction(io::IO, word::String, mod::Module)
×
UNCOV
860
    cors = map(quote_spaces, levsort(word, accessible(mod)))
×
UNCOV
861
    pre = "Perhaps you meant "
×
UNCOV
862
    print(io, pre)
×
UNCOV
863
    print_joined_cols(io, cors, ", ", " or "; cols = _displaysize(io)[2] - length(pre))
×
UNCOV
864
    println(io)
×
UNCOV
865
    return
×
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)
1✔
874

875
function accessible(mod::Module)
1✔
876
    bindings = Set(AccessibleBinding(s) for s in names(mod; all=true, imported=true)
1✔
877
                   if !isdeprecated(mod, s))
878
    for used in moduleusings(mod)
1✔
879
        union!(bindings, (AccessibleBinding(used, s) for s in names(used)
7✔
880
                          if !isdeprecated(used, s)))
881
    end
7✔
882
    union!(bindings, (AccessibleBinding(k) for k in keys(Base.Docs.keywords)))
1✔
883
    filter!(b -> !occursin('#', b.name), bindings)
1,437✔
884
    return collect(bindings)
1✔
885
end
886

UNCOV
887
function doc_completions(name, mod::Module=Main)
×
UNCOV
888
    res = fuzzysort(name, accessible(mod))
×
889

890
    # to insert an entry like `raw""` for `"@raw_str"` in `res`
UNCOV
891
    ms = map(c -> match(r"^@(.*?)_str$", c.name), res)
×
UNCOV
892
    idxs = findall(!isnothing, ms)
×
893

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

903

904
# Searching and apropos
905

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

921
## Searching specific documentation objects
922
function docsearch(haystack::MultiDoc, needle)
5,548✔
923
    for v in values(haystack.docs)
11,096✔
924
        docsearch(v, needle) && return true
6,397✔
925
    end
6,154✔
926
    false
927
end
928

929
function docsearch(haystack::DocStr, needle)
6,397✔
930
    docsearch(parsedoc(haystack), needle) && return true
6,397✔
931
    if haskey(haystack.data, :fields)
12,308✔
932
        for doc in values(haystack.data[:fields])
539✔
933
            docsearch(doc, needle) && return true
44✔
934
        end
44✔
935
    end
936
    false
937
end
938

939
## doc search
940

941
## Markdown search simply strips all markup and searches plain text version
942
docsearch(haystack::Markdown.MD, needle) = docsearch(stripmd(haystack.content), needle)
6,574✔
943

944
"""
945
    stripmd(x)
946

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

956
stripmd(x::Markdown.BlockQuote) = "$(stripmd(x.content))"
2✔
957
stripmd(x::Markdown.Admonition) = "$(stripmd(x.content))"
1,353✔
958
stripmd(x::Markdown.Bold) = "$(stripmd(x.text))"
38✔
959
stripmd(x::Markdown.Code) = "$(stripmd(x.code))"
44,318✔
960
stripmd(x::Markdown.Header) = stripmd(x.text)
2,501✔
961
stripmd(x::Markdown.HorizontalRule) = " "
70✔
962
stripmd(x::Markdown.Image) = "$(stripmd(x.alt)) $(x.url)"
16✔
963
stripmd(x::Markdown.Italic) = "$(stripmd(x.text))"
288✔
964
stripmd(x::Markdown.LaTeX) = "$(x.formula)"
426✔
965
stripmd(x::Markdown.LineBreak) = " "
42✔
966
stripmd(x::Markdown.Link) = "$(stripmd(x.text)) $(x.url)"
6,650✔
967
stripmd(x::Markdown.List) = join(map(stripmd, x.items), " ")
543✔
968
stripmd(x::Markdown.MD) = join(map(stripmd, x.content), " ")
6,569✔
969
stripmd(x::Markdown.Paragraph) = stripmd(x.content)
14,688✔
970
stripmd(x::Markdown.Footnote) = "$(stripmd(x.id)) $(stripmd(x.text))"
98✔
971
stripmd(x::Markdown.Table) =
25✔
972
    join([join(map(stripmd, r), " ") for r in x.rows], " ")
973

974
apropos(string) = apropos(stdout, string)
×
975
apropos(io::IO, string) = apropos(io, Regex("\\Q$string", "i"))
1✔
976

977
function apropos(io::IO, needle::Regex)
2✔
978
    for mod in modules
2✔
979
        # Module doc might be in README.md instead of the META dict
980
        docsearch(doc(mod), needle) && println(io, mod)
179✔
981
        dict = meta(mod; autoinit=false)
358✔
982
        isnothing(dict) && continue
358✔
983
        for (k, v) in dict
356✔
984
            docsearch(v, needle) && println(io, k)
5,548✔
985
        end
5,548✔
986
    end
179✔
987
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