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

JuliaLang / julia / #38072

15 May 2025 07:55AM UTC coverage: 25.741% (+0.04%) from 25.703%
#38072

push

local

web-flow
change the Compiler.jl stdlib version to 0.1.0 (#58420)

In JuliaRegistries/General#130304 I proposed naming the placeholder
version of the Compiler stdlib "v0.0.0" and made changes accordingly.
However, after further discussions around adjusting Pkg.jl (c.f.
JuliaLang/PKg.jl#4233), we decided to call it "v0.1.0" instead since the
idea may sound to be an abuse of semver and that versioning wouldn't be
able to handle cases when any changes to the implementation of that
special version are needed in the future.

As a result, this commit changes the version of the Compiler.jl stdlib
implementation maintained in the base from v0.0.0 to v0.1.0.

Since BaseCompiler.jl is a very internal, special package that doesn’t
yet follow proper versioning, there’s no need to worry about ecosystem
impact from this change.

12823 of 49815 relevant lines covered (25.74%)

659237.35 hits per line

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

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

35
function __init__()
2✔
36
    Base.REPL_MODULE_REF[] = REPL
2✔
37
    Base.Experimental.register_error_hint(UndefVarError_REPL_hint, UndefVarError)
2✔
38
    return nothing
2✔
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}
×
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)) =
×
122
        new(repl_channel, response_channel, in_eval, ast_transforms)
123
end
124
REPLBackend() = REPLBackend(Channel(1), Channel(1), false)
×
125

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

133
function destroy(ref::REPLBackendRef, state::Task)
×
134
    if istaskfailed(state)
×
135
        close(ref.repl_channel, TaskFailedException(state))
×
136
        close(ref.response_channel, TaskFailedException(state))
×
137
    end
138
    close(ref.repl_channel)
×
139
    close(ref.response_channel)
×
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)
×
149
    if ex isa Expr
×
150
        h = ex.head
×
151
        if h === :toplevel
×
152
            ex′ = Expr(h)
×
153
            map!(softscope, resize!(ex′.args, length(ex.args)), ex.args)
×
154
            return ex′
×
155
        elseif h in (:meta, :import, :using, :export, :module, :error, :incomplete, :thunk)
×
156
            return ex
×
157
        elseif h === :global && all(x->isa(x, Symbol), ex.args)
×
158
            return ex
×
159
        else
160
            return Expr(:block, Expr(:softscope, true), ex)
×
161
        end
162
    end
163
    return ex
×
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)
×
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
×
171
end
172

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

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

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

215
function collect_names_to_warn!(warnings, locals, current_module::Module, ast)
×
216
    ast isa Expr || return
×
217

218
    # don't recurse through module definitions
219
    ast.head === :module && return
×
220

221
    if Meta.isexpr(ast, :., 2)
×
222
        mod_name, name_being_accessed = ast.args
×
223
        # retrieve the (possibly-nested) module being named here
224
        mods = retrieve_modules(current_module, mod_name)
×
225
        all(x -> x isa Module, mods) || return
×
226
        outer_mod = first(mods)
×
227
        mod = last(mods)
×
228
        if name_being_accessed isa QuoteNode
×
229
            name_being_accessed = name_being_accessed.value
×
230
        end
231
        name_being_accessed isa Symbol || return
×
232
        owner = try
×
233
            which(mod, name_being_accessed)
×
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
×
240
        # Don't warn if the name is public in the module we are accessing it
241
        Base.ispublic(mod, name_being_accessed) && return
×
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
×
244
        push!(warnings, (; outer_mod, mod, owner, name_being_accessed))
×
245
        # no recursion
246
        return
×
247
    elseif Meta.isexpr(ast, :(=), 2)
×
248
        lhs, rhs = ast.args
×
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)
×
252
        # we'll recurse into the RHS only
253
        return collect_names_to_warn!(warnings, locals, current_module, rhs)
×
254
    elseif Meta.isexpr(ast, :function) && length(ast.args) >= 1
×
255

256
        if Meta.isexpr(ast.args[1], :call, 2)
×
257
            func_name, func_args = ast.args[1].args
×
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)
×
261
        end
262
        # fall through to general recursion
263
    end
264

265
    for arg in ast.args
×
266
        collect_names_to_warn!(warnings, locals, current_module, arg)
×
267
    end
×
268

269
    return nothing
×
270
end
271

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

282
function warn_on_non_owning_accesses(current_mod, ast)
×
283
    warnings = collect_qualified_access_warnings(current_mod, ast)
×
284
    for (; outer_mod, mod, owner, name_being_accessed) in warnings
×
285
        print_qualified_access_warning(mod, owner, name_being_accessed)
×
286
    end
×
287
    return ast
×
288
end
289
warn_on_non_owning_accesses(ast) = warn_on_non_owning_accesses(Base.active_module(), ast)
×
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}) =
×
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}) =
×
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))
×
308
    if !isexpr(ast, :toplevel)
×
309
        ast = invokelatest(__repl_entry_lower_with_loc, mod, ast, toplevel_file, toplevel_line)
×
310
        check_for_missing_packages_and_run_hooks(ast)
×
311
        return invokelatest(__repl_entry_eval_expanded_with_loc, mod, ast, toplevel_file, toplevel_line)
×
312
    end
313
    local value=nothing
×
314
    for i = 1:length(ast.args)
×
315
        value = toplevel_eval_with_hooks(mod, ast.args[i], toplevel_file, toplevel_line)
×
316
    end
×
317
    return value
×
318
end
319

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

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

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

401
function modules_to_be_loaded(ast::Expr, mods::Vector{Symbol} = Symbol[])
×
402
    _modules_to_be_loaded!(ast, mods)
×
403
    filter!(mod::Symbol -> !in(mod, (:Base, :Main, :Core)), mods) # Exclude special non-package modules
×
404
    return unique(mods)
×
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)
×
433
    backend.backend_task = Base.current_task()
×
434
    consumer(backend)
×
435
    repl_backend_loop(backend, get_module)
×
436
    return backend
×
437
end
438

439
function repl_backend_loop(backend::REPLBackend, get_module::Function)
×
440
    # include looks at this to determine the relative include path
441
    # nothing means cwd
442
    while true
×
443
        tls = task_local_storage()
×
444
        tls[:SOURCE_PATH] = nothing
