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

JuliaLang / julia / 1258

03 Sep 2025 01:56AM UTC coverage: 77.182% (+0.3%) from 76.913%
1258

push

buildkite

web-flow
make `Base.uabs` public 2 (#53503)

`Base.uabs` is helpful when writing code that works with generic
integers without worrying about silent overflows or overflow errors when
using integers from
[SaferIntegers.jl](https://github.com/JeffreySarnoff/SaferIntegers.jl)

`Base.uabs` is already being used by a few packages
https://juliahub.com/ui/Search?q=uabs&type=code&w=true and has a
docstring. However, it is currently an internal function.

Ref: https://github.com/JeffreySarnoff/SaferIntegers.jl/pull/41

Copy of #53376

61419 of 79577 relevant lines covered (77.18%)

23544311.79 hits per line

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

84.96
/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

20
function UndefVarError_REPL_hint(io::IO, ex::UndefVarError)
6✔
21
    var = ex.var
6✔
22
    if var === :or
6✔
23
        print(io, "\nSuggestion: Use `||` for short-circuiting boolean OR.")
×
24
    elseif var === :and
6✔
25
        print(io, "\nSuggestion: Use `&&` for short-circuiting boolean AND.")
×
26
    elseif var === :help
6✔
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]))
×
30
    elseif var === :quit
6✔
31
        print(io, "\nSuggestion: To exit Julia, use Ctrl-D, or type exit() and press enter.")
×
32
    end
33
end
34

35
function __init__()
13✔
36
    Base.REPL_MODULE_REF[] = REPL
13✔
37
    Base.Experimental.register_error_hint(UndefVarError_REPL_hint, UndefVarError)
13✔
38
    return nothing
13✔
39
end
40

41
using Base.Meta, Sockets, StyledStrings
42
using JuliaSyntaxHighlighting
43
import InteractiveUtils
44
import FileWatching
45
import Base.JuliaSyntax: kind, @K_str, @KSet_str, Tokenize.tokenize
46

47
export
48
    AbstractREPL,
49
    BasicREPL,
50
    LineEditREPL,
51
    StreamREPL
52

53
public TerminalMenus
54

55
import Base:
56
    AbstractDisplay,
57
    display,
58
    show,
59
    AnyDict,
60
    ==
61

62
_displaysize(io::IO) = displaysize(io)::Tuple{Int,Int}
189✔
63

64
include("Terminals.jl")
65
using .Terminals
66

67
abstract type AbstractREPL end
68

69
include("options.jl")
70

71
include("LineEdit.jl")
72
using .LineEdit
73
import .LineEdit:
74
    CompletionProvider,
75
    HistoryProvider,
76
    add_history,
77
    complete_line,
78
    history_next,
79
    history_next_prefix,
80
    history_prev,
81
    history_prev_prefix,
82
    history_first,
83
    history_last,
84
    history_search,
85
    setmodifiers!,
86
    terminal,
87
    MIState,
88
    PromptState,
89
    mode_idx
90

91
include("SyntaxUtil.jl")
92
include("REPLCompletions.jl")
93
using .REPLCompletions
94

95
include("TerminalMenus/TerminalMenus.jl")
96
include("docview.jl")
97

98
include("Pkg_beforeload.jl")
99

100
@nospecialize # use only declared type signatures
101

102
answer_color(::AbstractREPL) = ""
×
103

104
const JULIA_PROMPT = "julia> "
105
const PKG_PROMPT = "pkg> "
106
const SHELL_PROMPT = "shell> "
107
const HELP_PROMPT = "help?> "
108

109
mutable struct REPLBackend
110
    "channel for AST"
111
    repl_channel::Channel{Any}
112
    "channel for results: (value, iserror)"
113
    response_channel::Channel{Any}
114
    "flag indicating the state of this backend"
115
    in_eval::Bool
116
    "transformation functions to apply before evaluating expressions"
117
    ast_transforms::Vector{Any}
118
    "current backend task"
119
    backend_task::Task
120

121
    REPLBackend(repl_channel, response_channel, in_eval, ast_transforms=copy(repl_ast_transforms)) =
219✔
122
        new(repl_channel, response_channel, in_eval, ast_transforms)
123
end
124
REPLBackend() = REPLBackend(Channel(1), Channel(1), false)
144✔
125

126
# A reference to a backend that is not mutable
127
struct REPLBackendRef
128
    repl_channel::Channel{Any}
69✔
129
    response_channel::Channel{Any}
130
end
131
REPLBackendRef(backend::REPLBackend) = REPLBackendRef(backend.repl_channel, backend.response_channel)
69✔
132

133
function destroy(ref::REPLBackendRef, state::Task)
134
    if istaskfailed(state)
66✔
135
        close(ref.repl_channel, TaskFailedException(state))
×
136
        close(ref.response_channel, TaskFailedException(state))
×
137
    end
138
    close(ref.repl_channel)
66✔
139
    close(ref.response_channel)
66✔
140
end
141

142
"""
143
    softscope(ex)
144

145
Return a modified version of the parsed expression `ex` that uses
146
the REPL's "soft" scoping rules for global syntax blocks.
147
"""
148
function softscope(@nospecialize ex)
1,108✔
149
    if ex isa Expr
1,108✔
150
        h = ex.head
715✔
151
        if h === :toplevel
715✔
152
            ex′ = Expr(h)
405✔
153
            map!(softscope, resize!(ex′.args, length(ex.args)), ex.args)
405✔
154
            return ex′
405✔
155
        elseif h in (:meta, :import, :using, :export, :module, :error, :incomplete, :thunk)
310✔
156
            return ex
6✔
157
        elseif h === :global && all(x->isa(x, Symbol), ex.args)
307✔
158
            return ex
3✔
159
        else
160
            return Expr(:block, Expr(:softscope, true), ex)
301✔
161
        end
162
    end
163
    return ex
393✔
164
end
165

166
# Temporary alias until Documenter updates
167
const softscope! = softscope
168

169
function print_qualified_access_warning(mod::Module, owner::Module, name::Symbol)
6✔
170
    @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
6✔
171
end
172

173
function has_ancestor(query::Module, target::Module)
6✔
174
    query == target && return true
110✔
175
    while true
90✔
176
        next = parentmodule(query)
90✔
177
        next == target && return true
90✔
178
        next == query && return false
84✔
179
        query = next
45✔
180
    end
45✔
181
end
182

183
retrieve_modules(::Module, ::Any) = (nothing,)
18✔
184
function retrieve_modules(current_module::Module, mod_name::Symbol)
284✔
185
    mod = try
284✔
186
        getproperty(current_module, mod_name)
284✔
187
    catch
188
        return (nothing,)
57✔
189
    end
190
    return (mod isa Module ? mod : nothing,)
227✔
191
end
192
retrieve_modules(current_module::Module, mod_name::QuoteNode) = retrieve_modules(current_module, mod_name.value)
123✔
193
function retrieve_modules(current_module::Module, mod_expr::Expr)
123✔
194
    if Meta.isexpr(mod_expr, :., 2)
123✔
195
        current_module = retrieve_modules(current_module, mod_expr.args[1])[1]
123✔
196
        current_module === nothing && return (nothing,)
123✔
197
        return (current_module, retrieve_modules(current_module, mod_expr.args[2])...)
123✔
198
    else
199
        return (nothing,)
×
200
    end
201
end
202

203
add_locals!(locals, ast::Any) = nothing
3✔
204
function add_locals!(locals, ast::Expr)
42✔
205
    for arg in ast.args
42✔
206
        add_locals!(locals, arg)
69✔
207
    end
69✔
208
    return nothing
42✔
209
end
210
function add_locals!(locals, ast::Symbol)
171✔
211
    push!(locals, ast)
171✔
212
    return nothing
171✔
213
end
214

215
function collect_names_to_warn!(warnings, locals, current_module::Module, ast)
3,481✔
216
    ast isa Expr || return
5,039✔
217

218
    # don't recurse through module definitions
219
    ast.head === :module && return
1,923✔
220

221
    if Meta.isexpr(ast, :., 2)
1,923✔
222
        mod_name, name_being_accessed = ast.args
179✔
223
        # retrieve the (possibly-nested) module being named here
224
        mods = retrieve_modules(current_module, mod_name)
179✔
225
        all(x -> x isa Module, mods) || return
499✔
226
        outer_mod = first(mods)
104✔
227
        mod = last(mods)
104✔
228
        if name_being_accessed isa QuoteNode
104✔
229
            name_being_accessed = name_being_accessed.value
104✔
230
        end
231
        name_being_accessed isa Symbol || return
104✔
232
        owner = try
104✔
233
            which(mod, name_being_accessed)
104✔
234
        catch
235
            return
×
236
        end
237
        # if `owner` is a submodule of `mod`, then don't warn. E.g. the name `parse` is present in the module `JSON`
238
        # but is owned by `JSON.Parser`; we don't warn if it is accessed as `JSON.parse`.
239
        has_ancestor(owner, mod) && return
179✔
240
        # Don't warn if the name is public in the module we are accessing it
241
        Base.ispublic(mod, name_being_accessed) && return
36✔
242
        # Don't warn if accessing names defined in Core from Base if they are present in Base (e.g. `Base.throw`).
243
        mod === Base && Base.ispublic(Core, name_being_accessed) && return
33✔
244
        push!(warnings, (; outer_mod, mod, owner, name_being_accessed))
33✔
245
        # no recursion
246
        return
33✔
247
    elseif Meta.isexpr(ast, :(=), 2)
