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

JuliaLang / julia / 1285

25 Sep 2025 09:41PM UTC coverage: 70.929% (-6.0%) from 76.923%
1285

push

buildkite

web-flow
Fix gcdx and lcm with mixed signed/unsigned arguments (#59628)

Add `gcdx(a::Signed, b::Unsigned)` and `gcdx(a::Unsigned, b::Signed)`
methods to fix #58025:
```julia
julia> gcdx(UInt16(100), Int8(-101))  # pr
(0x0001, 0xffff, 0xffff)

julia> gcdx(UInt16(100), Int8(-101))  # master, incorrect result
(0x0005, 0xf855, 0x0003)
```

Also add the equivalent methods for `lcm` to fix the systematic
`InexactError` when one argument is a negative `Signed` and the other is
any `Unsigned`:
```julia
julia> lcm(UInt16(100), Int8(-101))  # pr
0x2774

julia> lcm(UInt16(100), Int8(-101))  # master, error
ERROR: InexactError: trunc(UInt16, -101)
Stacktrace:
 [1] throw_inexacterror(func::Symbol, to::Type, val::Int8)
   @ Core ./boot.jl:866
 [2] check_sign_bit
   @ ./boot.jl:872 [inlined]
 [3] toUInt16
   @ ./boot.jl:958 [inlined]
 [4] UInt16
   @ ./boot.jl:1011 [inlined]
 [5] convert
   @ ./number.jl:7 [inlined]
 [6] _promote
   @ ./promotion.jl:379 [inlined]
 [7] promote
   @ ./promotion.jl:404 [inlined]
 [8] lcm(a::UInt16, b::Int8)
   @ Base ./intfuncs.jl:152
 [9] top-level scope
   @ REPL[62]:1
```

Inspired by
https://github.com/JuliaLang/julia/pull/59487#issuecomment-3258209203.
The difference is that the solution proposed in this PR keeps the
current correct result type for inputs such as `(::Int16, ::UInt8)`.

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

4780 existing lines in 122 files now uncovered.

50633 of 71385 relevant lines covered (70.93%)

7422080.26 hits per line

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

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

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

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

79
function formatdoc(d::DocStr)
3✔
80
    buffer = IOBuffer()
3✔
81
    for part in d.text
6✔
82
        formatdoc(buffer, d, part)
3✔
83
    end
3✔
84
    md = Markdown.MD(Any[Markdown.parse(seekstart(buffer))])
6✔
85
    assume_julia_code!(md)
3✔
86
end
87
@noinline formatdoc(buffer, d, part) = print(buffer, part)
3✔
88

89
function parsedoc(d::DocStr)
3✔
90
    if d.object === nothing
3✔
91
        md = formatdoc(d)
3✔
92
        md.meta[:module] = d.data[:module]
3✔
93
        md.meta[:path]   = d.data[:path]
3✔
94
        d.object = md
3✔
95
    end
96
    d.object
3✔
97
end
98

99
"""
100
    assume_julia_code!(doc::Markdown.MD) -> doc
101

102
Assume that code blocks with no language specified are Julia code.
103
"""
104
function assume_julia_code!(doc::Markdown.MD)
3✔
105
    assume_julia_code!(doc.content)
6✔
106
    doc
6✔
107
end
108

109
function assume_julia_code!(blocks::Vector)
6✔
110
    for (i, block) in enumerate(blocks)
12✔
111
        if block isa Markdown.Code && block.language == ""
6✔
UNCOV
112
            blocks[i] = Markdown.Code("julia", block.code)
×
113
        elseif block isa Vector || block isa Markdown.MD
12✔
114
            assume_julia_code!(block)
3✔
115
        end
116
    end
6✔
117
    blocks
6✔
118
end
119

120
## Trimming long help ("# Extended help")
121

122
struct Message  # For direct messages to the terminal
123
    msg    # AbstractString
124
    fmt    # keywords to `printstyled`
125
end
126
Message(msg) = Message(msg, ())
×
127

128
function Markdown.term(io::IO, msg::Message, columns)
×
129
    printstyled(io, msg.msg; msg.fmt...)
×
130
end
131

UNCOV
132
trimdocs(doc, brief::Bool) = doc
×
133

UNCOV
134
function trimdocs(md::Markdown.MD, brief::Bool)
×
UNCOV
135
    brief || return md
×
UNCOV
136
    md, trimmed = _trimdocs(md, brief)
×
UNCOV
137
    if trimmed
×
UNCOV
138
        line = extended_help_on[]
×
UNCOV
139
        line = isa(line, AbstractString) ? line : ""
×
UNCOV
140
        push!(md.content, Message("Extended help is available with `??$line`", (color=Base.info_color(), bold=true)))
×
141
    end
UNCOV
142
    return md
×
143
end
144

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

UNCOV
165
_trimdocs(md, brief::Bool) = md, false
×
166

167

168
is_tuple(expr) = false
×
169
is_tuple(expr::Expr) = expr.head == :tuple
×
170

171
struct Logged{F}
172
    f::F
173
    mod::Module
174
    collection::Set{Pair{Module,Symbol}}
175
end
UNCOV
176
function (la::Logged)(m::Module, s::Symbol)
×
UNCOV
177
    m !== la.mod && Base.isdefined(m, s) && !Base.ispublic(m, s) && push!(la.collection, m => s)
×
UNCOV
178
    la.f(m, s)
×
179
end
180
(la::Logged)(args...) = la.f(args...)
×
181

UNCOV
182
function log_nonpublic_access(expr::Expr, mod::Module, internal_access::Set{Pair{Module,Symbol}})
×
UNCOV
183
    if expr.head === :. && length(expr.args) == 2 && !is_tuple(expr.args[2])
×
UNCOV
184
        Expr(:call, Logged(getproperty, mod, internal_access), log_nonpublic_access.(expr.args, (mod,), (internal_access,))...)
×
UNCOV
185
    elseif expr.head === :call && expr.args[1] === Base.Docs.Binding
×
UNCOV
186
        Expr(:call, Logged(Base.Docs.Binding, mod, internal_access), log_nonpublic_access.(expr.args[2:end], (mod,), (internal_access,))...)
×
187
    else
UNCOV
188
        Expr(expr.head, log_nonpublic_access.(expr.args, (mod,), (internal_access,))...)
×
189
    end
190
end
UNCOV
191
log_nonpublic_access(expr, ::Module, _) = expr
×
192

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

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

254
# Some additional convenience `doc` methods that take objects rather than `Binding`s.
UNCOV
255
doc(obj::UnionAll) = doc(Base.unwrap_unionall(obj))
×
256
doc(object, sig::Type = Union{}) = doc(aliasof(object, typeof(object)), sig)
6✔
257
doc(object, sig...)              = doc(object, Tuple{sig...})
×
258

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

296
# Object Summaries.
297
# =================
298

UNCOV
299
function summarize(binding::Binding, sig)
×
UNCOV
300
    io = IOBuffer()
×
UNCOV
301
    if defined(binding)
×
UNCOV
302
        binding_res = resolve(binding)
×
UNCOV
303
        if !isa(binding_res, Module)
×
UNCOV
304
            varstr = "$(binding.mod).$(binding.var)"
×
UNCOV
305
            if Base.ispublic(binding.mod, binding.var)
×
UNCOV
306
                println(io, "No documentation found for public binding `$varstr`.\n")
×
307
            else
UNCOV
308
                println(io, "No documentation found for private binding `$varstr`.\n")
×
309
            end
310
        end
UNCOV
311
        summarize(io, binding_res, binding)
×
312
    else
UNCOV
313
        println(io, "No documentation found.\n")
×
UNCOV
314
        quot = any(isspace, sprint(print, binding)) ? "'" : ""
×
UNCOV
315
        bpart = Base.lookup_binding_partition(Base.tls_world_age(), convert(Core.Binding, GlobalRef(binding.mod, binding.var)))
×
UNCOV
316
        if Base.binding_kind(bpart) === Base.PARTITION_KIND_GUARD
×
UNCOV
317
            println(io, "Binding ", quot, "`", binding, "`", quot, " does not exist.")
×
318
        else
UNCOV
319
            println(io, "Binding ", quot, "`", binding, "`", quot, " exists, but has not been assigned a value.")
×
320
        end
321
    end
UNCOV
322
    md = Markdown.parse(seekstart(io))
×
323
    # Save metadata in the generated markdown.
UNCOV
324
    md.meta[:results] = DocStr[]
×
UNCOV
325
    md.meta[:binding] = binding
×
UNCOV
326
    md.meta[:typesig] = sig
×
UNCOV
327
    return md
×
328
end
329

UNCOV
330
function summarize(io::IO, λ::Function, binding::Binding)
×
UNCOV
331
    kind = startswith(string(binding.var), '@') ? "macro" : "`Function`"
×
UNCOV
332
    println(io, "`", binding, "` is a ", kind, ".")
×
UNCOV
333
    println(io, "```\n", methods(λ), "\n```")
×
334
end
335

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

UNCOV
385
function find_readme(m::Module)::Union{String, Nothing}
×
UNCOV
386
    mpath = pathof(m)
×
UNCOV
387
    isnothing(mpath) && return nothing
×
UNCOV
388
    !isfile(mpath) && return nothing # modules in sysimage, where src files are omitted
×
UNCOV
389
    path = dirname(mpath)
×
UNCOV
390
    top_path = pkgdir(m)
×
UNCOV
391
    while true
×
UNCOV
392
        for entry in _readdirx(path; sort=true)
×
UNCOV
393
            isfile(entry) && (lowercase(entry.name) in ["readme.md", "readme"]) || continue
×
UNCOV
394
            return entry.path
×
UNCOV
395
        end
×
UNCOV
396
        path == top_path && break # go no further than pkgdir
×
UNCOV
397
        path = dirname(path) # work up through nested modules
×
UNCOV
398
    end
×
UNCOV
399
    return nothing
×
400
end
UNCOV
401
function summarize(io::IO, m::Module, binding::Binding; nlines::Int = 200)
×
UNCOV
402
    readme_path = find_readme(m)
×
UNCOV
403
    public = Base.ispublic(binding.mod, binding.var) ? "public" : "internal"
×
UNCOV
404
    if isnothing(readme_path)
×
UNCOV
405
        println(io, "No docstring or readme file found for $public module `$m`.\n")
×
406
    else
UNCOV
407
        println(io, "No docstring found for $public module `$m`.")
×
408
    end
UNCOV
409
    exports = filter!(!=(nameof(m)), names(m))
×
UNCOV
410
    if isempty(exports)
×
UNCOV
411
        println(io, "Module does not have any public names.")
×
412
    else
UNCOV
413
        println(io, "# Public names")
×
UNCOV
414
        print(io, "  `")
×
UNCOV
415
        join(io, exports, "`, `")
×
UNCOV
416
        println(io, "`\n")
×
417
    end
UNCOV
418
    if !isnothing(readme_path)
×
UNCOV
419
        readme_lines = readlines(readme_path)
×
UNCOV
420
        isempty(readme_lines) && return  # don't say we are going to print empty file
×
UNCOV
421
        println(io)
×
UNCOV
422
        println(io, "---")
×
UNCOV
423
        println(io, "_Package description from `$(basename(readme_path))`:_")
×
UNCOV
424
        for line in first(readme_lines, nlines)
×
UNCOV
425
            println(io, line)
×
UNCOV
426
        end
×
UNCOV
427
        length(readme_lines) > nlines && println(io, "\n[output truncated to first $nlines lines]")
×
428
    end
429
end
430

UNCOV
431
function summarize(io::IO, @nospecialize(T), binding::Binding)
×
UNCOV
432
    T = typeof(T)
×
UNCOV
433
    println(io, "`", binding, "` is of type `", T, "`.\n")
×
UNCOV
434
    summarize(io, T, binding)
×
435
end
436

437
# repl search and completions for help
438

439
# This type is returned from `accessible` and denotes a binding that is accessible within
440
# some context. It differs from `Base.Docs.Binding`, which is also used by the REPL, in
441
# that it doesn't track the defining module for a symbol unless the symbol is public but
442
# not exported, i.e. it's accessible but requires qualification. Using this type rather
443
# than `Base.Docs.Binding` simplifies things considerably, partially because REPL searching
444
# is based on `String`s, which this type stores, but `Base.Docs.Binding` stores a module
445
# and symbol and does not have any notion of the context from which the binding is accessed.
446
struct AccessibleBinding
447
    source::Union{String,Nothing}
448
    name::String
449
end
450

UNCOV
451
function AccessibleBinding(mod::Module, name::Symbol)
×
UNCOV
452
    m = isexported(mod, name) ? nothing : String(nameof(mod))
×
UNCOV
453
    return AccessibleBinding(m, String(name))
×
454
end
UNCOV
455
AccessibleBinding(name::Symbol) = AccessibleBinding(nothing, String(name))
×
456

UNCOV
457
function Base.show(io::IO, b::AccessibleBinding)
×
UNCOV
458
    b.source === nothing || print(io, b.source, '.')
×
UNCOV
459
    print(io, b.name)
×
460
end
461

UNCOV
462
quote_spaces(x) = any(isspace, x) ? "'" * x * "'" : x
×
UNCOV
463
quote_spaces(x::AccessibleBinding) = AccessibleBinding(x.source, quote_spaces(x.name))
×
464

UNCOV
465
function repl_search(io::IO, s::Union{Symbol,String}, mod::Module)
×
UNCOV
466
    pre = "search:"
×
UNCOV
467
    print(io, pre)
×
UNCOV
468
    printmatches(io, s, map(quote_spaces, doc_completions(s, mod)), cols = _displaysize(io)[2] - length(pre))
×
UNCOV
469
    println(io, "\n")
×
470
end
471

472
# TODO: document where this is used
473
repl_search(s, mod::Module) = repl_search(stdout, s, mod)
×
474

UNCOV
475
function repl_corrections(io::IO, s, mod::Module)
×
UNCOV
476
    print(io, "Couldn't find ")
×
UNCOV
477
    quot = any(isspace, s) ? "'" : ""
×
UNCOV
478
    print(io, quot)
×
UNCOV
479
    printstyled(io, s, color=:cyan)
×
UNCOV
480
    print(io, quot)
×
UNCOV
481
    if Base.identify_package(s) === nothing
×
UNCOV
482
        print(io, '\n')
×
483
    else
UNCOV
484
        print(io, ", but a loadable package with that name exists. If you are looking for the package docs load the package first.\n")
×
485
    end
UNCOV
486
    print_correction(io, s, mod)
×
487
end
488
repl_corrections(s) = repl_corrections(stdout, s)
×
489

490
# inverse of latex_symbols Dict, lazily created as needed
491
const symbols_latex = Dict{String,String}()
UNCOV
492
function symbol_latex(s::String)
×
UNCOV
493
    if isempty(symbols_latex)
×
494
        for (k,v) in Iterators.flatten((REPLCompletions.latex_symbols,
×
495
                                        REPLCompletions.emoji_symbols))
496
            symbols_latex[v] = k
×
497
        end
×
498

499
        # Overwrite with canonical mapping when a symbol has several completions (#39148)
500
        merge!(symbols_latex, REPLCompletions.symbols_latex_canonical)
×
501
    end
502

UNCOV
503
    return get(symbols_latex, s, "")
×
504
end
UNCOV
505
function repl_latex(io::IO, s0::String)
×
506
    # This has rampant `Core.Box` problems (#15276). Use the tricks of
507
    # https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-captured
508
    # We're changing some of the values so the `let` trick isn't applicable.
UNCOV
509
    s::String = s0
×
UNCOV
510
    latex::String = symbol_latex(s)
×
UNCOV
511
    if isempty(latex)
×
512
        # Decompose NFC-normalized identifier to match tab-completion
513
        # input if the first search came up empty.
UNCOV
514
        s = normalize(s, :NFD)
×
UNCOV
515
        latex = symbol_latex(s)
×
516
    end
UNCOV
517
    if !isempty(latex)
×
UNCOV
518
        print(io, "\"")
×
UNCOV
519
        printstyled(io, s, color=:cyan)
×
UNCOV
520
        print(io, "\" can be typed by ")
×
UNCOV
521
        printstyled(io, latex, "<tab>", color=:cyan)
×
UNCOV
522
        println(io, '\n')
×
UNCOV
523
    elseif any(c -> haskey(symbols_latex, string(c)), s)
×
UNCOV
524
        print(io, "\"")
×
UNCOV
525
        printstyled(io, s, color=:cyan)
×
UNCOV
526
        print(io, "\" can be typed by ")
×
UNCOV
527
        state::Char = '\0'
×
UNCOV
528
        with_output_color(:cyan, io) do io
×
UNCOV
529
            for c in s
×
UNCOV
530
                cstr = string(c)
×
UNCOV
531
                if haskey(symbols_latex, cstr)
×
UNCOV
532
                    latex = symbols_latex[cstr]
×
UNCOV
533
                    if length(latex) == 3 && latex[2] in ('^','_')
×
534
                        # coalesce runs of sub/superscripts
UNCOV
535
                        if state != latex[2]
×
UNCOV
536
                            '\0' != state && print(io, "<tab>")
×
UNCOV
537
                            print(io, latex[1:2])
×
UNCOV
538
                            state = latex[2]
×
539
                        end
UNCOV
540
                        print(io, latex[3])
×
541
                    else
UNCOV
542
                        if '\0' != state
×
UNCOV
543
                            print(io, "<tab>")
×
UNCOV
544
                            state = '\0'
×
545
                        end
UNCOV
546
                        print(io, latex, "<tab>")
×
547
                    end
548
                else
UNCOV
549
                    if '\0' != state
×
550
                        print(io, "<tab>")
×
551
                        state = '\0'
×
552
                    end
UNCOV
553
                    print(io, c)
×
554
                end
UNCOV
555
            end
×
UNCOV
556
            '\0' != state && print(io, "<tab>")
×
557
        end
UNCOV
558
        println(io, '\n')
×
559
    end
560
end
561
repl_latex(s::String) = repl_latex(stdout, s)
×
562

563
macro repl(ex, brief::Bool=false, mod::Module=Main) repl(ex; brief, mod) end
564
macro repl(io, ex, brief, mod, internal_accesses) repl(io, ex; brief, mod, internal_accesses) end
565

UNCOV
566
function repl(io::IO, s::Symbol; brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing)
×
UNCOV
567
    str = string(s)
×
UNCOV
568
    quote
×
UNCOV
569
        repl_latex($io, $str)
×
UNCOV
570
        repl_search($io, $str, $mod)
×
UNCOV
571
        $(if !isdefined(mod, s) && !haskey(keywords, s) && !Base.isoperator(s)
×
UNCOV
572
               :(repl_corrections($io, $str, $mod))
×
573
          end)
UNCOV
574
        $(_repl(s, brief, mod, internal_accesses))
×
575
    end
576
end
UNCOV
577
isregex(x) = isexpr(x, :macrocall, 3) && x.args[1] === Symbol("@r_str") && !isempty(x.args[3])
×
578

UNCOV
579
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)
×
UNCOV
580
repl(io::IO, str::AbstractString; brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing) = :(apropos($io, $str))
×
UNCOV
581
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
×
582
#repl(io::IO, other) = lookup_doc(other) # TODO
583

