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

JuliaLang / julia / #37997

29 Jan 2025 02:08AM UTC coverage: 17.283% (-68.7%) from 85.981%
#37997

push

local

web-flow
bpart: Start enforcing min_world for global variable definitions (#57150)

This is the analog of #57102 for global variables. Unlike for consants,
there is no automatic global backdate mechanism. The reasoning for this
is that global variables can be declared at any time, unlike constants
which can only be decalared once their value is available. As a result
code patterns using `Core.eval` to declare globals are rarer and likely
incorrect.

1 of 22 new or added lines in 3 files covered. (4.55%)

31430 existing lines in 188 files now uncovered.

7903 of 45728 relevant lines covered (17.28%)

98663.7 hits per line

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

0.38
/stdlib/REPL/src/REPL.jl
1
# This file is a part of Julia. License is MIT: https://julialang.org/license
2

3
"""
4
Run Evaluate Print Loop (REPL)
5

6
Example minimal code
7

8
```julia
9
import REPL
10
term = REPL.Terminals.TTYTerminal("dumb", stdin, stdout, stderr)
11
repl = REPL.LineEditREPL(term, true)
12
REPL.run_repl(repl)
13
```
14
"""
15
module REPL
16

17
Base.Experimental.@optlevel 1
18
Base.Experimental.@max_methods 1
19

UNCOV
20
function UndefVarError_hint(io::IO, ex::UndefVarError)
×
UNCOV
21
    var = ex.var
×
UNCOV
22
    if var === :or
×
23
        print(io, "\nSuggestion: Use `||` for short-circuiting boolean OR.")
×
UNCOV
24
    elseif var === :and
×
25
        print(io, "\nSuggestion: Use `&&` for short-circuiting boolean AND.")
×
UNCOV
26
    elseif var === :help
×
27
        println(io)
×
28
        # Show friendly help message when user types help or help() and help is undefined
29
        show(io, MIME("text/plain"), Base.Docs.parsedoc(Base.Docs.keywords[:help]))
×
UNCOV
30
    elseif var === :quit
×
31
        print(io, "\nSuggestion: To exit Julia, use Ctrl-D, or type exit() and press enter.")
×
32
    end
UNCOV
33
    if isdefined(ex, :scope)
×
UNCOV
34
        scope = ex.scope
×
UNCOV
35
        if scope isa Module
×
UNCOV
36
            bpart = Base.lookup_binding_partition(ex.world, GlobalRef(scope, var))
×
UNCOV
37
            kind = Base.binding_kind(bpart)
×
UNCOV
38
            if kind === Base.BINDING_KIND_GLOBAL || kind === Base.BINDING_KIND_UNDEF_CONST || kind == Base.BINDING_KIND_DECLARED
×
39
                print(io, "\nSuggestion: add an appropriate import or assignment. This global was declared but not assigned.")
×
UNCOV
40
            elseif kind === Base.BINDING_KIND_FAILED
×
UNCOV
41
                print(io, "\nHint: It looks like two or more modules export different ",
×
42
                "bindings with this name, resulting in ambiguity. Try explicitly ",
43
                "importing it from a particular module, or qualifying the name ",
44
                "with the module it should come from.")
UNCOV
45
            elseif kind === Base.BINDING_KIND_GUARD
×
UNCOV
46
                print(io, "\nSuggestion: check for spelling errors or missing imports.")
×
NEW
47
            elseif Base.is_some_imported(kind)
×
NEW
48
                print(io, "\nSuggestion: this global was defined as `$(Base.partition_restriction(bpart).globalref)` but not assigned a value.")
×
49
            end
50
        elseif scope === :static_parameter
×
51
            print(io, "\nSuggestion: run Test.detect_unbound_args to detect method arguments that do not fully constrain a type parameter.")
×
52
        elseif scope === :local
×
53
            print(io, "\nSuggestion: check for an assignment to a local variable that shadows a global of the same name.")
×
54
        end
55
    else
56
        scope = undef
×
57
    end
UNCOV
58
    if scope !== Base && !_UndefVarError_warnfor(io, Base, var)
×
UNCOV
59
        warned = false
×
UNCOV
60
        for m in Base.loaded_modules_order
×
UNCOV
61
            m === Core && continue
×
UNCOV
62
            m === Base && continue
×
UNCOV
63
            m === Main && continue
×
UNCOV
64
            m === scope && continue
×
UNCOV
65
            warned |= _UndefVarError_warnfor(io, m, var)
×
UNCOV
66
        end
×
UNCOV
67
        warned ||
×
68
            _UndefVarError_warnfor(io, Core, var) ||
69
            _UndefVarError_warnfor(io, Main, var)
70
    end
UNCOV
71
    return nothing
×
72
end
73

UNCOV
74
function _UndefVarError_warnfor(io::IO, m::Module, var::Symbol)
×
UNCOV
75
    Base.isbindingresolved(m, var) || return false
×
UNCOV
76
    (Base.isexported(m, var) || Base.ispublic(m, var)) || return false
×
UNCOV
77
    active_mod = Base.active_module()
×
UNCOV
78
    print(io, "\nHint: ")
×
UNCOV
79
    if isdefined(active_mod, Symbol(m))
×
UNCOV
80
        print(io, "a global variable of this name also exists in $m.")
×
81
    else
82
        if Symbol(m) == var
×
83
            print(io, "$m is loaded but not imported in the active module $active_mod.")
×
84
        else
85
            print(io, "a global variable of this name may be made accessible by importing $m in the current active module $active_mod")
×
86
        end
87
    end
UNCOV
88
    return true
×
89
end
90

91
function __init__()
1✔
92
    Base.REPL_MODULE_REF[] = REPL
1✔
93
    Base.Experimental.register_error_hint(UndefVarError_hint, UndefVarError)
1✔
94
    return nothing
1✔
95
end
96

97
using Base.Meta, Sockets, StyledStrings
98
using JuliaSyntaxHighlighting
99
import InteractiveUtils
100

101
export
102
    AbstractREPL,
103
    BasicREPL,
104
    LineEditREPL,
105
    StreamREPL
106

107
public TerminalMenus
108

109
import Base:
110
    AbstractDisplay,
111
    display,
112
    show,
113
    AnyDict,
114
    ==
115

UNCOV
116
_displaysize(io::IO) = displaysize(io)::Tuple{Int,Int}
×
117

118
include("Terminals.jl")
119
using .Terminals
120

121
abstract type AbstractREPL end
122

123
include("options.jl")
124

125
include("LineEdit.jl")
126
using .LineEdit
127
import .LineEdit:
128
    CompletionProvider,
129
    HistoryProvider,
130
    add_history,
131
    complete_line,
132
    history_next,
133
    history_next_prefix,
134
    history_prev,
135
    history_prev_prefix,
136
    history_first,
137
    history_last,
138
    history_search,
139
    setmodifiers!,
140
    terminal,
141
    MIState,
142
    PromptState,
143
    mode_idx
144

145
include("REPLCompletions.jl")
146
using .REPLCompletions
147

148
include("TerminalMenus/TerminalMenus.jl")
149
include("docview.jl")
150

151
include("Pkg_beforeload.jl")
152

153
@nospecialize # use only declared type signatures
154

155
answer_color(::AbstractREPL) = ""
×
156

157
const JULIA_PROMPT = "julia> "
158
const PKG_PROMPT = "pkg> "
159
const SHELL_PROMPT = "shell> "
160
const HELP_PROMPT = "help?> "
161

162
mutable struct REPLBackend
163
    "channel for AST"
164
    repl_channel::Channel{Any}
165
    "channel for results: (value, iserror)"
166
    response_channel::Channel{Any}
167
    "flag indicating the state of this backend"
168
    in_eval::Bool
169
    "transformation functions to apply before evaluating expressions"
170
    ast_transforms::Vector{Any}
171
    "current backend task"
172
    backend_task::Task
173

UNCOV
174
    REPLBackend(repl_channel, response_channel, in_eval, ast_transforms=copy(repl_ast_transforms)) =
×
175
        new(repl_channel, response_channel, in_eval, ast_transforms)
176
end
UNCOV
177
REPLBackend() = REPLBackend(Channel(1), Channel(1), false)
×
178

179
"""
180
    softscope(ex)
181

182
Return a modified version of the parsed expression `ex` that uses
183
the REPL's "soft" scoping rules for global syntax blocks.
184
"""
UNCOV
185
function softscope(@nospecialize ex)
×
UNCOV
186
    if ex isa Expr
×
UNCOV
187
        h = ex.head
×
UNCOV
188
        if h === :toplevel
×
UNCOV
189
            ex′ = Expr(h)
×
UNCOV
190
            map!(softscope, resize!(ex′.args, length(ex.args)), ex.args)
×
UNCOV
191
            return ex′
×
UNCOV
192
        elseif h in (:meta, :import, :using, :export, :module, :error, :incomplete, :thunk)
×
UNCOV
193
            return ex
×
UNCOV
194
        elseif h === :global && all(x->isa(x, Symbol), ex.args)
×
UNCOV
195
            return ex
×
196
        else
UNCOV
197
            return Expr(:block, Expr(:softscope, true), ex)
×
198
        end
199
    end
UNCOV
200
    return ex
×
201
end
202

203
# Temporary alias until Documenter updates
204
const softscope! = softscope
205

UNCOV
206
function print_qualified_access_warning(mod::Module, owner::Module, name::Symbol)
×
UNCOV
207
    @warn string(name, " is defined in ", owner, " and is not public in ", mod) maxlog = 1 _id = string("repl-warning-", mod, "-", owner, "-", name) _line = nothing _file = nothing _module = nothing
×
208
end
209

UNCOV
210
function has_ancestor(query::Module, target::Module)
×
UNCOV
211
    query == target && return true
×
UNCOV
212
    while true
×
UNCOV
213
        next = parentmodule(query)
×
UNCOV
214
        next == target && return true
×
UNCOV
215
        next == query && return false
×
216
        query = next
×
UNCOV
217
    end
×
218
end
219

220
retrieve_modules(::Module, ::Any) = (nothing,)
×
UNCOV
221
function retrieve_modules(current_module::Module, mod_name::Symbol)
×
UNCOV
222
    mod = try
×
UNCOV
223
        getproperty(current_module, mod_name)
×
224
    catch
UNCOV
225
        return (nothing,)
×
226
    end
UNCOV
227
    return (mod isa Module ? mod : nothing,)
×
228
end
UNCOV
229
retrieve_modules(current_module::Module, mod_name::QuoteNode) = retrieve_modules(current_module, mod_name.value)
×
UNCOV
230
function retrieve_modules(current_module::Module, mod_expr::Expr)
×
UNCOV
231
    if Meta.isexpr(mod_expr, :., 2)
×
UNCOV
232
        current_module = retrieve_modules(current_module, mod_expr.args[1])[1]
×
UNCOV
233
        current_module === nothing && return (nothing,)
×
UNCOV
234
        return (current_module, retrieve_modules(current_module, mod_expr.args[2])...)
×
235
    else
236
        return (nothing,)
×
237
    end
238
end
239

UNCOV
240
add_locals!(locals, ast::Any) = nothing
×
UNCOV
241
function add_locals!(locals, ast::Expr)
×
UNCOV
242
    for arg in ast.args
×
UNCOV
243
        add_locals!(locals, arg)
×
UNCOV
244
    end
×
UNCOV
245
    return nothing
×
246
end
UNCOV
247
function add_locals!(locals, ast::Symbol)
×
UNCOV
248
    push!(locals, ast)
×
UNCOV
249
    return nothing
×
250
end
251

UNCOV
252
function collect_names_to_warn!(warnings, locals, current_module::Module, ast)
×
UNCOV
253
    ast isa Expr || return
×
254

255
    # don't recurse through module definitions
UNCOV
256
    ast.head === :module && return
×
257

UNCOV
258
    if Meta.isexpr(ast, :., 2)
×
UNCOV
259
        mod_name, name_being_accessed = ast.args
×
260
        # retrieve the (possibly-nested) module being named here
UNCOV
261
        mods = retrieve_modules(current_module, mod_name)
×
UNCOV
262
        all(x -> x isa Module, mods) || return
×
UNCOV
263
        outer_mod = first(mods)
×
UNCOV
264
        mod = last(mods)
×
UNCOV
265
        if name_being_accessed isa QuoteNode
×
UNCOV
266
            name_being_accessed = name_being_accessed.value
×
267
        end
UNCOV
268
        name_being_accessed isa Symbol || return
×
UNCOV
269
        owner = try
×
UNCOV
270
            which(mod, name_being_accessed)
×
271
        catch
272
            return
×
273
        end
274
        # if `owner` is a submodule of `mod`, then don't warn. E.g. the name `parse` is present in the module `JSON`
275
        # but is owned by `JSON.Parser`; we don't warn if it is accessed as `JSON.parse`.
UNCOV
276
        has_ancestor(owner, mod) && return
×
277
        # Don't warn if the name is public in the module we are accessing it
UNCOV
278
        Base.ispublic(mod, name_being_accessed) && return
×
279
        # Don't warn if accessing names defined in Core from Base if they are present in Base (e.g. `Base.throw`).
UNCOV
280
        mod === Base && Base.ispublic(Core, name_being_accessed) && return
×
UNCOV
281
        push!(warnings, (; outer_mod, mod, owner, name_being_accessed))
×
282
        # no recursion
UNCOV
283
        return
×
UNCOV
284
    elseif Meta.isexpr(ast, :(=), 2)
×
UNCOV
285
        lhs, rhs = ast.args
×
286
        # any symbols we find on the LHS we will count as local. This can potentially be overzealous,
287
        # but we want to avoid false positives (unnecessary warnings) more than false negatives.
UNCOV
288
        add_locals!(locals, lhs)
×
289
        # we'll recurse into the RHS only
UNCOV
290
        return collect_names_to_warn!(warnings, locals, current_module, rhs)
×
UNCOV
291
    elseif Meta.isexpr(ast, :function) && length(ast.args) >= 1
×
292

UNCOV
293
        if Meta.isexpr(ast.args[1], :call, 2)
×
UNCOV
294
            func_name, func_args = ast.args[1].args
×
295
            # here we have a function definition and are inspecting it's arguments for local variables.
296
            # we will error on the conservative side by adding all symbols we find (regardless if they are local variables or possibly-global default values)
UNCOV
297
            add_locals!(locals, func_args)
×
298
        end
299
        # fall through to general recursion
300
    end
301

UNCOV
302
    for arg in ast.args
×
UNCOV
303
        collect_names_to_warn!(warnings, locals, current_module, arg)
×
UNCOV
304
    end
×
305

UNCOV
306
    return nothing
×
307
end
308

UNCOV
309
function collect_qualified_access_warnings(current_mod, ast)
×
UNCOV
310
    warnings = Set()
×
UNCOV
311
    locals = Set{Symbol}()
×
UNCOV
312
    collect_names_to_warn!(warnings, locals, current_mod, ast)
×
UNCOV
313
    filter!(warnings) do (; outer_mod)
×
UNCOV
314
        nameof(outer_mod) ∉ locals
×
315
    end
UNCOV
316
    return warnings
×
317
end
318

UNCOV
319
function warn_on_non_owning_accesses(current_mod, ast)
×
UNCOV
320
    warnings = collect_qualified_access_warnings(current_mod, ast)
×
UNCOV
321
    for (; outer_mod, mod, owner, name_being_accessed) in warnings
×
UNCOV
322
        print_qualified_access_warning(mod, owner, name_being_accessed)
×
UNCOV
323
    end
×
UNCOV
324
    return ast
×
325
end
UNCOV
326
warn_on_non_owning_accesses(ast) = warn_on_non_owning_accesses(Base.active_module(), ast)
×
327

328
const repl_ast_transforms = Any[softscope, warn_on_non_owning_accesses] # defaults for new REPL backends
329

330
# Allows an external package to add hooks into the code loading.
331
# The hook should take a Vector{Symbol} of package names and
332
# return true if all packages could be installed, false if not
333
# to e.g. install packages on demand
334
const install_packages_hooks = Any[]
335

336
# N.B.: Any functions starting with __repl_entry cut off backtraces when printing in the REPL.
337
# We need to do this for both the actual eval and macroexpand, since the latter can cause custom macro
338
# code to run (and error).
UNCOV
339
__repl_entry_lower_with_loc(mod::Module, @nospecialize(ast), toplevel_file::Ref{Ptr{UInt8}}, toplevel_line::Ref{Cint}) =
×
340
    ccall(:jl_expand_with_loc, Any, (Any, Any, Ptr{UInt8}, Cint), ast, mod, toplevel_file[], toplevel_line[])
UNCOV
341
__repl_entry_eval_expanded_with_loc(mod::Module, @nospecialize(ast), toplevel_file::Ref{Ptr{UInt8}}, toplevel_line::Ref{Cint}) =
×
342
    ccall(:jl_toplevel_eval_flex, Any, (Any, Any, Cint, Cint, Ptr{Ptr{UInt8}}, Ptr{Cint}), mod, ast, 1, 1, toplevel_file, toplevel_line)
343

UNCOV
344
function toplevel_eval_with_hooks(mod::Module, @nospecialize(ast), toplevel_file=Ref{Ptr{UInt8}}(Base.unsafe_convert(Ptr{UInt8}, :REPL)), toplevel_line=Ref{Cint}(1))
×
UNCOV
345
    if !isexpr(ast, :toplevel)
×
UNCOV
346
        ast = invokelatest(__repl_entry_lower_with_loc, mod, ast, toplevel_file, toplevel_line)
×
UNCOV
347
        check_for_missing_packages_and_run_hooks(ast)
×
UNCOV
348
        return invokelatest(__repl_entry_eval_expanded_with_loc, mod, ast, toplevel_file, toplevel_line)
×
349
    end
UNCOV
350
    local value=nothing
×
UNCOV
351
    for i = 1:length(ast.args)
×
UNCOV
352
        value = toplevel_eval_with_hooks(mod, ast.args[i], toplevel_file, toplevel_line)
×
UNCOV
353
    end
×
UNCOV
354
    return value
×
355
end
356

UNCOV
357
function eval_user_input(@nospecialize(ast), backend::REPLBackend, mod::Module)
×
UNCOV
358
    lasterr = nothing
×
UNCOV
359
    Base.sigatomic_begin()
×
UNCOV
360
    while true
×
UNCOV
361
        try
×
UNCOV
362
            Base.sigatomic_end()
×
UNCOV
363
            if lasterr !== nothing
×
UNCOV
364
                put!(backend.response_channel, Pair{Any, Bool}(lasterr, true))
×
365
            else
UNCOV
366
                backend.in_eval = true
×
UNCOV
367
                for xf in backend.ast_transforms
×
UNCOV
368
                    ast = Base.invokelatest(xf, ast)
×
UNCOV
369
                end
×
UNCOV
370
                value = toplevel_eval_with_hooks(mod, ast)
×
UNCOV
371
                backend.in_eval = false
×
UNCOV
372
                setglobal!(Base.MainInclude, :ans, value)
×
UNCOV
373
                put!(backend.response_channel, Pair{Any, Bool}(value, false))
×
374
            end
UNCOV
375
            break
×
376
        catch err
UNCOV
377
            if lasterr !== nothing
×
378
                println("SYSTEM ERROR: Failed to report error to REPL frontend")
×
379
                println(err)
×
380
            end
UNCOV
381
            lasterr = current_exceptions()
×
382
        end
UNCOV
383
    end
×
UNCOV
384
    Base.sigatomic_end()
×
UNCOV
385
    nothing
×
386
end
387

UNCOV
388
function check_for_missing_packages_and_run_hooks(ast)
×
UNCOV
389
    isa(ast, Expr) || return
×
UNCOV
390
    mods = modules_to_be_loaded(ast)
×
UNCOV
391
    filter!(mod -> isnothing(Base.identify_package(String(mod))), mods) # keep missing modules
×
UNCOV
392
    if !isempty(mods)
×
UNCOV
393
        isempty(install_packages_hooks) && load_pkg()
×
UNCOV
394
        for f in install_packages_hooks
×
UNCOV
395
            Base.invokelatest(f, mods) && return
×
396
        end
×
397
    end
398
end
399

UNCOV
400
function _modules_to_be_loaded!(ast::Expr, mods::Vector{Symbol})
×
UNCOV
401
    ast.head === :quote && return mods # don't search if it's not going to be run during this eval
×
UNCOV
402
    if ast.head === :using || ast.head === :import
×
UNCOV
403
        for arg in ast.args
×
UNCOV
404
            arg = arg::Expr
×
UNCOV
405
            arg1 = first(arg.args)
×
UNCOV
406
            if arg1 isa Symbol # i.e. `Foo`
×
UNCOV
407
                if arg1 != :. # don't include local import `import .Foo`
×
UNCOV
408
                    push!(mods, arg1)
×
409
                end
410
            else # i.e. `Foo: bar`
UNCOV
411
                sym = first((arg1::Expr).args)::Symbol
×
UNCOV
412
                if sym != :. # don't include local import `import .Foo: a`
×
UNCOV
413
                    push!(mods, sym)
×
414
                end
415
            end
UNCOV
416
        end
×
417
    end
UNCOV
418
    if ast.head !== :thunk
×
UNCOV
419
        for arg in ast.args
×
UNCOV
420
            if isexpr(arg, (:block, :if, :using, :import))
×
UNCOV
421
                _modules_to_be_loaded!(arg, mods)
×
422
            end
UNCOV
423
        end
×
424
    else
UNCOV
425
        code = ast.args[1]
×
UNCOV
426
        for arg in code.code
×
UNCOV
427
            isa(arg, Expr) || continue
×
UNCOV
428
            _modules_to_be_loaded!(arg, mods)
×
UNCOV
429
        end
×
430
    end
431
end
432

UNCOV
433
function modules_to_be_loaded(ast::Expr, mods::Vector{Symbol} = Symbol[])
×
UNCOV
434
    _modules_to_be_loaded!(ast, mods)
×
UNCOV
435
    filter!(mod::Symbol -> !in(mod, (:Base, :Main, :Core)), mods) # Exclude special non-package modules
×
UNCOV
436
    return unique(mods)
×
437
end
438

439
"""
440
    start_repl_backend(repl_channel::Channel, response_channel::Channel)
441

442
    Starts loop for REPL backend
443
    Returns a REPLBackend with backend_task assigned
444

445
    Deprecated since sync / async behavior cannot be selected
446
"""
447
function start_repl_backend(repl_channel::Channel{Any}, response_channel::Channel{Any}
×
448
                            ; get_module::Function = ()->Main)
449
    # Maintain legacy behavior of asynchronous backend
450
    backend = REPLBackend(repl_channel, response_channel, false)
×
451
    # Assignment will be made twice, but will be immediately available
452
    backend.backend_task = @async start_repl_backend(backend; get_module)
×
453
    return backend
×
454
end
455

456
"""
457
    start_repl_backend(backend::REPLBackend)
458

459
    Call directly to run backend loop on current Task.
460
    Use @async for run backend on new Task.
461

462
    Does not return backend until loop is finished.
463
"""
UNCOV
464
function start_repl_backend(backend::REPLBackend,  @nospecialize(consumer = x -> nothing); get_module::Function = ()->Main)
×
UNCOV
465
    backend.backend_task = Base.current_task()
×
UNCOV
466
    consumer(backend)
×
UNCOV
467
    repl_backend_loop(backend, get_module)
×
UNCOV
468
    return backend
×
469
end
470

UNCOV
471
function repl_backend_loop(backend::REPLBackend, get_module::Function)
×
472
    # include looks at this to determine the relative include path
473
    # nothing means cwd
UNCOV
474
    while true
×
UNCOV
475
        tls = task_local_storage()
×
UNCOV
476
        tls[:SOURCE_PATH] = nothing
×
UNCOV
477
        ast, show_value = take!(backend.repl_channel)
×
UNCOV
478
        if show_value == -1
×
479
            # exit flag
UNCOV
480
            break
×
481
        end
UNCOV
482
        eval_user_input(ast, backend, get_module())
×
UNCOV
483
    end
×
UNCOV
484
    return nothing
×
485
end
486

487
SHOW_MAXIMUM_BYTES::Int = 1_048_576
488

489
# Limit printing during REPL display
490
mutable struct LimitIO{IO_t <: IO} <: IO
491
    io::IO_t
492
    maxbytes::Int
493
    n::Int # max bytes to write
494
end
UNCOV
495
LimitIO(io::IO, maxbytes) = LimitIO(io, maxbytes, 0)
×
496

497
struct LimitIOException <: Exception
498
    maxbytes::Int
499
end
500

501
function Base.showerror(io::IO, e::LimitIOException)
×
502
    print(io, "$LimitIOException: aborted printing after attempting to print more than $(Base.format_bytes(e.maxbytes)) within a `LimitIO`.")
×
503
end
504

UNCOV
505
function Base.write(io::LimitIO, v::UInt8)
×
UNCOV
506
    io.n > io.maxbytes && throw(LimitIOException(io.maxbytes))
×
UNCOV
507
    n_bytes = write(io.io, v)
×
UNCOV
508
    io.n += n_bytes
×
UNCOV
509
    return n_bytes
×
510
end
511

512
# Semantically, we only need to override `Base.write`, but we also
513
# override `unsafe_write` for performance.
UNCOV
514
function Base.unsafe_write(limiter::LimitIO, p::Ptr{UInt8}, nb::UInt)
×
515
    # already exceeded? throw
UNCOV
516
    limiter.n > limiter.maxbytes && throw(LimitIOException(limiter.maxbytes))
×
UNCOV
517
    remaining = limiter.maxbytes - limiter.n # >= 0
×
518

519
    # Not enough bytes left; we will print up to the limit, then throw
UNCOV
520
    if remaining < nb
×
UNCOV
521
        if remaining > 0
×
UNCOV
522
            Base.unsafe_write(limiter.io, p, remaining)
×
523
        end
UNCOV
524
        throw(LimitIOException(limiter.maxbytes))
×
525
    end
526

527
    # We won't hit the limit so we'll write the full `nb` bytes
UNCOV
528
    bytes_written = Base.unsafe_write(limiter.io, p, nb)::Union{Int,UInt}
×
UNCOV
529
    limiter.n += bytes_written
×
UNCOV
530
    return bytes_written
×
531
end
532

533
struct REPLDisplay{Repl<:AbstractREPL} <: AbstractDisplay
534
    repl::Repl
535
end
536

UNCOV
537
function show_limited(io::IO, mime::MIME, x)
×
UNCOV
538
    try
×
539
        # We wrap in a LimitIO to limit the amount of printing.
540
        # We unpack `IOContext`s, since we will pass the properties on the outside.
UNCOV
541
        inner = io isa IOContext ? io.io : io
×
UNCOV
542
        wrapped_limiter = IOContext(LimitIO(inner, SHOW_MAXIMUM_BYTES), io)
×
543
        # `show_repl` to allow the hook with special syntax highlighting
UNCOV
544
        show_repl(wrapped_limiter, mime, x)
×
545
    catch e
UNCOV
546
        e isa LimitIOException || rethrow()
×
UNCOV
547
        printstyled(io, """…[printing stopped after displaying $(Base.format_bytes(e.maxbytes)); call `show(stdout, MIME"text/plain"(), ans)` to print without truncation]"""; color=:light_yellow, bold=true)
×
548
    end
549
end
550

UNCOV
551
function display(d::REPLDisplay, mime::MIME"text/plain", x)
×
UNCOV
552
    x = Ref{Any}(x)
×
UNCOV
553
    with_repl_linfo(d.repl) do io
×
UNCOV
554
        io = IOContext(io, :limit => true, :module => Base.active_module(d)::Module)
×
UNCOV
555
        if d.repl isa LineEditREPL
×
UNCOV
556
            mistate = d.repl.mistate
×
UNCOV
557
            mode = LineEdit.mode(mistate)
×
UNCOV
558
            if mode isa LineEdit.Prompt
×
UNCOV
559
                LineEdit.write_output_prefix(io, mode, get(io, :color, false)::Bool)
×
560
            end
561
        end
UNCOV
562
        get(io, :color, false)::Bool && write(io, answer_color(d.repl))
×
UNCOV
563
        if isdefined(d.repl, :options) && isdefined(d.repl.options, :iocontext)
×
564
            # this can override the :limit property set initially
UNCOV
565
            io = foldl(IOContext, d.repl.options.iocontext, init=io)
×
566
        end
UNCOV
567
        show_limited(io, mime, x[])
×
UNCOV
568
        println(io)
×
569
    end
UNCOV
570
    return nothing
×
571
end
572

UNCOV
573
display(d::REPLDisplay, x) = display(d, MIME("text/plain"), x)
×
574

UNCOV
575
show_repl(io::IO, mime::MIME"text/plain", x) = show(io, mime, x)
×
576

UNCOV
577
show_repl(io::IO, ::MIME"text/plain", ex::Expr) =
×
578
    print(io, JuliaSyntaxHighlighting.highlight(
579
        sprint(show, ex, context=IOContext(io, :color => false))))
580

UNCOV
581
function print_response(repl::AbstractREPL, response, show_value::Bool, have_color::Bool)
×
UNCOV
582
    repl.waserror = response[2]
×
UNCOV
583
    with_repl_linfo(repl) do io
×
UNCOV
584
        io = IOContext(io, :module => Base.active_module(repl)::Module)
×
UNCOV
585
        print_response(io, response, show_value, have_color, specialdisplay(repl))
×
586
    end
UNCOV
587
    return nothing
×
588
end
589

UNCOV
590
function repl_display_error(errio::IO, @nospecialize errval)
×
591
    # this will be set to true if types in the stacktrace are truncated
UNCOV
592
    limitflag = Ref(false)
×
UNCOV
593
    errio = IOContext(errio, :stacktrace_types_limited => limitflag)
×
UNCOV
594
    Base.invokelatest(Base.display_error, errio, errval)
×
UNCOV
595
    if limitflag[]
×
UNCOV
596
        print(errio, "Some type information was truncated. Use `show(err)` to see complete types.")
×
UNCOV
597
        println(errio)
×
598
    end
UNCOV
599
    return nothing
×
600
end
601

UNCOV
602
function print_response(errio::IO, response, show_value::Bool, have_color::Bool, specialdisplay::Union{AbstractDisplay,Nothing}=nothing)
×
UNCOV
603
    Base.sigatomic_begin()
×
UNCOV
604
    val, iserr = response
×
UNCOV
605
    while true
×
UNCOV
606
        try
×
UNCOV
607
            Base.sigatomic_end()
×
UNCOV
608
            if iserr
×
UNCOV
609
                val = Base.scrub_repl_backtrace(val)
×
UNCOV
610
                Base.istrivialerror(val) || setglobal!(Base.MainInclude, :err, val)
×
UNCOV
611
                repl_display_error(errio, val)
×
612
            else
UNCOV
613
                if val !== nothing && show_value
×
UNCOV
614
                    try
×
UNCOV
615
                        if specialdisplay === nothing
×
UNCOV
616
                            Base.invokelatest(display, val)
×
617
                        else
UNCOV
618
                            Base.invokelatest(display, specialdisplay, val)
×
619
                        end
620
                    catch
UNCOV
621
                        println(errio, "Error showing value of type ", typeof(val), ":")
×
UNCOV
622
                        rethrow()
×
623
                    end
624
                end
625
            end
UNCOV
626
            break
×
627
        catch ex
UNCOV
628
            if iserr
×
UNCOV
629
                println(errio) # an error during printing is likely to leave us mid-line
×
UNCOV
630
                println(errio, "SYSTEM (REPL): showing an error caused an error")
×
UNCOV
631
                try
×
UNCOV
632
                    excs = Base.scrub_repl_backtrace(current_exceptions())
×
UNCOV
633
                    setglobal!(Base.MainInclude, :err, excs)
×
UNCOV
634
                    repl_display_error(errio, excs)
×
635
                catch e
636
                    # at this point, only print the name of the type as a Symbol to
637
                    # minimize the possibility of further errors.
UNCOV
638
                    println(errio)
×
UNCOV
639
                    println(errio, "SYSTEM (REPL): caught exception of type ", typeof(e).name.name,
×
640
                            " while trying to handle a nested exception; giving up")
641
                end
UNCOV
642
                break
×
643
            end
UNCOV
644
            val = current_exceptions()
×
UNCOV
645
            iserr = true
×
646
        end
UNCOV
647
    end
×
UNCOV
648
    Base.sigatomic_end()
×
UNCOV
649
    nothing
×
650
end
651

652
# A reference to a backend that is not mutable
653
struct REPLBackendRef
654
    repl_channel::Channel{Any}
655
    response_channel::Channel{Any}
656
end
UNCOV
657
REPLBackendRef(backend::REPLBackend) = REPLBackendRef(backend.repl_channel, backend.response_channel)
×
658

UNCOV
659
function destroy(ref::REPLBackendRef, state::Task)
×
UNCOV
660
    if istaskfailed(state)
×
661
        close(ref.repl_channel, TaskFailedException(state))
×
662
        close(ref.response_channel, TaskFailedException(state))
×
663
    end
UNCOV
664
    close(ref.repl_channel)
×
UNCOV
665
    close(ref.response_channel)
×
666
end
667

668
"""
669
    run_repl(repl::AbstractREPL)
670
    run_repl(repl, consumer = backend->nothing; backend_on_current_task = true)
671

672
    Main function to start the REPL
673

674
    consumer is an optional function that takes a REPLBackend as an argument
675
"""
UNCOV
676
function run_repl(repl::AbstractREPL, @nospecialize(consumer = x -> nothing); backend_on_current_task::Bool = true, backend = REPLBackend())
×
UNCOV
677
    backend_ref = REPLBackendRef(backend)
×
UNCOV
678
    cleanup = @task try
×
UNCOV
679
            destroy(backend_ref, t)
×
680
        catch e
681
            Core.print(Core.stderr, "\nINTERNAL ERROR: ")
×
682
            Core.println(Core.stderr, e)
×
683
            Core.println(Core.stderr, catch_backtrace())
×
684
        end
UNCOV
685
    get_module = () -> Base.active_module(repl)
×
UNCOV
686
    if backend_on_current_task
×
UNCOV
687
        t = @async run_frontend(repl, backend_ref)
×
UNCOV
688
        errormonitor(t)
×
UNCOV
689
        Base._wait2(t, cleanup)
×
UNCOV
690
        start_repl_backend(backend, consumer; get_module)
×
691
    else
692
        t = @async start_repl_backend(backend, consumer; get_module)
×
693
        errormonitor(t)
×
694
        Base._wait2(t, cleanup)
×
695
        run_frontend(repl, backend_ref)
×
696
    end
UNCOV
697
    return backend
×
698
end
699

700
## BasicREPL ##
701

702
mutable struct BasicREPL <: AbstractREPL
703
    terminal::TextTerminal
704
    waserror::Bool
705
    frontend_task::Task
UNCOV
706
    BasicREPL(t) = new(t, false)
×
707
end
708

UNCOV
709
outstream(r::BasicREPL) = r.terminal
×
710
hascolor(r::BasicREPL) = hascolor(r.terminal)
×
711

UNCOV
712
function run_frontend(repl::BasicREPL, backend::REPLBackendRef)
×
UNCOV
713
    repl.frontend_task = current_task()
×
UNCOV
714
    d = REPLDisplay(repl)
×
UNCOV
715
    dopushdisplay = !in(d,Base.Multimedia.displays)
×
UNCOV
716
    dopushdisplay && pushdisplay(d)
×
UNCOV
717
    hit_eof = false
×
UNCOV
718
    while true
×
UNCOV
719
        Base.reseteof(repl.terminal)
×
UNCOV
720
        write(repl.terminal, JULIA_PROMPT)
×
UNCOV
721
        line = ""
×
UNCOV
722
        ast = nothing
×
UNCOV
723
        interrupted = false
×
UNCOV
724
        while true
×
UNCOV
725
            try
×
UNCOV
726
                line *= readline(repl.terminal, keep=true)
×
727
            catch e
728
                if isa(e,InterruptException)
×
729
                    try # raise the debugger if present
×
730
                        ccall(:jl_raise_debugger, Int, ())
×
731
                    catch
×
732
                    end
733
                    line = ""
×
734
                    interrupted = true
×
735
                    break
×
736
                elseif isa(e,EOFError)
×
737
                    hit_eof = true
×
738
                    break
×
739
                else
740
                    rethrow()
×
741
                end
742
            end
UNCOV
743
            ast = Base.parse_input_line(line)
×
UNCOV
744
            (isa(ast,Expr) && ast.head === :incomplete) || break
×
745
        end
×
UNCOV
746
        if !isempty(line)
×
UNCOV
747
            response = eval_with_backend(ast, backend)
×
UNCOV
748
            print_response(repl, response, !ends_with_semicolon(line), false)
×
749
        end
UNCOV
750
        write(repl.terminal, '\n')
×
UNCOV
751
        ((!interrupted && isempty(line)) || hit_eof) && break
×
UNCOV
752
    end
×
753
    # terminate backend
UNCOV
754
    put!(backend.repl_channel, (nothing, -1))
×
UNCOV
755
    dopushdisplay && popdisplay(d)
×
UNCOV
756
    nothing
×
757
end
758

759
## LineEditREPL ##
760

761
mutable struct LineEditREPL <: AbstractREPL
762
    t::TextTerminal
763
    hascolor::Bool
764
    prompt_color::String
765
    input_color::String
766
    answer_color::String
767
    shell_color::String
768
    help_color::String
769
    pkg_color::String
770
    history_file::Bool
771
    in_shell::Bool
772
    in_help::Bool
773
    envcolors::Bool
774
    waserror::Bool
775
    specialdisplay::Union{Nothing,AbstractDisplay}
776
    options::Options
777
    mistate::Union{MIState,Nothing}
778
    last_shown_line_infos::Vector{Tuple{String,Int}}
779
    interface::ModalInterface
780
    backendref::REPLBackendRef
781
    frontend_task::Task
UNCOV
782
    function LineEditREPL(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,pkg_color,history_file,in_shell,in_help,envcolors)
×
UNCOV
783
        opts = Options()
×
UNCOV
784
        opts.hascolor = hascolor
×
UNCOV
785
        if !hascolor
×
786
            opts.beep_colors = [""]
×
787
        end
UNCOV
788
        new(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,pkg_color,history_file,in_shell,
×
789
            in_help,envcolors,false,nothing, opts, nothing, Tuple{String,Int}[])
790
    end
791
end
UNCOV
792
outstream(r::LineEditREPL) = (t = r.t; t isa TTYTerminal ? t.out_stream : t)
×
UNCOV
793
specialdisplay(r::LineEditREPL) = r.specialdisplay
×
794
specialdisplay(r::AbstractREPL) = nothing
×
UNCOV
795
terminal(r::LineEditREPL) = r.t
×
UNCOV
796
hascolor(r::LineEditREPL) = r.hascolor
×
797

UNCOV
798
LineEditREPL(t::TextTerminal, hascolor::Bool, envcolors::Bool=false) =
×
799
    LineEditREPL(t, hascolor,
800
        hascolor ? Base.text_colors[:green] : "",
801
        hascolor ? Base.input_color() : "",
802
        hascolor ? Base.answer_color() : "",
803
        hascolor ? Base.text_colors[:red] : "",
804
        hascolor ? Base.text_colors[:yellow] : "",
805
        hascolor ? Base.text_colors[:blue] : "",
806
        false, false, false, envcolors
807
    )
808

809
mutable struct REPLCompletionProvider <: CompletionProvider
810
    modifiers::LineEdit.Modifiers
811
end
UNCOV
812
REPLCompletionProvider() = REPLCompletionProvider(LineEdit.Modifiers())
×
813

814
mutable struct ShellCompletionProvider <: CompletionProvider end
815
struct LatexCompletions <: CompletionProvider end
816

UNCOV
817
Base.active_module((; mistate)::LineEditREPL) = mistate === nothing ? Main : mistate.active_module
×
818
Base.active_module(::AbstractREPL) = Main
×
UNCOV
819
Base.active_module(d::REPLDisplay) = Base.active_module(d.repl)
×
820

821
setmodifiers!(c::CompletionProvider, m::LineEdit.Modifiers) = nothing
×
822

823
setmodifiers!(c::REPLCompletionProvider, m::LineEdit.Modifiers) = c.modifiers = m
×
824

825
"""
826
    activate(mod::Module=Main)
827

828
Set `mod` as the default contextual module in the REPL,
829
both for evaluating expressions and printing them.
830
"""
831
function activate(mod::Module=Main; interactive_utils::Bool=true)
×
832
    mistate = (Base.active_repl::LineEditREPL).mistate
×
833
    mistate === nothing && return nothing
×
834
    mistate.active_module = mod
×
835
    interactive_utils && Base.load_InteractiveUtils(mod)
×
836
    return nothing
×
837
end
838

UNCOV
839
beforecursor(buf::IOBuffer) = String(buf.data[1:buf.ptr-1])
×
840

UNCOV
841
function complete_line(c::REPLCompletionProvider, s::PromptState, mod::Module; hint::Bool=false)
×
UNCOV
842
    partial = beforecursor(s.input_buffer)
×
UNCOV
843
    full = LineEdit.input_string(s)
×
UNCOV
844
    ret, range, should_complete = completions(full, lastindex(partial), mod, c.modifiers.shift, hint)
×
UNCOV
845
    c.modifiers = LineEdit.Modifiers()
×
UNCOV
846
    return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), partial[range], should_complete