1,744✔
248
        lhs, rhs = ast.args
141✔
249
        # any symbols we find on the LHS we will count as local. This can potentially be overzealous,
250
        # but we want to avoid false positives (unnecessary warnings) more than false negatives.
251
        add_locals!(locals, lhs)
141✔
252
        # we'll recurse into the RHS only
253
        return collect_names_to_warn!(warnings, locals, current_module, rhs)
141✔
254
    elseif Meta.isexpr(ast, :function) && length(ast.args) >= 1
1,603✔
255

256
        if Meta.isexpr(ast.args[1], :call, 2)
9✔
257
            func_name, func_args = ast.args[1].args
6✔
258
            # here we have a function definition and are inspecting it's arguments for local variables.
259
            # we will error on the conservative side by adding all symbols we find (regardless if they are local variables or possibly-global default values)
260
            add_locals!(locals, func_args)
6✔
261
        end
262
        # fall through to general recursion
263
    end
264

265
    for arg in ast.args
1,603✔
266
        collect_names_to_warn!(warnings, locals, current_module, arg)
2,982✔
267
    end
2,982✔
268

269
    return nothing
1,603✔
270
end
271

272
function collect_qualified_access_warnings(current_mod, ast)
358✔
273
    warnings = Set()
358✔
274
    locals = Set{Symbol}()
358✔
275
    collect_names_to_warn!(warnings, locals, current_mod, ast)
358✔
276
    filter!(warnings) do (; outer_mod)
358✔
277
        nameof(outer_mod) ∉ locals
33✔
278
    end
279
    return warnings
358✔
280
end
281

282
function warn_on_non_owning_accesses(current_mod, ast)
322✔
283
    warnings = collect_qualified_access_warnings(current_mod, ast)
322✔
284
    for (; outer_mod, mod, owner, name_being_accessed) in warnings
644✔
285
        print_qualified_access_warning(mod, owner, name_being_accessed)
6✔
286
    end
12✔
287
    return ast
322✔
288
end
289
warn_on_non_owning_accesses(ast) = warn_on_non_owning_accesses(Base.active_module(), ast)
316✔
290

291
const repl_ast_transforms = Any[softscope, warn_on_non_owning_accesses] # defaults for new REPL backends
292

293
# Allows an external package to add hooks into the code loading.
294
# The hook should take a Vector{Symbol} of package names and
295
# return true if all packages could be installed, false if not
296
# to e.g. install packages on demand
297
const install_packages_hooks = Any[]
298

299
# N.B.: Any functions starting with __repl_entry cut off backtraces when printing in the REPL.
300
# We need to do this for both the actual eval and macroexpand, since the latter can cause custom macro
301
# code to run (and error).
302
__repl_entry_lower_with_loc(mod::Module, @nospecialize(ast), toplevel_file::Ref{Ptr{UInt8}}, toplevel_line::Ref{Csize_t}) =
637✔
303
    Core._lower(ast, mod, toplevel_file[], toplevel_line[])[1]
304
__repl_entry_eval_expanded_with_loc(mod::Module, @nospecialize(ast), toplevel_file::Ref{Ptr{UInt8}}, toplevel_line::Ref{Csize_t}) =
634✔
305
    ccall(:jl_toplevel_eval_flex, Any, (Any, Any, Cint, Cint, Ptr{Ptr{UInt8}}, Ptr{Csize_t}), mod, ast, 1, 1, toplevel_file, toplevel_line)
306

307
function toplevel_eval_with_hooks(mod::Module, @nospecialize(ast), toplevel_file=Ref{Ptr{UInt8}}(Base.unsafe_convert(Ptr{UInt8}, :REPL)), toplevel_line=Ref{Csize_t}(1))
1,015✔
308
    if !isexpr(ast, :toplevel)
1,331✔
309
        ast = invokelatest(__repl_entry_lower_with_loc, mod, ast, toplevel_file, toplevel_line)
637✔
310
        check_for_missing_packages_and_run_hooks(ast)
637✔
311
        return invokelatest(__repl_entry_eval_expanded_with_loc, mod, ast, toplevel_file, toplevel_line)
634✔
312
    end
313
    local value=nothing
378✔
314
    for i = 1:length(ast.args)
384✔
315
        value = toplevel_eval_with_hooks(mod, ast.args[i], toplevel_file, toplevel_line)
699✔
316
    end
1,002✔
317
    return value
360✔
318
end
319

320
function eval_user_input(@nospecialize(ast), backend::REPLBackend, mod::Module)
316✔
321
    lasterr = nothing
316✔
322
    Base.sigatomic_begin()
316✔
323
    while true
328✔
324
        try
328✔
325
            Base.sigatomic_end()
328✔
326
            if lasterr !== nothing
328✔
327
                put!(backend.response_channel, Pair{Any, Bool}(lasterr, true))
12✔
328
            else
329
                backend.in_eval = true
316✔
330
                for xf in backend.ast_transforms
316✔
331
                    ast = Base.invokelatest(xf, ast)
686✔
332
                end
686✔
333
                value = toplevel_eval_with_hooks(mod, ast)
316✔
334
                backend.in_eval = false
301✔
335
                setglobal!(Base.MainInclude, :ans, value)
301✔
336
                put!(backend.response_channel, Pair{Any, Bool}(value, false))
301✔
337
            end
338
            break
325✔
339
        catch err
340
            if lasterr !== nothing
12✔
341
                println("SYSTEM ERROR: Failed to report error to REPL frontend")
×
342
                println(err)
×
343
            end
344
            lasterr = current_exceptions()
12✔
345
        end
346
    end
12✔
347
    Base.sigatomic_end()
313✔
348
    nothing
313✔
349
end
350

351
function check_for_missing_packages_and_run_hooks(ast)
637✔
352
    isa(ast, Expr) || return
955✔
353
    mods = modules_to_be_loaded(ast)
319✔
354
    filter!(mod -> isnothing(Base.identify_package(String(mod))), mods) # keep missing modules
334✔
355
    if !isempty(mods)
319✔
356
        isempty(install_packages_hooks) && load_pkg()
3✔
357
        for f in install_packages_hooks
3✔
358
            Base.invokelatest(f, mods) && return
3✔
359
        end
×
360
    end
361
end
362

363
function _modules_to_be_loaded!(ast::Expr, mods::Vector{Symbol})
2,330✔
364
    function add!(ctx)
2,408✔
365
        if ctx.head == :as
78✔
366
            ctx = ctx.args[1]
3✔
367
        end
368
        if ctx.args[1] != :. # don't include local import `import .Foo`
156✔
369
            push!(mods, ctx.args[1])
69✔
370
        end
371
    end
372
    ast.head === :quote && return mods # don't search if it's not going to be run during this eval
2,330✔
373
    if ast.head == :call
2,330✔
374
        if length(ast.args) == 5 && ast.args[1] === GlobalRef(Base, :_eval_import)
1,135✔
375
            ctx = ast.args[4]
27✔
376
            if ctx isa QuoteNode # i.e. `Foo: bar`
27✔
377
                ctx = ctx.value
12✔
378
            else
379
                ctx = ast.args[5].value
15✔
380
            end
381
            add!(ctx)
27✔
382
        elseif length(ast.args) == 3 && ast.args[1] == GlobalRef(Base, :_eval_using)
1,108✔
383
            add!(ast.args[3].value)
51✔
384
        end
385
    end
386
    if ast.head !== :thunk
2,330✔
387
        for arg in ast.args
1,942✔
388
            if isexpr(arg, (:block, :if))
4,326✔
389
                _modules_to_be_loaded!(arg, mods)
12✔
390
            end
391
        end
4,326✔
392
    else
393
        code = ast.args[1]
388✔
394
        for arg in code.code
388✔
395
            isa(arg, Expr) || continue
3,406✔
396
            _modules_to_be_loaded!(arg, mods)
1,930✔
397
        end
3,406✔
398
    end
399
end
400

401
function modules_to_be_loaded(ast::Expr, mods::Vector{Symbol} = Symbol[])
402
    _modules_to_be_loaded!(ast, mods)
776✔
403
    filter!(mod::Symbol -> !in(mod, (:Base, :Main, :Core)), mods) # Exclude special non-package modules
457✔
404
    return unique(mods)
388✔
405
end
406

407
"""
408
    start_repl_backend(repl_channel::Channel, response_channel::Channel)
409

410
    Starts loop for REPL backend
411
    Returns a REPLBackend with backend_task assigned
412

413
    Deprecated since sync / async behavior cannot be selected
414
"""
415
function start_repl_backend(repl_channel::Channel{Any}, response_channel::Channel{Any}
×
416
                            ; get_module::Function = ()->Main)
417
    # Maintain legacy behavior of asynchronous backend
418
    backend = REPLBackend(repl_channel, response_channel, false)
×
419
    # Assignment will be made twice, but will be immediately available
420
    backend.backend_task = @async start_repl_backend(backend; get_module)
×
421
    return backend
×
422
end
423

424
"""
425
    start_repl_backend(backend::REPLBackend)
426

427
    Call directly to run backend loop on current Task.
428
    Use @async for run backend on new Task.
429

430
    Does not return backend until loop is finished.
431
"""
432
function start_repl_backend(backend::REPLBackend,  @nospecialize(consumer = x -> nothing); get_module::Function = ()->Main)
168✔
433
    backend.backend_task = Base.current_task()
75✔
434
    consumer(backend)
75✔
435
    repl_backend_loop(backend, get_module)
