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

JuliaLang / julia / 1298

07 Oct 2025 03:42PM UTC coverage: 70.016% (-0.02%) from 70.031%
1298

push

buildkite

web-flow
 set VERSION to 1.12.0 (#59757)

53617 of 76578 relevant lines covered (70.02%)

12240791.26 hits per line

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

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

35
function __init__()
14✔
36
    Base.REPL_MODULE_REF[] = REPL
14✔
37
    Base.Experimental.register_error_hint(UndefVarError_REPL_hint, UndefVarError)
14✔
38
    return nothing
14✔
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}
189✔
61

62
using Base.Terminals
63

64
abstract type AbstractREPL end
65

66
include("options.jl")
67

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

88
include("SyntaxUtil.jl")
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)) =
219✔
119
        new(repl_channel, response_channel, in_eval, ast_transforms)
120
end
121
REPLBackend() = REPLBackend(Channel(1), Channel(1), false)
144✔
122

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

130
function destroy(ref::REPLBackendRef, state::Task)
131
    if istaskfailed(state)
66✔
132
        close(ref.repl_channel, TaskFailedException(state))
×
133
        close(ref.response_channel, TaskFailedException(state))
×
134
    end
135
    close(ref.repl_channel)
66✔
136
    close(ref.response_channel)
66✔
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)
1,108✔
146
    if ex isa Expr
1,108✔
147
        h = ex.head
715✔
148
        if h === :toplevel
715✔
149
            ex′ = Expr(h)
405✔
150
            map!(softscope, resize!(ex′.args, length(ex.args)), ex.args)
405✔
151
            return ex′
405✔
152
        elseif h in (:meta, :import, :using, :export, :module, :error, :incomplete, :thunk)
310✔
153
            return ex
6✔
154
        elseif h === :global && all(x->isa(x, Symbol), ex.args)
307✔
155
            return ex
3✔
156
        else
157
            return Expr(:block, Expr(:softscope, true), ex)
301✔
158
        end
159
    end
160
    return ex
393✔
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)
6✔
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
6✔
168
end
169

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

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

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

212
function collect_names_to_warn!(warnings, locals, current_module::Module, ast)
3,481✔
213
    ast isa Expr || return
5,039✔
214

215
    # don't recurse through module definitions
216
    ast.head === :module && return
1,923✔
217

218
    if Meta.isexpr(ast, :., 2)
1,923✔
219
        mod_name, name_being_accessed = ast.args
179✔
220
        # retrieve the (possibly-nested) module being named here
221
        mods = retrieve_modules(current_module, mod_name)
179✔
222
        all(x -> x isa Module, mods) || return
499✔
223
        outer_mod = first(mods)
104✔
224
        mod = last(mods)
104✔
225
        if name_being_accessed isa QuoteNode
104✔
226
            name_being_accessed = name_being_accessed.value
104✔
227
        end
228
        name_being_accessed isa Symbol || return
104✔
229
        owner = try
104✔
230
            which(mod, name_being_accessed)
104✔
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
179✔
237
        # Don't warn if the name is public in the module we are accessing it
238
        Base.ispublic(mod, name_being_accessed) && return
36✔
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
33✔
241
        push!(warnings, (; outer_mod, mod, owner, name_being_accessed))
33✔
242
        # no recursion
243
        return
33✔
244
    elseif Meta.isexpr(ast, :(=), 2)
1,744✔
245
        lhs, rhs = ast.args
141✔
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)
141✔
249
        # we'll recurse into the RHS only
250
        return collect_names_to_warn!(warnings, locals, current_module, rhs)
141✔
251
    elseif Meta.isexpr(ast, :function) && length(ast.args) >= 1
1,603✔
252

253
        if Meta.isexpr(ast.args[1], :call, 2)
9✔
254
            func_name, func_args = ast.args[1].args
6✔
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)
6✔
258
        end
259
        # fall through to general recursion
260
    end
261

262
    for arg in ast.args
1,603✔
263
        collect_names_to_warn!(warnings, locals, current_module, arg)
2,982✔
264
    end
2,982✔
265

266
    return nothing
1,603✔
267
end
268

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

279
function warn_on_non_owning_accesses(current_mod, ast)
322✔
280
    warnings = collect_qualified_access_warnings(current_mod, ast)