×
847
end
848

UNCOV
849
function complete_line(c::ShellCompletionProvider, s::PromptState; hint::Bool=false)
×
850
    # First parse everything up to the current position
UNCOV
851
    partial = beforecursor(s.input_buffer)
×
UNCOV
852
    full = LineEdit.input_string(s)
×
UNCOV
853
    ret, range, should_complete = shell_completions(full, lastindex(partial), hint)
×
UNCOV
854
    return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), partial[range], should_complete
×
855
end
856

857
function complete_line(c::LatexCompletions, s; hint::Bool=false)
×
858
    partial = beforecursor(LineEdit.buffer(s))
×
859
    full = LineEdit.input_string(s)::String
×
860
    ret, range, should_complete = bslash_completions(full, lastindex(partial), hint)[2]
×
861
    return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), partial[range], should_complete
×
862
end
863

UNCOV
864
with_repl_linfo(f, repl) = f(outstream(repl))
×
UNCOV
865
function with_repl_linfo(f, repl::LineEditREPL)
×
UNCOV
866
    linfos = Tuple{String,Int}[]
×
UNCOV
867
    io = IOContext(outstream(repl), :last_shown_line_infos => linfos)
×
UNCOV
868
    f(io)
×
UNCOV
869
    if !isempty(linfos)