UNCOV
584
repl(x; brief::Bool=true, mod::Module=Main) = repl(stdout, x; brief, mod)
×
585

UNCOV
586
function _repl(x, brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing)
×
UNCOV
587
    if isexpr(x, :call)
×
UNCOV
588
        x = x::Expr
×
589
        # determine the types of the values
UNCOV
590
        kwargs = nothing
×
UNCOV
591
        pargs = Any[]
×
UNCOV
592
        for arg in x.args[2:end]
×
UNCOV
593
            if isexpr(arg, :parameters)
×
594
                kwargs = mapany(arg.args) do kwarg
×
UNCOV
595
                    if kwarg isa Symbol
×
UNCOV
596
                        kwarg = :($kwarg::Any)
×
UNCOV
597
                    elseif isexpr(kwarg, :kw)
×
UNCOV
598
                        lhs = kwarg.args[1]
×
UNCOV
599
                        rhs = kwarg.args[2]
×
UNCOV
600
                        if lhs isa Symbol
×
UNCOV
601
                            if rhs isa Symbol
×
UNCOV
602
                                kwarg.args[1] = :($lhs::(@isdefined($rhs) ? typeof($rhs) : Any))
×
603
                            else
UNCOV
604
                                kwarg.args[1] = :($lhs::typeof($rhs))