322✔
281
    for (; outer_mod, mod, owner, name_being_accessed) in warnings
644✔
282
        print_qualified_access_warning(mod, owner, name_being_accessed)
6✔
283
    end
12✔
284
    return ast
322✔
285
end
286
warn_on_non_owning_accesses(ast) = warn_on_non_owning_accesses(Base.active_module(), ast)
316✔
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}) =
637✔
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}) =
634✔
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))
1,015✔
305
    if !isexpr(ast, :toplevel)
1,331✔
306
        ast = invokelatest(__repl_entry_lower_with_loc, mod, ast, toplevel_file, toplevel_line)
637✔
307
        check_for_missing_packages_and_run_hooks(ast)
637✔
308
        return invokelatest(__repl_entry_eval_expanded_with_loc, mod, ast, toplevel_file, toplevel_line)
634✔
309
    end
310
    local value=nothing
378✔
311
    for i = 1:length(ast.args)
378✔
312
        value = toplevel_eval_with_hooks(mod, ast.args[i], toplevel_file, toplevel_line)
699✔
313
    end
1,002✔
314
    return value
360✔
315
end
316

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

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

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

393
function modules_to_be_loaded(ast::Expr, mods::Vector{Symbol} = Symbol[])
72✔
394
    _modules_to_be_loaded!(ast, mods)
782✔
395
    filter!(mod::Symbol -> !in(mod, (:Base, :Main, :Core)), mods) # Exclude special non-package modules
460✔
396
    return unique(mods)
391✔
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)
168✔
425
    backend.backend_task = Base.current_task()
75✔
426
    consumer(backend)
75✔
427
    repl_backend_loop(backend, get_module)
75✔
428
    return backend
72✔
429
end
430

431
function repl_backend_loop(backend::REPLBackend, get_module::Function)
75✔
432
    # include looks at this to determine the relative include path
433
    # nothing means cwd
434
    while true
565✔
435
        tls = task_local_storage()
565✔
436
        tls[:SOURCE_PATH] = nothing
565✔
437
        ast_or_func, show_value = take!(backend.repl_channel)
565✔
438
        if show_value == -1
565✔
439
            # exit flag
440
            break
72✔
441
        end
442
        if show_value == 2 # 2 indicates a function to be called
493✔
443
            f = ast_or_func
177✔
444
            try
177✔
445
                ret = f()
177✔
446
                put!(backend.response_channel, Pair{Any, Bool}(ret, false))
174✔
447
            catch
448
                put!(backend.response_channel, Pair{Any, Bool}(current_exceptions(), true))
3✔
449
            end
450
        else
451
            ast = ast_or_func
316✔
452
            eval_user_input(ast, backend, get_module())
316✔
453
        end
454
    end
490✔
455
    return nothing
72✔
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
210✔
463
    maxbytes::Int
464
    n::Int # max bytes to write
465
end
466
LimitIO(io::IO, maxbytes) = LimitIO(io, maxbytes, 0)
210✔
467

468
struct LimitIOException <: Exception
469
    maxbytes::Int
18✔
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
Base.displaysize(io::LimitIO) = _displaysize(io.io)
84✔
477

478
function Base.write(io::LimitIO, v::UInt8)
453✔
479
    io.n > io.maxbytes && throw(LimitIOException(io.maxbytes))
5,010✔
480
    n_bytes = write(io.io, v)
9,555✔
481
    io.n += n_bytes
5,004✔
482
    return n_bytes
5,004✔
483
end
484

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

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

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

506
struct REPLDisplay{Repl<:AbstractREPL} <: AbstractDisplay
507
    repl::Repl
102✔
508
end
509

510
function show_limited(io::IO, mime::MIME, x)
201✔
511
    try
201✔
512
        # We wrap in a LimitIO to limit the amount of printing.
513
        # We unpack `IOContext`s, since we will pass the properties on the outside.
514
        inner = io isa IOContext ? io.io : io
201✔
515
        wrapped_limiter = IOContext(LimitIO(inner, SHOW_MAXIMUM_BYTES), io)
201✔
516
        # `show_repl` to allow the hook with special syntax highlighting