×
UNCOV
870
        repl.last_shown_line_infos = linfos
×
871
    end
UNCOV
872
    nothing
×
873
end
874

875
mutable struct REPLHistoryProvider <: HistoryProvider
876
    history::Vector{String}
877
    file_path::String
878
    history_file::Union{Nothing,IO}
879
    start_idx::Int
880
    cur_idx::Int
881
    last_idx::Int
882
    last_buffer::IOBuffer
883
    last_mode::Union{Nothing,Prompt}
884
    mode_mapping::Dict{Symbol,Prompt}
885
    modes::Vector{Symbol}
886
end
UNCOV
887
REPLHistoryProvider(mode_mapping::Dict{Symbol}) =
×
888
    REPLHistoryProvider(String[], "", nothing, 0, 0, -1, IOBuffer(),
889
                        nothing, mode_mapping, UInt8[])
890

891
invalid_history_message(path::String) = """
×
892
Invalid history file ($path) format:
893
If you have a history file left over from an older version of Julia,
894
try renaming or deleting it.
895
Invalid character: """
896

897
munged_history_message(path::String) = """
×
898
Invalid history file ($path) format:
899
An editor may have converted tabs to spaces at line """
900

UNCOV
901
function hist_open_file(hp::REPLHistoryProvider)
×
UNCOV
902
    f = open(hp.file_path, read=true, write=true, create=true)