×
605
                            end
606
                        end
607
                    end
UNCOV
608
                    kwarg
×
609
                end
UNCOV
610
            elseif isexpr(arg, :kw)
×
UNCOV
611
                if kwargs === nothing
×
UNCOV
612
                    kwargs = Any[]
×
613
                end
UNCOV
614
                lhs = arg.args[1]
×
UNCOV
615
                rhs = arg.args[2]
×
UNCOV
616
                if lhs isa Symbol
×
UNCOV
617
                    if rhs isa Symbol
×
618
                        arg.args[1] = :($lhs::(@isdefined($rhs) ? typeof($rhs) : Any))
×
619
                    else
UNCOV
620
                        arg.args[1] = :($lhs::typeof($rhs))
×
621
                    end
622
                end
UNCOV
623
                push!(kwargs, arg)
×
624
            else
UNCOV
625
                if arg isa Symbol
×
UNCOV
626
                    arg = :($arg::(@isdefined($arg) ? typeof($arg) : Any))
×
UNCOV
627
                elseif !isexpr(arg, :(::))
×
UNCOV
628
                    arg = :(::typeof($arg))
×
629
                end
UNCOV
630
                push!(pargs, arg)
×
631
            end
UNCOV
632
        end
×
UNCOV
633
        if kwargs === nothing