517
        show_repl(wrapped_limiter, mime, x)
201✔
518
    catch e
519
        e isa LimitIOException || rethrow()
15✔
520
        printstyled(io, """…[printing stopped after displaying $(Base.format_bytes(e.maxbytes)); call `show(stdout, MIME"text/plain"(), ans)` to print without truncation]"""; color=:light_yellow, bold=true)
9✔
521
    end
522
end
523

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

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

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

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

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

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

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

629

630

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

635
    Main function to start the REPL
636

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

663
## BasicREPL ##
664

665
mutable struct BasicREPL <: AbstractREPL
666
    terminal::TextTerminal
667
    waserror::Bool
668
    frontend_task::Task
669
    BasicREPL(t) = new(t, false)
9✔
670
end
671

672
outstream(r::BasicREPL) = r.terminal
18✔
673
hascolor(r::BasicREPL) = hascolor(r.terminal)
×
674

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

722
## LineEditREPL ##
723

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

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

772
mutable struct REPLCompletionProvider <: CompletionProvider
773
    modifiers::LineEdit.Modifiers
72✔
774
end
775
REPLCompletionProvider() = REPLCompletionProvider(LineEdit.Modifiers())
72✔
776

777
mutable struct ShellCompletionProvider <: CompletionProvider end
72✔
778
struct LatexCompletions <: CompletionProvider end
779

780
Base.active_module((; mistate)::LineEditREPL) = mistate === nothing ? Main : mistate.active_module
11,521✔
781
Base.active_module(::AbstractREPL) = Main
36✔
782
Base.active_module(d::REPLDisplay) = Base.active_module(d.repl)
363✔
783

784
setmodifiers!(c::CompletionProvider, m::LineEdit.Modifiers) = nothing
×
785

786
setmodifiers!(c::REPLCompletionProvider, m::LineEdit.Modifiers) = c.modifiers = m
×
787

