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

JuliaLang / julia / #38059

03 May 2025 12:46AM UTC coverage: 25.84% (+0.02%) from 25.818%
#38059

push

local

web-flow
improve isdefined precision for 0 field types (#58220)

alternate to https://github.com/JuliaLang/julia/pull/58214.

---------

Co-authored-by: Jeff Bezanson <jeff.bezanson@gmail.com>

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

545 existing lines in 3 files now uncovered.

12959 of 50151 relevant lines covered (25.84%)

1023269.32 hits per line

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

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

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

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

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
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
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))
×
135
        close(ref.response_channel, TaskFailedException(state))
×
136
    end
137
    close(ref.repl_channel)
×
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)
×
157
            return ex
×
158
        else
159
            return Expr(:block, Expr(:softscope, true), ex)
×
160
        end
161
    end
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)
×
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
×
179
    end
×
180
end
181

182
retrieve_modules(::Module, ::Any) = (nothing,)
×
183
function retrieve_modules(current_module::Module, mod_name::Symbol)
×
184
    mod = try
×
185
        getproperty(current_module, mod_name)
×
186
    catch
187
        return (nothing,)
×
188
    end
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,)
×
196
        return (current_module, retrieve_modules(current_module, mod_expr.args[2])...)
×
197
    else
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
×
207
    return nothing
×
208
end
209
function add_locals!(locals, ast::Symbol)
×
210
    push!(locals, ast)
×
211
    return nothing
×
212
end
213

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

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

220
    if Meta.isexpr(ast, :., 2)
×
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
×
228
            name_being_accessed = name_being_accessed.value
×
229
        end
230
        name_being_accessed isa Symbol || return
×
231
        owner = try
×
232
            which(mod, name_being_accessed)