×
UNCOV
634
            x.args = Any[x.args[1], pargs...]
×
635
        else
UNCOV
636
            x.args = Any[x.args[1], Expr(:parameters, kwargs...), pargs...]
×
637
        end
638
    end
639
    #docs = lookup_doc(x) # TODO
UNCOV
640
    docs = esc(:(@doc $x))
×
UNCOV
641
    docs = if isfield(x)
×
UNCOV
642
        quote
×
UNCOV
643
            if isa($(esc(x.args[1])), DataType)
×
UNCOV
644
                fielddoc($(esc(x.args[1])), $(esc(x.args[2])))
×
645
            else
UNCOV
646
                $docs
×
647
            end
648
        end
649
    else
UNCOV
650
        docs
×
651
    end
UNCOV
652
    docs = log_nonpublic_access(macroexpand(mod, docs), mod, internal_accesses)
×
UNCOV
653
    :(REPL.trimdocs($docs, $brief))
×
654
end
655

656
"""
657
    fielddoc(binding, field)
658

659
Return documentation for a particular `field` of a type if it exists.
660
"""
UNCOV
661
function fielddoc(binding::Binding, field::Symbol)
×
UNCOV
662
    for mod in modules
×
UNCOV
663
        dict = meta(mod; autoinit=false)
×
UNCOV
664
        isnothing(dict) && continue