×
445
        ast_or_func, show_value = take!(backend.repl_channel)
×
446
        if show_value == -1
×
447
            # exit flag
448
            break
×
449
        end
450
        if show_value == 2 # 2 indicates a function to be called
×
451
            f = ast_or_func
×
452
            try
×
453
                ret = f()
×
454
                put!(backend.response_channel, Pair{Any, Bool}(ret, false))
×
455
            catch
456
                put!(backend.response_channel, Pair{Any, Bool}(current_exceptions(), true))
×
457
            end
458
        else
459
            ast = ast_or_func
×
460
            eval_user_input(ast, backend, get_module())
×
461
        end
462
    end
×
463
    return nothing
×
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
471
    maxbytes::Int
472
    n::Int # max bytes to write
473
end
474
LimitIO(io::IO, maxbytes) = LimitIO(io, maxbytes, 0)
×
475

476
struct LimitIOException <: Exception
477
    maxbytes::Int
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)
×
485

486
function Base.write(io::LimitIO, v::UInt8)
×
487
    io.n > io.maxbytes && throw(LimitIOException(io.maxbytes))
×
488
    n_bytes = write(io.io, v)
×
489
    io.n += n_bytes
×
490
    return n_bytes
×
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))
×
498
    remaining = limiter.maxbytes - limiter.n # >= 0
×
499

500
    # Not enough bytes left; we will print up to the limit, then throw
501
    if remaining < nb
×
502
        if remaining > 0
×
503
            Base.unsafe_write(limiter.io, p, remaining)
×
504
        end
505
        throw(LimitIOException(limiter.maxbytes))
×
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}
×
510
    limiter.n += bytes_written
×
511
    return bytes_written
×
512
end
513

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

518
function show_limited(io::IO, mime::MIME, x)
×
519
    try
×
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
×
523
        wrapped_limiter = IOContext(LimitIO(inner, SHOW_MAXIMUM_BYTES), io)
×
524
        # `show_repl` to allow the hook with special syntax highlighting
525
        show_repl(wrapped_limiter, mime, x)
×
526
    catch e
527
        e isa LimitIOException || rethrow()
×
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)
×
529
    end
530
end
531

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

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

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

558
show_repl(io::IO, ::MIME"text/plain", ex::Expr) =
×
559
    print(io, JuliaSyntaxHighlighting.highlight(
560
        sprint(show, ex, context=IOContext(io, :color => false))))
561

562
function print_response(repl::AbstractREPL, response, show_value::Bool, have_color::Bool)
×
563
    repl.waserror = response[2]
×
564
    with_repl_linfo(repl) do io
×
565
        io = IOContext(io, :module => Base.active_module(repl)::Module)
×
566
        print_response(io, response, backend(repl), show_value, have_color, specialdisplay(repl))
×
567
    end
568
    return nothing
×
569
end
570

571
function repl_display_error(errio::IO, @nospecialize errval)
×
572
    # this will be set to true if types in the stacktrace are truncated
573
    limitflag = Ref(false)
×
574
    errio = IOContext(errio, :stacktrace_types_limited => limitflag)
×
575
    Base.invokelatest(Base.display_error, errio, errval)
×
576
    if limitflag[]
×
577
        print(errio, "Some type information was truncated. Use `show(err)` to see complete types.")
×
578
        println(errio)
×
579
    end
580
    return nothing
×
581
end
582

583
function print_response(errio::IO, response, backend::Union{REPLBackendRef,Nothing}, show_value::Bool, have_color::Bool, specialdisplay::Union{AbstractDisplay,Nothing}=nothing)
×
584
    Base.sigatomic_begin()
×
585
    val, iserr = response
×
586
    while true
×
587
        try
×
588
            Base.sigatomic_end()
×
589
            if iserr
×
590
                val = Base.scrub_repl_backtrace(val)
×
591
                Base.istrivialerror(val) || setglobal!(Base.MainInclude, :err, val)
×
592
                repl_display_error(errio, val)
×
593
            else
594
                if val !== nothing && show_value
×
595
                    val2, iserr = if specialdisplay === nothing
×
596
                        # display calls may require being run on the main thread
597
                        call_on_backend(backend) do
×
598
                            Base.invokelatest(display, val)
×
599
                        end
600
                    else
601
                        call_on_backend(backend) do
×
602
                            Base.invokelatest(display, specialdisplay, val)
×
603
                        end
604
                    end
605
                    if iserr
×
606
                        println(errio, "Error showing value of type ", typeof(val), ":")
×
607
                        throw(val2)
×
608
                    end
609
                end
610
            end
611
            break
×
612
        catch ex
613
            if iserr
×
614
                println(errio) # an error during printing is likely to leave us mid-line
×
615
                println(errio, "SYSTEM (REPL): showing an error caused an error")
×
616
                try
×
617
                    excs = Base.scrub_repl_backtrace(current_exceptions())
×
618
                    setglobal!(Base.MainInclude, :err, excs)
×
619
                    repl_display_error(errio, excs)
×
620
                catch e
621
                    # at this point, only print the name of the type as a Symbol to
622
                    # minimize the possibility of further errors.
623
                    println(errio)
×
624
                    println(errio, "SYSTEM (REPL): caught exception of type ", typeof(e).name.name,
×
625
                            " while trying to handle a nested exception; giving up")
626
                end
627
                break
×
628
            end
629
            val = current_exceptions()
×
630
            iserr = true
×
631
        end
632
    end
×
633
    Base.sigatomic_end()
×
634
    nothing
×
635
end
636

637

638

639
"""
640
    run_repl(repl::AbstractREPL)
641
    run_repl(repl, consumer = backend->nothing; backend_on_current_task = true)
642

643
    Main function to start the REPL
644

645
    consumer is an optional function that takes a REPLBackend as an argument
646
"""
647
function run_repl(repl::AbstractREPL, @nospecialize(consumer = x -> nothing); backend_on_current_task::Bool = true, backend = REPLBackend())
×
648
    backend_ref = REPLBackendRef(backend)
×
649
    cleanup = @task try
×
650
            destroy(backend_ref, t)
×
651
        catch e
652
            Core.print(Core.stderr, "\nINTERNAL ERROR: ")
×
653
            Core.println(Core.stderr, e)
×
654
            Core.println(Core.stderr, catch_backtrace())