75✔
436
    return backend
72✔
437
end
438

439
function repl_backend_loop(backend::REPLBackend, get_module::Function)
75✔
440
    # include looks at this to determine the relative include path
441
    # nothing means cwd
442
    while true
565✔
443
        tls = task_local_storage()
565✔
444
        tls[:SOURCE_PATH] = nothing
565✔
445
        ast_or_func, show_value = take!(backend.repl_channel)
565✔
446
        if show_value == -1
565✔
447
            # exit flag
448
            break
72✔
449
        end
450
        if show_value == 2 # 2 indicates a function to be called
493✔
451
            f = ast_or_func
177✔
452
            try
177✔
453
                ret = f()
177✔
454
                put!(backend.response_channel, Pair{Any, Bool}(ret, false))
174✔
455
            catch
456
                put!(backend.response_channel, Pair{Any, Bool}(current_exceptions(), true))
3✔
457
            end
458
        else
459
            ast = ast_or_func
316✔
460
            eval_user_input(ast, backend, get_module())
316✔
461
        end
462
    end
490✔
463
    return nothing
72✔
464
end
465

466
SHOW_MAXIMUM_BYTES::Int = 1_048_576
467

468
# Limit printing during REPL display
469
mutable struct LimitIO{IO_t <: IO} <: IO
470
    io::IO_t
210✔
471
    maxbytes::Int
472
    n::Int # max bytes to write
473
end
474
LimitIO(io::IO, maxbytes) = LimitIO(io, maxbytes, 0)
210✔
475

476
struct LimitIOException <: Exception
477
    maxbytes::Int
18✔
478
end
479

480
function Base.showerror(io::IO, e::LimitIOException)
×
481
    print(io, "$LimitIOException: aborted printing after attempting to print more than $(Base.format_bytes(e.maxbytes)) within a `LimitIO`.")
×
482
end
483

484
Base.displaysize(io::LimitIO) = _displaysize(io.io)
84✔
485

486
function Base.write(io::LimitIO, v::UInt8)
453✔
487
    io.n > io.maxbytes && throw(LimitIOException(io.maxbytes))
5,010✔
488
    n_bytes = write(io.io, v)
9,555✔
489
    io.n += n_bytes
5,004✔
490
    return n_bytes
5,004✔
491
end
492

493
# Semantically, we only need to override `Base.write`, but we also
494
# override `unsafe_write` for performance.
495
function Base.unsafe_write(limiter::LimitIO, p::Ptr{UInt8}, nb::UInt)
496
    # already exceeded? throw
497
    limiter.n > limiter.maxbytes && throw(LimitIOException(limiter.maxbytes))
798✔
498
    remaining = limiter.maxbytes - limiter.n # >= 0
798✔
499

500
    # Not enough bytes left; we will print up to the limit, then throw
501
    if remaining < nb
798✔
502
        if remaining > 0
6✔
503
            Base.unsafe_write(limiter.io, p, remaining)
3✔
504
        end
505
        throw(LimitIOException(limiter.maxbytes))
6✔
506
    end
507

508
    # We won't hit the limit so we'll write the full `nb` bytes
509
    bytes_written = Base.unsafe_write(limiter.io, p, nb)::Union{Int,UInt}
792✔
510
    limiter.n += bytes_written
792✔
511
    return bytes_written
792✔
512
end
513

514
struct REPLDisplay{Repl<:AbstractREPL} <: AbstractDisplay
515
    repl::Repl
102✔
516
end
517

518
function show_limited(io::IO, mime::MIME, x)
201✔
519
    try
201✔
520
        # We wrap in a LimitIO to limit the amount of printing.
521
        # We unpack `IOContext`s, since we will pass the properties on the outside.
522
        inner = io isa IOContext ? io.io : io
201✔
523
        wrapped_limiter = IOContext(LimitIO(inner, SHOW_MAXIMUM_BYTES), io)
201✔
524
        # `show_repl` to allow the hook with special syntax highlighting
525
        show_repl(wrapped_limiter, mime, x)
201✔
526
    catch e
527
        e isa LimitIOException || rethrow()
15✔
528
        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)
9✔
529
    end
530
end
531

532
function display(d::REPLDisplay, mime::MIME"text/plain", x)
186✔
533
    x = Ref{Any}(x)
186✔
534
    with_repl_linfo(d.repl) do io
186✔
535
        io = IOContext(io, :limit => true, :module => Base.active_module(d)::Module)
363✔
536
        if d.repl isa LineEditREPL
186✔
537
            mistate = d.repl.mistate
177✔
538
            mode = LineEdit.mode(mistate)
177✔
539
            if mode isa LineEdit.Prompt
177✔
540
                LineEdit.write_output_prefix(io, mode, get(io, :color, false)::Bool)
708✔
541
            end
542
        end
543
        get(io, :color, false)::Bool && write(io, answer_color(d.repl))
204✔
544
        if isdefined(d.repl, :options) && isdefined(d.repl.options, :iocontext)
186✔
545
            # this can override the :limit property set initially
546
            io = foldl(IOContext, d.repl.options.iocontext, init=io)
177✔
547
        end
548
        show_limited(io, mime, x[])
186✔
549
        println(io)
183✔
550
    end
551
    return nothing
183✔
552
end
553

554
display(d::REPLDisplay, x) = display(d, MIME("text/plain"), x)
186✔
555

556
show_repl(io::IO, mime::MIME"text/plain", x) = show(io, mime, x)
195✔
557

558
function show_repl(io::IO, mime::MIME"text/plain", c::AbstractChar)
12✔
559
    show(io, mime, c) # Call the original Base.show
12✔
560
    # Check for LaTeX/emoji alias and print if found and using symbol_latex which is used in help?> mode
561
    latex = symbol_latex(string(c))
12✔
562
    if !isempty(latex)
12✔
563
        print(io, ", input as ")
6✔
564
        printstyled(io, latex, "<tab>"; color=:cyan)
6✔
565
    end
566
end
567

568
show_repl(io::IO, ::MIME"text/plain", ex::Expr) =
6✔
569
    print(io, JuliaSyntaxHighlighting.highlight(
570
        sprint(show, ex, context=IOContext(io, :color => false))))
571

572
function print_response(repl::AbstractREPL, response, show_value::Bool, have_color::Bool)
304✔
573
    repl.waserror = response[2]
304✔
574
    with_repl_linfo(repl) do io
608✔
575
        io = IOContext(io, :module => Base.active_module(repl)::Module)
599✔
576
        print_response(io, response, backend(repl), show_value, have_color, specialdisplay(repl))
304✔
577
    end
578
    return nothing
304✔
579
end
580

581
# N.B.: Any functions starting with __repl_entry cut off backtraces when printing in the REPL.
582
__repl_entry_display(val) = Base.invokelatest(display, val)
144✔
583
__repl_entry_display(specialdisplay::Union{AbstractDisplay,Nothing}, val) = Base.invokelatest(display, specialdisplay, val)
42✔
584

585
function __repl_entry_display_error(errio::IO, @nospecialize errval)
24✔
586
    # this will be set to true if types in the stacktrace are truncated
587
    limitflag = Ref(false)
24✔
588
    errio = IOContext(errio, :stacktrace_types_limited => limitflag)
24✔
589
    Base.invokelatest(Base.display_error, errio, errval)
24✔
590
    if limitflag[]
18✔
591
        print(errio, "Some type information was truncated. Use `show(err)` to see complete types.")
6✔
592
        println(errio)
6✔
593
    end
594
    return nothing
18✔
595
end
596

597
function print_response(errio::IO, response, backend::Union{REPLBackendRef,Nothing}, show_value::Bool, have_color::Bool, specialdisplay::Union{AbstractDisplay,Nothing}=nothing)
307✔
598
    Base.sigatomic_begin()
307✔
599
    val, iserr = response
307✔
600
    if !iserr
307✔
601
        # display result
602
        try
289✔
603
            if val !== nothing && show_value
289✔
604
                Base.sigatomic_end() # allow display to be interrupted
186✔
605
                val2, iserr = if specialdisplay === nothing
186✔
606
                    # display calls may require being run on the main thread
607
                    call_on_backend(backend) do
279✔
608
                        __repl_entry_display(val)
144✔
609
                    end
610
                else
611
                    call_on_backend(backend) do
228✔
612
                        __repl_entry_display(specialdisplay, val)
42✔
613
                    end
614
                end
615
                Base.sigatomic_begin()
186✔
616
                if iserr
186✔
617
                    println(errio)
3✔
618
                    println(errio, "Error showing value of type ", typeof(val), ":")
3✔
619
                    val = val2
3✔
620
                end
621
            end
622
        catch ex
623
            println(errio)
×
624
            println(errio, "SYSTEM (REPL): showing a value caused an error")
×
625
            val = current_exceptions()
×
626
            iserr = true
×
627
        end
628
    end
629
    if iserr
307✔
630
        # print error
631
        iserr = false
21✔
632
        while true
24✔
633
            try
24✔
634
                Base.sigatomic_end() # allow stacktrace printing to be interrupted
24✔
635
                val = Base.scrub_repl_backtrace(val)
24✔
636
                Base.istrivialerror(val) || setglobal!(Base.MainInclude, :err, val)
42✔
637
                __repl_entry_display_error(errio, val)
24✔
638
                break
24✔
639
            catch ex
640
                println(errio) # an error during printing is likely to leave us mid-line
6✔
641
                if !iserr