×
UNCOV
665
        multidoc = get(dict, binding, nothing)
×
UNCOV
666
        if multidoc !== nothing
×
UNCOV
667
            structdoc = get(multidoc.docs, Union{}, nothing)
×
UNCOV
668
            if structdoc !== nothing
×
UNCOV
669
                fieldsdoc = get(structdoc.data, :fields, nothing)
×
UNCOV
670
                if fieldsdoc !== nothing
×
UNCOV
671
                    fielddoc = get(fieldsdoc, field, nothing)
×
UNCOV
672
                    if fielddoc !== nothing
×
UNCOV
673
                        return isa(fielddoc, Markdown.MD) ?
×
674
                            fielddoc : Markdown.parse(fielddoc)
675
                    end
676
                end
677
            end
678
        end
UNCOV
679
    end
×
UNCOV
680
    fs = fieldnames(resolve(binding))
×
UNCOV
681
    fields = isempty(fs) ? "no fields" : (length(fs) == 1 ? "field " : "fields ") *
×
682
                                          join(("`$f`" for f in fs), ", ", ", and ")
UNCOV
683
    Markdown.parse("`$(resolve(binding))` has $fields.")
×
684
end
685

686
# As with the additional `doc` methods, this converts an object to a `Binding` first.
UNCOV
687
fielddoc(object, field::Symbol) = fielddoc(aliasof(object, typeof(object)), field)
×
688

689

690
# Search & Rescue
691
# Utilities for correcting user mistakes and (eventually)
692
# doing full documentation searches from the repl.
693

694
# Fuzzy Search Algorithm
695

UNCOV
696
function matchinds(needle, haystack; acronym::Bool = false)
×
UNCOV
697
    chars = collect(needle)
×
UNCOV
698
    is = Int[]
×
UNCOV
699
    lastc = '\0'
×
UNCOV
700
    for (i, char) in enumerate(haystack)