×
233
        catch
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`.
238
        has_ancestor(owner, mod) && return
×
239
        # Don't warn if the name is public in the module we are accessing it
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
×
243
        push!(warnings, (; outer_mod, mod, owner, name_being_accessed))
×
244
        # no recursion
245
        return
×
246
    elseif Meta.isexpr(ast, :(=), 2)
×
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.
250
        add_locals!(locals, lhs)
×
251
        # we'll recurse into the RHS only
252
        return collect_names_to_warn!(warnings, locals, current_module, rhs)
×
253
    elseif Meta.isexpr(ast, :function) && length(ast.args) >= 1
×
254

255
        if Meta.isexpr(ast.args[1], :call, 2)
×
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)
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)
×
266
    end
×
267

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)
×
276
        nameof(outer_mod) ∉ locals
×
277
    end
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
×
286
    return ast
×
287
end
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).
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)
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)
×
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
×
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
×
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)
×
335
                put!(backend.response_channel, Pair{Any, Bool}(value, false))
×
336
            end
337
            break
×
338
        catch err
339
            if lasterr !== nothing
×
340
                println("SYSTEM ERROR: Failed to report error to REPL frontend")
×
341
                println(err)
×
342
            end
343
            lasterr = current_exceptions()
×
344
        end
345
    end
×
346
    Base.sigatomic_end()
×
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
×
358
        end
×
359
    end
360
end
361

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

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

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

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

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

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

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

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

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

465
SHOW_MAXIMUM_BYTES::Int = 1_048_576
466

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

475
struct LimitIOException <: Exception
476
    maxbytes::Int
477
end
478

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

483
Base.displaysize(io::LimitIO) = _displaysize(io.io)
×
484

485
function Base.write(io::LimitIO, v::UInt8)
×
486
    io.n > io.maxbytes && throw(LimitIOException(io.maxbytes))
×
487
    n_bytes = write(io.io, v)
×
UNCOV
488
    io.n += n_bytes
×
UNCOV
489
    return n_bytes
×
490
end
491

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

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

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

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

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

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

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

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

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

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

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

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

636

637

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

642
    Main function to start the REPL
643

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

670
## BasicREPL ##
671

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

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

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

729
## LineEditREPL ##
730

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

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

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

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

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

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

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

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

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

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

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

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

822
function complete_line(c::ShellCompletionProvider, s::PromptState; hint::Bool=false)
×
823
    full = LineEdit.input_string(s)
×
824
    ret, range, should_complete = shell_completions(full, thisind(full, position(s)), hint)
×
UNCOV
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
function complete_line(c::LatexCompletions, s; hint::Bool=false)
×
830
    full = LineEdit.input_string(s)::String
×
831
    ret, range, should_complete = bslash_completions(full, thisind(full, position(s)), hint)[2]
×
UNCOV
832
    range = to_region(full, range)
×
UNCOV
833
    return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), range, should_complete
×
834
end
835

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

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

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

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

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

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

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

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

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

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

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

UNCOV
992
    return :ok
×
993
end
994

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

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

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

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

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

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

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

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

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

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

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

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

UNCOV
1144
    return false
×
1145
end
1146

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

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

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

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

1167

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

1186

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

1211
function reset(repl::LineEditREPL)
×
1212
    raw!(repl.t, false)
×
UNCOV
1213
    hascolor(repl) && print(repl.t, Base.text_colors[:normal])
×
UNCOV
1214
    nothing
×
1215
end
1216

UNCOV
1217
function prepare_next(repl::LineEditREPL)
×
UNCOV
1218
    println(terminal(repl))
×
1219
end
1220

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

UNCOV
1243
repl_filename(repl, hp::REPLHistoryProvider) = "REPL[$(max(length(hp.history)-hp.start_idx, 1))]"
×
UNCOV
1244
repl_filename(repl, hp) = "REPL"
×
1245

1246
const JL_PROMPT_PASTE = Ref(true)
1247
enable_promptpaste(v::Bool) = JL_PROMPT_PASTE[] = v
×
1248

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

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

1265

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

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

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

1297
    ############################### Stage I ################################
1298

1299
    # This will provide completions for REPL and help mode
UNCOV
1300
    replc = REPLCompletionProvider()
×
1301

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

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

1323

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

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

1369

1370
    ################################# Stage II #############################
1371

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

UNCOV
1402
    julia_prompt.on_done = respond(x->Base.parse_input_line(x,filename=repl_filename(repl,hp)), repl, julia_prompt)
×
1403

1404

UNCOV
1405
    search_prompt, skeymap = LineEdit.setup_search_keymap(hp)
×
1406
    search_prompt.complete = LatexCompletions()
×
1407

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

1413
    # Canonicalize user keymap input
UNCOV
1414
    if isa(extra_repl_keymap, Dict)
×
UNCOV
1415
        extra_repl_keymap = AnyDict[extra_repl_keymap]
×
1416
    end
1417

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

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

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

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

1644
    prefix_prompt, prefix_keymap = LineEdit.setup_prefix_keymap(hp, julia_prompt)
×
1645

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

1649
    julia_prompt.keymap_dict = LineEdit.keymap(a)
×
1650

1651
    mk = mode_keymap(julia_prompt)
×
1652

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

1656
    shell_mode.keymap_dict = help_mode.keymap_dict = dummy_pkg_mode.keymap_dict = LineEdit.keymap(b)
×
1657

UNCOV
1658
    allprompts = LineEdit.TextInterface[julia_prompt, shell_mode, help_mode, dummy_pkg_mode, search_prompt, prefix_prompt]
×
UNCOV
1659
    return ModalInterface(allprompts)
×
1660
end
1661

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

1681
## StreamREPL ##
1682

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

UNCOV
1695
outstream(s::StreamREPL) = s.stream
×
1696
hascolor(s::StreamREPL) = get(s.stream, :color, false)::Bool
×
1697

1698
answer_color(r::LineEditREPL) = r.envcolors ? Base.answer_color() : r.answer_color
×
1699
answer_color(r::StreamREPL) = r.answer_color
×
UNCOV
1700
input_color(r::LineEditREPL) = r.envcolors ? Base.input_color() : r.input_color
×
UNCOV
1701
input_color(r::StreamREPL) = r.input_color
×
1702

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

1741
# heuristic function to decide if the presence of a semicolon
1742
# at the end of the expression was intended for suppressing output
1743
ends_with_semicolon(code::AbstractString) = ends_with_semicolon(String(code))
×
UNCOV
1744
ends_with_semicolon(code::Union{String,SubString{String}}) =
×
1745
    contains(_rm_strings_and_comments(code), r";\s*$")
×
1746

1747
function banner(io::IO = stdout; short = false)
×
1748
    if Base.GIT_VERSION_INFO.tagged_commit
×
1749
        commit_string = Base.TAGGED_RELEASE_BANNER
×
UNCOV
1750
    elseif isempty(Base.GIT_VERSION_INFO.commit)
×
1751
        commit_string = ""
×
1752
    else
1753
        days = Int(floor((ccall(:jl_clock_now, Float64, ()) - Base.GIT_VERSION_INFO.fork_master_timestamp) / (60 * 60 * 24)))
×
1754
        days = max(0, days)
×
1755
        unit = days == 1 ? "day" : "days"
×
UNCOV
1756
        distance = Base.GIT_VERSION_INFO.fork_master_distance
×
1757
        commit = Base.GIT_VERSION_INFO.commit_short
×
1758

UNCOV
1759
        if distance == 0
×
1760
            commit_string = "Commit $(commit) ($(days) $(unit) old master)"
×
1761
        else
UNCOV
1762
            branch = Base.GIT_VERSION_INFO.branch
×
UNCOV
1763
            commit_string = "$(branch)/$(commit) (fork: $(distance) commits, $(days) $(unit))"
×
1764
        end
1765
    end
1766

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

1769
    if get(io, :color, false)::Bool
×
1770
        c = Base.text_colors
×
1771
        tx = c[:normal] # text
×
1772
        jl = c[:normal] # julia
×
1773
        d1 = c[:bold] * c[:blue]    # first dot
×
1774
        d2 = c[:bold] * c[:red]     # second dot
×
UNCOV
1775
        d3 = c[:bold] * c[:green]   # third dot
×
1776
        d4 = c[:bold] * c[:magenta] # fourth dot
×
1777

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

1793
            """)
1794
        end
1795
    else
UNCOV
1796
        if short
×
UNCOV
1797
            print(io,"""
×
1798
              o  |  Version $(VERSION)$(commit_date)
1799
             o o |  $(commit_string)
1800
            """)
1801
        else
UNCOV
1802
            print(io,"""
×
1803
                           _
1804
               _       _ _(_)_     |  Documentation: https://docs.julialang.org
1805
              (_)     | (_) (_)    |
1806
               _ _   _| |_  __ _   |  Type \"?\" for help, \"]?\" for Pkg help.
1807
              | | | | | | |/ _` |  |
