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

JuliaLang / julia / #38052

25 Apr 2025 04:34PM UTC coverage: 25.765% (-0.002%) from 25.767%
#38052

push

local

web-flow
optimize Type method table queries and insertions (#58216)

Ensure a total split of constructors and non-constructors, so they do
not end up seaching the opposing table. Instead cache all kind lookups
as keyed by typename(Type). Not really needed on its own, but it avoids
a minor performance regression in
https://github.com/JuliaLang/julia/pull/58131.

0 of 1 new or added line in 1 file covered. (0.0%)

479 existing lines in 3 files now uncovered.

12885 of 50010 relevant lines covered (25.76%)

1078891.92 hits per line

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

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

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

6
Example minimal code
7

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

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

20
function UndefVarError_REPL_hint(io::IO, ex::UndefVarError)
×
21
    var = ex.var
×
22
    if var === :or
×
23
        print(io, "\nSuggestion: Use `||` for short-circuiting boolean OR.")
×
24
    elseif var === :and
×
25
        print(io, "\nSuggestion: Use `&&` for short-circuiting boolean AND.")
×
26
    elseif var === :help
×
27
        println(io)
×
28
        # Show friendly help message when user types help or help() and help is undefined
29
        show(io, MIME("text/plain"), Base.Docs.parsedoc(Base.Docs.keywords[:help]))
×
30
    elseif var === :quit
×
31
        print(io, "\nSuggestion: To exit Julia, use Ctrl-D, or type exit() and press enter.")
×
32
    end
33
end
34

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

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

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

52
public TerminalMenus
53

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

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

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

66
abstract type AbstractREPL end
67

68
include("options.jl")
69

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

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

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

97
include("Pkg_beforeload.jl")
98

99
@nospecialize # use only declared type signatures
100

UNCOV
101
answer_color(::AbstractREPL) = ""
×
102

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

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

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

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

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

141
"""
142
    softscope(ex)
143

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

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

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

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

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

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

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

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

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

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

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

UNCOV
268
    return nothing
×
269
end
270

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

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

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

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

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

306
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))
×
307
    if !isexpr(ast, :toplevel)
×
308
        ast = invokelatest(__repl_entry_lower_with_loc, mod, ast, toplevel_file, toplevel_line)
×
309
        check_for_missing_packages_and_run_hooks(ast)
×
UNCOV
310
        return invokelatest(__repl_entry_eval_expanded_with_loc, mod, ast, toplevel_file, toplevel_line)
×
311
    end
312
    local value=nothing
×
313
    for i = 1:length(ast.args)
×
314
        value = toplevel_eval_with_hooks(mod, ast.args[i], toplevel_file, toplevel_line)
×
315
    end
×
UNCOV
316
    return value
×
317
end
318

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

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

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

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

401
"""
402
    start_repl_backend(repl_channel::Channel, response_channel::Channel)
403

404
    Starts loop for REPL backend
405
    Returns a REPLBackend with backend_task assigned
406

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

418
"""
419
    start_repl_backend(backend::REPLBackend)
420

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

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

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

460
SHOW_MAXIMUM_BYTES::Int = 1_048_576
461

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

470
struct LimitIOException <: Exception
471
    maxbytes::Int
472
end
473

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

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

485
# Semantically, we only need to override `Base.write`, but we also
486
# override `unsafe_write` for performance.
UNCOV
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))
×
UNCOV
490
    remaining = limiter.maxbytes - limiter.n # >= 0
×
491

492
    # Not enough bytes left; we will print up to the limit, then throw
493
    if remaining < nb
×
494
        if remaining > 0
×
UNCOV
495
            Base.unsafe_write(limiter.io, p, remaining)
×
496
        end
UNCOV
497
        throw(LimitIOException(limiter.maxbytes))
×
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}
×
502
    limiter.n += bytes_written
×
UNCOV
503
    return bytes_written
×
504
end
505

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

510
function show_limited(io::IO, mime::MIME, x)
×
UNCOV
511
    try
×
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
×
UNCOV
515
        wrapped_limiter = IOContext(LimitIO(inner, SHOW_MAXIMUM_BYTES), io)
×
516
        # `show_repl` to allow the hook with special syntax highlighting
UNCOV
517
        show_repl(wrapped_limiter, mime, x)
×
518
    catch e
519
        e isa LimitIOException || rethrow()
×
UNCOV
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)
×
521
    end
522
end
523

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

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

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

UNCOV
550
show_repl(io::IO, ::MIME"text/plain", ex::Expr) =
×
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)
×
555
    repl.waserror = response[2]
×
556
    with_repl_linfo(repl) do io
×
557
        io = IOContext(io, :module => Base.active_module(repl)::Module)
×
UNCOV
558
        print_response(io, response, backend(repl), show_value, have_color, specialdisplay(repl))
×
559
    end
UNCOV
560
    return nothing
×
561
end
562

UNCOV
563
function repl_display_error(errio::IO, @nospecialize errval)
×
564
    # this will be set to true if types in the stacktrace are truncated
565
    limitflag = Ref(false)
×
566
    errio = IOContext(errio, :stacktrace_types_limited => limitflag)
×
567
    Base.invokelatest(Base.display_error, errio, errval)
×
568
    if limitflag[]
×
569
        print(errio, "Some type information was truncated. Use `show(err)` to see complete types.")
×
UNCOV
570
        println(errio)
×
571
    end
UNCOV
572
    return nothing
×
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)
×
576
    Base.sigatomic_begin()
×
577
    val, iserr = response
×
578
    while true
×
579
        try
×
580
            Base.sigatomic_end()
×
581
            if iserr
×
582
                val = Base.scrub_repl_backtrace(val)
×
583
                Base.istrivialerror(val) || setglobal!(Base.MainInclude, :err, val)
×
UNCOV
584
                repl_display_error(errio, val)
×
585
            else
586
                if val !== nothing && show_value
×
UNCOV
587
                    val2, iserr = if specialdisplay === nothing
×
588
                        # display calls may require being run on the main thread
589
                        eval_with_backend(backend) do
×
UNCOV
590
                            Base.invokelatest(display, val)
×
591
                        end
592
                    else
593
                        eval_with_backend(backend) do
×
UNCOV
594
                            Base.invokelatest(display, specialdisplay, val)
×
595
                        end
596
                    end
597
                    if iserr
×
598
                        println(errio, "Error showing value of type ", typeof(val), ":")
×
UNCOV
599
                        throw(val2)
×
600
                    end
601
                end
602
            end
UNCOV
603
            break
×
604
        catch ex
605
            if iserr
×
606
                println(errio) # an error during printing is likely to leave us mid-line
×
607
                println(errio, "SYSTEM (REPL): showing an error caused an error")
×
608
                try
×
609
                    excs = Base.scrub_repl_backtrace(current_exceptions())
×
610
                    setglobal!(Base.MainInclude, :err, excs)
×
UNCOV
611
                    repl_display_error(errio, excs)
×
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)
×
UNCOV
616
                    println(errio, "SYSTEM (REPL): caught exception of type ", typeof(e).name.name,
×
617
                            " while trying to handle a nested exception; giving up")
618
                end
UNCOV
619
                break
×
620
            end
621
            val = current_exceptions()
×
UNCOV
622
            iserr = true
×
623
        end
624
    end
×
625
    Base.sigatomic_end()
×
UNCOV
626
    nothing
×
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())
×
640
    backend_ref = REPLBackendRef(backend)
×
641
    cleanup = @task try
×
UNCOV
642
            destroy(backend_ref, t)
×
643
        catch e
644
            Core.print(Core.stderr, "\nINTERNAL ERROR: ")
×
645
            Core.println(Core.stderr, e)
×
UNCOV
646
            Core.println(Core.stderr, catch_backtrace())
×
647
        end
648
    get_module = () -> Base.active_module(repl)
×
649
    if backend_on_current_task
×
650
        t = @async run_frontend(repl, backend_ref)
×
651
        errormonitor(t)
×
652
        Base._wait2(t, cleanup)
×
UNCOV
653
        start_repl_backend(backend, consumer; get_module)
×
654
    else
655
        t = @async start_repl_backend(backend, consumer; get_module)
×
656
        errormonitor(t)
×
657
        Base._wait2(t, cleanup)
×
UNCOV
658
        run_frontend(repl, backend_ref)
×
659
    end
UNCOV
660
    return backend
×
661
end
662

663
## BasicREPL ##
664

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

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

675
function run_frontend(repl::BasicREPL, backend::REPLBackendRef)
×
676
    repl.frontend_task = current_task()
×
677
    d = REPLDisplay(repl)
×
678
    dopushdisplay = !in(d,Base.Multimedia.displays)
×
679
    dopushdisplay && pushdisplay(d)
×
680
    hit_eof = false
×
681
    while true
×
682
        Base.reseteof(repl.terminal)
×
683
        write(repl.terminal, JULIA_PROMPT)
×
684
        line = ""
×
685
        ast = nothing
×
686
        interrupted = false
×
687
        while true
×
688
            try
×
UNCOV
689
                line *= readline(repl.terminal, keep=true)
×
690
            catch e
691
                if isa(e,InterruptException)
×
692
                    try # raise the debugger if present
×
693
                        ccall(:jl_raise_debugger, Int, ())
×
UNCOV
694
                    catch
×
695
                    end
696
                    line = ""
×
697
                    interrupted = true
×
698
                    break
×
699
                elseif isa(e,EOFError)
×
700
                    hit_eof = true
×
UNCOV
701
                    break
×
702
                else
UNCOV
703
                    rethrow()
×
704
                end
705
            end
706
            ast = Base.parse_input_line(line)
×
707
            (isa(ast,Expr) && ast.head === :incomplete) || break
×
708
        end
×
709
        if !isempty(line)
×
710
            response = eval_with_backend(ast, backend)
×
UNCOV
711
            print_response(repl, response, !ends_with_semicolon(line), false)
×
712
        end
713
        write(repl.terminal, '\n')
×
714
        ((!interrupted && isempty(line)) || hit_eof) && break
×
UNCOV
715
    end
×
716
    # terminate backend
717
    put!(backend.repl_channel, (nothing, -1))
×
718
    dopushdisplay && popdisplay(d)
×
UNCOV
719
    nothing
×
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)
×
746
        opts = Options()
×
747
        opts.hascolor = hascolor
×
748
        if !hascolor
×
UNCOV
749
            opts.beep_colors = [""]
×
750
        end
UNCOV
751
        new(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,pkg_color,history_file,in_shell,
×
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)
×
756
specialdisplay(r::LineEditREPL) = r.specialdisplay
×
757
specialdisplay(r::AbstractREPL) = nothing
×
758
terminal(r::LineEditREPL) = r.t
×
UNCOV
759
hascolor(r::LineEditREPL) = r.hascolor
×
760

UNCOV
761
LineEditREPL(t::TextTerminal, hascolor::Bool, envcolors::Bool=false) =
×
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
774
end
UNCOV
775
REPLCompletionProvider() = REPLCompletionProvider(LineEdit.Modifiers())
×
776

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

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

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

UNCOV
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)
×
UNCOV
799
    return nothing
×
800
end
801

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

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

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

815
function complete_line(c::ShellCompletionProvider, s::PromptState; hint::Bool=false)
×
816
    full = LineEdit.input_string(s)
×
817
    ret, range, should_complete = shell_completions(full, thisind(full, position(s)), hint)
×
818
    range = to_region(full, range)
×
UNCOV
819
    return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), range, should_complete
×
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)
×
UNCOV
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))
×
830
function with_repl_linfo(f, repl::LineEditREPL)
×
831
    linfos = Tuple{String,Int}[]
×
832
    io = IOContext(outstream(repl), :last_shown_line_infos => linfos)
×
833
    f(io)
×
834
    if !isempty(linfos)
×
UNCOV
835
        repl.last_shown_line_infos = linfos
×
836
    end
UNCOV
837
    nothing
×
838
end
839

840
mutable struct REPLHistoryProvider <: HistoryProvider
841
    history::Vector{String}
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
UNCOV
852
REPLHistoryProvider(mode_mapping::Dict{Symbol}) =
×
853
    REPLHistoryProvider(String[], "", nothing, 0, 0, -1, IOBuffer(),
854
                        nothing, mode_mapping, UInt8[])
855

UNCOV
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

UNCOV
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)
×
868
    hp.history_file = f
×
UNCOV
869
    seekend(f)
×
870
end
871

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

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

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

951
function history_move(s::Union{LineEdit.MIState,LineEdit.PrefixSearchState}, hist::REPLHistoryProvider, idx::Int, save_idx::Int = hist.cur_idx)
×
952
    max_idx = length(hist.history) + 1
×
953
    @assert 1 <= hist.cur_idx <= max_idx
×
UNCOV
954
    (1 <= idx <= max_idx) || return :none
×
955
    idx != hist.cur_idx || return :none
×
956

957
    # save the current line
UNCOV
958
    if save_idx == max_idx
×
UNCOV
959
        hist.last_mode = LineEdit.mode(s)
×
960
        hist.last_buffer = copy(LineEdit.buffer(s))
×
961
    else
962
        hist.history[save_idx] = LineEdit.input_string(s)
×
963
        hist.modes[save_idx] = mode_idx(hist, LineEdit.mode(s))
×
964
    end
965

966
    # load the saved line
UNCOV
967
    if idx == max_idx
×
968
        last_buffer = hist.last_buffer
×
969
        LineEdit.transition(s, hist.last_mode) do
×
970
            LineEdit.replace_line(s, last_buffer)
×
971
        end
UNCOV
972
        hist.last_mode = nothing
×
973
        hist.last_buffer = IOBuffer()
×
974
    else
UNCOV
975
        if haskey(hist.mode_mapping, hist.modes[idx])
×
976
            LineEdit.transition(s, hist.mode_mapping[hist.modes[idx]]) do
×
UNCOV
977
                LineEdit.replace_line(s, hist.history[idx])
×
978
            end
979
        else
UNCOV
980
            return :skip
×
981
        end
982
    end
983
    hist.cur_idx = idx
×
984

UNCOV
985
    return :ok
×
986
end
987

988
# REPL History can also transitions modes
989
function LineEdit.accept_result_newmode(hist::REPLHistoryProvider)
×
UNCOV
990
    if 1 <= hist.cur_idx <= length(hist.modes)
×
991
        return hist.mode_mapping[hist.modes[hist.cur_idx]]
×
992
    end
993
    return nothing
×
994
end
995

996
function history_prev(s::LineEdit.MIState, hist::REPLHistoryProvider,
×
997
                      num::Int=1, save_idx::Int = hist.cur_idx)
UNCOV
998
    num <= 0 && return history_next(s, hist, -num, save_idx)
×
999
    hist.last_idx = -1
×
1000
    m = history_move(s, hist, hist.cur_idx-num, save_idx)
×
1001
    if m === :ok
×
UNCOV
1002
        LineEdit.move_input_start(s)
×
1003
        LineEdit.reset_key_repeats(s) do
×
UNCOV
1004
            LineEdit.move_line_end(s)
×
1005
        end
UNCOV
1006
        return LineEdit.refresh_line(s)
×
1007
    elseif m === :skip
×
UNCOV
1008
        return history_prev(s, hist, num+1, save_idx)
×
1009
    else
1010
        return Terminals.beep(s)
×
1011
    end
1012
end
1013

1014
function history_next(s::LineEdit.MIState, hist::REPLHistoryProvider,
×
1015
                      num::Int=1, save_idx::Int = hist.cur_idx)
1016
    if num == 0
×
UNCOV
1017
        Terminals.beep(s)
×
1018
        return
×
1019
    end
UNCOV
1020
    num < 0 && return history_prev(s, hist, -num, save_idx)
×
1021
    cur_idx = hist.cur_idx
×
1022
    max_idx = length(hist.history) + 1
×
1023
    if cur_idx == max_idx && 0 < hist.last_idx
×
1024
        # issue #6312
1025
        cur_idx = hist.last_idx
×
1026
        hist.last_idx = -1
×
1027
    end
1028
    m = history_move(s, hist, cur_idx+num, save_idx)
×
UNCOV
1029
    if m === :ok
×
UNCOV
1030
        LineEdit.move_input_end(s)
×
UNCOV
1031
        return LineEdit.refresh_line(s)
×
1032
    elseif m === :skip
×
UNCOV
1033
        return history_next(s, hist, num+1, save_idx)
×
1034
    else
UNCOV
1035
        return Terminals.beep(s)
×
1036
    end
1037
end
1038

1039
history_first(s::LineEdit.MIState, hist::REPLHistoryProvider) =
×
1040
    history_prev(s, hist, hist.cur_idx - 1 -
1041
                 (hist.cur_idx > hist.start_idx+1 ? hist.start_idx : 0))
1042

UNCOV
1043
history_last(s::LineEdit.MIState, hist::REPLHistoryProvider) =
×
1044
    history_next(s, hist, length(hist.history) - hist.cur_idx + 1)
1045

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

UNCOV
1087
function history_search(hist::REPLHistoryProvider, query_buffer::IOBuffer, response_buffer::IOBuffer,
×
1088
                        backwards::Bool=false, skip_current::Bool=false)
1089

UNCOV
1090
    qpos = position(query_buffer)
×
UNCOV
1091
    qpos > 0 || return true
×
1092
    searchdata = beforecursor(query_buffer)
×
1093
    response_str = String(take!(copy(response_buffer)))
×
1094

1095
    # Alright, first try to see if the current match still works
1096
    a = position(response_buffer) + 1 # position is zero-indexed
×
1097
    # FIXME: I'm pretty sure this is broken since it uses an index
1098
    # into the search data to index into the response string
1099
    b = a + sizeof(searchdata)
×
UNCOV
1100
    b = b ≤ ncodeunits(response_str) ? prevind(response_str, b) : b-1
×
1101
    b = min(lastindex(response_str), b) # ensure that b is valid
×
1102

UNCOV
1103
    searchstart = backwards ? b : a
×
UNCOV
1104
    if searchdata == response_str[a:b]
×
UNCOV
1105
        if skip_current
×
UNCOV
1106
            searchstart = backwards ? prevind(response_str, b) : nextind(response_str, a)
×
1107
        else
1108
            return true
×
1109
        end
1110
    end
1111

1112
    # Start searching
1113
    # First the current response buffer
UNCOV
1114
    if 1 <= searchstart <= lastindex(response_str)
×
UNCOV
1115
        match = backwards ? findprev(searchdata, response_str, searchstart) :
×
1116
                            findnext(searchdata, response_str, searchstart)
1117
        if match !== nothing
×
1118
            seek(response_buffer, first(match) - 1)
×
1119
            return true
×
1120
        end
1121
    end
1122

1123
    # Now search all the other buffers
1124
    idxs = backwards ? ((hist.cur_idx-1):-1:1) : ((hist.cur_idx+1):1:length(hist.history))
×
1125
    for idx in idxs
×
1126
        h = hist.history[idx]
×
UNCOV
1127
        match = backwards ? findlast(searchdata, h) : findfirst(searchdata, h)
×
1128
        if match !== nothing && h != response_str && haskey(hist.mode_mapping, hist.modes[idx])
×
UNCOV
1129
            truncate(response_buffer, 0)
×
1130
            write(response_buffer, h)
×
UNCOV
1131
            seek(response_buffer, first(match) - 1)
×
UNCOV
1132
            hist.cur_idx = idx
×
1133
            return true
×
1134
        end
1135
    end
×
1136

UNCOV
1137
    return false
×
1138
end
1139

1140
function history_reset_state(hist::REPLHistoryProvider)
×
UNCOV
1141
    if hist.cur_idx != length(hist.history) + 1
×
1142
        hist.last_idx = hist.cur_idx
×
1143
        hist.cur_idx = length(hist.history) + 1
×
1144
    end
UNCOV
1145
    nothing
×
1146
end
1147
LineEdit.reset_state(hist::REPLHistoryProvider) = history_reset_state(hist)
×
1148

UNCOV
1149
function return_callback(s)
×
UNCOV
1150
    ast = Base.parse_input_line(String(take!(copy(LineEdit.buffer(s)))), depwarn=false)
×
1151
    return !(isa(ast, Expr) && ast.head === :incomplete)
×
1152
end
1153

1154
find_hist_file() = get(ENV, "JULIA_HISTORY",
×
1155
                       !isempty(DEPOT_PATH) ? joinpath(DEPOT_PATH[1], "logs", "repl_history.jl") :
1156
                       error("DEPOT_PATH is empty and ENV[\"JULIA_HISTORY\"] not set."))
1157

1158
backend(r::AbstractREPL) = hasproperty(r, :backendref) ? r.backendref : nothing
×
1159

1160

UNCOV
1161
function eval_with_backend(ast::Expr, backend::REPLBackendRef)
×
UNCOV
1162
    put!(backend.repl_channel, (ast, 1)) # (f, show_value)
×
1163
    return take!(backend.response_channel) # (val, iserr)
×
1164
end
1165
function eval_with_backend(f, backend::REPLBackendRef)
×
1166
    put!(backend.repl_channel, (f, 2)) # (f, show_value) 2 indicates function (rather than ast)
×
UNCOV
1167
    return take!(backend.response_channel) # (val, iserr)
×
1168
end
1169
# if no backend just eval (used by tests)
UNCOV
1170
function eval_with_backend(f, backend::Nothing)
×
UNCOV
1171
    try
×
UNCOV
1172
        ret = f()
×
1173
        return (ret, false) # (val, iserr)
×
1174
    catch err
1175
        return (err, true)
×
1176
    end
1177
end
1178

1179

1180
function respond(f, repl, main; pass_empty::Bool = false, suppress_on_semicolon::Bool = true)
×
1181
    return function do_respond(s::MIState, buf, ok::Bool)
×
1182
        if !ok
×
1183
            return transition(s, :abort)
×
1184
        end
UNCOV
1185
        line = String(take!(buf)::Vector{UInt8})
×
1186
        if !isempty(line) || pass_empty
×
UNCOV
1187
            reset(repl)
×
1188
            local response
×
1189
            try
×
UNCOV
1190
                ast = Base.invokelatest(f, line)
×
1191
                response = eval_with_backend(ast, backend(repl))
×
1192
            catch
1193
                response = Pair{Any, Bool}(current_exceptions(), true)
×
1194
            end
UNCOV
1195
            hide_output = suppress_on_semicolon && ends_with_semicolon(line)
×
UNCOV
1196
            print_response(repl, response, !hide_output, hascolor(repl))
×
1197
        end
1198
        prepare_next(repl)
×
1199
        reset_state(s)
×
1200
        return s.current_mode.sticky ? true : transition(s, main)
×
1201
    end
1202
end
1203

1204
function reset(repl::LineEditREPL)
×
UNCOV
1205
    raw!(repl.t, false)
×
UNCOV
1206
    hascolor(repl) && print(repl.t, Base.text_colors[:normal])
×
1207
    nothing
×
1208
end
1209

1210
function prepare_next(repl::LineEditREPL)
×
1211
    println(terminal(repl))
×
1212
end
1213

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

1236
repl_filename(repl, hp::REPLHistoryProvider) = "REPL[$(max(length(hp.history)-hp.start_idx, 1))]"
×
1237
repl_filename(repl, hp) = "REPL"
×
1238

1239
const JL_PROMPT_PASTE = Ref(true)
1240
enable_promptpaste(v::Bool) = JL_PROMPT_PASTE[] = v
×
1241

UNCOV
1242
function contextual_prompt(repl::LineEditREPL, prompt::Union{String,Function})
×
UNCOV
1243
    function ()
×
1244
        mod = Base.active_module(repl)
×
UNCOV
1245
        prefix = mod == Main ? "" : string('(', mod, ") ")
×
UNCOV
1246
        pr = prompt isa String ? prompt : prompt()
×
UNCOV
1247
        prefix * pr
×
1248
    end
1249
end
1250

UNCOV
1251
setup_interface(
×
1252
    repl::LineEditREPL;
1253
    # those keyword arguments may be deprecated eventually in favor of the Options mechanism
1254
    hascolor::Bool = repl.options.hascolor,
1255
    extra_repl_keymap::Any = repl.options.extra_keymap
1256
) = setup_interface(repl, hascolor, extra_repl_keymap)
1257

1258

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

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

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

1290
    ############################### Stage I ################################
1291

1292
    # This will provide completions for REPL and help mode
UNCOV
1293
    replc = REPLCompletionProvider()
×
1294

1295
    # Set up the main Julia prompt
UNCOV
1296
    julia_prompt = Prompt(contextual_prompt(repl, JULIA_PROMPT);
×
1297
        # Copy colors from the prompt object
1298
        prompt_prefix = hascolor ? repl.prompt_color : "",
1299
        prompt_suffix = hascolor ?
1300
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1301
        repl = repl,
1302
        complete = replc,
1303
        on_enter = return_callback)
1304

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

1316

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

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

1362

1363
    ################################# Stage II #############################
1364

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

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

1397

UNCOV
1398
    search_prompt, skeymap = LineEdit.setup_search_keymap(hp)
×
UNCOV
1399
    search_prompt.complete = LatexCompletions()
×
1400

1401
    shell_prompt_len = length(SHELL_PROMPT)
×
UNCOV
1402
    help_prompt_len = length(HELP_PROMPT)
×
UNCOV
1403
    jl_prompt_regex = Regex("^In \\[[0-9]+\\]: |^(?:\\(.+\\) )?$JULIA_PROMPT")
×
1404
    pkg_prompt_regex = Regex("^(?:\\(.+\\) )?$PKG_PROMPT")
×
1405

1406
    # Canonicalize user keymap input
1407
    if isa(extra_repl_keymap, Dict)
×
1408
        extra_repl_keymap = AnyDict[extra_repl_keymap]
×
1409
    end
1410

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

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

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

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

1637
    prefix_prompt, prefix_keymap = LineEdit.setup_prefix_keymap(hp, julia_prompt)
×
1638

1639
    a = Dict{Any,Any}[skeymap, repl_keymap, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
×
1640
    prepend!(a, extra_repl_keymap)
×
1641

1642
    julia_prompt.keymap_dict = LineEdit.keymap(a)
×
1643

1644
    mk = mode_keymap(julia_prompt)
×
1645

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

1649
    shell_mode.keymap_dict = help_mode.keymap_dict = dummy_pkg_mode.keymap_dict = LineEdit.keymap(b)
×
1650

1651
    allprompts = LineEdit.TextInterface[julia_prompt, shell_mode, help_mode, dummy_pkg_mode, search_prompt, prefix_prompt]
×
1652
    return ModalInterface(allprompts)
×
1653
end
1654

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

1674
## StreamREPL ##
1675

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

UNCOV
1688
outstream(s::StreamREPL) = s.stream
×
UNCOV
1689
hascolor(s::StreamREPL) = get(s.stream, :color, false)::Bool
×
1690

UNCOV
1691
answer_color(r::LineEditREPL) = r.envcolors ? Base.answer_color() : r.answer_color
×
1692
answer_color(r::StreamREPL) = r.answer_color
×
1693
input_color(r::LineEditREPL) = r.envcolors ? Base.input_color() : r.input_color
×
1694
input_color(r::StreamREPL) = r.input_color
×
1695

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

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

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

UNCOV
1752
        if distance == 0
×
1753
            commit_string = "Commit $(commit) ($(days) $(unit) old master)"
×
1754
        else
1755
            branch = Base.GIT_VERSION_INFO.branch
×
1756
            commit_string = "$(branch)/$(commit) (fork: $(distance) commits, $(days) $(unit))"
×
1757
        end
1758
    end
1759

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

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

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

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

1805
            """)