6✔
642
                    println(errio, "SYSTEM (REPL): showing an error caused an error")
3✔
643
                    val = current_exceptions()
3✔
644
                    iserr = true
3✔
645
                else
646
                    println(errio, "SYSTEM (REPL): caught exception of type ", typeof(ex).name.name,
3✔
647
                        " while trying to print an exception; giving up")
648
                    break
6✔
649
                end
650
            end
651
        end
3✔
652
    end
653
    Base.sigatomic_end()
307✔
654
    nothing
307✔
655
end
656

657

658

659
"""
660
    run_repl(repl::AbstractREPL)
661
    run_repl(repl, consumer = backend->nothing; backend_on_current_task = true)
662

663
    Main function to start the REPL
664

665
    consumer is an optional function that takes a REPLBackend as an argument
666
"""
667
function run_repl(repl::AbstractREPL, @nospecialize(consumer = x -> nothing); backend_on_current_task::Bool = true, backend = REPLBackend())
270✔
668
    backend_ref = REPLBackendRef(backend)
69✔
669
    cleanup = @task try
135✔
670
            destroy(backend_ref, t)
66✔
671
        catch e
672
            Core.print(Core.stderr, "\nINTERNAL ERROR: ")
×
673
            Core.println(Core.stderr, e)
×
674
            Core.println(Core.stderr, catch_backtrace())
×
675
        end
676
    get_module = () -> Base.active_module(repl)
373✔
677
    if backend_on_current_task
69✔
678
        t = @async run_frontend(repl, backend_ref)
138✔
679
        errormonitor(t)
69✔
680
        Base._wait2(t, cleanup)
69✔
681
        start_repl_backend(backend, consumer; get_module)
69✔
682
    else
683
        t = @async start_repl_backend(backend, consumer; get_module)
×
684
        errormonitor(t)
×
685
        Base._wait2(t, cleanup)
×
686
        run_frontend(repl, backend_ref)
×
687
    end
688
    return backend
66✔
689
end
690

691
## BasicREPL ##
692

693
mutable struct BasicREPL <: AbstractREPL
694
    terminal::TextTerminal
695
    waserror::Bool
696
    frontend_task::Task
697
    BasicREPL(t) = new(t, false)
9✔
698
end
699

700
outstream(r::BasicREPL) = r.terminal
18✔
701
hascolor(r::BasicREPL) = hascolor(r.terminal)
×
702

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

750
## LineEditREPL ##
751

752
mutable struct LineEditREPL <: AbstractREPL
753
    t::TextTerminal
754
    hascolor::Bool
755
    prompt_color::String
756
    input_color::String
757
    answer_color::String
758
    shell_color::String
759
    help_color::String
760
    pkg_color::String
761
    history_file::Bool
762
    in_shell::Bool
763
    in_help::Bool
764
    envcolors::Bool
765
    waserror::Bool
766
    specialdisplay::Union{Nothing,AbstractDisplay}
767
    options::Options
768
    mistate::Union{MIState,Nothing}
769
    last_shown_line_infos::Vector{Tuple{String,Int}}
770
    interface::ModalInterface
771
    backendref::REPLBackendRef
772
    frontend_task::Task
773
    function LineEditREPL(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,pkg_color,history_file,in_shell,in_help,envcolors)
81✔
774
        opts = Options()
81✔
775
        opts.hascolor = hascolor
81✔
776
        if !hascolor
81✔
777
            opts.beep_colors = [""]
×
778
        end
779
        new(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,pkg_color,history_file,in_shell,
81✔
780
            in_help,envcolors,false,nothing, opts, nothing, Tuple{String,Int}[])
781
    end
782
end
783
outstream(r::LineEditREPL) = (t = r.t; t isa TTYTerminal ? t.out_stream : t)
500✔
784
specialdisplay(r::LineEditREPL) = r.specialdisplay
295✔
785
specialdisplay(r::AbstractREPL) = nothing
9✔
786
terminal(r::LineEditREPL) = r.t
460✔
787
hascolor(r::LineEditREPL) = r.hascolor
593✔
788

789
LineEditREPL(t::TextTerminal, hascolor::Bool, envcolors::Bool=false) =
162✔
790
    LineEditREPL(t, hascolor,
791
        hascolor ? Base.text_colors[:green] : "",
792
        hascolor ? Base.input_color() : "",
793
        hascolor ? Base.answer_color() : "",
794
        hascolor ? Base.text_colors[:red] : "",
795
        hascolor ? Base.text_colors[:yellow] : "",
796
        hascolor ? Base.text_colors[:blue] : "",
797
        false, false, false, envcolors
798
    )
799

800
mutable struct REPLCompletionProvider <: CompletionProvider
801
    modifiers::LineEdit.Modifiers
72✔
802
end
803
REPLCompletionProvider() = REPLCompletionProvider(LineEdit.Modifiers())
72✔
804

805
mutable struct ShellCompletionProvider <: CompletionProvider end
72✔
806
struct LatexCompletions <: CompletionProvider end
807

808
Base.active_module((; mistate)::LineEditREPL) = mistate === nothing ? Main : mistate.active_module
14,920✔
809
Base.active_module(::AbstractREPL) = Main
36✔
810
Base.active_module(d::REPLDisplay) = Base.active_module(d.repl)
363✔
811

812
setmodifiers!(c::CompletionProvider, m::LineEdit.Modifiers) = nothing
×
813

814
setmodifiers!(c::REPLCompletionProvider, m::LineEdit.Modifiers) = c.modifiers = m
×
815

816
"""
817
    activate(mod::Module=Main)
818

819
Set `mod` as the default contextual module in the REPL,
820
both for evaluating expressions and printing them.
821
"""
822
function activate(mod::Module=Main; interactive_utils::Bool=true)
×
823
    mistate = (Base.active_repl::LineEditREPL).mistate
×
824
    mistate === nothing && return nothing
×
825
    mistate.active_module = mod
×
826
    interactive_utils && Base.load_InteractiveUtils(mod)
×
827
    return nothing
×
828
end
829

830
beforecursor(buf::IOBuffer) = String(buf.data[1:buf.ptr-1])
96✔
831

832
# Convert inclusive-inclusive 1-based char indexing to inclusive-exclusive byte Region.
833
to_region(s, r) = first(r)-1 => (length(r) > 0 ? nextind(s, last(r))-1 : first(r)-1)
79✔
834

835
function complete_line(c::REPLCompletionProvider, s::PromptState, mod::Module; hint::Bool=false)
72✔
836
    full = LineEdit.input_string(s)
36✔
837
    ret, range, should_complete = completions(full, thisind(full, position(s)), mod, c.modifiers.shift, hint)
72✔
838
    range = to_region(full, range)
69✔
839
    c.modifiers = LineEdit.Modifiers()
36✔
840
    return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), range, should_complete
36✔
841
end
842

843
function complete_line(c::ShellCompletionProvider, s::PromptState; hint::Bool=false)
12✔
844
    full = LineEdit.input_string(s)
6✔
845
    ret, range, should_complete = shell_completions(full, thisind(full, position(s)), hint)
12✔
846
    range = to_region(full, range)
10✔
847
    return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), range, should_complete
6✔
848
end
849

850
function complete_line(c::LatexCompletions, s; hint::Bool=false)
×
851
    full = LineEdit.input_string(s)::String
×
852
    ret, range, should_complete = bslash_completions(full, thisind(full, position(s)), hint)[2]
×
853
    range = to_region(full, range)
×
854
    return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), range, should_complete
×
855
end
856

857
with_repl_linfo(f, repl) = f(outstream(repl))
18✔
858
function with_repl_linfo(f, repl::LineEditREPL)
472✔
859
    linfos = Tuple{String,Int}[]
472✔
860
    io = IOContext(outstream(repl), :last_shown_line_infos => linfos)
472✔
861
    f(io)
472✔
862
    if !isempty(linfos)
469✔
863
        repl.last_shown_line_infos = linfos
18✔
864
    end
865
    nothing
469✔
866
end
867

868
mutable struct REPLHistoryProvider <: HistoryProvider
869
    history::Vector{String}
87✔
870
    file_path::String
871
    history_file::Union{Nothing,IO}
872
    start_idx::Int
873
    cur_idx::Int
874
    last_idx::Int
875
    last_buffer::IOBuffer
876
    last_mode::Union{Nothing,Prompt}
877
    mode_mapping::Dict{Symbol,Prompt}
878
    modes::Vector{Symbol}
879
end
880
REPLHistoryProvider(mode_mapping::Dict{Symbol}) =
87✔
881
    REPLHistoryProvider(String[], "", nothing, 0, 0, -1, IOBuffer(),
882
                        nothing, mode_mapping, UInt8[])
883

884
invalid_history_message(path::String) = """
×
885
Invalid history file ($path) format:
886
If you have a history file left over from an older version of Julia,
887
try renaming or deleting it.
888
Invalid character: """
889

890
munged_history_message(path::String) = """
×
891
Invalid history file ($path) format:
892
An editor may have converted tabs to spaces at line """
893

894
function hist_open_file(hp::REPLHistoryProvider)
895
    f = open(hp.file_path, read=true, write=true, create=true)
12✔
896
    hp.history_file = f
12✔
897
    seekend(f)
12✔
898
end
899

900
function hist_from_file(hp::REPLHistoryProvider, path::String)
24✔
901
    getline(lines, i) = i > length(lines) ? "" : lines[i]
852✔
902
    file_lines = readlines(path)
24✔
903
    countlines = 0