×
655
        end
656
    get_module = () -> Base.active_module(repl)
×
657
    if backend_on_current_task
×
658
        t = @async run_frontend(repl, backend_ref)
×
659
        errormonitor(t)
×
660
        Base._wait2(t, cleanup)
×
661
        start_repl_backend(backend, consumer; get_module)
×
662
    else
663
        t = @async start_repl_backend(backend, consumer; get_module)
×
664
        errormonitor(t)
×
665
        Base._wait2(t, cleanup)
×
666
        run_frontend(repl, backend_ref)
×
667
    end
668
    return backend
×
669
end
670

671
## BasicREPL ##
672

673
mutable struct BasicREPL <: AbstractREPL
674
    terminal::TextTerminal
675
    waserror::Bool
676
    frontend_task::Task
677
    BasicREPL(t) = new(t, false)
×
678
end
679

680
outstream(r::BasicREPL) = r.terminal
×
681
hascolor(r::BasicREPL) = hascolor(r.terminal)
×
682

683
function run_frontend(repl::BasicREPL, backend::REPLBackendRef)
×
684
    repl.frontend_task = current_task()
×
685
    d = REPLDisplay(repl)
×
686
    dopushdisplay = !in(d,Base.Multimedia.displays)
×
687
    dopushdisplay && pushdisplay(d)
×
688
    hit_eof = false
×
689
    while true
×
690
        Base.reseteof(repl.terminal)
×
691
        write(repl.terminal, JULIA_PROMPT)
×
692
        line = ""
×
693
        ast = nothing
×
694
        interrupted = false
×
695
        while true
×
696
            try
×
697
                line *= readline(repl.terminal, keep=true)
×
698
            catch e
699
                if isa(e,InterruptException)
×
700
                    try # raise the debugger if present
×
701
                        ccall(:jl_raise_debugger, Int, ())
×
702
                    catch
×
703
                    end
704
                    line = ""
×
705
                    interrupted = true
×
706
                    break
×
707
                elseif isa(e,EOFError)
×
708
                    hit_eof = true
×
709
                    break
×
710
                else
711
                    rethrow()
×
712
                end
713
            end
714
            ast = Base.parse_input_line(line)
×
715
            (isa(ast,Expr) && ast.head === :incomplete) || break
×
716
        end
×
717
        if !isempty(line)
×
718
            response = eval_on_backend(ast, backend)
×
719
            print_response(repl, response, !ends_with_semicolon(line), false)
×
720
        end
721
        write(repl.terminal, '\n')
×
722
        ((!interrupted && isempty(line)) || hit_eof) && break
×
723
    end
×
724
    # terminate backend
725
    put!(backend.repl_channel, (nothing, -1))
×
726
    dopushdisplay && popdisplay(d)
×
727
    nothing
×
728
end
729

730
## LineEditREPL ##
731

732
mutable struct LineEditREPL <: AbstractREPL
733
    t::TextTerminal
734
    hascolor::Bool
735
    prompt_color::String
736
    input_color::String
737
    answer_color::String
738
    shell_color::String
739
    help_color::String
740
    pkg_color::String
741
    history_file::Bool
742
    in_shell::Bool
743
    in_help::Bool
744
    envcolors::Bool
745
    waserror::Bool
746
    specialdisplay::Union{Nothing,AbstractDisplay}
747
    options::Options
748
    mistate::Union{MIState,Nothing}
749
    last_shown_line_infos::Vector{Tuple{String,Int}}
750
    interface::ModalInterface
751
    backendref::REPLBackendRef
752
    frontend_task::Task
753
    function LineEditREPL(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,pkg_color,history_file,in_shell,in_help,envcolors)
×
754
        opts = Options()
×
755
        opts.hascolor = hascolor
×
756
        if !hascolor
×
757
            opts.beep_colors = [""]
×
758
        end
759
        new(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,pkg_color,history_file,in_shell,
×
760
            in_help,envcolors,false,nothing, opts, nothing, Tuple{String,Int}[])
761
    end
762
end
763
outstream(r::LineEditREPL) = (t = r.t; t isa TTYTerminal ? t.out_stream : t)
×
764
specialdisplay(r::LineEditREPL) = r.specialdisplay
×
765
specialdisplay(r::AbstractREPL) = nothing
×
766
terminal(r::LineEditREPL) = r.t
×
767
hascolor(r::LineEditREPL) = r.hascolor
×
768

769
LineEditREPL(t::TextTerminal, hascolor::Bool, envcolors::Bool=false) =
×
770
    LineEditREPL(t, hascolor,
771
        hascolor ? Base.text_colors[:green] : "",
772
        hascolor ? Base.input_color() : "",
773
        hascolor ? Base.answer_color() : "",
774
        hascolor ? Base.text_colors[:red] : "",
775
        hascolor ? Base.text_colors[:yellow] : "",
776
        hascolor ? Base.text_colors[:blue] : "",
777
        false, false, false, envcolors
778
    )
779

780
mutable struct REPLCompletionProvider <: CompletionProvider
781
    modifiers::LineEdit.Modifiers
782
end
783
REPLCompletionProvider() = REPLCompletionProvider(LineEdit.Modifiers())
×
784

785
mutable struct ShellCompletionProvider <: CompletionProvider end
786
struct LatexCompletions <: CompletionProvider end
787

788
Base.active_module((; mistate)::LineEditREPL) = mistate === nothing ? Main : mistate.active_module
×
789
Base.active_module(::AbstractREPL) = Main
×
790
Base.active_module(d::REPLDisplay) = Base.active_module(d.repl)
×
791

792
setmodifiers!(c::CompletionProvider, m::LineEdit.Modifiers) = nothing
×
793

794
setmodifiers!(c::REPLCompletionProvider, m::LineEdit.Modifiers) = c.modifiers = m
×
795

796
"""
797
    activate(mod::Module=Main)
798

799
Set `mod` as the default contextual module in the REPL,
800
both for evaluating expressions and printing them.
801
"""
802
function activate(mod::Module=Main; interactive_utils::Bool=true)
×
803
    mistate = (Base.active_repl::LineEditREPL).mistate
×
804
    mistate === nothing && return nothing
×
805
    mistate.active_module = mod
×
806
    interactive_utils && Base.load_InteractiveUtils(mod)
×
807
    return nothing
×
808
end
809

