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

JuliaLang / julia / #37905

15 Sep 2024 08:24PM UTC coverage: 87.687% (-0.001%) from 87.688%
#37905

push

local

web-flow
Print results of `runtests` with `printstyled` (#55780)

This ensures escape characters are used only if `stdout` can accept
them.

78263 of 89253 relevant lines covered (87.69%)

16521197.48 hits per line

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

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

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

79
function formatdoc(d::DocStr)
6,326✔
80
    buffer = IOBuffer()
6,326✔
81
    for part in d.text
12,652✔
82
        formatdoc(buffer, d, part)
7,774✔
83
    end
9,222✔
84
    Markdown.MD(Any[Markdown.parse(seekstart(buffer))])
6,326✔
85
end
86
@noinline formatdoc(buffer, d, part) = print(buffer, part)
7,772✔
87

88
function parsedoc(d::DocStr)
12,701✔
89
    if d.object === nothing
12,701✔
90
        md = formatdoc(d)
6,326✔
91
        md.meta[:module] = d.data[:module]
6,326✔
92
        md.meta[:path]   = d.data[:path]
6,326✔
93
        d.object = md
6,326✔
94
    end
95
    d.object
12,701✔
96
end
97

98
## Trimming long help ("# Extended help")
99

100
struct Message  # For direct messages to the terminal
101
    msg    # AbstractString
2✔
102
    fmt    # keywords to `printstyled`
103
end
104
Message(msg) = Message(msg, ())
×
105

106
function Markdown.term(io::IO, msg::Message, columns)
×
107
    printstyled(io, msg.msg; msg.fmt...)
×
108
end
109

110
trimdocs(doc, brief::Bool) = doc
2✔
111

112
function trimdocs(md::Markdown.MD, brief::Bool)
59✔
113
    brief || return md
68✔
114
    md, trimmed = _trimdocs(md, brief)
50✔
115
    if trimmed
50✔
116
        line = extended_help_on[]
2✔
117
        line = isa(line, AbstractString) ? line : ""
2✔
118
        push!(md.content, Message("Extended help is available with `??$line`", (color=Base.info_color(), bold=true)))
2✔
119
    end
120
    return md
50✔
121
end
122

123
function _trimdocs(md::Markdown.MD, brief::Bool)
90✔
124
    content, trimmed = [], false
90✔
125
    for c in md.content
90✔
126
        if isa(c, Markdown.Header{1}) && isa(c.text, AbstractArray) && !isempty(c.text)
219✔
127
            item = c.text[1]
20✔
128
            if isa(item, AbstractString) &&
20✔
129
                lowercase(item) ∈ ("extended help",
130
                                   "extended documentation",
131
                                   "extended docs")
132
                trimmed = true
×
133
                break
2✔
134
            end
135
        end
136
        c, trm = _trimdocs(c, brief)
217✔
137
        trimmed |= trm
217✔
138
        push!(content, c)
217✔
139
    end
217✔
140
    return Markdown.MD(content, md.meta), trimmed
90✔
141
end
142

143
_trimdocs(md, brief::Bool) = md, false
177✔
144

145

146
is_tuple(expr) = false
×
147
is_tuple(expr::Expr) = expr.head == :tuple
×
148

149
struct Logged{F}
150
    f::F
46✔
151
    mod::Module
152
    collection::Set{Pair{Module,Symbol}}
153
end
154
function (la::Logged)(m::Module, s::Symbol)
34✔
155
    m !== la.mod && Base.isdefined(m, s) && !Base.ispublic(m, s) && push!(la.collection, m => s)
34✔
156
    la.f(m, s)
34✔
157
end
158
(la::Logged)(args...) = la.f(args...)
×
159

160
function log_nonpublic_access(expr::Expr, mod::Module, internal_access::Set{Pair{Module,Symbol}})
217✔
161
    if expr.head === :. && length(expr.args) == 2 && !is_tuple(expr.args[2])
217✔
162
        Expr(:call, Logged(getproperty, mod, internal_access), log_nonpublic_access.(expr.args, (mod,), (internal_access,))...)
24✔
163
    elseif expr.head === :call && expr.args[1] === Base.Docs.Binding
193✔
164
        Expr(:call, Logged(Base.Docs.Binding, mod, internal_access), log_nonpublic_access.(expr.args[2:end], (mod,), (internal_access,))...)
22✔
165
    else
166
        Expr(expr.head, log_nonpublic_access.(expr.args, (mod,), (internal_access,))...)
171✔
167
    end
168
end
169
log_nonpublic_access(expr, ::Module, _) = expr
163✔
170

171
function insert_internal_warning(md::Markdown.MD, internal_access::Set{Pair{Module,Symbol}})
23✔
172
    if !isempty(internal_access)
23✔
173
        items = Any[Any[Markdown.Paragraph(Any[Markdown.Code("", s)])] for s in sort!(["$mod.$sym" for (mod, sym) in internal_access])]
6✔
174
        admonition = Markdown.Admonition("warning", "Warning", Any[
12✔
175
            Markdown.Paragraph(Any["The following bindings may be internal; they may change or be removed in future versions:"]),
176
            Markdown.List(items, -1, false)])
177
        pushfirst!(md.content, admonition)
6✔
178
    end
179
    md
×
180
end
181
function insert_internal_warning(other, internal_access::Set{Pair{Module,Symbol}})
×
182
    # We don't know how to insert an internal symbol warning into non-markdown
183
    # content, so we don't.
184
    other
×
185
end
186

187
function doc(binding::Binding, sig::Type = Union{})
577✔
188
    if defined(binding)
577✔
189
        result = getdoc(resolve(binding), sig)
471✔
190
        result === nothing || return result
17✔
191
    end
192
    results, groups = DocStr[], MultiDoc[]
480✔
193
    # Lookup `binding` and `sig` for matches in all modules of the docsystem.
194
    for mod in modules
480✔
195
        dict = meta(mod; autoinit=false)
82,638✔
196
        isnothing(dict) && continue
82,638✔
197
        if haskey(dict, binding)
41,319✔
198
            multidoc = dict[binding]
276✔
199
            push!(groups, multidoc)
276✔
200
            for msig in multidoc.order
276✔
201
                sig <: msig && push!(results, multidoc.docs[msig])
323✔
202
            end
323✔
203
        end
204
    end
41,319✔
205
    if isempty(groups)
480✔
206
        # When no `MultiDoc`s are found that match `binding` then we check whether `binding`
207
        # is an alias of some other `Binding`. When it is we then re-run `doc` with that
208
        # `Binding`, otherwise if it's not an alias then we generate a summary for the
209
        # `binding` and display that to the user instead.
210
        alias = aliasof(binding)
210✔
211
        alias == binding ? summarize(alias, sig) : doc(alias, sig)
210✔
212
    else
213
        # There was at least one match for `binding` while searching. If there weren't any
214
        # matches for `sig` then we concatenate *all* the docs from the matching `Binding`s.
215
        if isempty(results)
270✔
216
            for group in groups, each in group.order
13✔
217
                push!(results, group.docs[each])
16✔
218
            end
16✔
219
        end
220
        # Get parsed docs and concatenate them.
221
        md = catdoc(mapany(parsedoc, results)...)
287✔
222
        # Save metadata in the generated markdown.
223
        if isa(md, Markdown.MD)
270✔
224
            md.meta[:results] = results
267✔
225
            md.meta[:binding] = binding
267✔
226
            md.meta[:typesig] = sig
267✔
227
        end
228
        return md
270✔
229
    end
230
end
231

232
# Some additional convenience `doc` methods that take objects rather than `Binding`s.
233
doc(obj::UnionAll) = doc(Base.unwrap_unionall(obj))
2✔
234
doc(object, sig::Type = Union{}) = doc(aliasof(object, typeof(object)), sig)
699✔
235
doc(object, sig...)              = doc(object, Tuple{sig...})
×
236

237
function lookup_doc(ex)
152✔
238
    if isa(ex, Expr) && ex.head !== :(.) && Base.isoperator(ex.head)
161✔
239
        # handle syntactic operators, e.g. +=, ::, .=
240
        ex = ex.head
×
241
    end
242
    if haskey(keywords, ex)
194✔
243
        return parsedoc(keywords[ex])
6✔
244
    elseif Meta.isexpr(ex, :incomplete)
112✔
245
        return :($(Markdown.md"No documentation found."))
1✔
246
    elseif !isa(ex, Expr) && !isa(ex, Symbol)
111✔
247
        return :($(doc)($(typeof)($(esc(ex)))))
2✔
248
    end
249
    if isa(ex, Symbol) && Base.isoperator(ex)
171✔
250
        str = string(ex)
6✔
251
        isdotted = startswith(str, ".")
6✔
252
        if endswith(str, "=") && Base.operator_precedence(ex) == Base.prec_assignment && ex !== :(:=)
6✔
253
            op = chop(str)
2✔
254
            eq = isdotted ? ".=" : "="
2✔
255
            return Markdown.parse("`x $op= y` is a synonym for `x $eq x $op y`")
2✔
256
        elseif isdotted && ex !== :(..)
4✔
257
            op = str[2:end]
2✔
258
            if op in ("&&", "||")
2✔
259
                return Markdown.parse("`x $ex y` broadcasts the boolean operator `$op` to `x` and `y`. See [`broadcast`](@ref).")
×
260
            else
261
                return Markdown.parse("`x $ex y` is akin to `broadcast($op, x, y)`. See [`broadcast`](@ref).")
1✔
262
            end
263
        end
264
    end
265
    binding = esc(bindingexpr(namify(ex)))
169✔
266
    if isexpr(ex, :call) || isexpr(ex, :macrocall) || isexpr(ex, :where)
200✔
267
        sig = esc(signature(ex))
41✔
268
        :($(doc)($binding, $sig))
41✔
269
    else
270
        :($(doc)($binding))
99✔
271
    end
272
end
273

274
# Object Summaries.
275
# =================
276

277
function summarize(binding::Binding, sig)
211✔
278
    io = IOBuffer()
211✔
279
    if defined(binding)
211✔
280
        binding_res = resolve(binding)
198✔
281
        if !isa(binding_res, Module)
198✔
282
            if Base.ispublic(binding.mod, binding.var)
24✔
283
                println(io, "No documentation found for public symbol.\n")
5✔
284
            else
285
                println(io, "No documentation found for private symbol.\n")
19✔
286
            end
287
        end
288
        summarize(io, binding_res, binding)
198✔
289
    else
290
        println(io, "No documentation found.\n")
13✔
291
        quot = any(isspace, sprint(print, binding)) ? "'" : ""
13✔
292
        if Base.isbindingresolved(binding.mod, binding.var)
13✔
293
            println(io, "Binding ", quot, "`", binding, "`", quot, " exists, but has not been assigned a value.")
1✔
294
        else
295
            println(io, "Binding ", quot, "`", binding, "`", quot, " does not exist.")
12✔
296
        end
297
    end
298
    md = Markdown.parse(seekstart(io))
422✔
299
    # Save metadata in the generated markdown.
300
    md.meta[:results] = DocStr[]
211✔
301
    md.meta[:binding] = binding
211✔
302
    md.meta[:typesig] = sig
211✔
303
    return md
211✔
304
end
305

306
function summarize(io::IO, λ::Function, binding::Binding)
5✔
307
    kind = startswith(string(binding.var), '@') ? "macro" : "`Function`"
10✔
308
    println(io, "`", binding, "` is a ", kind, ".")
5✔
309
    println(io, "```\n", methods(λ), "\n```")
5✔
310
end
311

312
function summarize(io::IO, TT::Type, binding::Binding)
19✔
313
    println(io, "# Summary")
19✔
314
    T = Base.unwrap_unionall(TT)
19✔
315
    if T isa DataType
19✔
316
        println(io, "```")
16✔
317
        print(io,
27✔
318
            Base.isabstracttype(T) ? "abstract type " :
319
            Base.ismutabletype(T)  ? "mutable struct " :
320
            Base.isstructtype(T) ? "struct " :
321
            "primitive type ")
322
        supert = supertype(T)
16✔
323
        println(io, T)
16✔
324
        println(io, "```")
16✔
325
        if !Base.isabstracttype(T) && T.name !== Tuple.name && !isempty(fieldnames(T))
16✔
326
            println(io, "# Fields")
6✔
327
            println(io, "```")
6✔
328
            pad = maximum(length(string(f)) for f in fieldnames(T))
6✔
329
            for (f, t) in zip(fieldnames(T), fieldtypes(T))
6✔
330
                println(io, rpad(f, pad), " :: ", t)
11✔
331
            end
11✔
332
            println(io, "```")
6✔
333
        end
334
        subt = subtypes(TT)
16✔
335
        if !isempty(subt)
21✔
336
            println(io, "# Subtypes")
5✔
337
            println(io, "```")
5✔
338
            for t in subt
5✔
339
                println(io, Base.unwrap_unionall(t))
12✔
340
            end
12✔
341
            println(io, "```")
5✔
342
        end
343
        if supert != Any
16✔
344
            println(io, "# Supertype Hierarchy")
12✔
345
            println(io, "```")
12✔
346
            Base.show_supertypes(io, T)
12✔
347
            println(io)
12✔
348
            println(io, "```")
12✔
349
        end
350
    elseif T isa Union
3✔
351
        println(io, "`", binding, "` is of type `", typeof(TT), "`.\n")
3✔
352
        println(io, "# Union Composed of Types")
3✔
353
        for T1 in Base.uniontypes(T)
3✔
354
            println(io, " - `", Base.rewrap_unionall(T1, TT), "`")
10✔
355
        end
10✔
356
    else # unreachable?
357
        println(io, "`", binding, "` is of type `", typeof(TT), "`.\n")
×
358
    end
359
end
360

361
function find_readme(m::Module)::Union{String, Nothing}
174✔
362
    mpath = pathof(m)
174✔
363
    isnothing(mpath) && return nothing
186✔
364
    !isfile(mpath) && return nothing # modules in sysimage, where src files are omitted
12✔
365
    path = dirname(mpath)
12✔
366
    top_path = pkgdir(m)
12✔
367
    while true
24✔
368
        for entry in _readdirx(path; sort=true)
24✔
369
            isfile(entry) && (lowercase(entry.name) in ["readme.md", "readme"]) || continue
268✔
370
            return entry.path
12✔
371
        end
96✔
372
        path == top_path && break # go no further than pkgdir
24✔
373
        path = dirname(path) # work up through nested modules
12✔
374
    end
12✔
375
    return nothing
×
376
end
377
function summarize(io::IO, m::Module, binding::Binding; nlines::Int = 200)
348✔
378
    readme_path = find_readme(m)
174✔
379
    public = Base.ispublic(binding.mod, binding.var) ? "public" : "internal"
174✔
380
    if isnothing(readme_path)
186✔
381
        println(io, "No docstring or readme file found for $public module `$m`.\n")
162✔
382
    else
383
        println(io, "No docstring found for $public module `$m`.")
12✔
384
    end
385
    exports = filter!(!=(nameof(m)), names(m))
348✔
386
    if isempty(exports)
174✔
387
        println(io, "Module does not have any public names.")
51✔
388
    else
389
        println(io, "# Public names")
123✔
390
        print(io, "  `")
123✔
391
        join(io, exports, "`, `")
123✔
392
        println(io, "`\n")
123✔
393
    end
394
    if !isnothing(readme_path)
186✔
395
        readme_lines = readlines(readme_path)
12✔
396
        isempty(readme_lines) && return  # don't say we are going to print empty file
12✔
397
        println(io, "# Displaying contents of readme found at `$(readme_path)`")
12✔
398
        for line in first(readme_lines, nlines)
12✔
399
            println(io, line)
1,008✔
400
        end
1,008✔
401
        length(readme_lines) > nlines && println(io, "\n[output truncated to first $nlines lines]")
12✔
402
    end
403
end
404

405
function summarize(io::IO, @nospecialize(T), binding::Binding)
4✔
406
    T = typeof(T)
4✔
407
    println(io, "`", binding, "` is of type `", T, "`.\n")
4✔
408
    summarize(io, T, binding)
4✔
409
end
410

411
# repl search and completions for help
412

413
# This type is returned from `accessible` and denotes a binding that is accessible within
414
# some context. It differs from `Base.Docs.Binding`, which is also used by the REPL, in
415
# that it doesn't track the defining module for a symbol unless the symbol is public but
416
# not exported, i.e. it's accessible but requires qualification. Using this type rather
417
# than `Base.Docs.Binding` simplifies things considerably, partially because REPL searching
418
# is based on `String`s, which this type stores, but `Base.Docs.Binding` stores a module
419
# and symbol and does not have any notion of the context from which the binding is accessed.
420
struct AccessibleBinding
421
    source::Union{String,Nothing}
234,952✔
422
    name::String
423
end
424

425
function AccessibleBinding(mod::Module, name::Symbol)
56,186✔
426
    m = isexported(mod, name) ? nothing : String(nameof(mod))
59,512✔
427
    return AccessibleBinding(m, String(name))
56,186✔
428
end
429
AccessibleBinding(name::Symbol) = AccessibleBinding(nothing, String(name))
111,903✔
430

431
function Base.show(io::IO, b::AccessibleBinding)
432
    b.source === nothing || print(io, b.source, '.')
31,335✔
433
    print(io, b.name)
29,427✔
434
end
435

436
quote_spaces(x) = any(isspace, x) ? "'" * x * "'" : x
66,274✔
437
quote_spaces(x::AccessibleBinding) = AccessibleBinding(x.source, quote_spaces(x.name))
67,257✔
438

439
function repl_search(io::IO, s::Union{Symbol,String}, mod::Module)
25✔
440
    pre = "search:"
25✔
441
    print(io, pre)
25✔
442
    printmatches(io, s, map(quote_spaces, doc_completions(s, mod)), cols = _displaysize(io)[2] - length(pre))
25✔
443
    println(io, "\n")
25✔
444
end
445

446
# TODO: document where this is used
447
repl_search(s, mod::Module) = repl_search(stdout, s, mod)
×
448

449
function repl_corrections(io::IO, s, mod::Module)
7✔
450
    print(io, "Couldn't find ")
7✔
451
    quot = any(isspace, s) ? "'" : ""
7✔
452
    print(io, quot)
7✔
453
    printstyled(io, s, color=:cyan)
7✔
454
    print(io, quot, '\n')
7✔
455
    print_correction(io, s, mod)
7✔
456
end
457
repl_corrections(s) = repl_corrections(stdout, s)
×
458

459
# inverse of latex_symbols Dict, lazily created as needed
460
const symbols_latex = Dict{String,String}()
461
function symbol_latex(s::String)
53✔
462
    if isempty(symbols_latex)
53✔
463
        for (k,v) in Iterators.flatten((REPLCompletions.latex_symbols,
×
464
                                        REPLCompletions.emoji_symbols))
465
            symbols_latex[v] = k
×
466
        end
×
467

468
        # Overwrite with canonical mapping when a symbol has several completions (#39148)
469
        merge!(symbols_latex, REPLCompletions.symbols_latex_canonical)
×
470
    end
471

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

532
macro repl(ex, brief::Bool=false, mod::Module=Main) repl(ex; brief, mod) end
12✔
533
macro repl(io, ex, brief, mod, internal_accesses) repl(io, ex; brief, mod, internal_accesses) end
53✔
534

535
function repl(io::IO, s::Symbol; brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing)
50✔
536
    str = string(s)
25✔
537
    quote
32✔
538
        repl_latex($io, $str)
539
        repl_search($io, $str, $mod)
540
        $(if !isdefined(mod, s) && !Base.isbindingresolved(mod, s) && !haskey(keywords, s) && !Base.isoperator(s)
541
               # n.b. we call isdefined for the side-effect of resolving the binding, if possible
542
               :(repl_corrections($io, $str, $mod))
7✔
543
          end)
544
        $(_repl(s, brief, mod, internal_accesses))
545
    end
546
end
547
isregex(x) = isexpr(x, :macrocall, 3) && x.args[1] === Symbol("@r_str") && !isempty(x.args[3])
31✔
548

549
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✔
550
repl(io::IO, str::AbstractString; brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing) = :(apropos($io, $str))
2✔
551
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✔
552
#repl(io::IO, other) = lookup_doc(other) # TODO
553

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

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

626
"""
627
    fielddoc(binding, field)
628

629
Return documentation for a particular `field` of a type if it exists.
630
"""
631
function fielddoc(binding::Binding, field::Symbol)
5✔
632
    for mod in modules
5✔
633
        dict = meta(mod; autoinit=false)
810✔
634
        isnothing(dict) && continue
810✔
635
        if haskey(dict, binding)
405✔
636
            multidoc = dict[binding]
1✔
637
            if haskey(multidoc.docs, Union{})
1✔
638
                fields = multidoc.docs[Union{}].data[:fields]
1✔
639
                if haskey(fields, field)
1✔
640
                    doc = fields[field]
1✔
641
                    return isa(doc, Markdown.MD) ? doc : Markdown.parse(doc)
1✔
642
                end
643
            end
644
        end
645
    end
404✔
646
    fs = fieldnames(resolve(binding))
4✔
647
    fields = isempty(fs) ? "no fields" : (length(fs) == 1 ? "field " : "fields ") *
7✔
648
                                          join(("`$f`" for f in fs), ", ", ", and ")
649
    Markdown.parse("`$(resolve(binding))` has $fields.")
4✔
650
end
651

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

655

656
# Search & Rescue
657
# Utilities for correcting user mistakes and (eventually)
658
# doing full documentation searches from the repl.
659

660
# Fuzzy Search Algorithm
661

662
function matchinds(needle, haystack; acronym::Bool = false)
510✔
663
    chars = collect(needle)
510✔
664
    is = Int[]
255✔
665
    lastc = '\0'
×
666
    for (i, char) in enumerate(haystack)
510✔
667
        while !isempty(chars) && isspace(first(chars))
2,231✔
668
            popfirst!(chars) # skip spaces
4✔
669
        end
4✔
670
        isempty(chars) && break
1,125✔
671
        if lowercase(char) == lowercase(chars[1]) &&
1,240✔
672
           (!acronym || !isletter(lastc))
673
            push!(is, i)
332✔
674
            popfirst!(chars)
332✔
675
        end
676
        lastc = char
×
677
    end
1,102✔
678
    return is
255✔
679
end
680

681
matchinds(needle, (; name)::AccessibleBinding; acronym::Bool=false) =
×
682
    matchinds(needle, name; acronym)
683

684
longer(x, y) = length(x) ≥ length(y) ? (x, true) : (y, false)
127✔
685

686
bestmatch(needle, haystack) =
127✔
687
    longer(matchinds(needle, haystack, acronym = true),
688
           matchinds(needle, haystack))
689

690
# Optimal string distance: Counts the minimum number of insertions, deletions,
691
# transpositions or substitutions to go from one string to the other.
692
function string_distance(a::AbstractString, lena::Integer, b::AbstractString, lenb::Integer)
107,410✔
693
    if lena > lenb
107,410✔
694
        a, b = b, a
×
695
        lena, lenb = lenb, lena
×
696
    end
697
    start = 0
107,410✔
698
    for (i, j) in zip(a, b)
213,383✔
699
        if a == b
106,144✔
700
            start += 1
228✔
701
        else
702
            break
×
703
        end
704
    end
399✔
705
    start == lena && return lenb - start
107,410✔
706
    vzero = collect(1:(lenb - start))
1,073,679✔
707
    vone = similar(vzero)
211,832✔
708
    prev_a, prev_b = first(a), first(b)
209,921✔
709
    current = 0
×
710
    for (i, ai) in enumerate(a)
211,832✔
711
        i > start || (prev_a = ai; continue)
340,266✔
712
        left = i - start - 1
340,266✔
713
        current = i - start
340,266✔
714
        transition_next = 0
×
715
        for (j, bj) in enumerate(b)
680,532✔
716
            j > start || (prev_b = bj; continue)
3,648,949✔
717
            # No need to look beyond window of lower right diagonal
718
            above = current
×
719
            this_transition = transition_next
×
720
            transition_next = vone[j - start]
3,648,949✔
721
            vone[j - start] = current = left
3,648,949✔
722
            left = vzero[j - start]
3,648,949✔
723
            if ai != bj
3,648,949✔
724
                # Minimum between substitution, deletion and insertion
725
                current = min(current + 1, above + 1, left + 1)
3,532,761✔
726
                if i > start + 1 && j > start + 1 && ai == prev_b && prev_a == bj
3,532,761✔
727
                    current = min(current, (this_transition += 1))
4,543✔
728
                end
729
            end
730
            vzero[j - start] = current
3,648,949✔
731
            prev_b = bj
×
732
        end
6,957,632✔
733
        prev_a = ai
340,266✔
734
    end
574,616✔
735
    current
105,916✔
736
end
737

738
function fuzzyscore(needle::AbstractString, haystack::AbstractString)
4✔
739
    lena, lenb = length(needle), length(haystack)
107,410✔
740
    1 - (string_distance(needle, lena, haystack, lenb) / max(lena, lenb))
107,410✔
741
end
742

743
function fuzzyscore(needle::AbstractString, haystack::AccessibleBinding)
107,406✔
744
    score = fuzzyscore(needle, haystack.name)
107,406✔
745
    haystack.source === nothing && return score
107,406✔
746
    # Apply a "penalty" of half an edit if the comparator binding is public but not
747
    # exported so that exported/local names that exactly match the search query are
748
    # listed first
749
    penalty = 1 / (2 * max(length(needle), length(haystack.name)))
3,230✔
750
    return max(score - penalty, 0)
3,230✔
751
end
752

753
function fuzzysort(search::String, candidates::Vector{AccessibleBinding})
45✔
754
    scores = map(cand -> fuzzyscore(search, cand), candidates)
93,240✔
755
    candidates[sortperm(scores)] |> reverse
45✔
756
end
757

758
# Levenshtein Distance
759

760
function levenshtein(s1, s2)
14,525✔
761
    a, b = collect(s1), collect(s2)
29,041✔
762
    m = length(a)
14,525✔
763
    n = length(b)
14,525✔
764
    d = Matrix{Int}(undef, m+1, n+1)
29,050✔
765

766
    d[1:m+1, 1] = 0:m
70,406✔
767
    d[1, 1:n+1] = 0:n
151,191✔
768

769
    for i = 1:m, j = 1:n
14,525✔
770
        d[i+1,j+1] = min(d[i  , j+1] + 1,
496,577✔
771
                         d[i+1, j  ] + 1,
772
                         d[i  , j  ] + (a[i] != b[j]))
773
    end
537,933✔
774

775
    return d[m+1, n+1]
14,525✔
776
end
777

778
function levsort(search::String, candidates::Vector{AccessibleBinding})
7✔
779
    scores = map(candidates) do cand
7✔
780
        (Float64(levenshtein(search, cand.name)), -fuzzyscore(search, cand))
14,061✔
781
    end
782
    candidates = candidates[sortperm(scores)]
7✔
783
    i = 0
7✔
784
    for outer i = 1:length(candidates)
7✔
785
        levenshtein(search, candidates[i].name) > 3 && break
464✔
786
    end
457✔
787
    return candidates[1:i]
7✔
788
end
789

790
# Result printing
791

792
function printmatch(io::IO, word, match)
127✔
793
    is, _ = bestmatch(word, match)
127✔
794
    for (i, char) = enumerate(match)
254✔
795
        if i in is
1,548✔
796
            printstyled(io, char, bold=true)
261✔
797
        else
798
            print(io, char)
311✔
799
        end
800
    end
572✔
801
end
802

803
function printmatch(io::IO, word, match::AccessibleBinding)
804
    match.source === nothing || print(io, match.source, '.')
133✔
805
    printmatch(io, word, match.name)
127✔
806
end
807

808
function matchlength(x::AccessibleBinding)
221✔
809
    n = length(x.name)
221✔
810
    if x.source !== nothing
221✔
811
        n += length(x.source) + 1  # the +1 is for the `.` separator
10✔
812
    end
813
    return n
221✔
814
end
815
matchlength(x) = length(x)
×
816

817
function printmatches(io::IO, word, matches; cols::Int = _displaysize(io)[2])
50✔
818
    total = 0
25✔
819
    for match in matches
25✔
820
        ml = matchlength(match)
152✔
821
        total + ml + 1 > cols && break
152✔
822
        fuzzyscore(word, match) < 0.5 && break
146✔
823
        print(io, " ")
127✔
824
        printmatch(io, word, match)
133✔
825
        total += ml + 1
127✔
826
    end
127✔
827
end
828

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

831
function print_joined_cols(io::IO, ss::Vector{AccessibleBinding}, delim = "", last = delim; cols::Int = _displaysize(io)[2])
14✔
832
    i = 0
7✔
833
    total = 0
7✔
834
    for outer i = 1:length(ss)
7✔
835
        total += matchlength(ss[i])
69✔
836
        total + max(i-2,0)*length(delim) + (i>1 ? 1 : 0)*length(last) > cols && (i-=1; break)
69✔
837
    end
64✔
838
    join(io, ss[1:i], delim, last)
7✔
839
end
840

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

843
function print_correction(io::IO, word::String, mod::Module)
7✔
844
    cors = map(quote_spaces, levsort(word, accessible(mod)))
7✔
845
    pre = "Perhaps you meant "
7✔
846
    print(io, pre)
7✔
847
    print_joined_cols(io, cors, ", ", " or "; cols = _displaysize(io)[2] - length(pre))
7✔
848
    println(io)
7✔
849
    return
7✔
850
end
851

852
# TODO: document where this is used
853
print_correction(word, mod::Module) = print_correction(stdout, word, mod)
×
854

855
# Completion data
856

857
moduleusings(mod) = ccall(:jl_module_usings, Any, (Any,), mod)
53✔
858

859
function accessible(mod::Module)
53✔
860
    bindings = Set(AccessibleBinding(s) for s in names(mod; all=true, imported=true)
53✔
861
                   if !isdeprecated(mod, s))
862
    for used in moduleusings(mod)
53✔
863
        union!(bindings, (AccessibleBinding(used, s) for s in names(used)
646✔
864
                          if !isdeprecated(used, s)))
865
    end
646✔
866
    union!(bindings, (AccessibleBinding(k) for k in keys(Base.Docs.keywords)))
53✔
867
    filter!(b -> !occursin('#', b.name), bindings)
219,404✔
868
    return collect(bindings)
53✔
869
end
870

871
function doc_completions(name, mod::Module=Main)
45✔
872
    res = fuzzysort(name, accessible(mod))
48✔
873

874
    # to insert an entry like `raw""` for `"@raw_str"` in `res`
875
    ms = map(c -> match(r"^@(.*?)_str$", c.name), res)
93,240✔
876
    idxs = findall(!isnothing, ms)
90✔
877

878
    # avoid messing up the order while inserting
879
    for i in reverse!(idxs)
45✔
880
        c = only((ms[i]::AbstractMatch).captures)
587✔
881
        insert!(res, i, AccessibleBinding(res[i].source, "$(c)\"\""))
587✔
882
    end
587✔
883
    res
×
884
end
885
doc_completions(name::Symbol) = doc_completions(string(name), mod)
×
886

887

888
# Searching and apropos
889

890
# Docsearch simply returns true or false if an object contains the given needle
891
docsearch(haystack::AbstractString, needle) = occursin(needle, haystack)
12,784✔
892
docsearch(haystack::Symbol, needle) = docsearch(string(haystack), needle)
×
893
docsearch(::Nothing, needle) = false
×
894
function docsearch(haystack::Array, needle)
×
895
    for elt in haystack
×
896
        docsearch(elt, needle) && return true
×
897
    end
×
898
    false
×
899
end
900
function docsearch(haystack, needle)
×
901
    @warn "Unable to search documentation of type $(typeof(haystack))" maxlog=1
×
902
    false
×
903
end
904

905
## Searching specific documentation objects
906
function docsearch(haystack::MultiDoc, needle)
11,144✔
907
    for v in values(haystack.docs)
22,288✔
908
        docsearch(v, needle) && return true
12,387✔
909
    end
9,037✔
910
    false
911
end
912

913
function docsearch(haystack::DocStr, needle)
12,387✔
914
    docsearch(parsedoc(haystack), needle) && return true
12,387✔
915
    if haskey(haystack.data, :fields)
18,074✔
916
        for doc in values(haystack.data[:fields])
795✔
917
            docsearch(doc, needle) && return true
54✔
918
        end
54✔
919
    end
920
    false
921
end
922

923
## doc search
924

925
## Markdown search simply strips all markup and searches plain text version
926
docsearch(haystack::Markdown.MD, needle) = docsearch(stripmd(haystack.content), needle)
12,728✔
927

928
"""
929
    stripmd(x)
930

931
Strip all Markdown markup from x, leaving the result in plain text. Used
932
internally by apropos to make docstrings containing more than one markdown
933
element searchable.
934
"""
935
stripmd(@nospecialize x) = string(x) # for random objects interpolated into the docstring
6✔
936
stripmd(x::AbstractString) = x  # base case
190,566✔
937
stripmd(x::Nothing) = " "
100✔
938
stripmd(x::Vector) = string(map(stripmd, x)...)
67,580✔
939

940
stripmd(x::Markdown.BlockQuote) = "$(stripmd(x.content))"
4✔
941
stripmd(x::Markdown.Admonition) = "$(stripmd(x.content))"
2,674✔
942
stripmd(x::Markdown.Bold) = "$(stripmd(x.text))"
74✔
943
stripmd(x::Markdown.Code) = "$(stripmd(x.code))"
87,659✔
944
stripmd(x::Markdown.Header) = stripmd(x.text)
4,856✔
945
stripmd(x::Markdown.HorizontalRule) = " "
136✔
946
stripmd(x::Markdown.Image) = "$(stripmd(x.alt)) $(x.url)"
40✔
947
stripmd(x::Markdown.Italic) = "$(stripmd(x.text))"
603✔
948
stripmd(x::Markdown.LaTeX) = "$(x.formula)"
801✔
949
stripmd(x::Markdown.LineBreak) = " "
76✔
950
stripmd(x::Markdown.Link) = "$(stripmd(x.text)) $(x.url)"
12,898✔
951
stripmd(x::Markdown.List) = join(map(stripmd, x.items), " ")
1,109✔
952
stripmd(x::Markdown.MD) = join(map(stripmd, x.content), " ")
12,731✔
953
stripmd(x::Markdown.Paragraph) = stripmd(x.content)
29,011✔
954
stripmd(x::Markdown.Footnote) = "$(stripmd(x.id)) $(stripmd(x.text))"
196✔
955
stripmd(x::Markdown.Table) =
50✔
956
    join([join(map(stripmd, r), " ") for r in x.rows], " ")
957

958
apropos(string) = apropos(stdout, string)
×
959
apropos(io::IO, string) = apropos(io, Regex("\\Q$string", "i"))
2✔
960

961
function apropos(io::IO, needle::Regex)
4✔
962
    for mod in modules
4✔
963
        # Module doc might be in README.md instead of the META dict
964
        docsearch(doc(mod), needle) && println(io, mod)
343✔
965
        dict = meta(mod; autoinit=false)
686✔
966
        isnothing(dict) && continue
686✔
967
        for (k, v) in dict
684✔
968
            docsearch(v, needle) && println(io, k)
11,144✔
969
        end
11,144✔
970
    end
343✔
971
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