24✔
904
    while true
126✔
905
        # First parse the metadata that starts with '#' in particular the REPL mode
906
        countlines += 1
126✔
907
        line = getline(file_lines, countlines)
228✔
908
        mode = :julia
126✔
909
        isempty(line) && break
126✔
910
        line[1] != '#' &&
204✔
911
            error(invalid_history_message(path), repr(line[1]), " at line ", countlines)
912
        while !isempty(line)
306✔
913
            startswith(line, '#') || break
612✔
914
            if startswith(line, "# mode: ")
204✔
915
                mode = Symbol(SubString(line, 9))
204✔
916
            end
917
            countlines += 1
204✔
918
            line = getline(file_lines, countlines)
408✔
919
        end
204✔
920
        isempty(line) && break
102✔
921

922
        # Now parse the code for the current REPL mode
923
        line[1] == ' '  &&
204✔
924
            error(munged_history_message(path), countlines)
925
        line[1] != '\t' &&
204✔
926
            error(invalid_history_message(path), repr(line[1]), " at line ", countlines)
927
        lines = String[]
102✔
928
        while !isempty(line)
102✔
929
            push!(lines, chomp(SubString(line, 2)))
204✔
930
            next_line = getline(file_lines, countlines+1)
192✔
931
            isempty(next_line) && break
102✔
932
            first(next_line) == ' '  && error(munged_history_message(path), countlines)
180✔
933
            # A line not starting with a tab means we are done with code for this entry
934
            first(next_line) != '\t' && break
180✔
935
            countlines += 1
×
936
            line = getline(file_lines, countlines)
×
937
        end
×
938
        push!(hp.modes, mode)
102✔
939
        push!(hp.history, join(lines, '\n'))
102✔
940
    end
102✔
941
    hp.start_idx = length(hp.history)
24✔
942
    return hp
24✔
943
end
944

945
function add_history(hist::REPLHistoryProvider, s::PromptState)
349✔
946
    str = rstrip(takestring!(copy(s.input_buffer)))
349✔
947
    isempty(strip(str)) && return
349✔
948
    mode = mode_idx(hist, LineEdit.mode(s))
295✔
949
    !isempty(hist.history) &&
295✔
950
        isequal(mode, hist.modes[end]) && str == hist.history[end] && return
951
    push!(hist.modes, mode)
277✔
952
    push!(hist.history, str)
277✔
953
    hist.history_file === nothing && return
277✔
954
    entry = """
48✔
955
    # time: $(Libc.strftime("%Y-%m-%d %H:%M:%S %Z", time()))
956
    # mode: $mode
957
    $(replace(str, r"^"ms => "\t"))
958
    """
959
    try
48✔
960
        seekend(hist.history_file)
48✔
961
    catch err
962
        (err isa SystemError) || rethrow()
×
963
        # File handle might get stale after a while, especially under network file systems
964
        # If this doesn't fix it (e.g. when file is deleted), we'll end up rethrowing anyway
965
        hist_open_file(hist)
×
966
    end
967
    if isfile(hist.file_path)
48✔
968
        FileWatching.mkpidlock(hist.file_path  * ".pid", stale_age=3) do
48✔
969
            print(hist.history_file, entry)
96✔
970
            flush(hist.history_file)
48✔
971
        end
972
    else # handle eg devnull
973
        print(hist.history_file, entry)
×
974
        flush(hist.history_file)
×
975
    end
976
    nothing
48✔
977
end
978

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

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

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

1013
    return :ok
234✔
1014
end
1015

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

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

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

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

1071
history_last(s::LineEdit.MIState, hist::REPLHistoryProvider) =
12✔
1072
    history_next(s, hist, length(hist.history) - hist.cur_idx + 1)
1073

1074
function history_move_prefix(s::LineEdit.PrefixSearchState,
114✔
1075
                             hist::REPLHistoryProvider,
1076
                             prefix::AbstractString,
1077
                             backwards::Bool,
1078
                             cur_idx::Int = hist.cur_idx)
1079
    cur_response = takestring!(copy(LineEdit.buffer(s)))
222✔
1080
    # when searching forward, start at last_idx
1081
    if !backwards && hist.last_idx > 0
114✔
1082
        cur_idx = hist.last_idx
3✔
1083
    end
1084
    hist.last_idx = -1
114✔
1085
    max_idx = length(hist.history)+1
114✔
1086
    idxs = backwards ? ((cur_idx-1):-1:1) : ((cur_idx+1):1:max_idx)
195✔
1087
    for idx in idxs
114✔
1088
        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)))
576✔
1089
            m = history_move(s, hist, idx)
108✔
1090
            if m === :ok
108✔
1091
                if idx == max_idx
102✔
1092
                    # on resuming the in-progress edit, leave the cursor where the user last had it
1093
                elseif isempty(prefix)
90✔
1094
                    # on empty prefix search, move cursor to the end
1095
                    LineEdit.move_input_end(s)
42✔
1096
                else
1097
                    # otherwise, keep cursor at the prefix position as a visual cue
1098
                    seek(LineEdit.buffer(s), sizeof(prefix))
48✔
1099
                end
1100
                LineEdit.refresh_line(s)
102✔
1101
                return :ok
102✔
1102
            elseif m === :skip
6✔
1103
                return history_move_prefix(s,hist,prefix,backwards,idx)
6✔
1104
            end
1105
        end
1106
    end
372✔
1107
    Terminals.beep(s)
6✔
1108
    nothing
6✔
1109
end
1110
history_next_prefix(s::LineEdit.PrefixSearchState, hist::REPLHistoryProvider, prefix::AbstractString) =
15✔
1111
    history_move_prefix(s, hist, prefix, false)
1112
history_prev_prefix(s::LineEdit.PrefixSearchState, hist::REPLHistoryProvider, prefix::AbstractString) =
93✔
1113
    history_move_prefix(s, hist, prefix, true)
1114

1115
function history_search(hist::REPLHistoryProvider, query_buffer::IOBuffer, response_buffer::IOBuffer,
84✔
1116
                        backwards::Bool=false, skip_current::Bool=false)
1117

1118
    qpos = position(query_buffer)
84✔
1119
    qpos > 0 || return true
84✔
1120
    searchdata = beforecursor(query_buffer)
84✔
1121
    response_str = takestring!(copy(response_buffer))
84✔
1122

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

1131
    searchstart = backwards ? b : a
84✔
1132
    if searchdata == response_str[a:b]
132✔
1133
        if skip_current
30✔
1134
            searchstart = backwards ? prevind(response_str, b) : nextind(response_str, a)
12✔
1135
        else
1136
            return true
18✔
1137
        end
1138
    end
1139

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

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

1165
    return false
12✔
1166
end
1167

1168
function history_reset_state(hist::REPLHistoryProvider)
1169
    if hist.cur_idx != length(hist.history) + 1
809✔
1170
        hist.last_idx = hist.cur_idx
388✔
1171
        hist.cur_idx = length(hist.history) + 1
388✔
1172
    end
1173
    nothing
809✔
1174
end
1175
LineEdit.reset_state(hist::REPLHistoryProvider) = history_reset_state(hist)
716✔
1176

1177
function return_callback(s)
288✔
1178
    ast = Base.parse_input_line(takestring!(copy(LineEdit.buffer(s))), depwarn=false)
288✔
1179
    return !(isa(ast, Expr) && ast.head === :incomplete)
288✔
1180
end
1181

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

1186
backend(r::AbstractREPL) = hasproperty(r, :backendref) ? r.backendref : nothing
596✔
1187

1188

1189
function eval_on_backend(ast, backend::REPLBackendRef)
304✔
1190
    put!(backend.repl_channel, (ast, 1)) # (f, show_value)
304✔
1191
    return take!(backend.response_channel) # (val, iserr)
304✔
1192
end
1193
function call_on_backend(f, backend::REPLBackendRef)
42✔
1194
    applicable(f) || error("internal error: f is not callable")
177✔
1195
    put!(backend.repl_channel, (f, 2)) # (f, show_value) 2 indicates function (rather than ast)
177✔
1196
    return take!(backend.response_channel) # (val, iserr)
177✔
1197
end
1198
# if no backend just eval (used by tests)
1199
eval_on_backend(ast, backend::Nothing) = error("no backend for eval ast")
×
1200
function call_on_backend(f, backend::Nothing)
9✔
1201
    try
9✔
1202
        ret = f()
9✔
1203
        return (ret, false) # (val, iserr)
9✔
1204
    catch
1205
        return (current_exceptions(), true)
×
1206
    end
1207
end
1208

1209

1210
function respond(f, repl, main; pass_empty::Bool = false, suppress_on_semicolon::Bool = true)
288✔
1211
    return function do_respond(s::MIState, buf, ok::Bool)
688✔
1212
        if !ok
400✔
1213
            return transition(s, :abort)
60✔
1214
        end
1215
        line = String(take!(buf)::Vector{UInt8})
635✔
1216
        if !isempty(line) || pass_empty
385✔
1217
            reset(repl)
295✔
1218
            local response
1219
            try
295✔
1220
                ast = Base.invokelatest(f, line)
295✔
1221
                response = eval_on_backend(ast, backend(repl))
292✔
1222
            catch
1223
                response = Pair{Any, Bool}(current_exceptions(), true)
3✔
1224
            end
1225
            hide_output = suppress_on_semicolon && ends_with_semicolon(line)
295✔
1226
            print_response(repl, response, !hide_output, hascolor(repl))