810
beforecursor(buf::IOBuffer) = String(buf.data[1:buf.ptr-1])
×
811

812
# Convert inclusive-inclusive 1-based char indexing to inclusive-exclusive byte Region.
813
to_region(s, r) = first(r)-1 => (length(r) > 0 ? nextind(s, last(r))-1 : first(r)-1)
×
814

815
function complete_line(c::REPLCompletionProvider, s::PromptState, mod::Module; hint::Bool=false)
×
816
    full = LineEdit.input_string(s)
×
817
    ret, range, should_complete = completions(full, thisind(full, position(s)), mod, c.modifiers.shift, hint)
×
818
    range = to_region(full, range)
×
819
    c.modifiers = LineEdit.Modifiers()
×
820
    return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), range, should_complete
×
821
end
822

823
function complete_line(c::ShellCompletionProvider, s::PromptState; hint::Bool=false)
×
824
    full = LineEdit.input_string(s)
×
825
    ret, range, should_complete = shell_completions(full, thisind(full, position(s)), hint)
×
826
    range = to_region(full, range)
×
827
    return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), range, should_complete
×
828
end
829

830
function complete_line(c::LatexCompletions, s; hint::Bool=false)
×
831
    full = LineEdit.input_string(s)::String
×
832
    ret, range, should_complete = bslash_completions(full, thisind(full, position(s)), hint)[2]
×
833
    range = to_region(full, range)
×
834
    return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), range, should_complete
×
835
end
836

837
with_repl_linfo(f, repl) = f(outstream(repl))
×
838
function with_repl_linfo(f, repl::LineEditREPL)
×
839
    linfos = Tuple{String,Int}[]
×
840
    io = IOContext(outstream(repl), :last_shown_line_infos => linfos)
×
841
    f(io)
×
842
    if !isempty(linfos)
×
843
        repl.last_shown_line_infos = linfos
×
844
    end
845
    nothing
×
846
end
847

848
mutable struct REPLHistoryProvider <: HistoryProvider
849
    history::Vector{String}
850
    file_path::String
851
    history_file::Union{Nothing,IO}
852
    start_idx::Int
853
    cur_idx::Int
854
    last_idx::Int
855
    last_buffer::IOBuffer
856
    last_mode::Union{Nothing,Prompt}
857
    mode_mapping::Dict{Symbol,Prompt}
858
    modes::Vector{Symbol}
859
end
860
REPLHistoryProvider(mode_mapping::Dict{Symbol}) =
×
861
    REPLHistoryProvider(String[], "", nothing, 0, 0, -1, IOBuffer(),
862
                        nothing, mode_mapping, UInt8[])
863

864
invalid_history_message(path::String) = """
×
865
Invalid history file ($path) format:
866
If you have a history file left over from an older version of Julia,
867
try renaming or deleting it.
868
Invalid character: """
869

870
munged_history_message(path::String) = """
×
871
Invalid history file ($path) format:
872
An editor may have converted tabs to spaces at line """
873

874
function hist_open_file(hp::REPLHistoryProvider)
×
875
    f = open(hp.file_path, read=true, write=true, create=true)
×
876
    hp.history_file = f
×
877
    seekend(f)
×
878
end
879

880
function hist_from_file(hp::REPLHistoryProvider, path::String)
×
881
    getline(lines, i) = i > length(lines) ? "" : lines[i]
×
882
    file_lines = readlines(path)
×
883
    countlines = 0
×
884
    while true
×
885
        # First parse the metadata that starts with '#' in particular the REPL mode
886
        countlines += 1
×
887
        line = getline(file_lines, countlines)
×
888
        mode = :julia
×
889
        isempty(line) && break
×
890
        line[1] != '#' &&
×
891
            error(invalid_history_message(path), repr(line[1]), " at line ", countlines)
892
        while !isempty(line)
×
893
            startswith(line, '#') || break
×
894
            if startswith(line, "# mode: ")
×
895
                mode = Symbol(SubString(line, 9))
×
896
            end
897
            countlines += 1
×
898
            line = getline(file_lines, countlines)
×
899
        end
×
900
        isempty(line) && break
×
901

902
        # Now parse the code for the current REPL mode
903
        line[1] == ' '  &&
×
904
            error(munged_history_message(path), countlines)
905
        line[1] != '\t' &&
×
906
            error(invalid_history_message(path), repr(line[1]), " at line ", countlines)
907
        lines = String[]
×
908
        while !isempty(line)
×
909
            push!(lines, chomp(SubString(line, 2)))
×
910
            next_line = getline(file_lines, countlines+1)
×
911
            isempty(next_line) && break
×
912
            first(next_line) == ' '  && error(munged_history_message(path), countlines)
×
913
            # A line not starting with a tab means we are done with code for this entry
914
            first(next_line) != '\t' && break
×
915
            countlines += 1
×
916
            line = getline(file_lines, countlines)
×
917
        end
×
918
        push!(hp.modes, mode)
×
919
        push!(hp.history, join(lines, '\n'))
×
920
    end
×
921
    hp.start_idx = length(hp.history)
×
922
    return hp
×
923
end
924

925
function add_history(hist::REPLHistoryProvider, s::PromptState)
×
926
    str = rstrip(String(take!(copy(s.input_buffer))))
×
927
    isempty(strip(str)) && return
×
928
    mode = mode_idx(hist, LineEdit.mode(s))
×
929
    !isempty(hist.history) &&
×
930
        isequal(mode, hist.modes[end]) && str == hist.history[end] && return
931
    push!(hist.modes, mode)
×
932
    push!(hist.history, str)
×
933
    hist.history_file === nothing && return
×
934
    entry = """
×
935
    # time: $(Libc.strftime("%Y-%m-%d %H:%M:%S %Z", time()))
936
    # mode: $mode
937
    $(replace(str, r"^"ms => "\t"))
×
938
    """
939
    try
×
940
        seekend(hist.history_file)
×
941
    catch err
942
        (err isa SystemError) || rethrow()
×
943
        # File handle might get stale after a while, especially under network file systems
944
        # If this doesn't fix it (e.g. when file is deleted), we'll end up rethrowing anyway
945
        hist_open_file(hist)
×
946
    end
947
    if isfile(hist.file_path)
×
948
        FileWatching.mkpidlock(hist.file_path  * ".pid", stale_age=3) do