×
UNCOV
701
        while !isempty(chars) && isspace(first(chars))
×
UNCOV
702
            popfirst!(chars) # skip spaces
×
UNCOV
703
        end
×
UNCOV
704
        isempty(chars) && break
×
UNCOV
705
        if lowercase(char) == lowercase(chars[1]) &&
×
706
           (!acronym || !isletter(lastc))
UNCOV
707
            push!(is, i)
×
UNCOV
708
            popfirst!(chars)
×
709
        end
UNCOV
710
        lastc = char
×
UNCOV
711
    end
×
UNCOV
712
    return is
×
713
end
714

715
matchinds(needle, (; name)::AccessibleBinding; acronym::Bool=false) =
×
716
    matchinds(needle, name; acronym)
717

UNCOV
718
longer(x, y) = length(x) ≥ length(y) ? (x, true) : (y, false)
×
719

UNCOV
720
bestmatch(needle, haystack) =
×
721
    longer(matchinds(needle, haystack, acronym = true),
722
           matchinds(needle, haystack))
723

724
# Optimal string distance: Counts the minimum number of insertions, deletions,
725
# transpositions or substitutions to go from one string to the other.
UNCOV
726
function string_distance(a::AbstractString, lena::Integer, b::AbstractString, lenb::Integer)
×
UNCOV
727
    if lena > lenb
×
UNCOV
728
        a, b = b, a
×
UNCOV
729
        lena, lenb = lenb, lena
×
730
    end
UNCOV
731
    start = 0
×
UNCOV
732
    for (i, j) in zip(a, b)
×
UNCOV
733
        if a == b
×
UNCOV
734
            start += 1
×
735
        else
UNCOV
736
            break
×
737
        end
UNCOV
738
    end
×
UNCOV
739
    start == lena && return lenb - start
×
UNCOV
740
    vzero = collect(1:(lenb - start))
×
UNCOV
741
    vone = similar(vzero)
×
UNCOV
742
    prev_a, prev_b = first(a), first(b)
×
UNCOV
743
    current = 0
×
UNCOV
744
    for (i, ai) in enumerate(a)
×
UNCOV
745
        i > start || (prev_a = ai; continue)
×
UNCOV
746
        left = i - start - 1
×
UNCOV
747
        current = i - start
×
UNCOV
748
        transition_next = 0
×
UNCOV
749
        for (j, bj) in enumerate(b)
×
UNCOV
750
            j > start || (prev_b = bj; continue)
×
751
            # No need to look beyond window of lower right diagonal
UNCOV
752
            above = current
×
UNCOV
753
            this_transition = transition_next
×
UNCOV
754
            transition_next = vone[j - start]
×
UNCOV
755
            vone[j - start] = current = left
×
UNCOV
756
            left = vzero[j - start]
×
UNCOV
757
            if ai != bj
×
758
                # Minimum between substitution, deletion and insertion
UNCOV
759
                current = min(current + 1, above + 1, left + 1)
×
UNCOV
760
                if i > start + 1 && j > start + 1 && ai == prev_b && prev_a == bj
×
UNCOV
761
                    current = min(current, (this_transition += 1))
×
762
                end
763
            end
UNCOV
764
            vzero[j - start] = current
×
UNCOV
765
            prev_b = bj
×
UNCOV
766
        end
×
UNCOV
767
        prev_a = ai
×
UNCOV
768
    end
×
UNCOV
769
    current
×
770
end
771

UNCOV
772
function fuzzyscore(needle::AbstractString, haystack::AbstractString)
×
UNCOV
773
    lena, lenb = length(needle), length(haystack)
×
UNCOV
774
    1 - (string_distance(needle, lena, haystack, lenb) / max(lena, lenb))
×
775
end
776

UNCOV
777
function fuzzyscore(needle::AbstractString, haystack::AccessibleBinding)
×
UNCOV
778
    score = fuzzyscore(needle, haystack.name)
×
UNCOV
779
    haystack.source === nothing && return score
×
780
    # Apply a "penalty" of half an edit if the comparator binding is public but not
781
    # exported so that exported/local names that exactly match the search query are
782
    # listed first
UNCOV
783
    penalty = 1 / (2 * max(length(needle), length(haystack.name)))
×
UNCOV
784
    return max(score - penalty, 0)
×
785
end
786

UNCOV
787
function fuzzysort(search::String, candidates::Vector{AccessibleBinding})
×
UNCOV
788
    scores = map(cand -> fuzzyscore(search, cand), candidates)
×
UNCOV
789
    candidates[sortperm(scores)] |> reverse
×
790
end
791

792
# Levenshtein Distance
793

UNCOV
794
function levenshtein(s1, s2)
×
UNCOV
795
    a, b = collect(s1), collect(s2)
×
UNCOV
796
    m = length(a)
×
UNCOV
797
    n = length(b)
×
UNCOV
798
    d = Matrix{Int}(undef, m+1, n+1)
×
799

UNCOV
800
    d[1:m+1, 1] = 0:m
×
UNCOV
801
    d[1, 1:n+1] = 0:n
×
802