295✔
1227
        end
1228
        prepare_next(repl)
340✔
1229
        reset_state(s)
340✔
1230
        return s.current_mode.sticky ? true : transition(s, main)
340✔
1231
    end
1232
end
1233

1234
function reset(repl::LineEditREPL)
295✔
1235
    raw!(repl.t, false)
295✔
1236
    hascolor(repl) && print(repl.t, Base.text_colors[:normal])
295✔
1237
    nothing
295✔
1238
end
1239

1240
function prepare_next(repl::LineEditREPL)
340✔
1241
    println(terminal(repl))
340✔
1242
end
1243

1244
function mode_keymap(julia_prompt::Prompt)
1245
    AnyDict(
78✔
1246
    '\b' => function (s::MIState,o...)
19✔
1247
        if isempty(s) || position(LineEdit.buffer(s)) == 0
19✔
1248
            buf = copy(LineEdit.buffer(s))
19✔
1249
            transition(s, julia_prompt) do
19✔
1250
                LineEdit.state(s, julia_prompt).input_buffer = buf
19✔
1251
            end
1252
        else
1253
            LineEdit.edit_backspace(s)
×
1254
        end
1255
    end,
1256
    "^C" => function (s::MIState,o...)
1257
        LineEdit.move_input_end(s)
1258
        LineEdit.refresh_line(s)
1259
        print(LineEdit.terminal(s), "^C\n\n")
1260
        transition(s, julia_prompt)
1261
        transition(s, :reset)
1262
        LineEdit.refresh_line(s)
1263
    end)
1264
end
1265

1266
repl_filename(repl, hp::REPLHistoryProvider) = "REPL[$(max(length(hp.history)-hp.start_idx, 1))]"
264✔
1267
repl_filename(repl, hp) = "REPL"
×
1268

1269
const JL_PROMPT_PASTE = Ref(true)
1270
enable_promptpaste(v::Bool) = JL_PROMPT_PASTE[] = v
×
1271

1272
function contextual_prompt(repl::LineEditREPL, prompt::Union{String,Function})
1273
    function ()
6,855✔
1274
        mod = Base.active_module(repl)
13,392✔
1275
        prefix = mod == Main ? "" : string('(', mod, ") ")
6,790✔
1276
        pr = prompt isa String ? prompt : prompt()
6,708✔
1277
        prefix * pr
6,708✔
1278
    end
1279
end
1280

1281
setup_interface(
204✔
1282
    repl::LineEditREPL;
1283
    # those keyword arguments may be deprecated eventually in favor of the Options mechanism
1284
    hascolor::Bool = repl.options.hascolor,
1285
    extra_repl_keymap::Any = repl.options.extra_keymap
1286
) = setup_interface(repl, hascolor, extra_repl_keymap)
1287

1288

1289
# This non keyword method can be precompiled which is important
1290
function setup_interface(
72✔
1291
    repl::LineEditREPL,
1292
    hascolor::Bool,
1293
    extra_repl_keymap::Any, # Union{Dict,Vector{<:Dict}},
1294
)
1295
    # The precompile statement emitter has problem outputting valid syntax for the
1296
    # type of `Union{Dict,Vector{<:Dict}}` (see #28808).
1297
    # This function is however important to precompile for REPL startup time, therefore,
1298
    # make the type Any and just assert that we have the correct type below.
1299
    @assert extra_repl_keymap isa Union{Dict,Vector{<:Dict}}
72✔
1300

1301
    ###
1302
    #
1303
    # This function returns the main interface that describes the REPL
1304
    # functionality, it is called internally by functions that setup a
1305
    # Terminal-based REPL frontend.
1306
    #
1307
    # See run_frontend(repl::LineEditREPL, backend::REPLBackendRef)
1308
    # for usage
1309
    #
1310
    ###
1311

1312
    ###
1313
    # We setup the interface in two stages.
1314
    # First, we set up all components (prompt,rsearch,shell,help)
1315
    # Second, we create keymaps with appropriate transitions between them
1316
    #   and assign them to the components
1317
    #
1318
    ###
1319

1320
    ############################### Stage I ################################
1321

1322
    # This will provide completions for REPL and help mode
1323
    replc = REPLCompletionProvider()
72✔
1324

1325
    # Set up the main Julia prompt
1326
    julia_prompt = Prompt(contextual_prompt(repl, JULIA_PROMPT);
144✔
1327
        # Copy colors from the prompt object
1328
        prompt_prefix = hascolor ? repl.prompt_color : "",
1329
        prompt_suffix = hascolor ?
1330
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1331
        repl = repl,
1332
        complete = replc,
1333
        on_enter = return_callback)
1334

1335
    # Setup help mode
1336
    help_mode = Prompt(contextual_prompt(repl, HELP_PROMPT),
144✔
1337
        prompt_prefix = hascolor ? repl.help_color : "",
1338
        prompt_suffix = hascolor ?
1339
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1340
        repl = repl,
1341
        complete = replc,
1342
        # When we're done transform the entered line into a call to helpmode function
1343
        on_done = respond(line::String->helpmode(outstream(repl), line, repl.mistate.active_module),
6✔
1344
                          repl, julia_prompt, pass_empty=true, suppress_on_semicolon=false))
1345

1346

1347
    # Set up shell mode
1348
    shell_mode = Prompt(SHELL_PROMPT;
144✔
1349
        prompt_prefix = hascolor ? repl.shell_color : "",
1350
        prompt_suffix = hascolor ?
1351
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1352
        repl = repl,
1353
        complete = ShellCompletionProvider(),
1354
        # Transform "foo bar baz" into `foo bar baz` (shell quoting)
1355
        # and pass into Base.repl_cmd for processing (handles `ls` and `cd`
1356
        # special)
1357
        on_done = respond(repl, julia_prompt) do line
1358
            Expr(:call, :(Base.repl_cmd),
25✔
1359
                :(Base.cmd_gen($(Base.shell_parse(line::String)[1]))),
1360
                outstream(repl))
1361
        end,
1362
        sticky = true)
1363

1364
    # Set up dummy Pkg mode that will be replaced once Pkg is loaded
1365
    # use 6 dots to occupy the same space as the most likely "@v1.xx" env name
1366
    dummy_pkg_mode = Prompt(Pkg_promptf,
144✔
1367
        prompt_prefix = hascolor ? repl.pkg_color : "",
1368
        prompt_suffix = hascolor ?
1369
        (repl.envcolors ? Base.input_color : repl.input_color) : "",
1370
        repl = repl,
1371
        complete = LineEdit.EmptyCompletionProvider(),
1372
        on_done = respond(line->nothing, repl, julia_prompt),
1373
        on_enter = function (s::MIState)
1374
                # This is hit when the user tries to execute a command before the real Pkg mode has been
1375
                # switched to. Ok to do this even if Pkg is loading on the other task because of the loading lock.
1376
                REPLExt = load_pkg()
1377
                if REPLExt isa Module && isdefined(REPLExt, :PkgCompletionProvider)
1378
                    for mode in repl.interface.modes
1379
                        if mode isa LineEdit.Prompt && mode.complete isa REPLExt.PkgCompletionProvider
1380
                            # pkg mode
1381
                            buf = copy(LineEdit.buffer(s))
1382
                            transition(s, mode) do
1383
                                LineEdit.state(s, mode).input_buffer = buf
1384
                            end
1385
                        end
1386
                    end
1387
                end
1388
                return true
1389
            end,
1390
        sticky = true)
1391

1392

1393
    ################################# Stage II #############################
1394

1395
    # Setup history
1396
    # We will have a unified history for all REPL modes
1397
    hp = REPLHistoryProvider(Dict{Symbol,Prompt}(:julia => julia_prompt,
72✔
1398
                                                 :shell => shell_mode,
1399
                                                 :help  => help_mode,
1400
                                                 :pkg  => dummy_pkg_mode))
1401
    if repl.history_file
72✔
1402
        try
12✔
1403
            hist_path = find_hist_file()
12✔
1404
            mkpath(dirname(hist_path))
12✔
1405
            hp.file_path = hist_path
12✔
1406
            hist_open_file(hp)
12✔
1407
            finalizer(replc) do replc
12✔
1408
                close(hp.history_file)
12✔
1409
            end
1410
            hist_from_file(hp, hist_path)
12✔
1411
        catch
1412
            # use REPL.hascolor to avoid using the local variable with the same name
1413
            print_response(repl, Pair{Any, Bool}(current_exceptions(), true), true, REPL.hascolor(repl))
×
1414
            println(outstream(repl))
×
1415
            @info "Disabling history file for this session"
×
1416
            repl.history_file = false
×
1417
        end
1418
    end
1419
    history_reset_state(hp)
72✔
1420
    julia_prompt.hist = hp
72✔
1421
    shell_mode.hist = hp
72✔
1422
    help_mode.hist = hp
72✔
1423
    dummy_pkg_mode.hist = hp
72✔
1424

1425
    julia_prompt.on_done = respond(x->Base.parse_input_line(x,filename=repl_filename(repl,hp)), repl, julia_prompt)
336✔
1426

1427

1428
    search_prompt, skeymap = LineEdit.setup_search_keymap(hp)
72✔
1429
    search_prompt.complete = LatexCompletions()
72✔
1430

1431
    shell_prompt_len = length(SHELL_PROMPT)
72✔
1432
    help_prompt_len = length(HELP_PROMPT)
72✔
1433
    jl_prompt_regex = Regex("^In \\[[0-9]+\\]: |^(?:\\(.+\\) )?$JULIA_PROMPT")