1806
        end
1807
    end
1808
end
1809

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

1841
module Numbered
1842

1843
using ..REPL
1844

1845
__current_ast_transforms() = Base.active_repl_backend !== nothing ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1846

1847
function repl_eval_counter(hp)
×
1848
    return length(hp.history) - hp.start_idx
×
1849
end
1850

UNCOV
1851
function out_transform(@nospecialize(x), n::Ref{Int})
×
UNCOV
1852
    return Expr(:toplevel, get_usings!([], x)..., quote
×
1853
        let __temp_val_a72df459 = $x
×
1854
            $capture_result($n, __temp_val_a72df459)
×
UNCOV
1855
            __temp_val_a72df459
×
1856
        end
1857
    end)
1858
end
1859

1860
function get_usings!(usings, ex)
×
UNCOV
1861
    ex isa Expr || return usings
×
1862
    # get all `using` and `import` statements which are at the top level
1863
    for (i, arg) in enumerate(ex.args)
×
UNCOV
1864
        if Base.isexpr(arg, :toplevel)
×
UNCOV
1865
            get_usings!(usings, arg)
×
1866
        elseif Base.isexpr(arg, [:using, :import])
×
1867
            push!(usings, popat!(ex.args, i))
×
1868
        end
1869
    end
×
1870
    return usings