×
UNCOV
903
    hp.history_file = f
×
UNCOV
904
    seekend(f)
×
905
end
906

UNCOV
907
function hist_from_file(hp::REPLHistoryProvider, path::String)
×
UNCOV
908
    getline(lines, i) = i > length(lines) ? "" : lines[i]
×
UNCOV
909
    file_lines = readlines(path)
×
UNCOV
910
    countlines = 0
×
UNCOV
911
    while true
×
912
        # First parse the metadata that starts with '#' in particular the REPL mode
UNCOV
913
        countlines += 1
×
UNCOV
914
        line = getline(file_lines, countlines)
×
UNCOV
915
        mode = :julia
×
UNCOV
916
        isempty(line) && break
×
UNCOV
917
        line[1] != '#' &&
×
918
            error(invalid_history_message(path), repr(line[1]), " at line ", countlines)
UNCOV
919
        while !isempty(line)
×
UNCOV
920
            startswith(line, '#') || break
×
UNCOV
921
            if startswith(line, "# mode: ")
×
UNCOV
922
                mode = Symbol(SubString(line, 9))
×
923
            end
UNCOV
924
            countlines += 1
×
UNCOV
925
            line = getline(file_lines, countlines)
×
UNCOV
926
        end
×
UNCOV
927
        isempty(line) && break
×
928

929
        # Now parse the code for the current REPL mode
UNCOV
930
        line[1] == ' '  &&
×
931
            error(munged_history_message(path), countlines)
UNCOV
932
        line[1] != '\t' &&
×
933
            error(invalid_history_message(path), repr(line[1]), " at line ", countlines)
UNCOV
934
        lines = String[]
×
UNCOV
935
        while !isempty(line)
×
UNCOV
936
            push!(lines, chomp(SubString(line, 2)))
×
UNCOV
937
            next_line = getline(file_lines, countlines+1)
×
UNCOV
938
            isempty(next_line) && break
×
UNCOV
939
            first(next_line) == ' '  && error(munged_history_message(path), countlines)
×
940
            # A line not starting with a tab means we are done with code for this entry
UNCOV
941
            first(next_line) != '\t' && break
×
942
            countlines += 1
×
943
            line = getline(file_lines, countlines)
×
944
        end
×
UNCOV
945
        push!(hp.modes, mode)
×
UNCOV
946
        push!(hp.history, join(lines, '\n'))
×
UNCOV
947
    end
×
UNCOV
948
    hp.start_idx = length(hp.history)
×
UNCOV
949
    return hp
×
950
end
951

UNCOV
952
function add_history(hist::REPLHistoryProvider, s::PromptState)
×
UNCOV
953
    str = rstrip(String(take!(copy(s.input_buffer))))
×
UNCOV
954
    isempty(strip(str)) && return
×
UNCOV
955
    mode = mode_idx(hist, LineEdit.mode(s))
×
UNCOV
956
    !isempty(hist.history) &&
×
957
        isequal(mode, hist.modes[end]) && str == hist.history[end] && return
UNCOV
958
    push!(hist.modes, mode)
×
UNCOV
959
    push!(hist.history, str)
×
UNCOV
960
    hist.history_file === nothing && return
×
UNCOV
961
    entry = """
×
962
    # time: $(Libc.strftime("%Y-%m-%d %H:%M:%S %Z", time()))
963
    # mode: $mode
UNCOV
964
    $(replace(str, r"^"ms => "\t"))
×
965
    """
966
    # TODO: write-lock history file
UNCOV
967
    try
×
UNCOV
968
        seekend(hist.history_file)
×
969
    catch err
970
        (err isa SystemError) || rethrow()
×
971
        # File handle might get stale after a while, especially under network file systems
972
        # If this doesn't fix it (e.g. when file is deleted), we'll end up rethrowing anyway
973
        hist_open_file(hist)
×
974
    end
UNCOV
975
    print(hist.history_file, entry)
×
UNCOV
976
    flush(hist.history_file)
×
UNCOV
977
    nothing
×
978
end
979

UNCOV
980
function history_move(s::Union{LineEdit.MIState,LineEdit.PrefixSearchState}, hist::REPLHistoryProvider, idx::Int, save_idx::Int = hist.cur_idx)
×
UNCOV
981
    max_idx = length(hist.history) + 1
×
UNCOV
982
    @assert 1 <= hist.cur_idx <= max_idx
×
UNCOV
983
    (1 <= idx <= max_idx) || return :none
×
UNCOV
984
    idx != hist.cur_idx || return :none
×
985

986
    # save the current line
UNCOV
987
    if save_idx == max_idx
×
UNCOV
988
        hist.last_mode = LineEdit.mode(s)
×
UNCOV
989
        hist.last_buffer = copy(LineEdit.buffer(s))
×
990
    else
UNCOV
991
        hist.history[save_idx] = LineEdit.input_string(s)
×
UNCOV
992
        hist.modes[save_idx] = mode_idx(hist, LineEdit.mode(s))
×
993
    end
994

995
    # load the saved line
UNCOV
996
    if idx == max_idx
×
UNCOV
997
        last_buffer = hist.last_buffer
×
UNCOV
998
        LineEdit.transition(s, hist.last_mode) do
×
UNCOV
999
            LineEdit.replace_line(s, last_buffer)
×
1000
        end
UNCOV
1001
        hist.last_mode = nothing
×
UNCOV
1002
        hist.last_buffer = IOBuffer()
×
1003
    else
UNCOV
1004
        if haskey(hist.mode_mapping, hist.modes[idx])
×
UNCOV
1005
            LineEdit.transition(s, hist.mode_mapping[hist.modes[idx]]) do
×
UNCOV
1006
                LineEdit.replace_line(s, hist.history[idx])
×
1007
            end
1008
        else
UNCOV
1009
            return :skip
×
1010
        end
1011
    end
UNCOV
1012
    hist.cur_idx = idx
×
1013

UNCOV
1014
    return :ok
×
1015
end
1016

1017
# REPL History can also transitions modes
UNCOV
1018
function LineEdit.accept_result_newmode(hist::REPLHistoryProvider)
×
UNCOV
1019
    if 1 <= hist.cur_idx <= length(hist.modes)
×
UNCOV
1020
        return hist.mode_mapping[hist.modes[hist.cur_idx]]
×
1021
    end
UNCOV
1022
    return nothing
×
1023
end
1024

UNCOV
1025
function history_prev(s::LineEdit.MIState, hist::REPLHistoryProvider,
×
1026
                      num::Int=1, save_idx::Int = hist.cur_idx)
UNCOV
1027
    num <= 0 && return history_next(s, hist, -num, save_idx)
×
UNCOV
1028
    hist.last_idx = -1
×
UNCOV
1029
    m = history_move(s, hist, hist.cur_idx-num, save_idx)
×
UNCOV
1030
    if m === :ok