72✔
1434
    pkg_prompt_regex = Regex("^(?:\\(.+\\) )?$PKG_PROMPT")
72✔
1435

1436
    # Canonicalize user keymap input
1437
    if isa(extra_repl_keymap, Dict)
72✔
1438
        extra_repl_keymap = AnyDict[extra_repl_keymap]
×
1439
    end
1440

1441
    repl_keymap = AnyDict(
72✔
1442
        ';' => function (s::MIState,o...)
163✔
1443
            if isempty(s) || position(LineEdit.buffer(s)) == 0
307✔
1444
                buf = copy(LineEdit.buffer(s))
19✔
1445
                transition(s, shell_mode) do
19✔
1446
                    LineEdit.state(s, shell_mode).input_buffer = buf
19✔
1447
                end
1448
            else
1449
                edit_insert(s, ';')
144✔
1450
                LineEdit.check_show_hint(s)
144✔
1451
            end
1452
        end,
1453
        '?' => function (s::MIState,o...)
3✔
1454
            if isempty(s) || position(LineEdit.buffer(s)) == 0
3✔
1455
                buf = copy(LineEdit.buffer(s))
3✔
1456
                transition(s, help_mode) do
3✔
1457
                    LineEdit.state(s, help_mode).input_buffer = buf
3✔
1458
                end
1459
            else
1460
                edit_insert(s, '?')
×
1461
                LineEdit.check_show_hint(s)
×
1462
            end
1463
        end,
1464
        ']' => function (s::MIState,o...)
9✔
1465
            if isempty(s) || position(LineEdit.buffer(s)) == 0
18✔
1466
                buf = copy(LineEdit.buffer(s))
×
1467
                transition(s, dummy_pkg_mode) do
×
1468
                    LineEdit.state(s, dummy_pkg_mode).input_buffer = buf
1469
                end
1470
                # load Pkg on another thread if available so that typing in the dummy Pkg prompt
1471
                # isn't blocked, but instruct the main REPL task to do the transition via s.async_channel
1472
                t_replswitch = Threads.@spawn begin
×
1473
                    REPLExt = load_pkg()
1474
                    if REPLExt isa Module && isdefined(REPLExt, :PkgCompletionProvider)
1475
                        put!(s.async_channel,
1476
                            function (s::MIState)
1477
                                LineEdit.mode(s) === dummy_pkg_mode || return :ok
1478
                                for mode in repl.interface.modes
1479
                                    if mode isa LineEdit.Prompt && mode.complete isa REPLExt.PkgCompletionProvider
1480
                                        buf = copy(LineEdit.buffer(s))
1481
                                        transition(s, mode) do
1482
                                            LineEdit.state(s, mode).input_buffer = buf
1483
                                        end
1484
                                        if !isempty(s)
1485
                                            @invokelatest(LineEdit.check_show_hint(s))
1486
                                        end
1487
                                        break
1488
                                    end
1489
                                end
1490
                                return :ok
1491
                            end
1492
                        )
1493
                    end
1494
                end
1495
                Base.errormonitor(t_replswitch)
×
1496
            else
1497
                edit_insert(s, ']')
9✔
1498
                LineEdit.check_show_hint(s)
9✔
1499
            end
1500
        end,
1501

1502
        # Bracketed Paste Mode
1503
        "\e[200~" => (s::MIState,o...)->begin
24✔
1504
            input = LineEdit.bracketed_paste(s) # read directly from s until reaching the end-bracketed-paste marker
24✔
1505
            sbuffer = LineEdit.buffer(s)
24✔
1506
            curspos = position(sbuffer)
24✔
1507
            seek(sbuffer, 0)
48✔
1508
            shouldeval = (bytesavailable(sbuffer) == curspos && !occursin(UInt8('\n'), sbuffer))
24✔
1509
            seek(sbuffer, curspos)
48✔
1510
            if curspos == 0
24✔
1511
                # if pasting at the beginning, strip leading whitespace
1512
                input = lstrip(input)
21✔
1513
            end
1514
            if !shouldeval
24✔
1515
                # when pasting in the middle of input, just paste in place
1516
                # don't try to execute all the WIP, since that's rather confusing
1517
                # and is often ill-defined how it should behave
1518
                edit_insert(s, input)
×
1519
                return
×
1520
            end
1521
            LineEdit.push_undo(s)
24✔
1522
            edit_insert(sbuffer, input)
24✔
1523
            input = String(take!(sbuffer))
48✔
1524
            oldpos = firstindex(input)
24✔
1525
            firstline = true
24✔
1526
            isprompt_paste = false
24✔
1527
            curr_prompt_len = 0
24✔
1528
            pasting_help = false
24✔
1529

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

1642
        # Open the editor at the location of a stackframe or method
1643
        # This is accessing a contextual variable that gets set in
1644
        # the show_backtrace and show_method_table functions.
1645
        "^Q" => (s::MIState, o...) -> begin
1646
            linfos = repl.last_shown_line_infos
1647
            str = String(take!(LineEdit.buffer(s)))
1648
            n = tryparse(Int, str)
1649
            n === nothing && @goto writeback
1650
            if n <= 0 || n > length(linfos) || startswith(linfos[n][1], "REPL[")
1651
                @goto writeback
1652
            end
1653
            try
1654
                InteractiveUtils.edit(Base.fixup_stdlib_path(linfos[n][1]), linfos[n][2])
1655
            catch ex
1656
                ex isa ProcessFailedException || ex isa Base.IOError || ex isa SystemError || rethrow()
1657
                @info "edit failed" _exception=ex
1658
            end
1659
            LineEdit.refresh_line(s)
1660
            return
1661
            @label writeback
1662
            write(LineEdit.buffer(s), str)
1663
            return
1664
        end,
1665
    )
1666

1667
    prefix_prompt, prefix_keymap = LineEdit.setup_prefix_keymap(hp, julia_prompt)
72✔
1668