×
949
            print(hist.history_file, entry)
×
950
            flush(hist.history_file)
×
951
        end
952
    else # handle eg devnull
953
        print(hist.history_file, entry)
×
954
        flush(hist.history_file)
×
955
    end
956
    nothing
×
957
end
958

959
function history_move(s::Union{LineEdit.MIState,LineEdit.PrefixSearchState}, hist::REPLHistoryProvider, idx::Int, save_idx::Int = hist.cur_idx)
×
960
    max_idx = length(hist.history) + 1
×
961
    @assert 1 <= hist.cur_idx <= max_idx
×
962
    (1 <= idx <= max_idx) || return :none
×
963
    idx != hist.cur_idx || return :none
×
964

965
    # save the current line
966
    if save_idx == max_idx
×
967
        hist.last_mode = LineEdit.mode(s)
×
968
        hist.last_buffer = copy(LineEdit.buffer(s))
×
969
    else
970
        hist.history[save_idx] = LineEdit.input_string(s)
×
971
        hist.modes[save_idx] = mode_idx(hist, LineEdit.mode(s))
×
972
    end
973

974
    # load the saved line
975
    if idx == max_idx
×
976
        last_buffer = hist.last_buffer
×
977
        LineEdit.transition(s, hist.last_mode) do
×
978
            LineEdit.replace_line(s, last_buffer)
×
979
        end
980
        hist.last_mode = nothing
×
981
        hist.last_buffer = IOBuffer()
×
982
    else
983
        if haskey(hist.mode_mapping, hist.modes[idx])
×
984
            LineEdit.transition(s, hist.mode_mapping[hist.modes[idx]]) do
×
985
                LineEdit.replace_line(s, hist.history[idx])
×
986
            end
987
        else
988
            return :skip
×
989
        end
990
    end
991
    hist.cur_idx = idx
×
992

993
    return :ok
×
994
end
995

996
# REPL History can also transitions modes
997
function LineEdit.accept_result_newmode(hist::REPLHistoryProvider)
×
998
    if 1 <= hist.cur_idx <= length(hist.modes)
×
999
        return hist.mode_mapping[hist.modes[hist.cur_idx]]
×
1000
    end
1001
    return nothing
×
1002
end
1003

1004
function history_prev(s::LineEdit.MIState, hist::REPLHistoryProvider,
×
1005
                      num::Int=1, save_idx::Int = hist.cur_idx)
1006
    num <= 0 && return history_next(s, hist, -num, save_idx)
×
1007
    hist.last_idx = -1
×
1008
    m = history_move(s, hist, hist.cur_idx-num, save_idx)
×
1009
    if m === :ok
×
1010
        LineEdit.move_input_start(s)
×
1011
        LineEdit.reset_key_repeats(s) do
×
1012
            LineEdit.move_line_end(s)
×
1013
        end
1014
        return LineEdit.refresh_line(s)
×
1015
    elseif m === :skip
×
1016
        return history_prev(s, hist, num+1, save_idx)
×
1017
    else
1018
        return Terminals.beep(s)
×
1019
    end
1020
end
1021

1022
function history_next(s::LineEdit.MIState, hist::REPLHistoryProvider,
×
1023
                      num::Int=1, save_idx::Int = hist.cur_idx)
1024
    if num == 0
×
1025
        Terminals.beep(s)
×
1026
        return
×
1027
    end
1028
    num < 0 && return history_prev(s, hist, -num, save_idx)
×
1029
    cur_idx = hist.cur_idx
×
1030
    max_idx = length(hist.history) + 1
×
1031
    if cur_idx == max_idx && 0 < hist.last_idx
×
1032
        # issue #6312
1033
        cur_idx = hist.last_idx
×
1034
        hist.last_idx = -1
×
1035
    end
1036
    m = history_move(s, hist, cur_idx+num, save_idx)
×
1037
    if m === :ok
×
1038
        LineEdit.move_input_end(s)
×
1039
        return LineEdit.refresh_line(s)
×
1040
    elseif m === :skip
×
1041
        return history_next(s, hist, num+1, save_idx)
×
1042
    else
1043
        return Terminals.beep(s)
×
1044
    end
1045
end
1046

1047
history_first(s::LineEdit.MIState, hist::REPLHistoryProvider) =
×
1048
    history_prev(s, hist, hist.cur_idx - 1 -
1049
                 (hist.cur_idx > hist.start_idx+1 ? hist.start_idx : 0))
1050

1051
history_last(s::LineEdit.MIState, hist::REPLHistoryProvider) =
×
1052
    history_next(s, hist, length(hist.history) - hist.cur_idx + 1)
1053

1054
function history_move_prefix(s::LineEdit.PrefixSearchState,
×
1055
                             hist::REPLHistoryProvider,
1056
                             prefix::AbstractString,
1057
                             backwards::Bool,
1058
                             cur_idx::Int = hist.cur_idx)
1059
    cur_response = String(take!(copy(LineEdit.buffer(s))))
×
1060
    # when searching forward, start at last_idx
1061
    if !backwards && hist.last_idx > 0
×
1062
        cur_idx = hist.last_idx
×
1063
    end
1064
    hist.last_idx = -1
×
1065
    max_idx = length(hist.history)+1
×
1066
    idxs = backwards ? ((cur_idx-1):-1:1) : ((cur_idx+1):1:max_idx)
×
1067
    for idx in idxs
×
1068
        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)))
×
1069
            m = history_move(s, hist, idx)
×
1070
            if m === :ok
×
1071
                if idx == max_idx
×
1072
                    # on resuming the in-progress edit, leave the cursor where the user last had it
1073
                elseif isempty(prefix)
×
1074
                    # on empty prefix search, move cursor to the end
1075
                    LineEdit.move_input_end(s)
×
1076
                else
1077
                    # otherwise, keep cursor at the prefix position as a visual cue
1078
                    seek(LineEdit.buffer(s), sizeof(prefix))
×
1079
                end
1080
                LineEdit.refresh_line(s)
×
1081
                return :ok
×
1082
            elseif m === :skip
×
1083
                return history_move_prefix(s,hist,prefix,backwards,idx)
×
1084
            end
1085
        end
1086
    end
×
1087
    Terminals.beep(s)
×
1088
    nothing
