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

JuliaLang / julia / #38034

23 Mar 2025 03:50PM UTC coverage: 20.441% (-2.7%) from 23.153%
#38034

push

local

web-flow
Document how return types and return values should be used in docstrign signature lines (#57583)

9975 of 48800 relevant lines covered (20.44%)

119883.51 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__()
1✔
36
    Base.REPL_MODULE_REF[] = REPL
1✔
37
    Base.Experimental.register_error_hint(UndefVarError_REPL_hint, UndefVarError)
1✔
38
    return nothing
1✔
39
end
40

41
using Base.Meta, Sockets, StyledStrings
42
using JuliaSyntaxHighlighting
43
import InteractiveUtils
44

45
export
46
    AbstractREPL,
47
    BasicREPL,
48
    LineEditREPL,
49
    StreamREPL
50

51
public TerminalMenus
52

53
import Base:
54
    AbstractDisplay,
55
    display,
56
    show,
57
    AnyDict,
58
    ==
59

60
_displaysize(io::IO) = displaysize(io)::Tuple{Int,Int}
×
61

62
include("Terminals.jl")
63
using .Terminals
64

65
abstract type AbstractREPL end
66

67
include("options.jl")
68

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

89
include("REPLCompletions.jl")
90
using .REPLCompletions
91

92
include("TerminalMenus/TerminalMenus.jl")
93
include("docview.jl")
94

95
include("Pkg_beforeload.jl")
96

97
@nospecialize # use only declared type signatures
98

99
answer_color(::AbstractREPL) = ""
×
100

101
const JULIA_PROMPT = "julia> "
102
const PKG_PROMPT = "pkg> "
103
const SHELL_PROMPT = "shell> "
104
const HELP_PROMPT = "help?> "
105

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

118
    REPLBackend(repl_channel, response_channel, in_eval, ast_transforms=copy(repl_ast_transforms)) =
×
119
        new(repl_channel, response_channel, in_eval, ast_transforms)
120
end
121
REPLBackend() = REPLBackend(Channel(1), Channel(1), false)
×
122

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

130
function destroy(ref::REPLBackendRef, state::Task)
×
131
    if istaskfailed(state)
×
132
        close(ref.repl_channel, TaskFailedException(state))
×
133
        close(ref.response_channel, TaskFailedException(state))
×
134
    end
135
    close(ref.repl_channel)
×
136
    close(ref.response_channel)
×
137
end
138

139
"""
140
    softscope(ex)
141

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

163
# Temporary alias until Documenter updates
164
const softscope! = softscope
165

166
function print_qualified_access_warning(mod::Module, owner::Module, name::Symbol)
×
167
    @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
×
168
end
169

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

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

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

212
function collect_names_to_warn!(warnings, locals, current_module::Module, ast)
×
213
    ast isa Expr || return
×
214

215
    # don't recurse through module definitions
216
    ast.head === :module && return
×
217

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

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

262
    for arg in ast.args
×
263
        collect_names_to_warn!(warnings, locals, current_module, arg)
×
264
    end
×
265

266
    return nothing
×
267
end
268

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

279
function warn_on_non_owning_accesses(current_mod, ast)
×
280
    warnings = collect_qualified_access_warnings(current_mod, ast)
×
281
    for (; outer_mod, mod, owner, name_being_accessed) in warnings
×
282
        print_qualified_access_warning(mod, owner, name_being_accessed)
×
283
    end
×
284
    return ast
×
285
end
286
warn_on_non_owning_accesses(ast) = warn_on_non_owning_accesses(Base.active_module(), ast)
×
287

288
const repl_ast_transforms = Any[softscope, warn_on_non_owning_accesses] # defaults for new REPL backends
289

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

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

304
function toplevel_eval_with_hooks(mod::Module, @nospecialize(ast), toplevel_file=Ref{Ptr{UInt8}}(Base.unsafe_convert(Ptr{UInt8}, :REPL)), toplevel_line=Ref{Cint}(1))
×
305
    if !isexpr(ast, :toplevel)
×
306
        ast = invokelatest(__repl_entry_lower_with_loc, mod, ast, toplevel_file, toplevel_line)
×
307
        check_for_missing_packages_and_run_hooks(ast)
×
308
        return invokelatest(__repl_entry_eval_expanded_with_loc, mod, ast, toplevel_file, toplevel_line)
×
309
    end
310
    local value=nothing
×
311
    for i = 1:length(ast.args)
×
312
        value = toplevel_eval_with_hooks(mod, ast.args[i], toplevel_file, toplevel_line)
×
313
    end
×
314
    return value
×
315
end
316

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

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

360
function _modules_to_be_loaded!(ast::Expr, mods::Vector{Symbol})
×
361
    ast.head === :quote && return mods # don't search if it's not going to be run during this eval
×
362
    if ast.head === :using || ast.head === :import
×
363
        for arg in ast.args
×
364
            arg = arg::Expr
×
365
            arg1 = first(arg.args)
×
366
            if arg1 isa Symbol # i.e. `Foo`
×
367
                if arg1 != :. # don't include local import `import .Foo`
×
368
                    push!(mods, arg1)
×
369
                end
370
            else # i.e. `Foo: bar`
371
                sym = first((arg1::Expr).args)::Symbol
×
372
                if sym != :. # don't include local import `import .Foo: a`
×
373
                    push!(mods, sym)
×
374
                end
375
            end
376
        end
×
377
    end
378
    if ast.head !== :thunk
×
379
        for arg in ast.args
×
380
            if isexpr(arg, (:block, :if, :using, :import))
×
381
                _modules_to_be_loaded!(arg, mods)
×
382
            end
383
        end
×
384
    else
385
        code = ast.args[1]
×
386
        for arg in code.code
×
387
            isa(arg, Expr) || continue
×
388
            _modules_to_be_loaded!(arg, mods)
×
389
        end
×
390
    end
391
end
392

393
function modules_to_be_loaded(ast::Expr, mods::Vector{Symbol} = Symbol[])
×
394
    _modules_to_be_loaded!(ast, mods)
×
395
    filter!(mod::Symbol -> !in(mod, (:Base, :Main, :Core)), mods) # Exclude special non-package modules
×
396
    return unique(mods)
×
397
end
398

399
"""
400
    start_repl_backend(repl_channel::Channel, response_channel::Channel)
401

402
    Starts loop for REPL backend
403
    Returns a REPLBackend with backend_task assigned
404

405
    Deprecated since sync / async behavior cannot be selected
406
"""
407
function start_repl_backend(repl_channel::Channel{Any}, response_channel::Channel{Any}
×
408
                            ; get_module::Function = ()->Main)
409
    # Maintain legacy behavior of asynchronous backend
410
    backend = REPLBackend(repl_channel, response_channel, false)
×
411
    # Assignment will be made twice, but will be immediately available
412
    backend.backend_task = @async start_repl_backend(backend; get_module)
×
413
    return backend
×
414
end
415

416
"""
417
    start_repl_backend(backend::REPLBackend)
418

419
    Call directly to run backend loop on current Task.
420
    Use @async for run backend on new Task.
421

422
    Does not return backend until loop is finished.
423
"""
424
function start_repl_backend(backend::REPLBackend,  @nospecialize(consumer = x -> nothing); get_module::Function = ()->Main)
×
425
    backend.backend_task = Base.current_task()
×
426
    consumer(backend)
×
427
    repl_backend_loop(backend, get_module)
×
428
    return backend
×
429
end
430

431
function repl_backend_loop(backend::REPLBackend, get_module::Function)
×
432
    # include looks at this to determine the relative include path
433
    # nothing means cwd
434
    while true
×
435
        tls = task_local_storage()
×
436
        tls[:SOURCE_PATH] = nothing
×
437
        ast_or_func, show_value = take!(backend.repl_channel)
×
438
        if show_value == -1
×
439
            # exit flag
440
            break
×
441
        end
442
        if show_value == 2 # 2 indicates a function to be called
×
443
            f = ast_or_func
×
444
            try
×
445
                ret = f()
×
446
                put!(backend.response_channel, Pair{Any, Bool}(ret, false))
×
447
            catch err
448
                put!(backend.response_channel, Pair{Any, Bool}(err, true))
×
449
            end
450
        else
451
            ast = ast_or_func
×
452
            eval_user_input(ast, backend, get_module())
×
453
        end
454
    end
×
455
    return nothing
×
456
end
457

458
SHOW_MAXIMUM_BYTES::Int = 1_048_576
459

460
# Limit printing during REPL display
461
mutable struct LimitIO{IO_t <: IO} <: IO
462
    io::IO_t
463
    maxbytes::Int
464
    n::Int # max bytes to write
465
end
466
LimitIO(io::IO, maxbytes) = LimitIO(io, maxbytes, 0)
×
467

468
struct LimitIOException <: Exception
469
    maxbytes::Int
470
end
471

472
function Base.showerror(io::IO, e::LimitIOException)
×
473
    print(io, "$LimitIOException: aborted printing after attempting to print more than $(Base.format_bytes(e.maxbytes)) within a `LimitIO`.")
×
474
end
475

476
function Base.write(io::LimitIO, v::UInt8)
×
477
    io.n > io.maxbytes && throw(LimitIOException(io.maxbytes))
×
478
    n_bytes = write(io.io, v)
×
479
    io.n += n_bytes
×
480
    return n_bytes
×
481
end
482

483
# Semantically, we only need to override `Base.write`, but we also
484
# override `unsafe_write` for performance.
485
function Base.unsafe_write(limiter::LimitIO, p::Ptr{UInt8}, nb::UInt)
×
486
    # already exceeded? throw
487
    limiter.n > limiter.maxbytes && throw(LimitIOException(limiter.maxbytes))
×
488
    remaining = limiter.maxbytes - limiter.n # >= 0
×
489

490
    # Not enough bytes left; we will print up to the limit, then throw
491
    if remaining < nb
×
492
        if remaining > 0
×
493
            Base.unsafe_write(limiter.io, p, remaining)
×
494
        end
495
        throw(LimitIOException(limiter.maxbytes))
×
496
    end
497

498
    # We won't hit the limit so we'll write the full `nb` bytes
499
    bytes_written = Base.unsafe_write(limiter.io, p, nb)::Union{Int,UInt}
×
500
    limiter.n += bytes_written
×
501
    return bytes_written
×
502
end
503

504
struct REPLDisplay{Repl<:AbstractREPL} <: AbstractDisplay
505
    repl::Repl
506
end
507

508
function show_limited(io::IO, mime::MIME, x)
×
509
    try
×
510
        # We wrap in a LimitIO to limit the amount of printing.
511
        # We unpack `IOContext`s, since we will pass the properties on the outside.
512
        inner = io isa IOContext ? io.io : io
×
513
        wrapped_limiter = IOContext(LimitIO(inner, SHOW_MAXIMUM_BYTES), io)
×
514
        # `show_repl` to allow the hook with special syntax highlighting
515
        show_repl(wrapped_limiter, mime, x)
×
516
    catch e
517
        e isa LimitIOException || rethrow()
×
518
        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)
×
519
    end
520
end
521

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

544
display(d::REPLDisplay, x) = display(d, MIME("text/plain"), x)
×
545

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

548
show_repl(io::IO, ::MIME"text/plain", ex::Expr) =
×
549
    print(io, JuliaSyntaxHighlighting.highlight(
550
        sprint(show, ex, context=IOContext(io, :color => false))))
551

552
function print_response(repl::AbstractREPL, response, show_value::Bool, have_color::Bool)
×
553
    repl.waserror = response[2]
×
554
    with_repl_linfo(repl) do io
×
555
        io = IOContext(io, :module => Base.active_module(repl)::Module)
×
556
        print_response(io, response, backend(repl), show_value, have_color, specialdisplay(repl))
×
557
    end
558
    return nothing
×
559
end
560

561
function repl_display_error(errio::IO, @nospecialize errval)
×
562
    # this will be set to true if types in the stacktrace are truncated
563
    limitflag = Ref(false)
×
564
    errio = IOContext(errio, :stacktrace_types_limited => limitflag)
×
565
    Base.invokelatest(Base.display_error, errio, errval)
×
566
    if limitflag[]
×
567
        print(errio, "Some type information was truncated. Use `show(err)` to see complete types.")
×
568
        println(errio)
×
569
    end
570
    return nothing
×
571
end
572

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

627

628

629
"""
630
    run_repl(repl::AbstractREPL)
631
    run_repl(repl, consumer = backend->nothing; backend_on_current_task = true)
632

633
    Main function to start the REPL
634

635
    consumer is an optional function that takes a REPLBackend as an argument
636
"""
637
function run_repl(repl::AbstractREPL, @nospecialize(consumer = x -> nothing); backend_on_current_task::Bool = true, backend = REPLBackend())
×
638
    backend_ref = REPLBackendRef(backend)
×
639
    cleanup = @task try
×
640
            destroy(backend_ref, t)
×
641
        catch e
642
            Core.print(Core.stderr, "\nINTERNAL ERROR: ")
×
643
            Core.println(Core.stderr, e)
×
644
            Core.println(Core.stderr, catch_backtrace())
×
645
        end
646
    get_module = () -> Base.active_module(repl)
×
647
    if backend_on_current_task
×
648
        t = @async run_frontend(repl, backend_ref)
×
649
        errormonitor(t)
×
650
        Base._wait2(t, cleanup)
×
651
        start_repl_backend(backend, consumer; get_module)
×
652
    else
653
        t = @async start_repl_backend(backend, consumer; get_module)
×
654
        errormonitor(t)
×
655
        Base._wait2(t, cleanup)
×
656
        run_frontend(repl, backend_ref)
×
657
    end
658
    return backend
×
659
end
660

661
## BasicREPL ##
662

663
mutable struct BasicREPL <: AbstractREPL
664
    terminal::TextTerminal
665
    waserror::Bool
666
    frontend_task::Task
667
    BasicREPL(t) = new(t, false)
×
668
end
669

670
outstream(r::BasicREPL) = r.terminal
×
671
hascolor(r::BasicREPL) = hascolor(r.terminal)
×
672

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

720
## LineEditREPL ##
721

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

759
LineEditREPL(t::TextTerminal, hascolor::Bool, envcolors::Bool=false) =
×
760
    LineEditREPL(t, hascolor,
761
        hascolor ? Base.text_colors[:green] : "",
762
        hascolor ? Base.input_color() : "",
763
        hascolor ? Base.answer_color() : "",
764
        hascolor ? Base.text_colors[:red] : "",
765
        hascolor ? Base.text_colors[:yellow] : "",
766
        hascolor ? Base.text_colors[:blue] : "",
767
        false, false, false, envcolors
768
    )
769

770
mutable struct REPLCompletionProvider <: CompletionProvider
771
    modifiers::LineEdit.Modifiers
772
end
773
REPLCompletionProvider() = REPLCompletionProvider(LineEdit.Modifiers())
×
774

775
mutable struct ShellCompletionProvider <: CompletionProvider end
776
struct LatexCompletions <: CompletionProvider end
777

778
Base.active_module((; mistate)::LineEditREPL) = mistate === nothing ? Main : mistate.active_module
×
779
Base.active_module(::AbstractREPL) = Main
×
780
Base.active_module(d::REPLDisplay) = Base.active_module(d.repl)
×
781

782
setmodifiers!(c::CompletionProvider, m::LineEdit.Modifiers) = nothing
×
783

784
setmodifiers!(c::REPLCompletionProvider, m::LineEdit.Modifiers) = c.modifiers = m
×
785

786
"""
787
    activate(mod::Module=Main)
788

789
Set `mod` as the default contextual module in the REPL,
790
both for evaluating expressions and printing them.
791
"""
792
function activate(mod::Module=Main; interactive_utils::Bool=true)
×
793
    mistate = (Base.active_repl::LineEditREPL).mistate
×
794
    mistate === nothing && return nothing
×
795
    mistate.active_module = mod
×
796
    interactive_utils && Base.load_InteractiveUtils(mod)
×
797
    return nothing
×
798
end
799

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

802
function complete_line(c::REPLCompletionProvider, s::PromptState, mod::Module; hint::Bool=false)
×
803
    partial = beforecursor(s.input_buffer)
×
804
    full = LineEdit.input_string(s)
×
805
    ret, range, should_complete = completions(full, lastindex(partial), mod, c.modifiers.shift, hint)
×
806
    c.modifiers = LineEdit.Modifiers()
×
807
    return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), partial[range], should_complete
×
808
end
809

810
function complete_line(c::ShellCompletionProvider, s::PromptState; hint::Bool=false)
×
811
    # First parse everything up to the current position
812
    partial = beforecursor(s.input_buffer)
×
813
    full = LineEdit.input_string(s)
×
814
    ret, range, should_complete = shell_completions(full, lastindex(partial), hint)
×
815
    return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), partial[range], should_complete
×
816
end
817

818
function complete_line(c::LatexCompletions, s; hint::Bool=false)
×
819
    partial = beforecursor(LineEdit.buffer(s))
×
820
    full = LineEdit.input_string(s)::String
×
821
    ret, range, should_complete = bslash_completions(full, lastindex(partial), hint)[2]
×
822
    return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), partial[range], should_complete
×
823
end
824

825
with_repl_linfo(f, repl) = f(outstream(repl))
×
826
function with_repl_linfo(f, repl::LineEditREPL)
×
827
    linfos = Tuple{String,Int}[]
×
828
    io = IOContext(outstream(repl), :last_shown_line_infos => linfos)
×
829
    f(io)
×
830
    if !isempty(linfos)
×
831
        repl.last_shown_line_infos = linfos
×
832
    end
833
    nothing
×
834
end
835

836
mutable struct REPLHistoryProvider <: HistoryProvider
837
    history::Vector{String}
838
    file_path::String
839
    history_file::Union{Nothing,IO}
840
    start_idx::Int
841
    cur_idx::Int
842
    last_idx::Int
843
    last_buffer::IOBuffer
844
    last_mode::Union{Nothing,Prompt}
845
    mode_mapping::Dict{Symbol,Prompt}
846
    modes::Vector{Symbol}
847
end
848
REPLHistoryProvider(mode_mapping::Dict{Symbol}) =
×
849
    REPLHistoryProvider(String[], "", nothing, 0, 0, -1, IOBuffer(),
850
                        nothing, mode_mapping, UInt8[])
851

852
invalid_history_message(path::String) = """
×
853
Invalid history file ($path) format:
854
If you have a history file left over from an older version of Julia,
855
try renaming or deleting it.
856
Invalid character: """
857

858
munged_history_message(path::String) = """
×
859
Invalid history file ($path) format:
860
An editor may have converted tabs to spaces at line """
861

862
function hist_open_file(hp::REPLHistoryProvider)
×
863
    f = open(hp.file_path, read=true, write=true, create=true)
×
864
    hp.history_file = f
×
865
    seekend(f)
×
866
end
867

868
function hist_from_file(hp::REPLHistoryProvider, path::String)
×
869
    getline(lines, i) = i > length(lines) ? "" : lines[i]
×
870
    file_lines = readlines(path)
×
871
    countlines = 0
×
872
    while true
×
873
        # First parse the metadata that starts with '#' in particular the REPL mode
874
        countlines += 1
×
875
        line = getline(file_lines, countlines)
×
876
        mode = :julia
×
877
        isempty(line) && break
×
878
        line[1] != '#' &&
×
879
            error(invalid_history_message(path), repr(line[1]), " at line ", countlines)
880
        while !isempty(line)
×
881
            startswith(line, '#') || break
×
882
            if startswith(line, "# mode: ")
×
883
                mode = Symbol(SubString(line, 9))
×
884
            end
885
            countlines += 1
×
886
            line = getline(file_lines, countlines)
×
887
        end
×
888
        isempty(line) && break
×
889

890
        # Now parse the code for the current REPL mode
891
        line[1] == ' '  &&
×
892
            error(munged_history_message(path), countlines)
893
        line[1] != '\t' &&
×
894
            error(invalid_history_message(path), repr(line[1]), " at line ", countlines)
895
        lines = String[]
×
896
        while !isempty(line)
×
897
            push!(lines, chomp(SubString(line, 2)))
×
898
            next_line = getline(file_lines, countlines+1)
×
899
            isempty(next_line) && break
×
900
            first(next_line) == ' '  && error(munged_history_message(path), countlines)
×
901
            # A line not starting with a tab means we are done with code for this entry
902
            first(next_line) != '\t' && break
×
903
            countlines += 1
×
904
            line = getline(file_lines, countlines)
×
905
        end
×
906
        push!(hp.modes, mode)
×
907
        push!(hp.history, join(lines, '\n'))
×
908
    end
×
909
    hp.start_idx = length(hp.history)
×
910
    return hp
×
911
end
912

913
function add_history(hist::REPLHistoryProvider, s::PromptState)
×
914
    str = rstrip(String(take!(copy(s.input_buffer))))
×
915
    isempty(strip(str)) && return
×
916
    mode = mode_idx(hist, LineEdit.mode(s))
×
917
    !isempty(hist.history) &&
×
918
        isequal(mode, hist.modes[end]) && str == hist.history[end] && return
919
    push!(hist.modes, mode)
×
920
    push!(hist.history, str)
×
921
    hist.history_file === nothing && return
×
922
    entry = """
×
923
    # time: $(Libc.strftime("%Y-%m-%d %H:%M:%S %Z", time()))
924
    # mode: $mode
925
    $(replace(str, r"^"ms => "\t"))
×
926
    """
927
    # TODO: write-lock history file
928
    try
×
929
        seekend(hist.history_file)
×
930
    catch err
931
        (err isa SystemError) || rethrow()
×
932
        # File handle might get stale after a while, especially under network file systems
933
        # If this doesn't fix it (e.g. when file is deleted), we'll end up rethrowing anyway
934
        hist_open_file(hist)
×
935
    end
936
    print(hist.history_file, entry)
×
937
    flush(hist.history_file)
×
938
    nothing
×
939
end
940

941
function history_move(s::Union{LineEdit.MIState,LineEdit.PrefixSearchState}, hist::REPLHistoryProvider, idx::Int, save_idx::Int = hist.cur_idx)
×
942
    max_idx = length(hist.history) + 1
×
943
    @assert 1 <= hist.cur_idx <= max_idx
×
944
    (1 <= idx <= max_idx) || return :none
×
945
    idx != hist.cur_idx || return :none
×
946

947
    # save the current line
948
    if save_idx == max_idx
×
949
        hist.last_mode = LineEdit.mode(s)
×
950
        hist.last_buffer = copy(LineEdit.buffer(s))
×
951
    else
952
        hist.history[save_idx] = LineEdit.input_string(s)
×
953
        hist.modes[save_idx] = mode_idx(hist, LineEdit.mode(s))
×
954
    end
955

956
    # load the saved line
957
    if idx == max_idx
×
958
        last_buffer = hist.last_buffer
×
959
        LineEdit.transition(s, hist.last_mode) do
×
960
            LineEdit.replace_line(s, last_buffer)
×
961
        end
962
        hist.last_mode = nothing
×
963
        hist.last_buffer = IOBuffer()
×
964
    else
965
        if haskey(hist.mode_mapping, hist.modes[idx])
×
966
            LineEdit.transition(s, hist.mode_mapping[hist.modes[idx]]) do
×
967
                LineEdit.replace_line(s, hist.history[idx])
×
968
            end
969
        else
970
            return :skip
×
971
        end
972
    end
973
    hist.cur_idx = idx
×
974

975
    return :ok
×
976
end
977

978
# REPL History can also transitions modes
979
function LineEdit.accept_result_newmode(hist::REPLHistoryProvider)
×
980
    if 1 <= hist.cur_idx <= length(hist.modes)
×
981
        return hist.mode_mapping[hist.modes[hist.cur_idx]]
×
982
    end
983
    return nothing
×
984
end
985

986
function history_prev(s::LineEdit.MIState, hist::REPLHistoryProvider,
×
987
                      num::Int=1, save_idx::Int = hist.cur_idx)
988
    num <= 0 && return history_next(s, hist, -num, save_idx)
×
989
    hist.last_idx = -1
×
990
    m = history_move(s, hist, hist.cur_idx-num, save_idx)
×
991
    if m === :ok
×
992
        LineEdit.move_input_start(s)
×
993
        LineEdit.reset_key_repeats(s) do
×
994
            LineEdit.move_line_end(s)
×
995
        end
996
        return LineEdit.refresh_line(s)
×
997
    elseif m === :skip
×
998
        return history_prev(s, hist, num+1, save_idx)
×
999
    else
1000
        return Terminals.beep(s)
×
1001
    end
1002
end
1003

1004
function history_next(s::LineEdit.MIState, hist::REPLHistoryProvider,
×
1005
                      num::Int=1, save_idx::Int = hist.cur_idx)
1006
    if num == 0
×
1007
        Terminals.beep(s)
×
1008
        return
×
1009
    end
1010
    num < 0 && return history_prev(s, hist, -num, save_idx)
×
1011
    cur_idx = hist.cur_idx
×
1012
    max_idx = length(hist.history) + 1
×
1013
    if cur_idx == max_idx && 0 < hist.last_idx
×
1014
        # issue #6312
1015
        cur_idx = hist.last_idx
×
1016
        hist.last_idx = -1
×
1017
    end
1018
    m = history_move(s, hist, cur_idx+num, save_idx)
×
1019
    if m === :ok
×
1020
        LineEdit.move_input_end(s)
×
1021
        return LineEdit.refresh_line(s)
×
1022
    elseif m === :skip
×
1023
        return history_next(s, hist, num+1, save_idx)
×
1024
    else
1025
        return Terminals.beep(s)
×
1026
    end
1027
end
1028

1029
history_first(s::LineEdit.MIState, hist::REPLHistoryProvider) =
×
1030
    history_prev(s, hist, hist.cur_idx - 1 -
1031
                 (hist.cur_idx > hist.start_idx+1 ? hist.start_idx : 0))
1032

1033
history_last(s::LineEdit.MIState, hist::REPLHistoryProvider) =
×
1034
    history_next(s, hist, length(hist.history) - hist.cur_idx + 1)
1035

1036
function history_move_prefix(s::LineEdit.PrefixSearchState,
×
1037
                             hist::REPLHistoryProvider,
1038
                             prefix::AbstractString,
1039
                             backwards::Bool,
1040
                             cur_idx::Int = hist.cur_idx)
1041
    cur_response = String(take!(copy(LineEdit.buffer(s))))
×
1042
    # when searching forward, start at last_idx
1043
    if !backwards && hist.last_idx > 0
×
1044
        cur_idx = hist.last_idx
×
1045
    end
1046
    hist.last_idx = -1
×
1047
    max_idx = length(hist.history)+1
×
1048
    idxs = backwards ? ((cur_idx-1):-1:1) : ((cur_idx+1):1:max_idx)
×
1049
    for idx in idxs
×
1050
        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)))
×
1051
            m = history_move(s, hist, idx)
×
1052
            if m === :ok
×
1053
                if idx == max_idx
×
1054
                    # on resuming the in-progress edit, leave the cursor where the user last had it
1055
                elseif isempty(prefix)
×
1056
                    # on empty prefix search, move cursor to the end
1057
                    LineEdit.move_input_end(s)
×
1058
                else
1059
                    # otherwise, keep cursor at the prefix position as a visual cue
1060
                    seek(LineEdit.buffer(s), sizeof(prefix))
×
1061
                end
1062
                LineEdit.refresh_line(s)
×
1063
                return :ok
×
1064
            elseif m === :skip
×
1065
                return history_move_prefix(s,hist,prefix,backwards,idx)
×
1066
            end
1067
        end
1068
    end
×
1069
    Terminals.beep(s)
×
1070
    nothing
×
1071
end
1072
history_next_prefix(s::LineEdit.PrefixSearchState, hist::REPLHistoryProvider, prefix::AbstractString) =
×
1073
    history_move_prefix(s, hist, prefix, false)
1074
history_prev_prefix(s::LineEdit.PrefixSearchState, hist::REPLHistoryProvider, prefix::AbstractString) =
×
1075
    history_move_prefix(s, hist, prefix, true)
1076

1077
function history_search(hist::REPLHistoryProvider, query_buffer::IOBuffer, response_buffer::IOBuffer,
×
1078
                        backwards::Bool=false, skip_current::Bool=false)
1079

1080
    qpos = position(query_buffer)
×
1081
    qpos > 0 || return true
×
1082
    searchdata = beforecursor(query_buffer)
×
1083
    response_str = String(take!(copy(response_buffer)))
×
1084

1085
    # Alright, first try to see if the current match still works
1086
    a = position(response_buffer) + 1 # position is zero-indexed
×
1087
    # FIXME: I'm pretty sure this is broken since it uses an index
1088
    # into the search data to index into the response string
1089
    b = a + sizeof(searchdata)
×
1090
    b = b ≤ ncodeunits(response_str) ? prevind(response_str, b) : b-1
×
1091
    b = min(lastindex(response_str), b) # ensure that b is valid
×
1092

1093
    searchstart = backwards ? b : a
×
1094
    if searchdata == response_str[a:b]
×
1095
        if skip_current
×
1096
            searchstart = backwards ? prevind(response_str, b) : nextind(response_str, a)
×
1097
        else
1098
            return true
×
1099
        end
1100
    end
1101

1102
    # Start searching
1103
    # First the current response buffer
1104
    if 1 <= searchstart <= lastindex(response_str)
×
1105
        match = backwards ? findprev(searchdata, response_str, searchstart) :
×
1106
                            findnext(searchdata, response_str, searchstart)
1107
        if match !== nothing
×
1108
            seek(response_buffer, first(match) - 1)
×
1109
            return true
×
1110
        end
1111
    end
1112

1113
    # Now search all the other buffers
1114
    idxs = backwards ? ((hist.cur_idx-1):-1:1) : ((hist.cur_idx+1):1:length(hist.history))
×
1115
    for idx in idxs
×
1116
        h = hist.history[idx]
×
1117
        match = backwards ? findlast(searchdata, h) : findfirst(searchdata, h)
×
1118
        if match !== nothing && h != response_str && haskey(hist.mode_mapping, hist.modes[idx])
×
1119
            truncate(response_buffer, 0)
×
1120
            write(response_buffer, h)
×
1121
            seek(response_buffer, first(match) - 1)
×
1122
            hist.cur_idx = idx
×
1123
            return true
×
1124
        end
1125
    end
×
1126

1127
    return false
×
1128
end
1129

1130
function history_reset_state(hist::REPLHistoryProvider)
×
1131
    if hist.cur_idx != length(hist.history) + 1
×
1132
        hist.last_idx = hist.cur_idx
×
1133
        hist.cur_idx = length(hist.history) + 1
×
1134
    end
1135
    nothing
×
1136
end
1137
LineEdit.reset_state(hist::REPLHistoryProvider) = history_reset_state(hist)
×
1138

1139
function return_callback(s)
×
1140
    ast = Base.parse_input_line(String(take!(copy(LineEdit.buffer(s)))), depwarn=false)
×
1141
    return !(isa(ast, Expr) && ast.head === :incomplete)
×
1142
end
1143

1144
find_hist_file() = get(ENV, "JULIA_HISTORY",
×
1145
                       !isempty(DEPOT_PATH) ? joinpath(DEPOT_PATH[1], "logs", "repl_history.jl") :
1146
                       error("DEPOT_PATH is empty and ENV[\"JULIA_HISTORY\"] not set."))
1147

1148
backend(r::AbstractREPL) = hasproperty(r, :backendref) ? r.backendref : nothing
×
1149

1150

1151
function eval_with_backend(ast::Expr, backend::REPLBackendRef)
×
1152
    put!(backend.repl_channel, (ast, 1)) # (f, show_value)
×
1153
    return take!(backend.response_channel) # (val, iserr)
×
1154
end
1155
function eval_with_backend(f, backend::REPLBackendRef)
×
1156
    put!(backend.repl_channel, (f, 2)) # (f, show_value) 2 indicates function (rather than ast)
×
1157
    return take!(backend.response_channel) # (val, iserr)
×
1158
end
1159
# if no backend just eval (used by tests)
1160
function eval_with_backend(f, backend::Nothing)
×
1161
    try
×
1162
        ret = f()
×
1163
        return (ret, false) # (val, iserr)
×
1164
    catch err
1165
        return (err, true)
×
1166
    end
1167
end
1168

1169

1170
function respond(f, repl, main; pass_empty::Bool = false, suppress_on_semicolon::Bool = true)
×
1171
    return function do_respond(s::MIState, buf, ok::Bool)
×
1172
        if !ok
×
1173
            return transition(s, :abort)
×
1174
        end
1175
        line = String(take!(buf)::Vector{UInt8})
×
1176
        if !isempty(line) || pass_empty
×
1177
            reset(repl)
×
1178
            local response
×
1179
            try
×
1180
                ast = Base.invokelatest(f, line)
×
1181
                response = eval_with_backend(ast, backend(repl))
×
1182
            catch
1183
                response = Pair{Any, Bool}(current_exceptions(), true)
×
1184
            end
1185
            hide_output = suppress_on_semicolon && ends_with_semicolon(line)
×
1186
            print_response(repl, response, !hide_output, hascolor(repl))
×
1187
        end
1188
        prepare_next(repl)
×
1189
        reset_state(s)
×
1190
        return s.current_mode.sticky ? true : transition(s, main)
×
1191
    end
1192
end
1193

1194
function reset(repl::LineEditREPL)
×
1195
    raw!(repl.t, false)
×
1196
    hascolor(repl) && print(repl.t, Base.text_colors[:normal])
×
1197
    nothing
×
1198
end
1199

1200
function prepare_next(repl::LineEditREPL)
×
1201
    println(terminal(repl))
×
1202
end
1203

1204
function mode_keymap(julia_prompt::Prompt)
×
1205
    AnyDict(
×
1206
    '\b' => function (s::MIState,o...)
×
1207
        if isempty(s) || position(LineEdit.buffer(s)) == 0
×
1208
            buf = copy(LineEdit.buffer(s))
×
1209
            transition(s, julia_prompt) do
×
1210
                LineEdit.state(s, julia_prompt).input_buffer = buf
×
1211
            end
1212
        else
1213
            LineEdit.edit_backspace(s)
×
1214
        end
1215
    end,
1216
    "^C" => function (s::MIState,o...)
×
1217
        LineEdit.move_input_end(s)
×
1218
        LineEdit.refresh_line(s)
×
1219
        print(LineEdit.terminal(s), "^C\n\n")
×
1220
        transition(s, julia_prompt)
×
1221
        transition(s, :reset)
×
1222
        LineEdit.refresh_line(s)
×
1223
    end)
1224
end
1225

1226
repl_filename(repl, hp::REPLHistoryProvider) = "REPL[$(max(length(hp.history)-hp.start_idx, 1))]"
×
1227
repl_filename(repl, hp) = "REPL"
×
1228

1229
const JL_PROMPT_PASTE = Ref(true)
1230
enable_promptpaste(v::Bool) = JL_PROMPT_PASTE[] = v
×
1231

1232
function contextual_prompt(repl::LineEditREPL, prompt::Union{String,Function})
×
1233
    function ()
×
1234
        mod = Base.active_module(repl)
×
1235
        prefix = mod == Main ? "" : string('(', mod, ") ")
×
1236
        pr = prompt isa String ? prompt : prompt()
×
1237
        prefix * pr
×
1238
    end
1239
end
1240

1241
setup_interface(
×
1242
    repl::LineEditREPL;
1243
    # those keyword arguments may be deprecated eventually in favor of the Options mechanism
1244
    hascolor::Bool = repl.options.hascolor,
1245
    extra_repl_keymap::Any = repl.options.extra_keymap
1246
) = setup_interface(repl, hascolor, extra_repl_keymap)
1247

1248

1249
# This non keyword method can be precompiled which is important
1250
function setup_interface(
×
1251
    repl::LineEditREPL,
1252
    hascolor::Bool,
1253
    extra_repl_keymap::Any, # Union{Dict,Vector{<:Dict}},
1254
)
1255
    # The precompile statement emitter has problem outputting valid syntax for the
1256
    # type of `Union{Dict,Vector{<:Dict}}` (see #28808).
1257
    # This function is however important to precompile for REPL startup time, therefore,
1258
    # make the type Any and just assert that we have the correct type below.
1259
    @assert extra_repl_keymap isa Union{Dict,Vector{<:Dict}}
×
1260

1261
    ###
1262
    #
1263
    # This function returns the main interface that describes the REPL
1264
    # functionality, it is called internally by functions that setup a
1265
    # Terminal-based REPL frontend.
1266
    #
1267
    # See run_frontend(repl::LineEditREPL, backend::REPLBackendRef)
1268
    # for usage
1269
    #
1270
    ###
1271

1272
    ###
1273
    # We setup the interface in two stages.
1274
    # First, we set up all components (prompt,rsearch,shell,help)
1275
    # Second, we create keymaps with appropriate transitions between them
1276
    #   and assign them to the components
1277
    #
1278
    ###
1279

1280
    ############################### Stage I ################################
1281

1282
    # This will provide completions for REPL and help mode
1283
    replc = REPLCompletionProvider()
×
1284

1285
    # Set up the main Julia prompt
1286
    julia_prompt = Prompt(contextual_prompt(repl, JULIA_PROMPT);
×
1287
        # Copy colors from the prompt object
1288
        prompt_prefix = hascolor ? repl.prompt_color : "",
1289
        prompt_suffix = hascolor ?
1290
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1291
        repl = repl,
1292
        complete = replc,
1293
        on_enter = return_callback)
1294

1295
    # Setup help mode
1296
    help_mode = Prompt(contextual_prompt(repl, HELP_PROMPT),
×
1297
        prompt_prefix = hascolor ? repl.help_color : "",
1298
        prompt_suffix = hascolor ?
1299
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1300
        repl = repl,
1301
        complete = replc,
1302
        # When we're done transform the entered line into a call to helpmode function
1303
        on_done = respond(line::String->helpmode(outstream(repl), line, repl.mistate.active_module),
×
1304
                          repl, julia_prompt, pass_empty=true, suppress_on_semicolon=false))
1305

1306

1307
    # Set up shell mode
1308
    shell_mode = Prompt(SHELL_PROMPT;
×
1309
        prompt_prefix = hascolor ? repl.shell_color : "",
1310
        prompt_suffix = hascolor ?
1311
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1312
        repl = repl,
1313
        complete = ShellCompletionProvider(),
1314
        # Transform "foo bar baz" into `foo bar baz` (shell quoting)
1315
        # and pass into Base.repl_cmd for processing (handles `ls` and `cd`
1316
        # special)
1317
        on_done = respond(repl, julia_prompt) do line
1318
            Expr(:call, :(Base.repl_cmd),
×
1319
                :(Base.cmd_gen($(Base.shell_parse(line::String)[1]))),
1320
                outstream(repl))
1321
        end,
1322
        sticky = true)
1323

1324
    # Set up dummy Pkg mode that will be replaced once Pkg is loaded
1325
    # use 6 dots to occupy the same space as the most likely "@v1.xx" env name
1326
    dummy_pkg_mode = Prompt(Pkg_promptf,
×
1327
        prompt_prefix = hascolor ? repl.pkg_color : "",
1328
        prompt_suffix = hascolor ?
1329
        (repl.envcolors ? Base.input_color : repl.input_color) : "",
1330
        repl = repl,
1331
        complete = LineEdit.EmptyCompletionProvider(),
1332
        on_done = respond(line->nothing, repl, julia_prompt),
×
1333
        on_enter = function (s::MIState)
×
1334
                # This is hit when the user tries to execute a command before the real Pkg mode has been
1335
                # switched to. Ok to do this even if Pkg is loading on the other task because of the loading lock.
1336
                REPLExt = load_pkg()
×
1337
                if REPLExt isa Module && isdefined(REPLExt, :PkgCompletionProvider)
×
1338
                    for mode in repl.interface.modes
×
1339
                        if mode isa LineEdit.Prompt && mode.complete isa REPLExt.PkgCompletionProvider
×
1340
                            # pkg mode
1341
                            buf = copy(LineEdit.buffer(s))
×
1342
                            transition(s, mode) do
×
1343
                                LineEdit.state(s, mode).input_buffer = buf
×
1344
                            end
1345
                        end
1346
                    end
×
1347
                end
1348
                return true
×
1349
            end,
1350
        sticky = true)
1351

1352

1353
    ################################# Stage II #############################
1354

1355
    # Setup history
1356
    # We will have a unified history for all REPL modes
1357
    hp = REPLHistoryProvider(Dict{Symbol,Prompt}(:julia => julia_prompt,
×
1358
                                                 :shell => shell_mode,
1359
                                                 :help  => help_mode,
1360
                                                 :pkg  => dummy_pkg_mode))
1361
    if repl.history_file
×
1362
        try
×
1363
            hist_path = find_hist_file()
×
1364
            mkpath(dirname(hist_path))
×
1365
            hp.file_path = hist_path
×
1366
            hist_open_file(hp)
×
1367
            finalizer(replc) do replc
×
1368
                close(hp.history_file)
×
1369
            end
1370
            hist_from_file(hp, hist_path)
×
1371
        catch
1372
            # use REPL.hascolor to avoid using the local variable with the same name
1373
            print_response(repl, Pair{Any, Bool}(current_exceptions(), true), true, REPL.hascolor(repl))
×
1374
            println(outstream(repl))
×
1375
            @info "Disabling history file for this session"
×
1376
            repl.history_file = false
×
1377
        end
1378
    end
1379
    history_reset_state(hp)
×
1380
    julia_prompt.hist = hp
×
1381
    shell_mode.hist = hp
×
1382
    help_mode.hist = hp
×
1383
    dummy_pkg_mode.hist = hp
×
1384

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

1387

1388
    search_prompt, skeymap = LineEdit.setup_search_keymap(hp)
×
1389
    search_prompt.complete = LatexCompletions()
×
1390

1391
    shell_prompt_len = length(SHELL_PROMPT)
×
1392
    help_prompt_len = length(HELP_PROMPT)
×
1393
    jl_prompt_regex = Regex("^In \\[[0-9]+\\]: |^(?:\\(.+\\) )?$JULIA_PROMPT")
×
1394
    pkg_prompt_regex = Regex("^(?:\\(.+\\) )?$PKG_PROMPT")
×
1395

1396
    # Canonicalize user keymap input
1397
    if isa(extra_repl_keymap, Dict)
×
1398
        extra_repl_keymap = AnyDict[extra_repl_keymap]
×
1399
    end
1400

1401
    repl_keymap = AnyDict(
×
1402
        ';' => function (s::MIState,o...)
×
1403
            if isempty(s) || position(LineEdit.buffer(s)) == 0
×
1404
                buf = copy(LineEdit.buffer(s))
×
1405
                transition(s, shell_mode) do
×
1406
                    LineEdit.state(s, shell_mode).input_buffer = buf
×
1407
                end
1408
            else
1409
                edit_insert(s, ';')
×
1410
                LineEdit.check_show_hint(s)
×
1411
            end
1412
        end,
1413
        '?' => function (s::MIState,o...)
×
1414
            if isempty(s) || position(LineEdit.buffer(s)) == 0
×
1415
                buf = copy(LineEdit.buffer(s))
×
1416
                transition(s, help_mode) do
×
1417
                    LineEdit.state(s, help_mode).input_buffer = buf
×
1418
                end
1419
            else
1420
                edit_insert(s, '?')
×
1421
                LineEdit.check_show_hint(s)
×
1422
            end
1423
        end,
1424
        ']' => function (s::MIState,o...)
×
1425
            if isempty(s) || position(LineEdit.buffer(s)) == 0
×
1426
                buf = copy(LineEdit.buffer(s))
×
1427
                transition(s, dummy_pkg_mode) do
×
1428
                    LineEdit.state(s, dummy_pkg_mode).input_buffer = buf
×
1429
                end
1430
                # load Pkg on another thread if available so that typing in the dummy Pkg prompt
1431
                # isn't blocked, but instruct the main REPL task to do the transition via s.async_channel
1432
                t_replswitch = Threads.@spawn begin
×
1433
                    REPLExt = load_pkg()
×
1434
                    if REPLExt isa Module && isdefined(REPLExt, :PkgCompletionProvider)
×
1435
                        put!(s.async_channel,
×
1436
                            function (s::MIState)
×
1437
                                LineEdit.mode(s) === dummy_pkg_mode || return :ok
×
1438
                                for mode in repl.interface.modes
×
1439
                                    if mode isa LineEdit.Prompt && mode.complete isa REPLExt.PkgCompletionProvider
×
1440
                                        buf = copy(LineEdit.buffer(s))
×
1441
                                        transition(s, mode) do
×
1442
                                            LineEdit.state(s, mode).input_buffer = buf
×
1443
                                        end
1444
                                        if !isempty(s)
×
1445
                                            @invokelatest(LineEdit.check_show_hint(s))
×
1446
                                        end
1447
                                        break
×
1448
                                    end
1449
                                end
×
1450
                                return :ok
×
1451
                            end
1452
                        )
1453
                    end
1454
                end
1455
                Base.errormonitor(t_replswitch)
×
1456
            else
1457
                edit_insert(s, ']')
×
1458
                LineEdit.check_show_hint(s)
×
1459
            end
1460
        end,
1461

1462
        # Bracketed Paste Mode
1463
        "\e[200~" => (s::MIState,o...)->begin
×
1464
            input = LineEdit.bracketed_paste(s) # read directly from s until reaching the end-bracketed-paste marker
×
1465
            sbuffer = LineEdit.buffer(s)
×
1466
            curspos = position(sbuffer)
×
1467
            seek(sbuffer, 0)
×
1468
            shouldeval = (bytesavailable(sbuffer) == curspos && !occursin(UInt8('\n'), sbuffer))
×
1469
            seek(sbuffer, curspos)
×
1470
            if curspos == 0
×
1471
                # if pasting at the beginning, strip leading whitespace
1472
                input = lstrip(input)
×
1473
            end
1474
            if !shouldeval
×
1475
                # when pasting in the middle of input, just paste in place
1476
                # don't try to execute all the WIP, since that's rather confusing
1477
                # and is often ill-defined how it should behave
1478
                edit_insert(s, input)
×
1479
                return
×
1480
            end
1481
            LineEdit.push_undo(s)
×
1482
            edit_insert(sbuffer, input)
×
1483
            input = String(take!(sbuffer))
×
1484
            oldpos = firstindex(input)
×
1485
            firstline = true
×
1486
            isprompt_paste = false
×
1487
            curr_prompt_len = 0
×
1488
            pasting_help = false
×
1489

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

1602
        # Open the editor at the location of a stackframe or method
1603
        # This is accessing a contextual variable that gets set in
1604
        # the show_backtrace and show_method_table functions.
1605
        "^Q" => (s::MIState, o...) -> begin
×
1606
            linfos = repl.last_shown_line_infos
×
1607
            str = String(take!(LineEdit.buffer(s)))
×
1608
            n = tryparse(Int, str)
×
1609
            n === nothing && @goto writeback
×
1610
            if n <= 0 || n > length(linfos) || startswith(linfos[n][1], "REPL[")
×
1611
                @goto writeback
×
1612
            end
1613
            try
×
1614
                InteractiveUtils.edit(Base.fixup_stdlib_path(linfos[n][1]), linfos[n][2])
×
1615
            catch ex
1616
                ex isa ProcessFailedException || ex isa Base.IOError || ex isa SystemError || rethrow()
×
1617
                @info "edit failed" _exception=ex
×
1618
            end
1619
            LineEdit.refresh_line(s)
×
1620
            return
×
1621
            @label writeback
×
1622
            write(LineEdit.buffer(s), str)
×
1623
            return
×
1624
        end,
1625
    )
1626

1627
    prefix_prompt, prefix_keymap = LineEdit.setup_prefix_keymap(hp, julia_prompt)
×
1628

1629
    a = Dict{Any,Any}[skeymap, repl_keymap, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
×
1630
    prepend!(a, extra_repl_keymap)
×
1631

1632
    julia_prompt.keymap_dict = LineEdit.keymap(a)
×
1633

1634
    mk = mode_keymap(julia_prompt)
×
1635

1636
    b = Dict{Any,Any}[skeymap, mk, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
×
1637
    prepend!(b, extra_repl_keymap)
×
1638

1639
    shell_mode.keymap_dict = help_mode.keymap_dict = dummy_pkg_mode.keymap_dict = LineEdit.keymap(b)
×
1640

1641
    allprompts = LineEdit.TextInterface[julia_prompt, shell_mode, help_mode, dummy_pkg_mode, search_prompt, prefix_prompt]
×
1642
    return ModalInterface(allprompts)
×
1643
end
1644

1645
function run_frontend(repl::LineEditREPL, backend::REPLBackendRef)
×
1646
    repl.frontend_task = current_task()
×
1647
    d = REPLDisplay(repl)
×
1648
    dopushdisplay = repl.specialdisplay === nothing && !in(d,Base.Multimedia.displays)
×
1649
    dopushdisplay && pushdisplay(d)
×
1650
    if !isdefined(repl,:interface)
×
1651
        interface = repl.interface = setup_interface(repl)
×
1652
    else
1653
        interface = repl.interface
×
1654
    end
1655
    repl.backendref = backend
×
1656
    repl.mistate = LineEdit.init_state(terminal(repl), interface)
×
1657
    run_interface(terminal(repl), interface, repl.mistate)
×
1658
    # Terminate Backend
1659
    put!(backend.repl_channel, (nothing, -1))
×
1660
    dopushdisplay && popdisplay(d)
×
1661
    nothing
×
1662
end
1663

1664
## StreamREPL ##
1665

1666
mutable struct StreamREPL <: AbstractREPL
1667
    stream::IO
1668
    prompt_color::String
1669
    input_color::String
1670
    answer_color::String
1671
    waserror::Bool
1672
    frontend_task::Task
1673
    StreamREPL(stream,pc,ic,ac) = new(stream,pc,ic,ac,false)
×
1674
end
1675
StreamREPL(stream::IO) = StreamREPL(stream, Base.text_colors[:green], Base.input_color(), Base.answer_color())
×
1676
run_repl(stream::IO) = run_repl(StreamREPL(stream))
×
1677

1678
outstream(s::StreamREPL) = s.stream
×
1679
hascolor(s::StreamREPL) = get(s.stream, :color, false)::Bool
×
1680

1681
answer_color(r::LineEditREPL) = r.envcolors ? Base.answer_color() : r.answer_color
×
1682
answer_color(r::StreamREPL) = r.answer_color
×
1683
input_color(r::LineEditREPL) = r.envcolors ? Base.input_color() : r.input_color
×
1684
input_color(r::StreamREPL) = r.input_color
×
1685

1686
let matchend = Dict("\"" => r"\"", "\"\"\"" => r"\"\"\"", "'" => r"'",
1687
    "`" => r"`", "```" => r"```", "#" => r"$"m, "#=" => r"=#|#=")
1688
    global _rm_strings_and_comments
1689
    function _rm_strings_and_comments(code::Union{String,SubString{String}})
×
1690
        buf = IOBuffer(sizehint = sizeof(code))
×
1691
        pos = 1
×
1692
        while true
×
1693
            i = findnext(r"\"(?!\"\")|\"\"\"|'|`(?!``)|```|#(?!=)|#=", code, pos)
×
1694
            isnothing(i) && break
×
1695
            match = SubString(code, i)
×
1696
            j = findnext(matchend[match]::Regex, code, nextind(code, last(i)))
×
1697
            if match == "#=" # possibly nested
×
1698
                nested = 1
×
1699
                while j !== nothing
×
1700
                    nested += SubString(code, j) == "#=" ? +1 : -1
×
1701
                    iszero(nested) && break
×
1702
                    j = findnext(r"=#|#=", code, nextind(code, last(j)))
×
1703
                end
×
1704
            elseif match[1] != '#' # quote match: check non-escaped
×
1705
                while j !== nothing
×
1706
                    notbackslash = findprev(!=('\\'), code, prevind(code, first(j)))::Int
×
1707
                    isodd(first(j) - notbackslash) && break # not escaped
×
1708
                    j = findnext(matchend[match]::Regex, code, nextind(code, first(j)))
×
1709
                end
×
1710
            end
1711
            isnothing(j) && break
×
1712
            if match[1] == '#'
×
1713
                print(buf, SubString(code, pos, prevind(code, first(i))))
×
1714
            else
1715
                print(buf, SubString(code, pos, last(i)), ' ', SubString(code, j))
×
1716
            end
1717
            pos = nextind(code, last(j))
×
1718
        end
×
1719
        print(buf, SubString(code, pos, lastindex(code)))
×
1720
        return String(take!(buf))
×
1721
    end
1722
end
1723

1724
# heuristic function to decide if the presence of a semicolon
1725
# at the end of the expression was intended for suppressing output
1726
ends_with_semicolon(code::AbstractString) = ends_with_semicolon(String(code))
×
1727
ends_with_semicolon(code::Union{String,SubString{String}}) =
×
1728
    contains(_rm_strings_and_comments(code), r";\s*$")
×
1729

1730
function banner(io::IO = stdout; short = false)
×
1731
    if Base.GIT_VERSION_INFO.tagged_commit
×
1732
        commit_string = Base.TAGGED_RELEASE_BANNER
×
1733
    elseif isempty(Base.GIT_VERSION_INFO.commit)
×
1734
        commit_string = ""
×
1735
    else
1736
        days = Int(floor((ccall(:jl_clock_now, Float64, ()) - Base.GIT_VERSION_INFO.fork_master_timestamp) / (60 * 60 * 24)))
×
1737
        days = max(0, days)
×
1738
        unit = days == 1 ? "day" : "days"
×
1739
        distance = Base.GIT_VERSION_INFO.fork_master_distance
×
1740
        commit = Base.GIT_VERSION_INFO.commit_short
×
1741

1742
        if distance == 0
×
1743
            commit_string = "Commit $(commit) ($(days) $(unit) old master)"
×
1744
        else
1745
            branch = Base.GIT_VERSION_INFO.branch
×
1746
            commit_string = "$(branch)/$(commit) (fork: $(distance) commits, $(days) $(unit))"
×
1747
        end
1748
    end
1749

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

1752
    if get(io, :color, false)::Bool
×
1753
        c = Base.text_colors
×
1754
        tx = c[:normal] # text
×
1755
        jl = c[:normal] # julia
×
1756
        d1 = c[:bold] * c[:blue]    # first dot
×
1757
        d2 = c[:bold] * c[:red]     # second dot
×
1758
        d3 = c[:bold] * c[:green]   # third dot
×
1759
        d4 = c[:bold] * c[:magenta] # fourth dot
×
1760

1761
        if short
×
1762
            print(io,"""
×
1763
              $(d3)o$(tx)  | Version $(VERSION)$(commit_date)
1764
             $(d2)o$(tx) $(d4)o$(tx) | $(commit_string)
1765
            """)
1766
        else
1767
            print(io,"""               $(d3)_$(tx)
×
1768
               $(d1)_$(tx)       $(jl)_$(tx) $(d2)_$(d3)(_)$(d4)_$(tx)     |  Documentation: https://docs.julialang.org
1769
              $(d1)(_)$(jl)     | $(d2)(_)$(tx) $(d4)(_)$(tx)    |
1770
               $(jl)_ _   _| |_  __ _$(tx)   |  Type \"?\" for help, \"]?\" for Pkg help.
1771
              $(jl)| | | | | | |/ _` |$(tx)  |
1772
              $(jl)| | |_| | | | (_| |$(tx)  |  Version $(VERSION)$(commit_date)
1773
             $(jl)_/ |\\__'_|_|_|\\__'_|$(tx)  |  $(commit_string)
1774
            $(jl)|__/$(tx)                   |
1775

1776
            """)
1777
        end
1778
    else
1779
        if short
×
1780
            print(io,"""
×
1781
              o  |  Version $(VERSION)$(commit_date)
1782
             o o |  $(commit_string)
1783
            """)
1784
        else
1785
            print(io,"""
×
1786
                           _
1787
               _       _ _(_)_     |  Documentation: https://docs.julialang.org
1788
              (_)     | (_) (_)    |
1789
               _ _   _| |_  __ _   |  Type \"?\" for help, \"]?\" for Pkg help.
1790
              | | | | | | |/ _` |  |
1791
              | | |_| | | | (_| |  |  Version $(VERSION)$(commit_date)
1792
             _/ |\\__'_|_|_|\\__'_|  |  $(commit_string)
1793
            |__/                   |
1794

1795
            """)
1796
        end
1797
    end
1798
end
1799

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

1831
module Numbered
1832

1833
using ..REPL
1834

1835
__current_ast_transforms() = Base.active_repl_backend !== nothing ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1836

1837
function repl_eval_counter(hp)
×
1838
    return length(hp.history) - hp.start_idx
×
1839
end
1840

1841
function out_transform(@nospecialize(x), n::Ref{Int})
×
1842
    return Expr(:toplevel, get_usings!([], x)..., quote
×
1843
        let __temp_val_a72df459 = $x
×
1844
            $capture_result($n, __temp_val_a72df459)
×
1845
            __temp_val_a72df459
×
1846
        end
1847
    end)
1848
end
1849

1850
function get_usings!(usings, ex)
×
1851
    ex isa Expr || return usings
×
1852
    # get all `using` and `import` statements which are at the top level
1853
    for (i, arg) in enumerate(ex.args)
×
1854
        if Base.isexpr(arg, :toplevel)
×
1855
            get_usings!(usings, arg)
×
1856
        elseif Base.isexpr(arg, [:using, :import])
×
1857
            push!(usings, popat!(ex.args, i))
×
1858
        end
1859
    end
×
1860
    return usings
×
1861
end
1862

1863
function create_global_out!(mod)
×
1864
    if !isdefinedglobal(mod, :Out)
×
1865
        out = Dict{Int, Any}()
×
1866
        @eval mod begin
×
1867
            const Out = $(out)
×
1868
            export Out
×
1869
        end
1870
        return out
×
1871
    end
1872
    return getglobal(mod, Out)
×
1873
end
1874

1875
function capture_result(n::Ref{Int}, @nospecialize(x))
×
1876
    n = n[]
×
1877
    mod = Base.MainInclude
×
1878
    # TODO: This invokelatest is only required due to backdated constants
1879
    # and should be removed after
1880
    out = isdefinedglobal(mod, :Out) ? invokelatest(getglobal, mod, :Out) : invokelatest(create_global_out!, mod)
×
1881
    if x !== out && x !== nothing # remove this?
×
1882
        out[n] = x
×
1883
    end
1884
    nothing
×
1885
end
1886

1887
function set_prompt(repl::LineEditREPL, n::Ref{Int})
×
1888
    julia_prompt = repl.interface.modes[1]
×
1889
    julia_prompt.prompt = function()
×
1890
        n[] = repl_eval_counter(julia_prompt.hist)+1
×
1891
        string("In [", n[], "]: ")
×
1892
    end
1893
    nothing
×
1894
end
1895

1896
function set_output_prefix(repl::LineEditREPL, n::Ref{Int})
×
1897
    julia_prompt = repl.interface.modes[1]
×
1898
    if REPL.hascolor(repl)
×
1899
        julia_prompt.output_prefix_prefix = Base.text_colors[:red]
×
1900
    end
1901
    julia_prompt.output_prefix = () -> string("Out[", n[], "]: ")
×
1902
    nothing
×
1903
end
1904

1905
function __current_ast_transforms(backend)
×
1906
    if backend === nothing
×
1907
        Base.active_repl_backend !== nothing ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1908
    else
1909
        backend.ast_transforms
×
1910
    end
1911
end
1912

1913
function numbered_prompt!(repl::LineEditREPL=Base.active_repl::LineEditREPL, backend=nothing)
×
1914
    n = Ref{Int}(0)
×
1915
    set_prompt(repl, n)
×
1916
    set_output_prefix(repl, n)
×
1917
    push!(__current_ast_transforms(backend), @nospecialize(ast) -> out_transform(ast, n))
×
1918
    return
×
1919
end
1920

1921
"""
1922
    Out[n]
1923

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

1927
See also [`ans`](@ref).
1928
"""
1929
Base.MainInclude.Out
1930

1931
end
1932

1933
import .Numbered.numbered_prompt!
1934

1935
# this assignment won't survive precompilation,
1936
# but will stick if REPL is baked into a sysimg.
1937
# Needs to occur after this module is finished.
1938
Base.REPL_MODULE_REF[] = REPL
1939

1940
if Base.generating_output()
1941
    include("precompile.jl")
1942
end
1943

1944
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