788
"""
789
    activate(mod::Module=Main)
790

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

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

804
# Convert inclusive-inclusive 1-based char indexing to inclusive-exclusive byte Region.
805
to_region(s, r) = first(r)-1 => (length(r) > 0 ? nextind(s, last(r))-1 : first(r)-1)
81✔
806

807
function complete_line(c::REPLCompletionProvider, s::PromptState, mod::Module; hint::Bool=false)
72✔
808
    full = LineEdit.input_string(s)
72✔
809
    ret, range, should_complete = completions(full, thisind(full, position(s)), mod, c.modifiers.shift, hint)
72✔
810
    range = to_region(full, range)
69✔
811
    c.modifiers = LineEdit.Modifiers()
36✔
812
    return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), range, should_complete
36✔
813
end
814

815
function complete_line(c::ShellCompletionProvider, s::PromptState; hint::Bool=false)
12✔
816
    full = LineEdit.input_string(s)
12✔
817
    ret, range, should_complete = shell_completions(full, thisind(full, position(s)), hint)
12✔
818
    range = to_region(full, range)
12✔
819
    return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), range, should_complete
6✔
820
end
821

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

829
with_repl_linfo(f, repl) = f(outstream(repl))
18✔
830
function with_repl_linfo(f, repl::LineEditREPL)
472✔
831
    linfos = Tuple{String,Int}[]
472✔
832
    io = IOContext(outstream(repl), :last_shown_line_infos => linfos)
472✔
833
    f(io)
472✔
834
    if !isempty(linfos)
469✔
835
        repl.last_shown_line_infos = linfos
18✔
836
    end
837
    nothing
469✔
838
end
839

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

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

862
munged_history_message(path::String) = """
×
863
Invalid history file ($path) format:
864
An editor may have converted tabs to spaces at line """
865

866
function hist_open_file(hp::REPLHistoryProvider)
867
    f = open(hp.file_path, read=true, write=true, create=true)
12✔
868
    hp.history_file = f
12✔
869
    seekend(f)
12✔
870
end
871

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

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

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

945
function history_move(s::Union{LineEdit.MIState,LineEdit.PrefixSearchState}, hist::REPLHistoryProvider, idx::Int, save_idx::Int = hist.cur_idx)
288✔
946
    max_idx = length(hist.history) + 1
408✔
947
    @assert 1 <= hist.cur_idx <= max_idx
288✔
948
    (1 <= idx <= max_idx) || return :none
294✔
949
    idx != hist.cur_idx || return :none
282✔
950

951
    # save the current line
952
    if save_idx == max_idx
282✔
953
        hist.last_mode = LineEdit.mode(s)
138✔
954
        hist.last_buffer = copy(LineEdit.buffer(s))
138✔
955
    else
956
        hist.history[save_idx] = LineEdit.input_string(s)
189✔
957
        hist.modes[save_idx] = mode_idx(hist, LineEdit.mode(s))
189✔
958
    end
959

960
    # load the saved line
961
    if idx == max_idx
282✔
962
        last_buffer = hist.last_buffer
30✔
963
        LineEdit.transition(s, hist.last_mode) do
42✔
964
            LineEdit.replace_line(s, last_buffer)
30✔
965
        end
966
        hist.last_mode = nothing
30✔
967
        hist.last_buffer = IOBuffer()
30✔
968
    else
969
        if haskey(hist.mode_mapping, hist.modes[idx])
504✔
970
            LineEdit.transition(s, hist.mode_mapping[hist.modes[idx]]) do
204✔
971
                LineEdit.replace_line(s, hist.history[idx])
204✔
972
            end
973
        else
974
            return :skip
48✔
975
        end
976
    end
977
    hist.cur_idx = idx
234✔
978

979
    return :ok
234✔
980
end
981

982
# REPL History can also transitions modes
983
function LineEdit.accept_result_newmode(hist::REPLHistoryProvider)
81✔
984
    if 1 <= hist.cur_idx <= length(hist.modes)
81✔
985
        return hist.mode_mapping[hist.modes[hist.cur_idx]]
69✔
986
    end
987
    return nothing
12✔
988
end
989

990
function history_prev(s::LineEdit.MIState, hist::REPLHistoryProvider,
102✔
991
                      num::Int=1, save_idx::Int = hist.cur_idx)
992
    num <= 0 && return history_next(s, hist, -num, save_idx)
174✔
993
    hist.last_idx = -1
96✔
994
    m = history_move(s, hist, hist.cur_idx-num, save_idx)
96✔
995
    if m === :ok
96✔
996
        LineEdit.move_input_start(s)
144✔
997
        LineEdit.reset_key_repeats(s) do
72✔
998
            LineEdit.move_line_end(s)
72✔
999
        end
1000
        return LineEdit.refresh_line(s)
72✔
1001
    elseif m === :skip
24✔
1002
        return history_prev(s, hist, num+1, save_idx)
24✔
1003
    else
1004
        return Terminals.beep(s)
×
1005
    end
1006
end
1007

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

1033
history_first(s::LineEdit.MIState, hist::REPLHistoryProvider) =
18✔
1034
    history_prev(s, hist, hist.cur_idx - 1 -
1035
                 (hist.cur_idx > hist.start_idx+1 ? hist.start_idx : 0))
1036

1037
history_last(s::LineEdit.MIState, hist::REPLHistoryProvider) =
12✔
1038
    history_next(s, hist, length(hist.history) - hist.cur_idx + 1)
1039

1040
function history_move_prefix(s::LineEdit.PrefixSearchState,
114✔
1041
                             hist::REPLHistoryProvider,
1042
                             prefix::AbstractString,
1043
                             backwards::Bool,
1044
                             cur_idx::Int = hist.cur_idx)
1045
    cur_response = String(take!(copy(LineEdit.buffer(s))))
303✔
1046
    # when searching forward, start at last_idx
1047
    if !backwards && hist.last_idx > 0
114✔
1048
        cur_idx = hist.last_idx
3✔
1049
    end
1050
    hist.last_idx = -1
114✔
1051
    max_idx = length(hist.history)+1
114✔
1052
    idxs = backwards ? ((cur_idx-1):-1:1) : ((cur_idx+1):1:max_idx)
195✔
1053
    for idx in idxs
114✔
1054
        if (idx == max_idx) || (startswith(hist.history[idx], prefix) && (hist.history[idx] != cur_response || get(hist.mode_mapping, hist.modes[idx], nothing) !== LineEdit.mode(s)))
576✔
1055
            m = history_move(s, hist, idx)
108✔
1056
            if m === :ok
108✔
1057
                if idx == max_idx
102✔
1058
                    # on resuming the in-progress edit, leave the cursor where the user last had it
1059
                elseif isempty(prefix)
90✔
1060
                    # on empty prefix search, move cursor to the end
1061
                    LineEdit.move_input_end(s)
42✔
1062
                else
1063
                    # otherwise, keep cursor at the prefix position as a visual cue
1064
                    seek(LineEdit.buffer(s), sizeof(prefix))
48✔
1065
                end
1066
                LineEdit.refresh_line(s)
102✔
1067
                return :ok
102✔
1068
            elseif m === :skip
6✔
1069
                return history_move_prefix(s,hist,prefix,backwards,idx)
6✔
1070
            end
1071
        end
1072
    end
372✔
1073
    Terminals.beep(s)
6✔
1074
    nothing
6✔
1075
end
1076
history_next_prefix(s::LineEdit.PrefixSearchState, hist::REPLHistoryProvider, prefix::AbstractString) =
15✔
1077
    history_move_prefix(s, hist, prefix, false)
1078
history_prev_prefix(s::LineEdit.PrefixSearchState, hist::REPLHistoryProvider, prefix::AbstractString) =
93✔
1079
    history_move_prefix(s, hist, prefix, true)
1080

1081
function history_search(hist::REPLHistoryProvider, query_buffer::IOBuffer, response_buffer::IOBuffer,
84✔
1082
                        backwards::Bool=false, skip_current::Bool=false)
1083

1084
    qpos = position(query_buffer)
84✔
1085
    qpos > 0 || return true
84✔
1086
    searchdata = beforecursor(query_buffer)
84✔
1087
    response_str = String(take!(copy(response_buffer)))
150✔
1088

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

1097
    searchstart = backwards ? b : a
84✔
1098
    if searchdata == response_str[a:b]
132✔
1099
        if skip_current
30✔
1100
            searchstart = backwards ? prevind(response_str, b) : nextind(response_str, a)
12✔
1101
        else
1102
            return true
18✔
1103
        end
1104
    end
1105

1106
    # Start searching
1107
    # First the current response buffer
1108
    if 1 <= searchstart <= lastindex(response_str)
108✔
1109
        match = backwards ? findprev(searchdata, response_str, searchstart) :
42✔
1110
                            findnext(searchdata, response_str, searchstart)
1111
        if match !== nothing
42✔
1112
            seek(response_buffer, first(match) - 1)
36✔
1113
            return true
18✔
1114
        end
1115
    end
1116

1117
    # Now search all the other buffers
1118
    idxs = backwards ? ((hist.cur_idx-1):-1:1) : ((hist.cur_idx+1):1:length(hist.history))
96✔
1119
    for idx in idxs
48✔
1120
        h = hist.history[idx]
120✔
1121
        match = backwards ? findlast(searchdata, h) : findfirst(searchdata, h)
240✔
1122
        if match !== nothing && h != response_str && haskey(hist.mode_mapping, hist.modes[idx])
162✔
1123
            truncate(response_buffer, 0)
36✔
1124
            write(response_buffer, h)
36✔
1125
            seek(response_buffer, first(match) - 1)
72✔
1126
            hist.cur_idx = idx
36✔
1127
            return true
36✔
1128
        end
1129
    end
156✔
1130

1131
    return false
12✔
1132
end
1133

1134
function history_reset_state(hist::REPLHistoryProvider)
1135
    if hist.cur_idx != length(hist.history) + 1
809✔
1136
        hist.last_idx = hist.cur_idx
388✔
1137
        hist.cur_idx = length(hist.history) + 1
388✔
1138
    end
1139
    nothing
809✔
1140
end
1141
LineEdit.reset_state(hist::REPLHistoryProvider) = history_reset_state(hist)
716✔
1142

1143
function return_callback(s)
288✔
1144
    ast = Base.parse_input_line(String(take!(copy(LineEdit.buffer(s)))), depwarn=false)
531✔
1145
    return !(isa(ast, Expr) && ast.head === :incomplete)
288✔
1146
end
1147

1148
find_hist_file() = get(ENV, "JULIA_HISTORY",
12✔
1149
                       !isempty(DEPOT_PATH) ? joinpath(DEPOT_PATH[1], "logs", "repl_history.jl") :
1150
                       error("DEPOT_PATH is empty and ENV[\"JULIA_HISTORY\"] not set."))
1151

1152
backend(r::AbstractREPL) = hasproperty(r, :backendref) && isdefined(r, :backendref) ? r.backendref : nothing
596✔
1153

1154

1155
function eval_on_backend(ast, backend::REPLBackendRef)
304✔
1156
    put!(backend.repl_channel, (ast, 1)) # (f, show_value)
304✔
1157
    return take!(backend.response_channel) # (val, iserr)
304✔
1158
end
1159
function call_on_backend(f, backend::REPLBackendRef)
42✔
1160
    applicable(f) || error("internal error: f is not callable")
177✔
1161
    put!(backend.repl_channel, (f, 2)) # (f, show_value) 2 indicates function (rather than ast)
177✔
1162
    return take!(backend.response_channel) # (val, iserr)
177✔
1163
end
1164
# if no backend just eval (used by tests)
1165
eval_on_backend(ast, backend::Nothing) = error("no backend for eval ast")
×
1166
function call_on_backend(f, backend::Nothing)
9✔
1167
    try
9✔
1168
        ret = f()
9✔
1169
        return (ret, false) # (val, iserr)
9✔
1170
    catch
1171
        return (current_exceptions(), true)
×
1172
    end
1173
end
1174

1175

1176
function respond(f, repl, main; pass_empty::Bool = false, suppress_on_semicolon::Bool = true)
288✔
1177
    return function do_respond(s::MIState, buf, ok::Bool)
688✔
1178
        if !ok
400✔
1179
            return transition(s, :abort)
60✔
1180
        end
1181
        line = String(take!(buf)::Vector{UInt8})
635✔
1182
        if !isempty(line) || pass_empty
385✔
1183
            reset(repl)
295✔
1184
            local response
1185
            try
295✔
1186
                ast = Base.invokelatest(f, line)
295✔
1187
                response = eval_on_backend(ast, backend(repl))
584✔
1188
            catch
1189
                response = Pair{Any, Bool}(current_exceptions(), true)
3✔
1190
            end
1191
            hide_output = suppress_on_semicolon && ends_with_semicolon(line)
295✔
1192
            print_response(repl, response, !hide_output, hascolor(repl))
295✔
1193
        end
1194
        prepare_next(repl)
340✔
1195
        reset_state(s)
340✔
1196
        return s.current_mode.sticky ? true : transition(s, main)
340✔
1197
    end
1198
end
1199

1200
function reset(repl::LineEditREPL)
295✔
1201
    raw!(repl.t, false)
295✔
1202
    hascolor(repl) && print(repl.t, Base.text_colors[:normal])
295✔
1203
    nothing
295✔
1204
end
1205

1206
function prepare_next(repl::LineEditREPL)
340✔
1207
    println(terminal(repl))
340✔
1208
end
1209

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

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

1235
const JL_PROMPT_PASTE = Ref(true)
1236
enable_promptpaste(v::Bool) = JL_PROMPT_PASTE[] = v
×
1237

1238
function contextual_prompt(repl::LineEditREPL, prompt::Union{String,Function})
1239
    function ()
5,151✔
1240
        mod = Base.active_module(repl)
9,993✔
1241
        prefix = mod == Main ? "" : string('(', mod, ") ")
5,089✔
1242
        pr = prompt isa String ? prompt : prompt()
5,007✔
1243
        prefix * pr
5,007✔
1244
    end
1245
end
1246

1247
setup_interface(
204✔
1248
    repl::LineEditREPL;
1249
    # those keyword arguments may be deprecated eventually in favor of the Options mechanism
1250
    hascolor::Bool = repl.options.hascolor,
1251
    extra_repl_keymap::Any = repl.options.extra_keymap
1252
) = setup_interface(repl, hascolor, extra_repl_keymap)
1253

1254

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

1267
    ###
1268
    #
1269
    # This function returns the main interface that describes the REPL
1270
    # functionality, it is called internally by functions that setup a
1271
    # Terminal-based REPL frontend.
1272
    #
1273
    # See run_frontend(repl::LineEditREPL, backend::REPLBackendRef)
1274
    # for usage
1275
    #
1276
    ###
1277

1278
    ###
1279
    # We setup the interface in two stages.
1280
    # First, we set up all components (prompt,rsearch,shell,help)
1281
    # Second, we create keymaps with appropriate transitions between them
1282
    #   and assign them to the components
1283
    #
1284
    ###
1285

1286
    ############################### Stage I ################################
1287

1288
    # This will provide completions for REPL and help mode
1289
    replc = REPLCompletionProvider()
72✔
1290

1291
    # Set up the main Julia prompt
1292
    julia_prompt = Prompt(contextual_prompt(repl, JULIA_PROMPT);
144✔
1293
        # Copy colors from the prompt object
1294
        prompt_prefix = hascolor ? repl.prompt_color : "",
1295
        prompt_suffix = hascolor ?
1296
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1297
        repl = repl,
1298
        complete = replc,
1299
        on_enter = return_callback)
1300

1301
    # Setup help mode
1302
    help_mode = Prompt(contextual_prompt(repl, HELP_PROMPT),
144✔
1303
        prompt_prefix = hascolor ? repl.help_color : "",
1304
        prompt_suffix = hascolor ?
1305
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1306
        repl = repl,
1307
        complete = replc,
1308
        # When we're done transform the entered line into a call to helpmode function
1309
        on_done = respond(line::String->helpmode(outstream(repl), line, repl.mistate.active_module),
6✔
1310
                          repl, julia_prompt, pass_empty=true, suppress_on_semicolon=false))
1311

1312

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

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

1358

1359
    ################################# Stage II #############################
1360

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

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

1393

1394
    search_prompt, skeymap = LineEdit.setup_search_keymap(hp)
72✔
1395
    search_prompt.complete = LatexCompletions()
72✔
1396

1397
    shell_prompt_len = length(SHELL_PROMPT)
72✔
1398
    help_prompt_len = length(HELP_PROMPT)
72✔
1399
    jl_prompt_regex = Regex("^In \\[[0-9]+\\]: |^(?:\\(.+\\) )?$JULIA_PROMPT")
72✔
1400
    pkg_prompt_regex = Regex("^(?:\\(.+\\) )?$PKG_PROMPT")
72✔
1401

1402
    # Canonicalize user keymap input
1403
    if isa(extra_repl_keymap, Dict)
72✔
1404
        extra_repl_keymap = AnyDict[extra_repl_keymap]
×
1405
    end
1406

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

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

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

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

1633
    prefix_prompt, prefix_keymap = LineEdit.setup_prefix_keymap(hp, julia_prompt)
72✔
1634

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

1638
    julia_prompt.keymap_dict = LineEdit.keymap(a)
72✔
1639

1640
    mk = mode_keymap(julia_prompt)
72✔
1641

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

1645
    shell_mode.keymap_dict = help_mode.keymap_dict = dummy_pkg_mode.keymap_dict = LineEdit.keymap(b)
72✔
1646

1647
    allprompts = LineEdit.TextInterface[julia_prompt, shell_mode, help_mode, dummy_pkg_mode, search_prompt, prefix_prompt]
72✔
1648
    return ModalInterface(allprompts)
72✔
1649
end
1650

1651
function run_frontend(repl::LineEditREPL, backend::REPLBackendRef)
60✔
1652
    repl.frontend_task = current_task()
60✔
1653
    d = REPLDisplay(repl)
60✔
1654
    dopushdisplay = repl.specialdisplay === nothing && !in(d,Base.Multimedia.displays)
102✔
1655
    dopushdisplay && pushdisplay(d)
60✔
1656
    if !isdefined(repl,:interface)
60✔
1657
        interface = repl.interface = setup_interface(repl)
72✔
1658
    else
1659
        interface = repl.interface
24✔
1660
    end
1661
    repl.backendref = backend
60✔
1662
    repl.mistate = LineEdit.init_state(terminal(repl), interface)
60✔
1663
    run_interface(terminal(repl), interface, repl.mistate)
60✔
1664
    # Terminate Backend
1665
    put!(backend.repl_channel, (nothing, -1))
60✔
1666
    dopushdisplay && popdisplay(d)
60✔
1667
    nothing
60✔
1668
end
1669

1670
## StreamREPL ##
1671

1672
mutable struct StreamREPL <: AbstractREPL
1673
    stream::IO
1674
    prompt_color::String
1675
    input_color::String
1676
    answer_color::String
1677
    waserror::Bool
1678
    frontend_task::Task
1679
    StreamREPL(stream,pc,ic,ac) = new(stream,pc,ic,ac,false)
×
1680
end
1681
StreamREPL(stream::IO) = StreamREPL(stream, Base.text_colors[:green], Base.input_color(), Base.answer_color())
×
1682
run_repl(stream::IO) = run_repl(StreamREPL(stream))
×
1683

1684
outstream(s::StreamREPL) = s.stream
×
1685
hascolor(s::StreamREPL) = get(s.stream, :color, false)::Bool
×
1686

1687
answer_color(r::LineEditREPL) = r.envcolors ? Base.answer_color() : r.answer_color
×
1688
answer_color(r::StreamREPL) = r.answer_color
×
1689
input_color(r::LineEditREPL) = r.envcolors ? Base.input_color() : r.input_color
×
1690
input_color(r::StreamREPL) = r.input_color
×
1691

1692
let matchend = Dict("\"" => r"\"", "\"\"\"" => r"\"\"\"", "'" => r"'",
1693
    "`" => r"`", "```" => r"```", "#" => r"$"m, "#=" => r"=#|#=")
1694
    global _rm_strings_and_comments
1695
    function _rm_strings_and_comments(code::Union{String,SubString{String}})
370✔
1696
        buf = IOBuffer(sizehint = sizeof(code))
740✔
1697
        pos = 1
370✔
1698
        while true
490✔
1699
            i = findnext(r"\"(?!\"\")|\"\"\"|'|`(?!``)|```|#(?!=)|#=", code, pos)
980✔
1700
            isnothing(i) && break
628✔
1701
            match = SubString(code, i)
138✔
1702
            j = findnext(matchend[match]::Regex, code, nextind(code, last(i)))
276✔
1703
            if match == "#=" # possibly nested
138✔
1704
                nested = 1
15✔
1705
                while j !== nothing
33✔
1706
                    nested += SubString(code, j) == "#=" ? +1 : -1
30✔
1707
                    iszero(nested) && break
30✔
1708
                    j = findnext(r"=#|#=", code, nextind(code, last(j)))
36✔
1709
                end
18✔
1710
            elseif match[1] != '#' # quote match: check non-escaped
246✔
1711
                while j !== nothing
114✔
1712
                    notbackslash = findprev(!=('\\'), code, prevind(code, first(j)))::Int
198✔
1713
                    isodd(first(j) - notbackslash) && break # not escaped
99✔
1714
                    j = findnext(matchend[match]::Regex, code, nextind(code, first(j)))
42✔
1715
                end
21✔
1716
            end
1717
            isnothing(j) && break
258✔
1718
            if match[1] == '#'
240✔
1719
                print(buf, SubString(code, pos, prevind(code, first(i))))
42✔
1720
            else
1721
                print(buf, SubString(code, pos, last(i)), ' ', SubString(code, j))
78✔
1722
            end
1723
            pos = nextind(code, last(j))
120✔
1724
        end
120✔
1725
        print(buf, SubString(code, pos, lastindex(code)))
370✔
1726
        return String(take!(buf))
370✔
1727
    end
1728
end
1729

1730
# heuristic function to decide if the presence of a semicolon
1731
# at the end of the expression was intended for suppressing output
1732
ends_with_semicolon(code::AbstractString) = ends_with_semicolon(String(code))
×
1733
ends_with_semicolon(code::Union{String,SubString{String}}) =
370✔
1734
    contains(_rm_strings_and_comments(code), r";\s*$")
1735

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

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

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

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

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

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

1801
            """)
1802
        end
1803
    end
1804
end
1805

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

1837
module Numbered
1838

1839
using ..REPL
1840

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

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

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

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

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

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

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

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

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

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

1927
"""
1928
    Out[n]
1929

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

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

1937
end
1938

1939
import .Numbered.numbered_prompt!
1940

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

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

1950
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