×
1089
end
1090
history_next_prefix(s::LineEdit.PrefixSearchState, hist::REPLHistoryProvider, prefix::AbstractString) =
×
1091
    history_move_prefix(s, hist, prefix, false)
1092
history_prev_prefix(s::LineEdit.PrefixSearchState, hist::REPLHistoryProvider, prefix::AbstractString) =
×
1093
    history_move_prefix(s, hist, prefix, true)
1094

1095
function history_search(hist::REPLHistoryProvider, query_buffer::IOBuffer, response_buffer::IOBuffer,
×
1096
                        backwards::Bool=false, skip_current::Bool=false)
1097

1098
    qpos = position(query_buffer)
×
1099
    qpos > 0 || return true
×
1100
    searchdata = beforecursor(query_buffer)
×
1101
    response_str = String(take!(copy(response_buffer)))
×
1102

1103
    # Alright, first try to see if the current match still works
1104
    a = position(response_buffer) + 1 # position is zero-indexed
×
1105
    # FIXME: I'm pretty sure this is broken since it uses an index
1106
    # into the search data to index into the response string
1107
    b = a + sizeof(searchdata)
×
1108
    b = b ≤ ncodeunits(response_str) ? prevind(response_str, b) : b-1
×
1109
    b = min(lastindex(response_str), b) # ensure that b is valid
×
1110

1111
    searchstart = backwards ? b : a
×
1112
    if searchdata == response_str[a:b]
×
1113
        if skip_current
×
1114
            searchstart = backwards ? prevind(response_str, b) : nextind(response_str, a)
×
1115
        else
1116
            return true
×
1117
        end
1118
    end
1119

1120
    # Start searching
1121
    # First the current response buffer
1122
    if 1 <= searchstart <= lastindex(response_str)
×
1123
        match = backwards ? findprev(searchdata, response_str, searchstart) :
×
1124
                            findnext(searchdata, response_str, searchstart)
1125
        if match !== nothing
×
1126
            seek(response_buffer, first(match) - 1)
×
1127
            return true
×
1128
        end
1129
    end
1130

1131
    # Now search all the other buffers
1132
    idxs = backwards ? ((hist.cur_idx-1):-1:1) : ((hist.cur_idx+1):1:length(hist.history))
×
1133
    for idx in idxs
×
1134
        h = hist.history[idx]
×
1135
        match = backwards ? findlast(searchdata, h) : findfirst(searchdata, h)
×
1136
        if match !== nothing && h != response_str && haskey(hist.mode_mapping, hist.modes[idx])
×
1137
            truncate(response_buffer, 0)
×
1138
            write(response_buffer, h)
×
1139
            seek(response_buffer, first(match) - 1)
×
1140
            hist.cur_idx = idx
×
1141
            return true
×
1142
        end
1143
    end
×
1144

1145
    return false
×
1146
end
1147

1148
function history_reset_state(hist::REPLHistoryProvider)
×
1149
    if hist.cur_idx != length(hist.history) + 1
×
1150
        hist.last_idx = hist.cur_idx
×
1151
        hist.cur_idx = length(hist.history) + 1
×
1152
    end
1153
    nothing
×
1154
end
1155
LineEdit.reset_state(hist::REPLHistoryProvider) = history_reset_state(hist)
×
1156

1157
function return_callback(s)
×
1158
    ast = Base.parse_input_line(String(take!(copy(LineEdit.buffer(s)))), depwarn=false)
×
1159
    return !(isa(ast, Expr) && ast.head === :incomplete)
×
1160
end
1161

1162
find_hist_file() = get(ENV, "JULIA_HISTORY",
×
1163
                       !isempty(DEPOT_PATH) ? joinpath(DEPOT_PATH[1], "logs", "repl_history.jl") :
1164
                       error("DEPOT_PATH is empty and ENV[\"JULIA_HISTORY\"] not set."))
1165

1166
backend(r::AbstractREPL) = hasproperty(r, :backendref) ? r.backendref : nothing
×
1167

1168

1169
function eval_on_backend(ast, backend::REPLBackendRef)
×
1170
    put!(backend.repl_channel, (ast, 1)) # (f, show_value)
×
1171
    return take!(backend.response_channel) # (val, iserr)
×
1172
end
1173
function call_on_backend(f, backend::REPLBackendRef)
×
1174
    applicable(f) || error("internal error: f is not callable")
×
1175
    put!(backend.repl_channel, (f, 2)) # (f, show_value) 2 indicates function (rather than ast)
×
1176
    return take!(backend.response_channel) # (val, iserr)
×
1177
end
1178
# if no backend just eval (used by tests)
1179
eval_on_backend(ast, backend::Nothing) = error("no backend for eval ast")
×
1180
function call_on_backend(f, backend::Nothing)
×
1181
    try
×
1182
        ret = f()
×
1183
        return (ret, false) # (val, iserr)
×
1184
    catch
1185
        return (current_exceptions(), true)
×
1186
    end
1187
end
1188

1189

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

1214
function reset(repl::LineEditREPL)
×
1215
    raw!(repl.t, false)
×
1216
    hascolor(repl) && print(repl.t, Base.text_colors[:normal])
×
1217
    nothing
×
1218
end
1219

1220
function prepare_next(repl::LineEditREPL)
×
1221
    println(terminal(repl))
×
1222
end
1223

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

1246
repl_filename(repl, hp::REPLHistoryProvider) = "REPL[$(max(length(hp.history)-hp.start_idx, 1))]"
×
1247
repl_filename(repl, hp) = "REPL"
×
1248

1249
const JL_PROMPT_PASTE = Ref(true)
1250
enable_promptpaste(v::Bool) = JL_PROMPT_PASTE[] = v
×
1251

1252
function contextual_prompt(repl::LineEditREPL, prompt::Union{String,Function})
×
1253
    function ()
×
1254
        mod = Base.active_module(repl)
×
1255
        prefix = mod == Main ? "" : string('(', mod, ") ")
×
1256
        pr = prompt isa String ? prompt : prompt()
×
1257
        prefix * pr
×
1258
    end
1259
end
1260

1261
setup_interface(
×
1262
    repl::LineEditREPL;
1263
    # those keyword arguments may be deprecated eventually in favor of the Options mechanism
1264
    hascolor::Bool = repl.options.hascolor,
1265
    extra_repl_keymap::Any = repl.options.extra_keymap
1266
) = setup_interface(repl, hascolor, extra_repl_keymap)
1267