×
UNCOV
1031
        LineEdit.move_input_start(s)
×
UNCOV
1032
        LineEdit.reset_key_repeats(s) do
×
UNCOV
1033
            LineEdit.move_line_end(s)
×
1034
        end
UNCOV
1035
        return LineEdit.refresh_line(s)
×
UNCOV
1036
    elseif m === :skip
×
UNCOV
1037
        return history_prev(s, hist, num+1, save_idx)
×
1038
    else
1039
        return Terminals.beep(s)
×
1040
    end
1041
end
1042

UNCOV
1043
function history_next(s::LineEdit.MIState, hist::REPLHistoryProvider,
×
1044
                      num::Int=1, save_idx::Int = hist.cur_idx)
UNCOV
1045
    if num == 0
×
1046
        Terminals.beep(s)
×
1047
        return
×
1048
    end
UNCOV
1049
    num < 0 && return history_prev(s, hist, -num, save_idx)
×
UNCOV
1050
    cur_idx = hist.cur_idx
×
UNCOV
1051
    max_idx = length(hist.history) + 1
×
UNCOV
1052
    if cur_idx == max_idx && 0 < hist.last_idx
×
1053
        # issue #6312
1054
        cur_idx = hist.last_idx
×
1055
        hist.last_idx = -1
×
1056
    end
UNCOV
1057
    m = history_move(s, hist, cur_idx+num, save_idx)
×
UNCOV
1058
    if m === :ok
×
UNCOV
1059
        LineEdit.move_input_end(s)
×
UNCOV
1060
        return LineEdit.refresh_line(s)
×
UNCOV
1061
    elseif m === :skip
×
UNCOV
1062
        return history_next(s, hist, num+1, save_idx)
×
1063
    else
UNCOV
1064
        return Terminals.beep(s)
×
1065
    end
1066
end
1067

UNCOV
1068
history_first(s::LineEdit.MIState, hist::REPLHistoryProvider) =
×
1069
    history_prev(s, hist, hist.cur_idx - 1 -
1070
                 (hist.cur_idx > hist.start_idx+1 ? hist.start_idx : 0))
1071

UNCOV
1072
history_last(s::LineEdit.MIState, hist::REPLHistoryProvider) =
×
1073
    history_next(s, hist, length(hist.history) - hist.cur_idx + 1)
1074

UNCOV
1075
function history_move_prefix(s::LineEdit.PrefixSearchState,
×
1076
                             hist::REPLHistoryProvider,
1077
                             prefix::AbstractString,
1078
                             backwards::Bool,
1079
                             cur_idx::Int = hist.cur_idx)
UNCOV
1080
    cur_response = String(take!(copy(LineEdit.buffer(s))))
×
1081
    # when searching forward, start at last_idx
UNCOV
1082
    if !backwards && hist.last_idx > 0
×
UNCOV
1083
        cur_idx = hist.last_idx
×
1084
    end
UNCOV
1085
    hist.last_idx = -1
×
UNCOV
1086
    max_idx = length(hist.history)+1
×
UNCOV
1087
    idxs = backwards ? ((cur_idx-1):-1:1) : ((cur_idx+1):1:max_idx)
×
UNCOV
1088
    for idx in idxs
×
UNCOV
1089
        if (idx == max_idx) || (startswith(hist.history[idx], prefix) && (hist.history[idx] != cur_response || get(hist.mode_mapping, hist.modes[idx], nothing) !== LineEdit.mode(s)))
×
UNCOV
1090
            m = history_move(s, hist, idx)
×
UNCOV
1091
            if m === :ok
×
UNCOV
1092
                if idx == max_idx
×
1093
                    # on resuming the in-progress edit, leave the cursor where the user last had it
UNCOV
1094
                elseif isempty(prefix)
×
1095
                    # on empty prefix search, move cursor to the end
UNCOV
1096
                    LineEdit.move_input_end(s)
×
1097
                else
1098
                    # otherwise, keep cursor at the prefix position as a visual cue
UNCOV
1099
                    seek(LineEdit.buffer(s), sizeof(prefix))
×
1100
                end
UNCOV
1101
                LineEdit.refresh_line(s)
×
UNCOV
1102
                return :ok
×
UNCOV
1103
            elseif m === :skip
×
UNCOV
1104
                return history_move_prefix(s,hist,prefix,backwards,idx)
×
1105
            end
1106
        end
UNCOV
1107
    end
×
UNCOV
1108
    Terminals.beep(s)
×
UNCOV
1109
    nothing
×
1110
end
UNCOV
1111
history_next_prefix(s::LineEdit.PrefixSearchState, hist::REPLHistoryProvider, prefix::AbstractString) =
×
1112
    history_move_prefix(s, hist, prefix, false)
UNCOV
1113
history_prev_prefix(s::LineEdit.PrefixSearchState, hist::REPLHistoryProvider, prefix::AbstractString) =
×
1114
    history_move_prefix(s, hist, prefix, true)
1115

UNCOV
1116
function history_search(hist::REPLHistoryProvider, query_buffer::IOBuffer, response_buffer::IOBuffer,
×
1117
                        backwards::Bool=false, skip_current::Bool=false)
1118

UNCOV
1119
    qpos = position(query_buffer)
×
UNCOV
1120
    qpos > 0 || return true
×
UNCOV
1121
    searchdata = beforecursor(query_buffer)
×
UNCOV
1122
    response_str = String(take!(copy(response_buffer)))
×
1123

1124
    # Alright, first try to see if the current match still works
UNCOV
1125
    a = position(response_buffer) + 1 # position is zero-indexed
×
1126
    # FIXME: I'm pretty sure this is broken since it uses an index
1127
    # into the search data to index into the response string
UNCOV
1128
    b = a + sizeof(searchdata)
×
UNCOV
1129
    b = b ≤ ncodeunits(response_str) ? prevind(response_str, b) : b-1
×
UNCOV
1130
    b = min(lastindex(response_str), b) # ensure that b is valid
×
1131

UNCOV
1132
    searchstart = backwards ? b : a
×
UNCOV
1133
    if searchdata == response_str[a:b]
×
UNCOV
1134
        if skip_current
×
UNCOV
1135
            searchstart = backwards ? prevind(response_str, b) : nextind(response_str, a)
×
1136
        else
UNCOV
1137
            return true
×
1138
        end
1139
    end
1140

1141
    # Start searching
1142
    # First the current response buffer
UNCOV
1143
    if 1 <= searchstart <= lastindex(response_str)
×
UNCOV
1144
        match = backwards ? findprev(searchdata, response_str, searchstart) :
×
1145
                            findnext(searchdata, response_str, searchstart)
UNCOV
1146
        if match !== nothing
×
UNCOV
1147
            seek(response_buffer, first(match) - 1)
×
UNCOV
1148
            return true
×
1149
        end
1150
    end
1151

1152
    # Now search all the other buffers
UNCOV
1153
    idxs = backwards ? ((hist.cur_idx-1):-1:1) : ((hist.cur_idx+1):1:length(hist.history))
×
UNCOV
1154
    for idx in idxs
×
UNCOV
1155
        h = hist.history[idx]
×
UNCOV
1156
        match = backwards ? findlast(searchdata, h) : findfirst(searchdata, h)
×
UNCOV
1157
        if match !== nothing && h != response_str && haskey(hist.mode_mapping, hist.modes[idx])
×
UNCOV
1158
            truncate(response_buffer, 0)
×
UNCOV
1159
            write(response_buffer, h)
×
UNCOV
1160
            seek(response_buffer, first(match) - 1)
×
UNCOV
1161
            hist.cur_idx = idx
×
UNCOV
1162
            return true
×
1163
        end
UNCOV
1164
    end
×
1165

UNCOV
1166
    return false
×
1167
end
1168

UNCOV
1169
function history_reset_state(hist::REPLHistoryProvider)
×
UNCOV
1170
    if hist.cur_idx != length(hist.history) + 1
×
UNCOV
1171
        hist.last_idx = hist.cur_idx
×
UNCOV
1172
        hist.cur_idx = length(hist.history) + 1
×
1173
    end
1174
    nothing
×
1175
end
UNCOV
1176
LineEdit.reset_state(hist::REPLHistoryProvider) = history_reset_state(hist)
×
1177

UNCOV
1178
function return_callback(s)
×
UNCOV
1179
    ast = Base.parse_input_line(String(take!(copy(LineEdit.buffer(s)))), depwarn=false)
×
UNCOV
1180
    return !(isa(ast, Expr) && ast.head === :incomplete)
×
1181
end
1182

UNCOV
1183
find_hist_file() = get(ENV, "JULIA_HISTORY",
×
1184
                       !isempty(DEPOT_PATH) ? joinpath(DEPOT_PATH[1], "logs", "repl_history.jl") :
1185
                       error("DEPOT_PATH is empty and ENV[\"JULIA_HISTORY\"] not set."))
1186

UNCOV
1187
backend(r::AbstractREPL) = r.backendref
×
1188

UNCOV
1189
function eval_with_backend(ast, backend::REPLBackendRef)
×
UNCOV
1190
    put!(backend.repl_channel, (ast, 1))
×
UNCOV
1191
    return take!(backend.response_channel) # (val, iserr)
×
1192
end
1193

UNCOV
1194
function respond(f, repl, main; pass_empty::Bool = false, suppress_on_semicolon::Bool = true)
×
UNCOV
1195
    return function do_respond(s::MIState, buf, ok::Bool)
×
UNCOV
1196
        if !ok
×
UNCOV
1197
            return transition(s, :abort)
×
1198
        end
UNCOV
1199
        line = String(take!(buf)::Vector{UInt8})
×
UNCOV
1200
        if !isempty(line) || pass_empty
×
UNCOV
1201
            reset(repl)
×
UNCOV
1202
            local response
×
UNCOV
1203
            try
×
UNCOV
1204
                ast = Base.invokelatest(f, line)
×
UNCOV
1205
                response = eval_with_backend(ast, backend(repl))
×
1206
            catch
UNCOV
1207
                response = Pair{Any, Bool}(current_exceptions(), true)
×
1208
            end
UNCOV
1209
            hide_output = suppress_on_semicolon && ends_with_semicolon(line)
×
UNCOV
1210
            print_response(repl, response, !hide_output, hascolor(repl))
×
1211
        end
UNCOV
1212
        prepare_next(repl)
×
UNCOV
1213
        reset_state(s)
×
UNCOV
1214
        return s.current_mode.sticky ? true : transition(s, main)
×
1215
    end
1216
end
1217

UNCOV
1218
function reset(repl::LineEditREPL)
×
UNCOV
1219
    raw!(repl.t, false)
×
UNCOV
1220
    hascolor(repl) && print(repl.t, Base.text_colors[:normal])
×
UNCOV
1221
    nothing
×
1222
end
1223

UNCOV
1224
function prepare_next(repl::LineEditREPL)
×
UNCOV
1225
    println(terminal(repl))
×
1226
end
1227

UNCOV
1228
function mode_keymap(julia_prompt::Prompt)
×
UNCOV
1229
    AnyDict(
×
UNCOV
1230
    '\b' => function (s::MIState,o...)
×
UNCOV
1231
        if isempty(s) || position(LineEdit.buffer(s)) == 0
×
UNCOV
1232
            buf = copy(LineEdit.buffer(s))
×
UNCOV
1233
            transition(s, julia_prompt) do
×
UNCOV
1234
                LineEdit.state(s, julia_prompt).input_buffer = buf
×
1235
            end
1236
        else
1237
            LineEdit.edit_backspace(s)
×
1238
        end
1239
    end,
UNCOV
1240
    "^C" => function (s::MIState,o...)
×
UNCOV
1241
        LineEdit.move_input_end(s)
×
UNCOV
1242
        LineEdit.refresh_line(s)
×
UNCOV
1243
        print(LineEdit.terminal(s), "^C\n\n")
×
UNCOV
1244
        transition(s, julia_prompt)
×
UNCOV
1245
        transition(s, :reset)
×
UNCOV
1246
        LineEdit.refresh_line(s)
×
1247
    end)
1248
end
1249

UNCOV
1250
repl_filename(repl, hp::REPLHistoryProvider) = "REPL[$(max(length(hp.history)-hp.start_idx, 1))]"
×
1251
repl_filename(repl, hp) = "REPL"
×
1252

1253
const JL_PROMPT_PASTE = Ref(true)
1254
enable_promptpaste(v::Bool) = JL_PROMPT_PASTE[] = v
×
1255

UNCOV
1256
function contextual_prompt(repl::LineEditREPL, prompt::Union{String,Function})
×
UNCOV
1257
    function ()
×
UNCOV
1258
        mod = Base.active_module(repl)
×
UNCOV
1259
        prefix = mod == Main ? "" : string('(', mod, ") ")
×
UNCOV
1260
        pr = prompt isa String ? prompt : prompt()