1808
              | | |_| | | | (_| |  |  Version $(VERSION)$(commit_date)
1809
             _/ |\\__'_|_|_|\\__'_|  |  $(commit_string)
1810
            |__/                   |
1811

1812
            """)
1813
        end
1814
    end
1815
end
1816

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

1848
module Numbered
1849

1850
using ..REPL
1851

1852
__current_ast_transforms() = Base.active_repl_backend !== nothing ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1853

UNCOV
1854
function repl_eval_counter(hp)
×
UNCOV
1855
    return length(hp.history) - hp.start_idx
×
1856
end
1857

1858
function out_transform(@nospecialize(x), n::Ref{Int})
×
1859
    return Expr(:toplevel, get_usings!([], x)..., quote
×
1860
        let __temp_val_a72df459 = $x
×
UNCOV
1861
            $capture_result($n, __temp_val_a72df459)
×
UNCOV
1862
            __temp_val_a72df459
×
1863
        end
1864
    end)
1865
end
1866

UNCOV
1867
function get_usings!(usings, ex)
×
1868
    ex isa Expr || return usings
×
1869
    # get all `using` and `import` statements which are at the top level
1870
    for (i, arg) in enumerate(ex.args)
×
1871
        if Base.isexpr(arg, :toplevel)
×
1872
            get_usings!(usings, arg)
×
UNCOV
1873
        elseif Base.isexpr(arg, [:using, :import])
×
1874
            push!(usings, popat!(ex.args, i))
×
1875
        end
UNCOV
1876
    end
×
UNCOV
1877
    return usings
×
1878
end
1879

1880
function create_global_out!(mod)
×
1881
    if !isdefinedglobal(mod, :Out)
×
1882
        out = Dict{Int, Any}()
×
1883
        @eval mod begin
×
UNCOV
1884
            const Out = $(out)
×
1885
            export Out
×
1886
        end
1887
        return out
×
1888
    end
UNCOV
1889
    return getglobal(mod, Out)
×
1890
end
1891

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

1904
function set_prompt(repl::LineEditREPL, n::Ref{Int})
×
1905
    julia_prompt = repl.interface.modes[1]
×
1906
    julia_prompt.prompt = function()
×
UNCOV
1907
        n[] = repl_eval_counter(julia_prompt.hist)+1
×
1908
        string("In [", n[], "]: ")
×
1909
    end
UNCOV
1910
    nothing
×
1911
end
1912

1913
function set_output_prefix(repl::LineEditREPL, n::Ref{Int})
×
1914
    julia_prompt = repl.interface.modes[1]
×
UNCOV
1915
    if REPL.hascolor(repl)
×
1916
        julia_prompt.output_prefix_prefix = Base.text_colors[:red]
×
1917
    end
UNCOV
1918
    julia_prompt.output_prefix = () -> string("Out[", n[], "]: ")
×
UNCOV
1919
    nothing
×
1920
end
1921

1922
function __current_ast_transforms(backend)
×
UNCOV
1923
    if backend === nothing
×
1924
        Base.active_repl_backend !== nothing ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1925
    else
UNCOV
1926
        backend.ast_transforms
×
1927
    end
1928
end
1929

1930
function numbered_prompt!(repl::LineEditREPL=Base.active_repl::LineEditREPL, backend=nothing)
×
1931
    n = Ref{Int}(0)
×
1932
    set_prompt(repl, n)
×
1933
    set_output_prefix(repl, n)
×
UNCOV
1934
    push!(__current_ast_transforms(backend), @nospecialize(ast) -> out_transform(ast, n))
×
UNCOV
1935
    return
×
1936
end
1937

1938
"""
1939
    Out[n]
1940

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

1944
See also [`ans`](@ref).
1945
"""
1946
Base.MainInclude.Out
1947

1948
end
1949

1950
import .Numbered.numbered_prompt!
1951

1952
# this assignment won't survive precompilation,
1953
# but will stick if REPL is baked into a sysimg.
1954
# Needs to occur after this module is finished.
1955
Base.REPL_MODULE_REF[] = REPL
1956

1957
if Base.generating_output()
1958
    include("precompile.jl")
1959
end
1960

1961
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