1268

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

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

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

1300
    ############################### Stage I ################################
1301

1302
    # This will provide completions for REPL and help mode
1303
    replc = REPLCompletionProvider()
×
1304

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

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

1326

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

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

1372

1373
    ################################# Stage II #############################
1374

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

1405
    julia_prompt.on_done = respond(x->Base.parse_input_line(x,filename=repl_filename(repl,hp)), repl, julia_prompt)
×
1406

1407

1408
    search_prompt, skeymap = LineEdit.setup_search_keymap(hp)
×
1409
    search_prompt.complete = LatexCompletions()
×
1410

1411
    shell_prompt_len = length(SHELL_PROMPT)
×
1412
    help_prompt_len = length(HELP_PROMPT)
×
1413
    jl_prompt_regex = Regex("^In \\[[0-9]+\\]: |^(?:\\(.+\\) )?$JULIA_PROMPT")
×
1414
    pkg_prompt_regex = Regex("^(?:\\(.+\\) )?$PKG_PROMPT")
×
1415

1416
    # Canonicalize user keymap input
1417
    if isa(extra_repl_keymap, Dict)
×
1418
        extra_repl_keymap = AnyDict[extra_repl_keymap]
×
1419
    end
1420

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

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

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

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

1647
    prefix_prompt, prefix_keymap = LineEdit.setup_prefix_keymap(hp, julia_prompt)
×
1648