×
UNCOV
1261
        prefix * pr
×
1262
    end
1263
end
1264

UNCOV
1265
setup_interface(
×
1266
    repl::LineEditREPL;
1267
    # those keyword arguments may be deprecated eventually in favor of the Options mechanism
1268
    hascolor::Bool = repl.options.hascolor,
1269
    extra_repl_keymap::Any = repl.options.extra_keymap
1270
) = setup_interface(repl, hascolor, extra_repl_keymap)
1271

1272

1273
# This non keyword method can be precompiled which is important
UNCOV
1274
function setup_interface(
×
1275
    repl::LineEditREPL,
1276
    hascolor::Bool,
1277
    extra_repl_keymap::Any, # Union{Dict,Vector{<:Dict}},
1278
)
1279
    # The precompile statement emitter has problem outputting valid syntax for the
1280
    # type of `Union{Dict,Vector{<:Dict}}` (see #28808).
1281
    # This function is however important to precompile for REPL startup time, therefore,
1282
    # make the type Any and just assert that we have the correct type below.
UNCOV
1283
    @assert extra_repl_keymap isa Union{Dict,Vector{<:Dict}}
×
1284

1285
    ###
1286
    #
1287
    # This function returns the main interface that describes the REPL
1288
    # functionality, it is called internally by functions that setup a
1289
    # Terminal-based REPL frontend.
1290
    #
1291
    # See run_frontend(repl::LineEditREPL, backend::REPLBackendRef)
1292
    # for usage
1293
    #
1294
    ###
1295

1296
    ###
1297
    # We setup the interface in two stages.
1298
    # First, we set up all components (prompt,rsearch,shell,help)
1299
    # Second, we create keymaps with appropriate transitions between them
1300
    #   and assign them to the components
1301
    #
1302
    ###
1303

1304
    ############################### Stage I ################################
1305

1306
    # This will provide completions for REPL and help mode
UNCOV
1307
    replc = REPLCompletionProvider()
×
1308

1309
    # Set up the main Julia prompt
UNCOV
1310
    julia_prompt = Prompt(contextual_prompt(repl, JULIA_PROMPT);
×
1311
        # Copy colors from the prompt object
1312
        prompt_prefix = hascolor ? repl.prompt_color : "",
1313
        prompt_suffix = hascolor ?
1314
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1315
        repl = repl,
1316
        complete = replc,
1317
        on_enter = return_callback)
1318

1319
    # Setup help mode
UNCOV
1320
    help_mode = Prompt(contextual_prompt(repl, HELP_PROMPT),
×
1321
        prompt_prefix = hascolor ? repl.help_color : "",
1322
        prompt_suffix = hascolor ?
1323
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1324
        repl = repl,
1325
        complete = replc,
1326
        # When we're done transform the entered line into a call to helpmode function
UNCOV
1327
        on_done = respond(line::String->helpmode(outstream(repl), line, repl.mistate.active_module),
×
1328
                          repl, julia_prompt, pass_empty=true, suppress_on_semicolon=false))
1329

1330

1331
    # Set up shell mode
UNCOV
1332
    shell_mode = Prompt(SHELL_PROMPT;
×
1333
        prompt_prefix = hascolor ? repl.shell_color : "",
1334
        prompt_suffix = hascolor ?
1335
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1336
        repl = repl,
1337
        complete = ShellCompletionProvider(),
1338
        # Transform "foo bar baz" into `foo bar baz` (shell quoting)
1339
        # and pass into Base.repl_cmd for processing (handles `ls` and `cd`
1340
        # special)
1341
        on_done = respond(repl, julia_prompt) do line
UNCOV
1342
            Expr(:call, :(Base.repl_cmd),
×
1343
                :(Base.cmd_gen($(Base.shell_parse(line::String)[1]))),
1344
                outstream(repl))
1345
        end,
1346
        sticky = true)
1347

1348
    # Set up dummy Pkg mode that will be replaced once Pkg is loaded
1349
    # use 6 dots to occupy the same space as the most likely "@v1.xx" env name
UNCOV
1350
    dummy_pkg_mode = Prompt(Pkg_promptf,
×
1351
        prompt_prefix = hascolor ? repl.pkg_color : "",
1352
        prompt_suffix = hascolor ?
1353
        (repl.envcolors ? Base.input_color : repl.input_color) : "",
1354
        repl = repl,
1355
        complete = LineEdit.EmptyCompletionProvider(),
UNCOV
1356
        on_done = respond(line->nothing, repl, julia_prompt),
×
UNCOV
1357
        on_enter = function (s::MIState)
×
1358
                # This is hit when the user tries to execute a command before the real Pkg mode has been
1359
                # switched to. Ok to do this even if Pkg is loading on the other task because of the loading lock.
UNCOV
1360
                REPLExt = load_pkg()
×
UNCOV
1361
                if REPLExt isa Module && isdefined(REPLExt, :PkgCompletionProvider)
×
UNCOV
1362
                    for mode in repl.interface.modes
×
UNCOV
1363
                        if mode isa LineEdit.Prompt && mode.complete isa REPLExt.PkgCompletionProvider
×
1364
                            # pkg mode
UNCOV
1365
                            buf = copy(LineEdit.buffer(s))
×
UNCOV
1366
                            transition(s, mode) do
×
UNCOV
1367
                                LineEdit.state(s, mode).input_buffer = buf
×
1368
                            end
1369
                        end
UNCOV
1370
                    end
×
1371
                end
UNCOV
1372
                return true
×
1373
            end,
1374
        sticky = true)
1375

1376

1377
    ################################# Stage II #############################
1378

1379
    # Setup history
1380
    # We will have a unified history for all REPL modes
UNCOV
1381
    hp = REPLHistoryProvider(Dict{Symbol,Prompt}(:julia => julia_prompt,
×
1382
                                                 :shell => shell_mode,
1383
                                                 :help  => help_mode,
1384
                                                 :pkg  => dummy_pkg_mode))
UNCOV
1385
    if repl.history_file
×
UNCOV
1386
        try
×
UNCOV
1387
            hist_path = find_hist_file()
×
UNCOV
1388
            mkpath(dirname(hist_path))
×
UNCOV
1389
            hp.file_path = hist_path
×
UNCOV
1390
            hist_open_file(hp)
×
UNCOV
1391
            finalizer(replc) do replc
×
UNCOV
1392
                close(hp.history_file)
×
1393
            end
UNCOV
1394
            hist_from_file(hp, hist_path)
×
1395
        catch
1396
            # use REPL.hascolor to avoid using the local variable with the same name
1397
            print_response(repl, Pair{Any, Bool}(current_exceptions(), true), true, REPL.hascolor(repl))
×
1398
            println(outstream(repl))
×
1399
            @info "Disabling history file for this session"
×
1400
            repl.history_file = false
×
1401
        end
1402
    end
UNCOV
1403
    history_reset_state(hp)
×
UNCOV
1404
    julia_prompt.hist = hp
×
UNCOV
1405
    shell_mode.hist = hp
×
UNCOV
1406
    help_mode.hist = hp
×
UNCOV
1407
    dummy_pkg_mode.hist = hp
×
1408

UNCOV
1409
    julia_prompt.on_done = respond(x->Base.parse_input_line(x,filename=repl_filename(repl,hp)), repl, julia_prompt)
×
1410

1411

UNCOV
1412
    search_prompt, skeymap = LineEdit.setup_search_keymap(hp)
×
UNCOV
1413
    search_prompt.complete = LatexCompletions()
×
1414

UNCOV
1415
    shell_prompt_len = length(SHELL_PROMPT)
×
UNCOV
1416
    help_prompt_len = length(HELP_PROMPT)
×
UNCOV
1417
    jl_prompt_regex = Regex("^In \\[[0-9]+\\]: |^(?:\\(.+\\) )?$JULIA_PROMPT")
×
UNCOV
1418
    pkg_prompt_regex = Regex("^(?:\\(.+\\) )?$PKG_PROMPT")
×
1419

1420
    # Canonicalize user keymap input
UNCOV
1421
    if isa(extra_repl_keymap, Dict)
×
1422
        extra_repl_keymap = AnyDict[extra_repl_keymap]
×
1423
    end
1424