×
1871
end
1872

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

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

UNCOV
1897
function set_prompt(repl::LineEditREPL, n::Ref{Int})
×
UNCOV
1898
    julia_prompt = repl.interface.modes[1]
×
1899
    julia_prompt.prompt = function()
×
1900
        n[] = repl_eval_counter(julia_prompt.hist)+1
×
1901
        string("In [", n[], "]: ")
×
1902
    end
UNCOV
1903
    nothing
×
1904
end
1905

UNCOV
1906
function set_output_prefix(repl::LineEditREPL, n::Ref{Int})
×
UNCOV
1907
    julia_prompt = repl.interface.modes[1]
×
1908
    if REPL.hascolor(repl)
×
1909
        julia_prompt.output_prefix_prefix = Base.text_colors[:red]
×
1910
    end
UNCOV
1911
    julia_prompt.output_prefix = () -> string("Out[", n[], "]: ")
×
1912
    nothing
×
1913
end
1914

UNCOV
1915
function __current_ast_transforms(backend)
×
1916
    if backend === nothing
×
1917
        Base.active_repl_backend !== nothing ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1918
    else
1919
        backend.ast_transforms
×
1920
    end
1921
end
1922

UNCOV
1923
function numbered_prompt!(repl::LineEditREPL=Base.active_repl::LineEditREPL, backend=nothing)
×
UNCOV
1924
    n = Ref{Int}(0)
×
UNCOV
1925
    set_prompt(repl, n)
×
UNCOV
1926
    set_output_prefix(repl, n)
×
UNCOV
1927
    push!(__current_ast_transforms(backend), @nospecialize(ast) -> out_transform(ast, n))
×
UNCOV
1928
    return
×
1929
end
1930

1931
"""
1932
    Out[n]
1933

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

1937
See also [`ans`](@ref).
1938
"""
1939
Base.MainInclude.Out
1940

1941
end
1942

1943
import .Numbered.numbered_prompt!
1944

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

1950
if Base.generating_output()
1951
    include("precompile.jl")
1952
end
1953

1954
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