1649
    a = Dict{Any,Any}[skeymap, repl_keymap, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
×
1650
    prepend!(a, extra_repl_keymap)
×
1651

1652
    julia_prompt.keymap_dict = LineEdit.keymap(a)
×
1653

1654
    mk = mode_keymap(julia_prompt)
×
1655

1656
    b = Dict{Any,Any}[skeymap, mk, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
×
1657
    prepend!(b, extra_repl_keymap)
×
1658

1659
    shell_mode.keymap_dict = help_mode.keymap_dict = dummy_pkg_mode.keymap_dict = LineEdit.keymap(b)
×
1660

1661
    allprompts = LineEdit.TextInterface[julia_prompt, shell_mode, help_mode, dummy_pkg_mode, search_prompt, prefix_prompt]
×
1662
    return ModalInterface(allprompts)
×
1663
end
1664

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

1684
## StreamREPL ##
1685

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

1698
outstream(s::StreamREPL) = s.stream
×
1699
hascolor(s::StreamREPL) = get(s.stream, :color, false)::Bool
×
1700

1701
answer_color(r::LineEditREPL) = r.envcolors ? Base.answer_color() : r.answer_color
×
1702
answer_color(r::StreamREPL) = r.answer_color
×
1703
input_color(r::LineEditREPL) = r.envcolors ? Base.input_color() : r.input_color
×
1704
input_color(r::StreamREPL) = r.input_color
×
1705

1706
# heuristic function to decide if the presence of a semicolon
1707
# at the end of the expression was intended for suppressing output
1708
function ends_with_semicolon(code)
×
1709
    semi = false
×
1710
    for tok in tokenize(code)
×
1711
        kind(tok) in KSet"Whitespace NewlineWs Comment EndMarker" && continue
×
1712
        semi = kind(tok) == K";"
×
1713
    end
×
1714
    return semi
×
1715
end
1716

1717
function banner(io::IO = stdout; short = false)
×
1718
    if Base.GIT_VERSION_INFO.tagged_commit
×
1719
        commit_string = Base.TAGGED_RELEASE_BANNER
×
1720
    elseif isempty(Base.GIT_VERSION_INFO.commit)
×
1721
        commit_string = ""
×
1722
    else
1723
        days = Int(floor((ccall(:jl_clock_now, Float64, ()) - Base.GIT_VERSION_INFO.fork_master_timestamp) / (60 * 60 * 24)))
×
1724
        days = max(0, days)
×
1725
        unit = days == 1 ? "day" : "days"
×
1726
        distance = Base.GIT_VERSION_INFO.fork_master_distance
×
1727
        commit = Base.GIT_VERSION_INFO.commit_short
×
1728

1729
        if distance == 0
×
1730
            commit_string = "Commit $(commit) ($(days) $(unit) old master)"
×
1731
        else
1732
            branch = Base.GIT_VERSION_INFO.branch
×
1733
            commit_string = "$(branch)/$(commit) (fork: $(distance) commits, $(days) $(unit))"
×
1734
        end
1735
    end
1736

1737
    commit_date = isempty(Base.GIT_VERSION_INFO.date_string) ? "" : " ($(split(Base.GIT_VERSION_INFO.date_string)[1]))"
×
1738

1739
    if get(io, :color, false)::Bool
×
1740
        c = Base.text_colors
×
1741
        tx = c[:normal] # text
×
1742
        jl = c[:normal] # julia
×
1743
        d1 = c[:bold] * c[:blue]    # first dot
×
1744
        d2 = c[:bold] * c[:red]     # second dot
×
1745
        d3 = c[:bold] * c[:green]   # third dot
×
1746
        d4 = c[:bold] * c[:magenta] # fourth dot
×
1747

1748
        if short
×
1749
            print(io,"""
×
1750
              $(d3)o$(tx)  | Version $(VERSION)$(commit_date)
1751
             $(d2)o$(tx) $(d4)o$(tx) | $(commit_string)
1752
            """)
1753
        else
1754
            print(io,"""               $(d3)_$(tx)
×
1755
               $(d1)_$(tx)       $(jl)_$(tx) $(d2)_$(d3)(_)$(d4)_$(tx)     |  Documentation: https://docs.julialang.org
1756
              $(d1)(_)$(jl)     | $(d2)(_)$(tx) $(d4)(_)$(tx)    |
1757
               $(jl)_ _   _| |_  __ _$(tx)   |  Type \"?\" for help, \"]?\" for Pkg help.
1758
              $(jl)| | | | | | |/ _` |$(tx)  |
1759
              $(jl)| | |_| | | | (_| |$(tx)  |  Version $(VERSION)$(commit_date)
1760
             $(jl)_/ |\\__'_|_|_|\\__'_|$(tx)  |  $(commit_string)
1761
            $(jl)|__/$(tx)                   |
1762

1763
            """)
1764
        end
1765
    else
1766
        if short
×
1767
            print(io,"""
×
1768
              o  |  Version $(VERSION)$(commit_date)
1769
             o o |  $(commit_string)
1770
            """)
1771
        else
1772
            print(io,"""
×
1773
                           _
1774
               _       _ _(_)_     |  Documentation: https://docs.julialang.org
1775
              (_)     | (_) (_)    |
1776
               _ _   _| |_  __ _   |  Type \"?\" for help, \"]?\" for Pkg help.
1777
              | | | | | | |/ _` |  |
1778
              | | |_| | | | (_| |  |  Version $(VERSION)$(commit_date)
1779
             _/ |\\__'_|_|_|\\__'_|  |  $(commit_string)
1780
            |__/                   |
1781

1782
            """)
1783
        end
1784
    end
1785
end
1786

1787
function run_frontend(repl::StreamREPL, backend::REPLBackendRef)
×
1788
    repl.frontend_task = current_task()
×
1789
    have_color = hascolor(repl)
×
1790
    banner(repl.stream)
×
1791
    d = REPLDisplay(repl)
×
1792
    dopushdisplay = !in(d,Base.Multimedia.displays)
×
1793
    dopushdisplay && pushdisplay(d)
×
1794
    while !eof(repl.stream)::Bool
×
1795
        if have_color
×
1796
            print(repl.stream,repl.prompt_color)
×
1797
        end
1798
        print(repl.stream, JULIA_PROMPT)
×
1799
        if have_color
×
1800
            print(repl.stream, input_color(repl))
×
1801
        end
1802
        line = readline(repl.stream, keep=true)
×
1803
        if !isempty(line)
×
1804
            ast = Base.parse_input_line(line)
×
1805
            if have_color
×
1806
                print(repl.stream, Base.color_normal)
×
1807
            end
1808
            response = eval_on_backend(ast, backend)
×
1809
            print_response(repl, response, !ends_with_semicolon(line), have_color)
×
1810
        end
1811
    end
×
1812
    # Terminate Backend
1813
    put!(backend.repl_channel, (nothing, -1))
×
1814
    dopushdisplay && popdisplay(d)
×
1815
    nothing
×
1816
end
1817

1818
module Numbered
1819

1820
using ..REPL
1821

1822
__current_ast_transforms() = Base.active_repl_backend !== nothing ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1823

1824
function repl_eval_counter(hp)
×
1825
    return length(hp.history) - hp.start_idx
×
1826
end
1827

1828
function out_transform(@nospecialize(x), n::Ref{Int})
×
1829
    return Expr(:toplevel, get_usings!([], x)..., quote
×
1830
        let __temp_val_a72df459 = $x
×
1831
            $capture_result($n, __temp_val_a72df459)
×
1832
            __temp_val_a72df459
×
1833
        end
1834
    end)
1835
end
1836

1837
function get_usings!(usings, ex)
×
1838
    ex isa Expr || return usings
×
1839
    # get all `using` and `import` statements which are at the top level
1840
    for (i, arg) in enumerate(ex.args)
×
1841
        if Base.isexpr(arg, :toplevel)
×
1842
            get_usings!(usings, arg)
×
1843
        elseif Base.isexpr(arg, [:using, :import])
×
1844
            push!(usings, popat!(ex.args, i))
×
1845
        end
1846
    end
×
1847
    return usings
×
1848
end
1849

1850
function create_global_out!(mod)
×
1851
    if !isdefinedglobal(mod, :Out)
×
1852
        out = Dict{Int, Any}()
×
1853
        @eval mod begin
×
1854
            const Out = $(out)
×
1855
            export Out
×
1856
        end
1857
        return out
×
1858
    end
1859
    return getglobal(mod, Out)
×
1860
end
1861

1862
function capture_result(n::Ref{Int}, @nospecialize(x))
×
1863
    n = n[]
×
1864
    mod = Base.MainInclude
×
1865
    # TODO: This invokelatest is only required due to backdated constants
1866
    # and should be removed after
1867
    out = isdefinedglobal(mod, :Out) ? invokelatest(getglobal, mod, :Out) : invokelatest(create_global_out!, mod)
×
1868
    if x !== out && x !== nothing # remove this?
×
1869
        out[n] = x
×
1870
    end
1871
    nothing
×
1872
end
1873

1874
function set_prompt(repl::LineEditREPL, n::Ref{Int})
×
1875
    julia_prompt = repl.interface.modes[1]
×
1876
    julia_prompt.prompt = function()
×
1877
        n[] = repl_eval_counter(julia_prompt.hist)+1
×
1878
        string("In [", n[], "]: ")
×
1879
    end
1880
    nothing
×
1881
end
1882

1883
function set_output_prefix(repl::LineEditREPL, n::Ref{Int})
×
1884
    julia_prompt = repl.interface.modes[1]
×
1885
    if REPL.hascolor(repl)
×
1886
        julia_prompt.output_prefix_prefix = Base.text_colors[:red]
×
1887
    end
1888
    julia_prompt.output_prefix = () -> string("Out[", n[], "]: ")
×
1889
    nothing
×
1890
end
1891

1892
function __current_ast_transforms(backend)
×
1893
    if backend === nothing
×
1894
        Base.active_repl_backend !== nothing ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1895
    else
1896
        backend.ast_transforms
×
1897
    end
1898
end
1899

1900
function numbered_prompt!(repl::LineEditREPL=Base.active_repl::LineEditREPL, backend=nothing)
×
1901
    n = Ref{Int}(0)
×
1902
    set_prompt(repl, n)
×
1903
    set_output_prefix(repl, n)
×
1904
    push!(__current_ast_transforms(backend), @nospecialize(ast) -> out_transform(ast, n))
×
1905
    return
×
1906
end
1907

1908
"""
1909
    Out[n]
1910

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

1914
See also [`ans`](@ref).
1915
"""
1916
Base.MainInclude.Out
1917

1918
end
1919

1920
import .Numbered.numbered_prompt!
1921

1922
# this assignment won't survive precompilation,
1923
# but will stick if REPL is baked into a sysimg.
1924
# Needs to occur after this module is finished.
1925
Base.REPL_MODULE_REF[] = REPL
1926

1927
if Base.generating_output()
1928
    include("precompile.jl")
1929
end
1930

1931
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