UNCOV
1425
    repl_keymap = AnyDict(
×
UNCOV
1426
        ';' => function (s::MIState,o...)
×
UNCOV
1427
            if isempty(s) || position(LineEdit.buffer(s)) == 0
×
UNCOV
1428
                buf = copy(LineEdit.buffer(s))
×
UNCOV
1429
                transition(s, shell_mode) do
×
UNCOV
1430
                    LineEdit.state(s, shell_mode).input_buffer = buf
×
1431
                end
1432
            else
UNCOV
1433
                edit_insert(s, ';')
×
UNCOV
1434
                LineEdit.check_for_hint(s) && LineEdit.refresh_line(s)
×
1435
            end
1436
        end,
UNCOV
1437
        '?' => function (s::MIState,o...)
×
UNCOV
1438
            if isempty(s) || position(LineEdit.buffer(s)) == 0
×
UNCOV
1439
                buf = copy(LineEdit.buffer(s))
×
UNCOV
1440
                transition(s, help_mode) do
×
UNCOV
1441
                    LineEdit.state(s, help_mode).input_buffer = buf
×
1442
                end
1443
            else
1444
                edit_insert(s, '?')
×
1445
                LineEdit.check_for_hint(s) && LineEdit.refresh_line(s)
×
1446
            end
1447
        end,
UNCOV
1448
        ']' => function (s::MIState,o...)
×
UNCOV
1449
            if isempty(s) || position(LineEdit.buffer(s)) == 0
×
1450
                buf = copy(LineEdit.buffer(s))
×
1451
                transition(s, dummy_pkg_mode) do
×
UNCOV
1452
                    LineEdit.state(s, dummy_pkg_mode).input_buffer = buf
×
1453
                end
1454
                # load Pkg on another thread if available so that typing in the dummy Pkg prompt
1455
                # isn't blocked, but instruct the main REPL task to do the transition via s.async_channel
1456
                t_replswitch = Threads.@spawn begin
×
UNCOV
1457
                    REPLExt = load_pkg()
×
UNCOV
1458
                    if REPLExt isa Module && isdefined(REPLExt, :PkgCompletionProvider)
×
UNCOV
1459
                        put!(s.async_channel,
×
UNCOV
1460
                            function (s::MIState)
×
UNCOV
1461
                                LineEdit.mode(s) === dummy_pkg_mode || return :ok
×
UNCOV
1462
                                for mode in repl.interface.modes
×
UNCOV
1463
                                    if mode isa LineEdit.Prompt && mode.complete isa REPLExt.PkgCompletionProvider
×
UNCOV
1464
                                        buf = copy(LineEdit.buffer(s))
×
UNCOV
1465
                                        transition(s, mode) do
×
UNCOV
1466
                                            LineEdit.state(s, mode).input_buffer = buf
×
1467
                                        end
UNCOV
1468
                                        if !isempty(s) && @invokelatest(LineEdit.check_for_hint(s))
×
UNCOV
1469
                                            @invokelatest(LineEdit.refresh_line(s))
×
1470
                                        end
UNCOV
1471
                                        break
×
1472
                                    end
UNCOV
1473
                                end
×
UNCOV
1474
                                return :ok
×
1475
                            end
1476
                        )
1477
                    end
1478
                end
1479
                Base.errormonitor(t_replswitch)
×
1480
            else
UNCOV
1481
                edit_insert(s, ']')
×
UNCOV
1482
                LineEdit.check_for_hint(s) && LineEdit.refresh_line(s)
×
1483
            end
1484
        end,
1485

1486
        # Bracketed Paste Mode
UNCOV
1487
        "\e[200~" => (s::MIState,o...)->begin
×
UNCOV
1488
            input = LineEdit.bracketed_paste(s) # read directly from s until reaching the end-bracketed-paste marker
×
UNCOV
1489
            sbuffer = LineEdit.buffer(s)
×
UNCOV
1490
            curspos = position(sbuffer)
×
UNCOV
1491
            seek(sbuffer, 0)
×
UNCOV
1492
            shouldeval = (bytesavailable(sbuffer) == curspos && !occursin(UInt8('\n'), sbuffer))
×
UNCOV
1493
            seek(sbuffer, curspos)
×
UNCOV
1494
            if curspos == 0
×
1495
                # if pasting at the beginning, strip leading whitespace
UNCOV
1496
                input = lstrip(input)
×
1497
            end
UNCOV
1498
            if !shouldeval
×
1499
                # when pasting in the middle of input, just paste in place
1500
                # don't try to execute all the WIP, since that's rather confusing
1501
                # and is often ill-defined how it should behave
1502
                edit_insert(s, input)
×
1503
                return
×
1504
            end
UNCOV
1505
            LineEdit.push_undo(s)
×
UNCOV
1506
            edit_insert(sbuffer, input)
×
UNCOV
1507
            input = String(take!(sbuffer))
×
UNCOV
1508
            oldpos = firstindex(input)
×
UNCOV
1509
            firstline = true
×
UNCOV
1510
            isprompt_paste = false
×
UNCOV
1511
            curr_prompt_len = 0
×
UNCOV
1512
            pasting_help = false
×
1513

UNCOV
1514
            while oldpos <= lastindex(input) # loop until all lines have been executed
×
UNCOV
1515
                if JL_PROMPT_PASTE[]
×
1516
                    # Check if the next statement starts with a prompt i.e. "julia> ", in that case
1517
                    # skip it. But first skip whitespace unless pasting in a docstring which may have
1518
                    # indented prompt examples that we don't want to execute
UNCOV
1519
                    while input[oldpos] in (pasting_help ? ('\n') : ('\n', ' ', '\t'))
×
UNCOV
1520
                        oldpos = nextind(input, oldpos)
×
UNCOV
1521
                        oldpos >= sizeof(input) && return
×
UNCOV
1522
                    end
×
UNCOV
1523
                    substr = SubString(input, oldpos)
×
1524
                    # Check if input line starts with "julia> ", remove it if we are in prompt paste mode
UNCOV
1525
                    if (firstline || isprompt_paste) && startswith(substr, jl_prompt_regex)
×
UNCOV
1526
                        detected_jl_prompt = match(jl_prompt_regex, substr).match
×
UNCOV
1527
                        isprompt_paste = true
×
UNCOV
1528
                        curr_prompt_len = sizeof(detected_jl_prompt)
×
UNCOV
1529
                        oldpos += curr_prompt_len
×
UNCOV
1530
                        transition(s, julia_prompt)
×
UNCOV
1531
                        pasting_help = false
×
1532
                    # Check if input line starts with "pkg> " or "(...) pkg> ", remove it if we are in prompt paste mode and switch mode
UNCOV
1533
                    elseif (firstline || isprompt_paste) && startswith(substr, pkg_prompt_regex)
×
1534
                        detected_pkg_prompt = match(pkg_prompt_regex, substr).match
×
1535
                        isprompt_paste = true
×
1536
                        curr_prompt_len = sizeof(detected_pkg_prompt)
×
1537
                        oldpos += curr_prompt_len
×
1538
                        Base.active_repl.interface.modes[1].keymap_dict[']'](s, o...)
×
1539
                        pasting_help = false
×
1540
                    # Check if input line starts with "shell> ", remove it if we are in prompt paste mode and switch mode
UNCOV
1541
                    elseif (firstline || isprompt_paste) && startswith(substr, SHELL_PROMPT)
×
UNCOV
1542
                        isprompt_paste = true
×
UNCOV
1543
                        oldpos += shell_prompt_len
×
UNCOV
1544
                        curr_prompt_len = shell_prompt_len
×
UNCOV
1545
                        transition(s, shell_mode)
×
UNCOV
1546
                        pasting_help = false
×
1547
                    # Check if input line starts with "help?> ", remove it if we are in prompt paste mode and switch mode
UNCOV
1548
                    elseif (firstline || isprompt_paste) && startswith(substr, HELP_PROMPT)
×
UNCOV
1549
                        isprompt_paste = true
×
UNCOV
1550
                        oldpos += help_prompt_len
×
UNCOV
1551
                        curr_prompt_len = help_prompt_len
×
UNCOV
1552
                        transition(s, help_mode)
×
UNCOV
1553
                        pasting_help = true
×
1554
                    # If we are prompt pasting and current statement does not begin with a mode prefix, skip to next line
UNCOV
1555
                    elseif isprompt_paste
×
UNCOV
1556
                        while input[oldpos] != '\n'
×
UNCOV
1557
                            oldpos = nextind(input, oldpos)
×
UNCOV
1558
                            oldpos >= sizeof(input) && return
×
UNCOV
1559
                        end
×
UNCOV
1560
                        continue
×
1561
                    end
1562
                end
UNCOV
1563
                dump_tail = false
×
UNCOV
1564
                nl_pos = findfirst('\n', input[oldpos:end])
×
UNCOV
1565
                if s.current_mode == julia_prompt
×
UNCOV
1566
                    ast, pos = Meta.parse(input, oldpos, raise=false, depwarn=false)
×
UNCOV
1567
                    if (isa(ast, Expr) && (ast.head === :error || ast.head === :incomplete)) ||
×
1568
                            (pos > ncodeunits(input) && !endswith(input, '\n'))
1569
                        # remaining text is incomplete (an error, or parser ran to the end but didn't stop with a newline):
1570
                        # Insert all the remaining text as one line (might be empty)
UNCOV
1571
                        dump_tail = true
×
1572
                    end
UNCOV
1573
                elseif isnothing(nl_pos) # no newline at end, so just dump the tail into the prompt and don't execute
×
1574
                    dump_tail = true
×
UNCOV
1575
                elseif s.current_mode == shell_mode # handle multiline shell commands
×
UNCOV
1576
                    lines = split(input[oldpos:end], '\n')
×
UNCOV
1577
                    pos = oldpos + sizeof(lines[1]) + 1
×
UNCOV
1578
                    if length(lines) > 1
×
UNCOV
1579
                        for line in lines[2:end]
×
1580
                            # to be recognized as a multiline shell command, the lines must be indented to the
1581
                            # same prompt position
UNCOV
1582
                            if !startswith(line, ' '^curr_prompt_len)
×
UNCOV
1583
                                break
×
1584
                            end
UNCOV
1585
                            pos += sizeof(line) + 1
×
UNCOV
1586
                        end
×
1587
                    end
1588
                else
UNCOV
1589
                    pos = oldpos + nl_pos
×
1590
                end
UNCOV
1591
                if dump_tail
×
UNCOV
1592
                    tail = input[oldpos:end]
×
UNCOV
1593
                    if !firstline
×
1594
                        # strip leading whitespace, but only if it was the result of executing something
1595
                        # (avoids modifying the user's current leading wip line)
UNCOV
1596
                        tail = lstrip(tail)
×
1597
                    end
UNCOV
1598
                    if isprompt_paste # remove indentation spaces corresponding to the prompt
×
UNCOV
1599
                        tail = replace(tail, r"^"m * ' '^curr_prompt_len => "")
×
1600
                    end
UNCOV
1601
                    LineEdit.replace_line(s, tail, true)
×
UNCOV
1602
                    LineEdit.refresh_line(s)
×
UNCOV
1603
                    break
×
1604
                end
1605
                # get the line and strip leading and trailing whitespace
UNCOV
1606
                line = strip(input[oldpos:prevind(input, pos)])
×
UNCOV
1607
                if !isempty(line)
×
UNCOV
1608
                    if isprompt_paste # remove indentation spaces corresponding to the prompt
×
UNCOV
1609
                        line = replace(line, r"^"m * ' '^curr_prompt_len => "")
×
1610
                    end
1611
                    # put the line on the screen and history
UNCOV
1612
                    LineEdit.replace_line(s, line)
×
UNCOV
1613
                    LineEdit.commit_line(s)
×
1614
                    # execute the statement
UNCOV
1615
                    terminal = LineEdit.terminal(s) # This is slightly ugly but ok for now
×
UNCOV
1616
                    raw!(terminal, false) && disable_bracketed_paste(terminal)
×
UNCOV
1617
                    @invokelatest LineEdit.mode(s).on_done(s, LineEdit.buffer(s), true)
×
UNCOV
1618
                    raw!(terminal, true) && enable_bracketed_paste(terminal)
×
UNCOV
1619
                    LineEdit.push_undo(s) # when the last line is incomplete
×
1620
                end
UNCOV
1621
                oldpos = pos
×
UNCOV
1622
                firstline = false
×
UNCOV
1623
            end
×
1624
        end,
1625

1626
        # Open the editor at the location of a stackframe or method
1627
        # This is accessing a contextual variable that gets set in
1628
        # the show_backtrace and show_method_table functions.
UNCOV
1629
        "^Q" => (s::MIState, o...) -> begin
×
UNCOV
1630
            linfos = repl.last_shown_line_infos
×
UNCOV
1631
            str = String(take!(LineEdit.buffer(s)))
×
UNCOV
1632
            n = tryparse(Int, str)
×
UNCOV
1633
            n === nothing && @goto writeback
×
UNCOV
1634
            if n <= 0 || n > length(linfos) || startswith(linfos[n][1], "REPL[")
×
UNCOV
1635
                @goto writeback
×
1636
            end
UNCOV
1637
            try
×
UNCOV
1638
                InteractiveUtils.edit(Base.fixup_stdlib_path(linfos[n][1]), linfos[n][2])
×
1639
            catch ex
UNCOV
1640
                ex isa ProcessFailedException || ex isa Base.IOError || ex isa SystemError || rethrow()
×
UNCOV
1641
                @info "edit failed" _exception=ex
×
1642
            end
UNCOV
1643
            LineEdit.refresh_line(s)
×
UNCOV
1644
            return
×
UNCOV
1645
            @label writeback
×
UNCOV
1646
            write(LineEdit.buffer(s), str)
×
UNCOV
1647
            return
×
1648
        end,
1649
    )
1650

UNCOV
1651
    prefix_prompt, prefix_keymap = LineEdit.setup_prefix_keymap(hp, julia_prompt)
×
1652