UNCOV
803
    for i = 1:m, j = 1:n
×
UNCOV
804
        d[i+1,j+1] = min(d[i  , j+1] + 1,
×
805
                         d[i+1, j  ] + 1,
806
                         d[i  , j  ] + (a[i] != b[j]))
UNCOV
807
    end
×
808

UNCOV
809
    return d[m+1, n+1]
×
810
end
811

UNCOV
812
function levsort(search::String, candidates::Vector{AccessibleBinding})
×
UNCOV
813
    scores = map(candidates) do cand
×
UNCOV
814
        (Float64(levenshtein(search, cand.name)), -fuzzyscore(search, cand))
×
815
    end
UNCOV
816
    candidates = candidates[sortperm(scores)]
×
UNCOV
817
    i = 0
×
UNCOV
818
    for outer i = 1:length(candidates)
×
UNCOV
819
        levenshtein(search, candidates[i].name) > 3 && break
×
UNCOV
820
    end
×
UNCOV
821
    return candidates[1:i]
×
822
end
823

824
# Result printing
825

UNCOV
826
function printmatch(io::IO, word, match)
×
UNCOV
827
    is, _ = bestmatch(word, match)
×
UNCOV
828
    for (i, char) = enumerate(match)
×
UNCOV
829
        if i in is
×
UNCOV
830
            printstyled(io, char, bold=true)
×
831
        else
UNCOV
832
            print(io, char)
×
833
        end
UNCOV
834
    end
×
835
end
836

UNCOV
837
function printmatch(io::IO, word, match::AccessibleBinding)
×
UNCOV
838
    match.source === nothing || print(io, match.source, '.')
×
UNCOV
839
    printmatch(io, word, match.name)
×
840
end
841

UNCOV
842
function matchlength(x::AccessibleBinding)
×
UNCOV
843
    n = length(x.name)
×
UNCOV
844
    if x.source !== nothing
×
UNCOV
845
        n += length(x.source) + 1  # the +1 is for the `.` separator
×
846
    end
UNCOV
847
    return n
×
848
end
849
matchlength(x) = length(x)
×
850

UNCOV
851
function printmatches(io::IO, word, matches; cols::Int = _displaysize(io)[2])
×
UNCOV
852
    total = 0
×
UNCOV
853
    for match in matches
×
UNCOV
854
        ml = matchlength(match)
×
UNCOV
855
        total + ml + 1 > cols && break
×
UNCOV
856
        fuzzyscore(word, match) < 0.5 && break
×
UNCOV
857
        print(io, " ")
×
UNCOV
858
        printmatch(io, word, match)
×
UNCOV
859
        total += ml + 1
×
UNCOV
860
    end
×
861
end
862

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

UNCOV
865
function print_joined_cols(io::IO, ss::Vector{AccessibleBinding}, delim = "", last = delim; cols::Int = _displaysize(io)[2])
×
UNCOV
866
    i = 0
×
UNCOV
867
    total = 0
×
UNCOV
868
    for outer i = 1:length(ss)
×
UNCOV
869
        total += matchlength(ss[i])
×
UNCOV
870
        total + max(i-2,0)*length(delim) + (i>1 ? 1 : 0)*length(last) > cols && (i-=1; break)
×
UNCOV
871
    end
×
UNCOV
872
    join(io, ss[1:i], delim, last)
×
873
end
874

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

UNCOV
877
function print_correction(io::IO, word::String, mod::Module)
×
UNCOV
878
    cors = map(quote_spaces, levsort(word, accessible(mod)))
×
UNCOV
879
    pre = "Perhaps you meant "
×
UNCOV
880
    print(io, pre)
×
UNCOV
881
    print_joined_cols(io, cors, ", ", " or "; cols = _displaysize(io)[2] - length(pre))
×
UNCOV
882
    println(io)
×
UNCOV
883
    return
×
884
end
885

886
# TODO: document where this is used
887
print_correction(word, mod::Module) = print_correction(stdout, word, mod)
×
888

889
# Completion data
890

UNCOV
891
moduleusings(mod) = ccall(:jl_module_usings, Any, (Any,), mod)
×
892

UNCOV
893
function accessible(mod::Module)
×
UNCOV
894
    bindings = Set(AccessibleBinding(s) for s in names(mod; all=true, imported=true)
×
895
                   if !isdeprecated(mod, s))
UNCOV
896
    for used in moduleusings(mod)
×
UNCOV
897
        union!(bindings, (AccessibleBinding(used, s) for s in names(used)
×
898
                          if !isdeprecated(used, s)))
UNCOV
899
    end
×
UNCOV
900
    union!(bindings, (AccessibleBinding(k) for k in keys(Base.Docs.keywords)))
×
UNCOV
901
    filter!(b -> !occursin('#', b.name), bindings)
×
UNCOV
902
    return collect(bindings)
×
903
end
904

UNCOV
905
function doc_completions(name, mod::Module=Main)
×
UNCOV
906
    res = fuzzysort(name, accessible(mod))
×
907

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

912
    # avoid messing up the order while inserting
UNCOV
913
    for i in reverse!(idxs)