1669
    a = Dict{Any,Any}[skeymap, repl_keymap, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
432✔
1670
    prepend!(a, extra_repl_keymap)
72✔
1671

1672
    julia_prompt.keymap_dict = LineEdit.keymap(a)
72✔
1673

1674
    mk = mode_keymap(julia_prompt)
72✔
1675

1676
    b = Dict{Any,Any}[skeymap, mk, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
432✔
1677
    prepend!(b, extra_repl_keymap)
72✔
1678

1679
    shell_mode.keymap_dict = help_mode.keymap_dict = dummy_pkg_mode.keymap_dict = LineEdit.keymap(b)
72✔
1680

1681
    allprompts = LineEdit.TextInterface[julia_prompt, shell_mode, help_mode, dummy_pkg_mode, search_prompt, prefix_prompt]
72✔
1682
    return ModalInterface(allprompts)
72✔
1683
end
1684

1685
function run_frontend(repl::LineEditREPL, backend::REPLBackendRef)
60✔
1686
    repl.frontend_task = current_task()
60✔
1687
    d = REPLDisplay(repl)
60✔
1688
    dopushdisplay = repl.specialdisplay === nothing && !in(d,Base.Multimedia.displays)
102✔
1689
    dopushdisplay && pushdisplay(d)
60✔
1690
    if !isdefined(repl,:interface)
60✔
1691
        interface = repl.interface = setup_interface(repl)
72✔
1692
    else
1693
        interface = repl.interface
24✔
1694
    end
1695
    repl.backendref = backend
60✔
1696
    repl.mistate = LineEdit.init_state(terminal(repl), interface)
60✔
1697
    run_interface(terminal(repl), interface, repl.mistate)
60✔
1698
    # Terminate Backend
1699
    put!(backend.repl_channel, (nothing, -1))
60✔
1700
    dopushdisplay && popdisplay(d)
60✔
1701
    nothing
60✔
1702
end
1703

1704
## StreamREPL ##
1705

1706
mutable struct StreamREPL <: AbstractREPL
1707
    stream::IO
1708
    prompt_color::String
1709
    input_color::String
1710
    answer_color::String
1711
    waserror::Bool
1712
    frontend_task::Task
1713
    StreamREPL(stream,pc,ic,ac) = new(stream,pc,ic,ac,false)
×
1714
end
1715
StreamREPL(stream::IO) = StreamREPL(stream, Base.text_colors[:green], Base.input_color(), Base.answer_color())
×
1716
run_repl(stream::IO) = run_repl(StreamREPL(stream))
×
1717

1718
outstream(s::StreamREPL) = s.stream
×
1719
hascolor(s::StreamREPL) = get(s.stream, :color, false)::Bool
×
1720

1721
answer_color(r::LineEditREPL) = r.envcolors ? Base.answer_color() : r.answer_color
×
1722
answer_color(r::StreamREPL) = r.answer_color
×
1723
input_color(r::LineEditREPL) = r.envcolors ? Base.input_color() : r.input_color
×
1724
input_color(r::StreamREPL) = r.input_color
×
1725

1726
# heuristic function to decide if the presence of a semicolon
1727
# at the end of the expression was intended for suppressing output
1728
function ends_with_semicolon(code)
385✔
1729
    semi = false
385✔
1730
    for tok in tokenize(code)
385✔
1731
        kind(tok) in KSet"Whitespace NewlineWs Comment EndMarker" && continue
7,201✔
1732
        semi = kind(tok) == K";"
2,774✔
1733
    end
3,892✔
1734
    return semi
385✔
1735
end
1736

1737
function banner(io::IO = stdout; short = false)
12✔
1738
    if Base.GIT_VERSION_INFO.tagged_commit
6✔
1739
        commit_string = Base.TAGGED_RELEASE_BANNER
×
1740
    elseif isempty(Base.GIT_VERSION_INFO.commit)
6✔
1741
        commit_string = ""
×
1742
    else
1743
        days = Int(floor((ccall(:jl_clock_now, Float64, ()) - Base.GIT_VERSION_INFO.fork_master_timestamp) / (60 * 60 * 24)))
6✔
1744
        days = max(0, days)
6✔
1745
        unit = days == 1 ? "day" : "days"
6✔
1746
        distance = Base.GIT_VERSION_INFO.fork_master_distance
6✔
1747
        commit = Base.GIT_VERSION_INFO.commit_short
6✔
1748

1749
        if distance == 0
6✔
1750
            commit_string = "Commit $(commit) ($(days) $(unit) old master)"
6✔
1751
        else
1752
            branch = Base.GIT_VERSION_INFO.branch
×
1753
            commit_string = "$(branch)/$(commit) (fork: $(distance) commits, $(days) $(unit))"
×
1754
        end
1755
    end
1756

1757
    commit_date = isempty(Base.GIT_VERSION_INFO.date_string) ? "" : " ($(split(Base.GIT_VERSION_INFO.date_string)[1]))"
6✔
1758

1759
    if get(io, :color, false)::Bool
6✔
1760
        c = Base.text_colors
×
1761
        tx = c[:normal] # text
×
1762
        jl = c[:normal] # julia
×
1763
        d1 = c[:bold] * c[:blue]    # first dot
×
1764
        d2 = c[:bold] * c[:red]     # second dot
×
1765
        d3 = c[:bold] * c[:green]   # third dot
×
1766
        d4 = c[:bold] * c[:magenta] # fourth dot
×
1767

1768
        if short
×
1769
            print(io,"""
×
1770
              $(d3)o$(tx)  | Version $(VERSION)$(commit_date)
1771
             $(d2)o$(tx) $(d4)o$(tx) | $(commit_string)
1772
            """)
1773
        else
1774
            print(io,"""               $(d3)_$(tx)
×
1775
               $(d1)_$(tx)       $(jl)_$(tx) $(d2)_$(d3)(_)$(d4)_$(tx)     |  Documentation: https://docs.julialang.org
1776
              $(d1)(_)$(jl)     | $(d2)(_)$(tx) $(d4)(_)$(tx)    |
1777
               $(jl)_ _   _| |_  __ _$(tx)   |  Type \"?\" for help, \"]?\" for Pkg help.
1778
              $(jl)| | | | | | |/ _` |$(tx)  |
1779
              $(jl)| | |_| | | | (_| |$(tx)  |  Version $(VERSION)$(commit_date)
1780
             $(jl)_/ |\\__'_|_|_|\\__'_|$(tx)  |  $(commit_string)
1781
            $(jl)|__/$(tx)                   |
1782

1783
            """)
1784
        end
1785
    else
1786
        if short
6✔
1787
            print(io,"""
3✔
1788
              o  |  Version $(VERSION)$(commit_date)
1789
             o o |  $(commit_string)
1790
            """)
1791
        else
1792
            print(io,"""
3✔
1793
                           _
1794
               _       _ _(_)_     |  Documentation: https://docs.julialang.org
1795
              (_)     | (_) (_)    |
1796
               _ _   _| |_  __ _   |  Type \"?\" for help, \"]?\" for Pkg help.
1797
              | | | | | | |/ _` |  |
1798
              | | |_| | | | (_| |  |  Version $(VERSION)$(commit_date)
1799
             _/ |\\__'_|_|_|\\__'_|  |  $(commit_string)
1800
            |__/                   |
1801

1802
            """)
1803
        end
1804
    end
1805
end
1806

1807
function run_frontend(repl::StreamREPL, backend::REPLBackendRef)
×
1808
    repl.frontend_task = current_task()
×
1809
    have_color = hascolor(repl)
×
1810
    banner(repl.stream)
×
1811
    d = REPLDisplay(repl)
×
1812
    dopushdisplay = !in(d,Base.Multimedia.displays)
×
1813
    dopushdisplay && pushdisplay(d)
×
1814
    while !eof(repl.stream)::Bool
×
1815
        if have_color
×
1816
            print(repl.stream,repl.prompt_color)
×
1817
        end
1818
        print(repl.stream, JULIA_PROMPT)
×
1819
        if have_color
×
1820
            print(repl.stream, input_color(repl))
×
1821
        end
1822
        line = readline(repl.stream, keep=true)
×
1823
        if !isempty(line)
×
1824
            ast = Base.parse_input_line(line)
×
1825
            if have_color
×
1826
                print(repl.stream, Base.color_normal)
×
1827
            end
1828
            response = eval_on_backend(ast, backend)
×
1829
            print_response(repl, response, !ends_with_semicolon(line), have_color)
×
1830
        end
1831
    end
×
1832
    # Terminate Backend
1833
    put!(backend.repl_channel, (nothing, -1))
×
1834
    dopushdisplay && popdisplay(d)
×
1835
    nothing
×
1836
end
1837

1838
module Numbered
1839

1840
using ..REPL
1841

1842
__current_ast_transforms() = Base.active_repl_backend !== nothing ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1843

1844
function repl_eval_counter(hp)
1,701✔
1845
    return length(hp.history) - hp.start_idx
1,701✔
1846
end
1847

1848
function out_transform(@nospecialize(x), n::Ref{Int})
48✔
1849
    return Expr(:toplevel, get_usings!([], x)..., quote
48✔
1850
        let __temp_val_a72df459 = $x
1851
            $capture_result($n, __temp_val_a72df459)
1852
            __temp_val_a72df459
1853
        end
1854
    end)
1855
end
1856

1857
function get_usings!(usings, ex)
75✔
1858
    ex isa Expr || return usings
75✔
1859
    # get all `using` and `import` statements which are at the top level
1860
    for (i, arg) in enumerate(ex.args)
75✔
1861
        if Base.isexpr(arg, :toplevel)
144✔
1862
            get_usings!(usings, arg)
27✔
1863
        elseif Base.isexpr(arg, [:using, :import])
192✔
1864
            push!(usings, popat!(ex.args, i))
6✔
1865
        end
1866
    end
213✔
1867
    return usings
75✔
1868
end
1869

1870
function create_global_out!(mod)
3✔
1871
    if !isdefinedglobal(mod, :Out)
3✔
1872
        out = Dict{Int, Any}()
3✔
1873
        @eval mod begin
3✔
1874
            const Out = $(out)
1875
            export Out
1876
        end
1877
        return out
3✔
1878
    end
1879
    return getglobal(mod, :Out)
×
1880
end
1881

1882
function capture_result(n::Ref{Int}, @nospecialize(x))
48✔
1883
    n = n[]
48✔
1884
    mod = Base.MainInclude
48✔
1885
    # TODO: This invokelatest is only required due to backdated constants
1886
    # and should be removed after
1887
    out = isdefinedglobal(mod, :Out) ? invokelatest(getglobal, mod, :Out) : invokelatest(create_global_out!, mod)
51✔
1888
    if x !== out && x !== nothing # remove this?
48✔
1889
        out[n] = x
42✔
1890
    end
1891
    nothing
48✔
1892
end
1893

1894
function set_prompt(repl::LineEditREPL, n::Ref{Int})
3✔
1895
    julia_prompt = repl.interface.modes[1]
3✔
1896
    julia_prompt.prompt = REPL.contextual_prompt(repl, function()
1,704✔
1897
        n[] = repl_eval_counter(julia_prompt.hist)+1
1,701✔
1898
        string("In [", n[], "]: ")
1,701✔
1899
    end)
1900
    nothing
3✔
1901
end
1902

1903
function set_output_prefix(repl::LineEditREPL, n::Ref{Int})
3✔
1904
    julia_prompt = repl.interface.modes[1]
3✔
1905
    if REPL.hascolor(repl)
3✔
1906
        julia_prompt.output_prefix_prefix = Base.text_colors[:red]
3✔
1907
    end
1908
    julia_prompt.output_prefix = () -> string("Out[", n[], "]: ")
45✔
1909
    nothing
3✔
1910
end
1911

1912
function __current_ast_transforms(backend)
1913
    if backend === nothing
3✔
1914
        Base.active_repl_backend !== nothing ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1915
    else
1916
        backend.ast_transforms
3✔
1917
    end
1918
end
1919

1920
function numbered_prompt!(repl::LineEditREPL=Base.active_repl::LineEditREPL, backend=nothing)
1921
    n = Ref{Int}(0)
3✔
1922
    set_prompt(repl, n)
3✔
1923
    set_output_prefix(repl, n)
3✔
1924
    push!(__current_ast_transforms(backend), @nospecialize(ast) -> out_transform(ast, n))
51✔
1925
    return
3✔
1926
end
1927

1928
"""
1929
    Out[n]
1930

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

1934
See also [`ans`](@ref).
1935
"""
1936
Base.MainInclude.Out
1937

1938
end
1939

1940
import .Numbered.numbered_prompt!
1941

1942
# this assignment won't survive precompilation,
1943
# but will stick if REPL is baked into a sysimg.
1944
# Needs to occur after this module is finished.
1945
Base.REPL_MODULE_REF[] = REPL
1946

1947
if Base.generating_output()
1948
    include("precompile.jl")
1949
end
1950

1951
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