UNCOV
1653
    a = Dict{Any,Any}[skeymap, repl_keymap, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
×
UNCOV
1654
    prepend!(a, extra_repl_keymap)
×
1655

UNCOV
1656
    julia_prompt.keymap_dict = LineEdit.keymap(a)
×
1657

UNCOV
1658
    mk = mode_keymap(julia_prompt)
×
1659

UNCOV
1660
    b = Dict{Any,Any}[skeymap, mk, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
×
UNCOV
1661
    prepend!(b, extra_repl_keymap)
×
1662

UNCOV
1663
    shell_mode.keymap_dict = help_mode.keymap_dict = dummy_pkg_mode.keymap_dict = LineEdit.keymap(b)
×
1664

UNCOV
1665
    allprompts = LineEdit.TextInterface[julia_prompt, shell_mode, help_mode, dummy_pkg_mode, search_prompt, prefix_prompt]
×
UNCOV
1666
    return ModalInterface(allprompts)
×
1667
end
1668

UNCOV
1669
function run_frontend(repl::LineEditREPL, backend::REPLBackendRef)
×
UNCOV
1670
    repl.frontend_task = current_task()
×
UNCOV
1671
    d = REPLDisplay(repl)
×
UNCOV
1672
    dopushdisplay = repl.specialdisplay === nothing && !in(d,Base.Multimedia.displays)
×
UNCOV
1673
    dopushdisplay && pushdisplay(d)
×
UNCOV
1674
    if !isdefined(repl,:interface)
×
UNCOV
1675
        interface = repl.interface = setup_interface(repl)
×
1676
    else
UNCOV
1677
        interface = repl.interface
×
1678
    end
UNCOV
1679
    repl.backendref = backend
×
UNCOV
1680
    repl.mistate = LineEdit.init_state(terminal(repl), interface)
×
UNCOV
1681
    run_interface(terminal(repl), interface, repl.mistate)
×
1682
    # Terminate Backend
UNCOV
1683
    put!(backend.repl_channel, (nothing, -1))
×
UNCOV
1684
    dopushdisplay && popdisplay(d)
×
UNCOV
1685
    nothing
×
1686
end
1687

1688
## StreamREPL ##
1689

1690
mutable struct StreamREPL <: AbstractREPL
1691
    stream::IO
1692
    prompt_color::String
1693
    input_color::String
1694
    answer_color::String
1695
    waserror::Bool
1696
    frontend_task::Task
1697
    StreamREPL(stream,pc,ic,ac) = new(stream,pc,ic,ac,false)
×
1698
end
1699
StreamREPL(stream::IO) = StreamREPL(stream, Base.text_colors[:green], Base.input_color(), Base.answer_color())
×
1700
run_repl(stream::IO) = run_repl(StreamREPL(stream))
×
1701

1702
outstream(s::StreamREPL) = s.stream
×
1703
hascolor(s::StreamREPL) = get(s.stream, :color, false)::Bool
×
1704

1705
answer_color(r::LineEditREPL) = r.envcolors ? Base.answer_color() : r.answer_color
×
1706
answer_color(r::StreamREPL) = r.answer_color
×
1707
input_color(r::LineEditREPL) = r.envcolors ? Base.input_color() : r.input_color
×
1708
input_color(r::StreamREPL) = r.input_color
×
1709

1710
let matchend = Dict("\"" => r"\"", "\"\"\"" => r"\"\"\"", "'" => r"'",
1711
    "`" => r"`", "```" => r"```", "#" => r"$"m, "#=" => r"=#|#=")
1712
    global _rm_strings_and_comments
UNCOV
1713
    function _rm_strings_and_comments(code::Union{String,SubString{String}})
×
UNCOV
1714
        buf = IOBuffer(sizehint = sizeof(code))
×
UNCOV
1715
        pos = 1
×
UNCOV
1716
        while true
×
UNCOV
1717
            i = findnext(r"\"(?!\"\")|\"\"\"|'|`(?!``)|```|#(?!=)|#=", code, pos)
×
UNCOV
1718
            isnothing(i) && break
×
UNCOV
1719
            match = SubString(code, i)
×
UNCOV
1720
            j = findnext(matchend[match]::Regex, code, nextind(code, last(i)))
×
UNCOV
1721
            if match == "#=" # possibly nested
×
UNCOV
1722
                nested = 1
×
UNCOV
1723
                while j !== nothing
×
UNCOV
1724
                    nested += SubString(code, j) == "#=" ? +1 : -1
×
UNCOV
1725
                    iszero(nested) && break
×
UNCOV
1726
                    j = findnext(r"=#|#=", code, nextind(code, last(j)))
×
UNCOV
1727
                end
×
UNCOV
1728
            elseif match[1] != '#' # quote match: check non-escaped
×
UNCOV
1729
                while j !== nothing
×
UNCOV
1730
                    notbackslash = findprev(!=('\\'), code, prevind(code, first(j)))::Int
×
UNCOV
1731
                    isodd(first(j) - notbackslash) && break # not escaped
×
UNCOV
1732
                    j = findnext(matchend[match]::Regex, code, nextind(code, first(j)))
×
UNCOV
1733
                end
×
1734
            end
UNCOV
1735
            isnothing(j) && break
×
UNCOV
1736
            if match[1] == '#'
×
UNCOV
1737
                print(buf, SubString(code, pos, prevind(code, first(i))))
×
1738
            else
UNCOV
1739
                print(buf, SubString(code, pos, last(i)), ' ', SubString(code, j))
×
1740
            end
UNCOV
1741
            pos = nextind(code, last(j))
×
UNCOV
1742
        end
×
UNCOV
1743
        print(buf, SubString(code, pos, lastindex(code)))
×
UNCOV
1744
        return String(take!(buf))
×
1745
    end
1746
end
1747

1748
# heuristic function to decide if the presence of a semicolon
1749
# at the end of the expression was intended for suppressing output
1750
ends_with_semicolon(code::AbstractString) = ends_with_semicolon(String(code))
×
UNCOV
1751
ends_with_semicolon(code::Union{String,SubString{String}}) =
×
UNCOV
1752
    contains(_rm_strings_and_comments(code), r";\s*$")
×
1753

UNCOV
1754
function banner(io::IO = stdout; short = false)
×
UNCOV
1755
    if Base.GIT_VERSION_INFO.tagged_commit
×
1756
        commit_string = Base.TAGGED_RELEASE_BANNER
×
UNCOV
1757
    elseif isempty(Base.GIT_VERSION_INFO.commit)
×
1758
        commit_string = ""
×
1759
    else
UNCOV
1760
        days = Int(floor((ccall(:jl_clock_now, Float64, ()) - Base.GIT_VERSION_INFO.fork_master_timestamp) / (60 * 60 * 24)))
×
UNCOV
1761
        days = max(0, days)
×
UNCOV
1762
        unit = days == 1 ? "day" : "days"
×
UNCOV
1763
        distance = Base.GIT_VERSION_INFO.fork_master_distance
×
UNCOV
1764
        commit = Base.GIT_VERSION_INFO.commit_short
×
1765

UNCOV
1766
        if distance == 0
×
UNCOV
1767
            commit_string = "Commit $(commit) ($(days) $(unit) old master)"
×
1768
        else
1769
            branch = Base.GIT_VERSION_INFO.branch
×
1770
            commit_string = "$(branch)/$(commit) (fork: $(distance) commits, $(days) $(unit))"
×
1771
        end
1772
    end
1773

UNCOV
1774
    commit_date = isempty(Base.GIT_VERSION_INFO.date_string) ? "" : " ($(split(Base.GIT_VERSION_INFO.date_string)[1]))"
×
1775

UNCOV
1776
    if get(io, :color, false)::Bool
×
1777
        c = Base.text_colors
×
1778
        tx = c[:normal] # text
×
1779
        jl = c[:normal] # julia
×
1780
        d1 = c[:bold] * c[:blue]    # first dot
×
1781
        d2 = c[:bold] * c[:red]     # second dot
×
1782
        d3 = c[:bold] * c[:green]   # third dot
×
1783
        d4 = c[:bold] * c[:magenta] # fourth dot
×
1784

1785
        if short
×
1786
            print(io,"""
×
1787
              $(d3)o$(tx)  | Version $(VERSION)$(commit_date)
1788
             $(d2)o$(tx) $(d4)o$(tx) | $(commit_string)
1789
            """)
1790
        else
1791
            print(io,"""               $(d3)_$(tx)
×
1792
               $(d1)_$(tx)       $(jl)_$(tx) $(d2)_$(d3)(_)$(d4)_$(tx)     |  Documentation: https://docs.julialang.org
1793
              $(d1)(_)$(jl)     | $(d2)(_)$(tx) $(d4)(_)$(tx)    |
1794
               $(jl)_ _   _| |_  __ _$(tx)   |  Type \"?\" for help, \"]?\" for Pkg help.
1795
              $(jl)| | | | | | |/ _` |$(tx)  |
1796
              $(jl)| | |_| | | | (_| |$(tx)  |  Version $(VERSION)$(commit_date)
1797
             $(jl)_/ |\\__'_|_|_|\\__'_|$(tx)  |  $(commit_string)
1798
            $(jl)|__/$(tx)                   |
1799

1800
            """)
1801
        end
1802
    else
UNCOV
1803
        if short
×
UNCOV
1804
            print(io,"""
×
1805
              o  |  Version $(VERSION)$(commit_date)
1806
             o o |  $(commit_string)
1807
            """)
1808
        else
UNCOV
1809
            print(io,"""
×
1810
                           _
1811
               _       _ _(_)_     |  Documentation: https://docs.julialang.org
1812
              (_)     | (_) (_)    |
1813
               _ _   _| |_  __ _   |  Type \"?\" for help, \"]?\" for Pkg help.
1814
              | | | | | | |/ _` |  |
1815
              | | |_| | | | (_| |  |  Version $(VERSION)$(commit_date)
1816
             _/ |\\__'_|_|_|\\__'_|  |  $(commit_string)
1817
            |__/                   |
1818

1819
            """)
1820
        end
1821
    end
1822
end
1823

1824
function run_frontend(repl::StreamREPL, backend::REPLBackendRef)
×
1825
    repl.frontend_task = current_task()
×
1826
    have_color = hascolor(repl)
×
1827
    banner(repl.stream)
×
1828
    d = REPLDisplay(repl)
×
1829
    dopushdisplay = !in(d,Base.Multimedia.displays)
×
1830
    dopushdisplay && pushdisplay(d)
×
1831
    while !eof(repl.stream)::Bool
×
1832
        if have_color
×
1833
            print(repl.stream,repl.prompt_color)
×
1834
        end
1835
        print(repl.stream, JULIA_PROMPT)
×
1836
        if have_color
×
1837
            print(repl.stream, input_color(repl))
×
1838
        end
1839
        line = readline(repl.stream, keep=true)
×
1840
        if !isempty(line)
×
1841
            ast = Base.parse_input_line(line)
×
1842
            if have_color
×
1843
                print(repl.stream, Base.color_normal)
×
1844
            end
1845
            response = eval_with_backend(ast, backend)
×
1846
            print_response(repl, response, !ends_with_semicolon(line), have_color)
×
1847
        end
1848
    end
×
1849
    # Terminate Backend
1850
    put!(backend.repl_channel, (nothing, -1))
×
1851
    dopushdisplay && popdisplay(d)
×
1852
    nothing
×
1853
end
1854

1855
module Numbered
1856

1857
using ..REPL
1858

1859
__current_ast_transforms() = Base.active_repl_backend !== nothing ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1860

UNCOV
1861
function repl_eval_counter(hp)
×
UNCOV
1862
    return length(hp.history) - hp.start_idx
×
1863
end
1864

UNCOV
1865
function out_transform(@nospecialize(x), n::Ref{Int})
×
UNCOV
1866
    return Expr(:toplevel, get_usings!([], x)..., quote
×
UNCOV
1867
        let __temp_val_a72df459 = $x
×
UNCOV
1868
            $capture_result($n, __temp_val_a72df459)
×
UNCOV
1869
            __temp_val_a72df459
×
1870
        end
1871
    end)
1872
end
1873

UNCOV
1874
function get_usings!(usings, ex)
×
UNCOV
1875
    ex isa Expr || return usings
×
1876
    # get all `using` and `import` statements which are at the top level
UNCOV
1877
    for (i, arg) in enumerate(ex.args)
×
UNCOV
1878
        if Base.isexpr(arg, :toplevel)
×
UNCOV
1879
            get_usings!(usings, arg)
×
UNCOV
1880
        elseif Base.isexpr(arg, [:using, :import])
×
UNCOV
1881
            push!(usings, popat!(ex.args, i))
×
1882
        end
UNCOV
1883
    end
×
UNCOV
1884
    return usings
×
1885
end
1886

NEW
1887
function create_global_out!(mod)
×
NEW
1888
    if !isdefinedglobal(mod, :Out)
×
NEW
1889
        out = Dict{Int, Any}()
×
NEW
1890
        @eval mod begin
×
NEW
1891
            const Out = $(out)
×
NEW
1892
            export Out
×
1893
        end
NEW
1894
        return out
×
1895
    end
NEW
1896
    return getglobal(mod, Out)
×
1897
end
1898

UNCOV
1899
function capture_result(n::Ref{Int}, @nospecialize(x))
×
UNCOV
1900
    n = n[]
×
UNCOV
1901
    mod = Base.MainInclude
×
1902
    # TODO: This invokelatest is only required due to backdated constants
1903
    # and should be removed after
NEW
1904
    out = isdefinedglobal(mod, :Out) ? invokelatest(getglobal, mod, :Out) : invokelatest(create_global_out!, mod)
×
NEW
1905
    if x !== out && x !== nothing # remove this?
×
NEW
1906
        out[n] = x
×
1907
    end
UNCOV
1908
    nothing
×
1909
end
1910

UNCOV
1911
function set_prompt(repl::LineEditREPL, n::Ref{Int})
×
UNCOV
1912
    julia_prompt = repl.interface.modes[1]
×
UNCOV
1913
    julia_prompt.prompt = function()
×
UNCOV
1914
        n[] = repl_eval_counter(julia_prompt.hist)+1
×
UNCOV
1915
        string("In [", n[], "]: ")
×
1916
    end
UNCOV
1917
    nothing
×
1918
end
1919

UNCOV
1920
function set_output_prefix(repl::LineEditREPL, n::Ref{Int})
×
UNCOV
1921
    julia_prompt = repl.interface.modes[1]
×
UNCOV
1922
    if REPL.hascolor(repl)
×
UNCOV
1923
        julia_prompt.output_prefix_prefix = Base.text_colors[:red]
×
1924
    end
UNCOV
1925
    julia_prompt.output_prefix = () -> string("Out[", n[], "]: ")
×
UNCOV
1926
    nothing
×
1927
end
1928

UNCOV
1929
function __current_ast_transforms(backend)
×
UNCOV
1930
    if backend === nothing
×
1931
        Base.active_repl_backend !== nothing ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1932
    else
UNCOV
1933
        backend.ast_transforms
×
1934
    end
1935
end
1936

UNCOV
1937
function numbered_prompt!(repl::LineEditREPL=Base.active_repl::LineEditREPL, backend=nothing)
×
UNCOV
1938
    n = Ref{Int}(0)
×
UNCOV
1939
    set_prompt(repl, n)
×
UNCOV
1940
    set_output_prefix(repl, n)
×
UNCOV
1941
    push!(__current_ast_transforms(backend), @nospecialize(ast) -> out_transform(ast, n))
×
UNCOV
1942
    return
×
1943
end
1944

1945
"""
1946
    Out[n]
1947

1948
A variable referring to all previously computed values, automatically imported to the interactive prompt.
1949
Only defined and exists while using [Numbered prompt](@ref Numbered-prompt).
1950

1951
See also [`ans`](@ref).
1952
"""
1953
Base.MainInclude.Out
1954

1955
end
1956

1957
import .Numbered.numbered_prompt!
1958

1959
# this assignment won't survive precompilation,
1960
# but will stick if REPL is baked into a sysimg.
1961
# Needs to occur after this module is finished.
1962
Base.REPL_MODULE_REF[] = REPL
1963

1964
if Base.generating_output()
1965
    include("precompile.jl")
1966
end
1967

1968
end # module
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