×
UNCOV
914
        c = only((ms[i]::AbstractMatch).captures)
×
UNCOV
915
        insert!(res, i, AccessibleBinding(res[i].source, "$(c)\"\""))
×
UNCOV
916
    end
×
UNCOV
917
    res
×
918
end
919
doc_completions(name::Symbol) = doc_completions(string(name), mod)
×
920

921

922
# Searching and apropos
923

924
# Docsearch simply returns true or false if an object contains the given needle
UNCOV
925
docsearch(haystack::AbstractString, needle) = occursin(needle, haystack)
×
926
docsearch(haystack::Symbol, needle) = docsearch(string(haystack), needle)
×
927
docsearch(::Nothing, needle) = false
×
928
function docsearch(haystack::Array, needle)
×
929
    for elt in haystack
×
930
        docsearch(elt, needle) && return true
×
931
    end
×
932
    false
×
933
end
934
function docsearch(haystack, needle)
×
935
    @warn "Unable to search documentation of type $(typeof(haystack))" maxlog=1
×
936
    false
×
937
end
938

939
## Searching specific documentation objects
UNCOV
940
function docsearch(haystack::MultiDoc, needle)
×
UNCOV
941
    for v in values(haystack.docs)
×
UNCOV
942
        docsearch(v, needle) && return true
×
UNCOV
943
    end
×
UNCOV
944
    false
×
945
end
946

UNCOV
947
function docsearch(haystack::DocStr, needle)
×
UNCOV
948
    docsearch(parsedoc(haystack), needle) && return true
×
UNCOV
949
    if haskey(haystack.data, :fields)
×
UNCOV
950
        for doc in values(haystack.data[:fields])
×
UNCOV
951
            docsearch(doc, needle) && return true
×
UNCOV
952
        end
×
953
    end
UNCOV
954
    false
×
955
end
956

957
## doc search
958

959
## Markdown search simply strips all markup and searches plain text version
UNCOV
960
docsearch(haystack::Markdown.MD, needle) = docsearch(stripmd(haystack.content), needle)
×
961

962
"""
963
    stripmd(x)
964

965
Strip all Markdown markup from x, leaving the result in plain text. Used
966
internally by apropos to make docstrings containing more than one markdown
967
element searchable.
968
"""
UNCOV
969
stripmd(@nospecialize x) = string(x) # for random objects interpolated into the docstring
×
UNCOV
970
stripmd(x::AbstractString) = x  # base case
×
UNCOV
971
stripmd(x::Nothing) = " "
×
UNCOV
972
stripmd(x::Vector) = string(map(stripmd, x)...)
×
973

UNCOV
974
stripmd(x::Markdown.BlockQuote) = "$(stripmd(x.content))"
×
UNCOV
975
stripmd(x::Markdown.Admonition) = "$(stripmd(x.content))"
×
UNCOV
976
stripmd(x::Markdown.Bold) = "$(stripmd(x.text))"
×
UNCOV
977
stripmd(x::Markdown.Code) = "$(stripmd(x.code))"
×
UNCOV
978
stripmd(x::Markdown.Header) = stripmd(x.text)
×
UNCOV
979
stripmd(x::Markdown.HorizontalRule) = " "
×
UNCOV
980
stripmd(x::Markdown.Image) = "$(stripmd(x.alt)) $(x.url)"
×
UNCOV
981
stripmd(x::Markdown.Italic) = "$(stripmd(x.text))"
×
UNCOV
982
stripmd(x::Markdown.LaTeX) = "$(x.formula)"
×
UNCOV
983
stripmd(x::Markdown.LineBreak) = " "
×
UNCOV
984
stripmd(x::Markdown.Link) = "$(stripmd(x.text)) $(x.url)"
×
UNCOV
985
stripmd(x::Markdown.List) = join(map(stripmd, x.items), " ")
×
UNCOV
986
stripmd(x::Markdown.MD) = join(map(stripmd, x.content), " ")
×
UNCOV
987
stripmd(x::Markdown.Paragraph) = stripmd(x.content)
×
UNCOV
988
stripmd(x::Markdown.Footnote) = "$(stripmd(x.id)) $(stripmd(x.text))"
×
UNCOV
989
stripmd(x::Markdown.Table) =
×
990
    join([join(map(stripmd, r), " ") for r in x.rows], " ")
991

992
apropos(string) = apropos(stdout, string)
×
UNCOV
993
apropos(io::IO, string) = apropos(io, Regex("\\Q$string", "i"))
×
994

UNCOV
995
function apropos(io::IO, needle::Regex)
×
UNCOV
996
    for mod in modules
×
997
        # Module doc might be in README.md instead of the META dict
UNCOV
998
        docsearch(doc(mod), needle) && println(io, mod)
×
UNCOV
999
        dict = meta(mod; autoinit=false)
×
UNCOV
1000
        isnothing(dict) && continue
×
UNCOV
1001
        for (k, v) in dict
×
UNCOV
1002
            docsearch(v, needle) && println(io, k)
×
UNCOV
1003
        end
×
UNCOV
1004
    end
×
1005
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

© 2026 Coveralls, Inc