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

JuliaLang / julia / #38028

15 Mar 2025 07:57AM UTC coverage: 20.131% (+0.1%) from 19.998%
#38028

push

local

web-flow
lowering: Don't closure-convert in `import` or `using` (#57774)

Fixes #57702. We're calling cl-convert- on `using` and `import`
statements when we shouldn't, so if there's a nearby local that gets
boxed (recursive function definition in this case), and the local shares
a name with something in an import statement, we get a box access where
we want a raw symbol.

Before:
```
julia> let; let; import SHA: R; end; let; R(x...) = R(x); end; end
ERROR: TypeError: in import, expected Symbol, got a value of type Expr
Stacktrace:
 [1] top-level scope
   @ REPL[1]:1
```

After:
```
julia> let; let; import SHA: R; end; let; R(x...) = R(x); end; end
(::var"#R#R##0") (generic function with 1 method)
```

Previously, symbols in `import`/`using` statements would be wrapped with
`outerref`, which cl-convert- wouldn't peek into. This protected us from
this problem in 1.11.

9803 of 48697 relevant lines covered (20.13%)

120021.83 hits per line

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

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

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

6
Example minimal code
7

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

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

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

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

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

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

51
public TerminalMenus
52

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

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

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

65
abstract type AbstractREPL end
66

67
include("options.jl")
68

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

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

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

95
include("Pkg_beforeload.jl")
96

97
@nospecialize # use only declared type signatures
98

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

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

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

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

123
"""
124
    softscope(ex)
125

126
Return a modified version of the parsed expression `ex` that uses
127
the REPL's "soft" scoping rules for global syntax blocks.
128
"""
129
function softscope(@nospecialize ex)
×
130
    if ex isa Expr
×
131
        h = ex.head
×
132
        if h === :toplevel
×
133
            ex′ = Expr(h)
×
134
            map!(softscope, resize!(ex′.args, length(ex.args)), ex.args)
×
135
            return ex′
×
136
        elseif h in (:meta, :import, :using, :export, :module, :error, :incomplete, :thunk)
×
137
            return ex
×
138
        elseif h === :global && all(x->isa(x, Symbol), ex.args)
×
139
            return ex
×
140
        else
141
            return Expr(:block, Expr(:softscope, true), ex)
×
142
        end
143
    end
144
    return ex
×
145
end
146

147
# Temporary alias until Documenter updates
148
const softscope! = softscope
149

150
function print_qualified_access_warning(mod::Module, owner::Module, name::Symbol)
×
151
    @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
×
152
end
153

154
function has_ancestor(query::Module, target::Module)
×
155
    query == target && return true
×
156
    while true
×
157
        next = parentmodule(query)
×
158
        next == target && return true
×
159
        next == query && return false
×
160
        query = next
×
161
    end
×
162
end
163

164
retrieve_modules(::Module, ::Any) = (nothing,)
×
165
function retrieve_modules(current_module::Module, mod_name::Symbol)
×
166
    mod = try
×
167
        getproperty(current_module, mod_name)
×
168
    catch
169
        return (nothing,)
×
170
    end
171
    return (mod isa Module ? mod : nothing,)
×
172
end
173
retrieve_modules(current_module::Module, mod_name::QuoteNode) = retrieve_modules(current_module, mod_name.value)
×
174
function retrieve_modules(current_module::Module, mod_expr::Expr)
×
175
    if Meta.isexpr(mod_expr, :., 2)
×
176
        current_module = retrieve_modules(current_module, mod_expr.args[1])[1]
×
177
        current_module === nothing && return (nothing,)
×
178
        return (current_module, retrieve_modules(current_module, mod_expr.args[2])...)
×
179
    else
180
        return (nothing,)
×
181
    end
182
end
183

184
add_locals!(locals, ast::Any) = nothing
×
185
function add_locals!(locals, ast::Expr)
×
186
    for arg in ast.args
×
187
        add_locals!(locals, arg)
×
188
    end
×
189
    return nothing
×
190
end
191
function add_locals!(locals, ast::Symbol)
×
192
    push!(locals, ast)
×
193
    return nothing
×
194
end
195

196
function collect_names_to_warn!(warnings, locals, current_module::Module, ast)
×
197
    ast isa Expr || return
×
198

199
    # don't recurse through module definitions
200
    ast.head === :module && return
×
201

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

237
        if Meta.isexpr(ast.args[1], :call, 2)
×
238
            func_name, func_args = ast.args[1].args
×
239
            # here we have a function definition and are inspecting it's arguments for local variables.
240
            # we will error on the conservative side by adding all symbols we find (regardless if they are local variables or possibly-global default values)
241
            add_locals!(locals, func_args)
×
242
        end
243
        # fall through to general recursion
244
    end
245

246
    for arg in ast.args
×
247
        collect_names_to_warn!(warnings, locals, current_module, arg)
×
248
    end
×
249

250
    return nothing
×
251
end
252

253
function collect_qualified_access_warnings(current_mod, ast)
×
254
    warnings = Set()
×
255
    locals = Set{Symbol}()
×
256
    collect_names_to_warn!(warnings, locals, current_mod, ast)
×
257
    filter!(warnings) do (; outer_mod)
×
258
        nameof(outer_mod) ∉ locals
×
259
    end
260
    return warnings
×
261
end
262

263
function warn_on_non_owning_accesses(current_mod, ast)
×
264
    warnings = collect_qualified_access_warnings(current_mod, ast)
×
265
    for (; outer_mod, mod, owner, name_being_accessed) in warnings
×
266
        print_qualified_access_warning(mod, owner, name_being_accessed)
×
267
    end
×
268
    return ast
×
269
end
270
warn_on_non_owning_accesses(ast) = warn_on_non_owning_accesses(Base.active_module(), ast)
×
271

272
const repl_ast_transforms = Any[softscope, warn_on_non_owning_accesses] # defaults for new REPL backends
273

274
# Allows an external package to add hooks into the code loading.
275
# The hook should take a Vector{Symbol} of package names and
276
# return true if all packages could be installed, false if not
277
# to e.g. install packages on demand
278
const install_packages_hooks = Any[]
279

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

288
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))
×
289
    if !isexpr(ast, :toplevel)
×
290
        ast = invokelatest(__repl_entry_lower_with_loc, mod, ast, toplevel_file, toplevel_line)
×
291
        check_for_missing_packages_and_run_hooks(ast)
×
292
        return invokelatest(__repl_entry_eval_expanded_with_loc, mod, ast, toplevel_file, toplevel_line)
×
293
    end
294
    local value=nothing
×
295
    for i = 1:length(ast.args)
×
296
        value = toplevel_eval_with_hooks(mod, ast.args[i], toplevel_file, toplevel_line)
×
297
    end
×
298
    return value
×
299
end
300

301
function eval_user_input(@nospecialize(ast), backend::REPLBackend, mod::Module)
×
302
    lasterr = nothing
×
303
    Base.sigatomic_begin()
×
304
    while true
×
305
        try
×
306
            Base.sigatomic_end()
×
307
            if lasterr !== nothing
×
308
                put!(backend.response_channel, Pair{Any, Bool}(lasterr, true))
×
309
            else
310
                backend.in_eval = true
×
311
                for xf in backend.ast_transforms
×
312
                    ast = Base.invokelatest(xf, ast)
×
313
                end
×
314
                value = toplevel_eval_with_hooks(mod, ast)
×
315
                backend.in_eval = false
×
316
                setglobal!(Base.MainInclude, :ans, value)
×
317
                put!(backend.response_channel, Pair{Any, Bool}(value, false))
×
318
            end
319
            break
×
320
        catch err
321
            if lasterr !== nothing
×
322
                println("SYSTEM ERROR: Failed to report error to REPL frontend")
×
323
                println(err)
×
324
            end
325
            lasterr = current_exceptions()
×
326
        end
327
    end
×
328
    Base.sigatomic_end()
×
329
    nothing
×
330
end
331

332
function check_for_missing_packages_and_run_hooks(ast)
×
333
    isa(ast, Expr) || return
×
334
    mods = modules_to_be_loaded(ast)
×
335
    filter!(mod -> isnothing(Base.identify_package(String(mod))), mods) # keep missing modules
×
336
    if !isempty(mods)
×
337
        isempty(install_packages_hooks) && load_pkg()
×
338
        for f in install_packages_hooks
×
339
            Base.invokelatest(f, mods) && return
×
340
        end
×
341
    end
342
end
343

344
function _modules_to_be_loaded!(ast::Expr, mods::Vector{Symbol})
×
345
    ast.head === :quote && return mods # don't search if it's not going to be run during this eval
×
346
    if ast.head === :using || ast.head === :import
×
347
        for arg in ast.args
×
348
            arg = arg::Expr
×
349
            arg1 = first(arg.args)
×
350
            if arg1 isa Symbol # i.e. `Foo`
×
351
                if arg1 != :. # don't include local import `import .Foo`
×
352
                    push!(mods, arg1)
×
353
                end
354
            else # i.e. `Foo: bar`
355
                sym = first((arg1::Expr).args)::Symbol
×
356
                if sym != :. # don't include local import `import .Foo: a`
×
357
                    push!(mods, sym)
×
358
                end
359
            end
360
        end
×
361
    end
362
    if ast.head !== :thunk
×
363
        for arg in ast.args
×
364
            if isexpr(arg, (:block, :if, :using, :import))
×
365
                _modules_to_be_loaded!(arg, mods)
×
366
            end
367
        end
×
368
    else
369
        code = ast.args[1]
×
370
        for arg in code.code
×
371
            isa(arg, Expr) || continue
×
372
            _modules_to_be_loaded!(arg, mods)
×
373
        end
×
374
    end
375
end
376

377
function modules_to_be_loaded(ast::Expr, mods::Vector{Symbol} = Symbol[])
×
378
    _modules_to_be_loaded!(ast, mods)
×
379
    filter!(mod::Symbol -> !in(mod, (:Base, :Main, :Core)), mods) # Exclude special non-package modules
×
380
    return unique(mods)
×
381
end
382

383
"""
384
    start_repl_backend(repl_channel::Channel, response_channel::Channel)
385

386
    Starts loop for REPL backend
387
    Returns a REPLBackend with backend_task assigned
388

389
    Deprecated since sync / async behavior cannot be selected
390
"""
391
function start_repl_backend(repl_channel::Channel{Any}, response_channel::Channel{Any}
×
392
                            ; get_module::Function = ()->Main)
393
    # Maintain legacy behavior of asynchronous backend
394
    backend = REPLBackend(repl_channel, response_channel, false)
×
395
    # Assignment will be made twice, but will be immediately available
396
    backend.backend_task = @async start_repl_backend(backend; get_module)
×
397
    return backend
×
398
end
399

400
"""
401
    start_repl_backend(backend::REPLBackend)
402

403
    Call directly to run backend loop on current Task.
404
    Use @async for run backend on new Task.
405

406
    Does not return backend until loop is finished.
407
"""
408
function start_repl_backend(backend::REPLBackend,  @nospecialize(consumer = x -> nothing); get_module::Function = ()->Main)
×
409
    backend.backend_task = Base.current_task()
×
410
    consumer(backend)
×
411
    repl_backend_loop(backend, get_module)
×
412
    return backend
×
413
end
414

415
function repl_backend_loop(backend::REPLBackend, get_module::Function)
×
416
    # include looks at this to determine the relative include path
417
    # nothing means cwd
418
    while true
×
419
        tls = task_local_storage()
×
420
        tls[:SOURCE_PATH] = nothing
×
421
        ast, show_value = take!(backend.repl_channel)
×
422
        if show_value == -1
×
423
            # exit flag
424
            break
×
425
        end
426
        eval_user_input(ast, backend, get_module())
×
427
    end
×
428
    return nothing
×
429
end
430

431
SHOW_MAXIMUM_BYTES::Int = 1_048_576
432

433
# Limit printing during REPL display
434
mutable struct LimitIO{IO_t <: IO} <: IO
435
    io::IO_t
436
    maxbytes::Int
437
    n::Int # max bytes to write
438
end
439
LimitIO(io::IO, maxbytes) = LimitIO(io, maxbytes, 0)
×
440

441
struct LimitIOException <: Exception
442
    maxbytes::Int
443
end
444

445
function Base.showerror(io::IO, e::LimitIOException)
×
446
    print(io, "$LimitIOException: aborted printing after attempting to print more than $(Base.format_bytes(e.maxbytes)) within a `LimitIO`.")
×
447
end
448

449
function Base.write(io::LimitIO, v::UInt8)
×
450
    io.n > io.maxbytes && throw(LimitIOException(io.maxbytes))
×
451
    n_bytes = write(io.io, v)
×
452
    io.n += n_bytes
×
453
    return n_bytes
×
454
end
455

456
# Semantically, we only need to override `Base.write`, but we also
457
# override `unsafe_write` for performance.
458
function Base.unsafe_write(limiter::LimitIO, p::Ptr{UInt8}, nb::UInt)
×
459
    # already exceeded? throw
460
    limiter.n > limiter.maxbytes && throw(LimitIOException(limiter.maxbytes))
×
461
    remaining = limiter.maxbytes - limiter.n # >= 0
×
462

463
    # Not enough bytes left; we will print up to the limit, then throw
464
    if remaining < nb
×
465
        if remaining > 0
×
466
            Base.unsafe_write(limiter.io, p, remaining)
×
467
        end
468
        throw(LimitIOException(limiter.maxbytes))
×
469
    end
470

471
    # We won't hit the limit so we'll write the full `nb` bytes
472
    bytes_written = Base.unsafe_write(limiter.io, p, nb)::Union{Int,UInt}
×
473
    limiter.n += bytes_written
×
474
    return bytes_written
×
475
end
476

477
struct REPLDisplay{Repl<:AbstractREPL} <: AbstractDisplay
478
    repl::Repl
479
end
480

481
function show_limited(io::IO, mime::MIME, x)
×
482
    try
×
483
        # We wrap in a LimitIO to limit the amount of printing.
484
        # We unpack `IOContext`s, since we will pass the properties on the outside.
485
        inner = io isa IOContext ? io.io : io
×
486
        wrapped_limiter = IOContext(LimitIO(inner, SHOW_MAXIMUM_BYTES), io)
×
487
        # `show_repl` to allow the hook with special syntax highlighting
488
        show_repl(wrapped_limiter, mime, x)
×
489
    catch e
490
        e isa LimitIOException || rethrow()
×
491
        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)
×
492
    end
493
end
494

495
function display(d::REPLDisplay, mime::MIME"text/plain", x)
×
496
    x = Ref{Any}(x)
×
497
    with_repl_linfo(d.repl) do io
×
498
        io = IOContext(io, :limit => true, :module => Base.active_module(d)::Module)
×
499
        if d.repl isa LineEditREPL
×
500
            mistate = d.repl.mistate
×
501
            mode = LineEdit.mode(mistate)
×
502
            if mode isa LineEdit.Prompt
×
503
                LineEdit.write_output_prefix(io, mode, get(io, :color, false)::Bool)
×
504
            end
505
        end
506
        get(io, :color, false)::Bool && write(io, answer_color(d.repl))
×
507
        if isdefined(d.repl, :options) && isdefined(d.repl.options, :iocontext)
×
508
            # this can override the :limit property set initially
509
            io = foldl(IOContext, d.repl.options.iocontext, init=io)
×
510
        end
511
        show_limited(io, mime, x[])
×
512
        println(io)
×
513
    end
514
    return nothing
×
515
end
516

517
display(d::REPLDisplay, x) = display(d, MIME("text/plain"), x)
×
518

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

521
show_repl(io::IO, ::MIME"text/plain", ex::Expr) =
×
522
    print(io, JuliaSyntaxHighlighting.highlight(
523
        sprint(show, ex, context=IOContext(io, :color => false))))
524

525
function print_response(repl::AbstractREPL, response, show_value::Bool, have_color::Bool)
×
526
    repl.waserror = response[2]
×
527
    with_repl_linfo(repl) do io
×
528
        io = IOContext(io, :module => Base.active_module(repl)::Module)
×
529
        print_response(io, response, show_value, have_color, specialdisplay(repl))
×
530
    end
531
    return nothing
×
532
end
533

534
function repl_display_error(errio::IO, @nospecialize errval)
×
535
    # this will be set to true if types in the stacktrace are truncated
536
    limitflag = Ref(false)
×
537
    errio = IOContext(errio, :stacktrace_types_limited => limitflag)
×
538
    Base.invokelatest(Base.display_error, errio, errval)
×
539
    if limitflag[]
×
540
        print(errio, "Some type information was truncated. Use `show(err)` to see complete types.")
×
541
        println(errio)
×
542
    end
543
    return nothing
×
544
end
545

546
function print_response(errio::IO, response, show_value::Bool, have_color::Bool, specialdisplay::Union{AbstractDisplay,Nothing}=nothing)
×
547
    Base.sigatomic_begin()
×
548
    val, iserr = response
×
549
    while true
×
550
        try
×
551
            Base.sigatomic_end()
×
552
            if iserr
×
553
                val = Base.scrub_repl_backtrace(val)
×
554
                Base.istrivialerror(val) || setglobal!(Base.MainInclude, :err, val)
×
555
                repl_display_error(errio, val)
×
556
            else
557
                if val !== nothing && show_value
×
558
                    try
×
559
                        if specialdisplay === nothing
×
560
                            Base.invokelatest(display, val)
×
561
                        else
562
                            Base.invokelatest(display, specialdisplay, val)
×
563
                        end
564
                    catch
565
                        println(errio, "Error showing value of type ", typeof(val), ":")
×
566
                        rethrow()
×
567
                    end
568
                end
569
            end
570
            break
×
571
        catch ex
572
            if iserr
×
573
                println(errio) # an error during printing is likely to leave us mid-line
×
574
                println(errio, "SYSTEM (REPL): showing an error caused an error")
×
575
                try
×
576
                    excs = Base.scrub_repl_backtrace(current_exceptions())
×
577
                    setglobal!(Base.MainInclude, :err, excs)
×
578
                    repl_display_error(errio, excs)
×
579
                catch e
580
                    # at this point, only print the name of the type as a Symbol to
581
                    # minimize the possibility of further errors.
582
                    println(errio)
×
583
                    println(errio, "SYSTEM (REPL): caught exception of type ", typeof(e).name.name,
×
584
                            " while trying to handle a nested exception; giving up")
585
                end
586
                break
×
587
            end
588
            val = current_exceptions()
×
589
            iserr = true
×
590
        end
591
    end
×
592
    Base.sigatomic_end()
×
593
    nothing
×
594
end
595

596
# A reference to a backend that is not mutable
597
struct REPLBackendRef
598
    repl_channel::Channel{Any}
599
    response_channel::Channel{Any}
600
end
601
REPLBackendRef(backend::REPLBackend) = REPLBackendRef(backend.repl_channel, backend.response_channel)
×
602

603
function destroy(ref::REPLBackendRef, state::Task)
×
604
    if istaskfailed(state)
×
605
        close(ref.repl_channel, TaskFailedException(state))
×
606
        close(ref.response_channel, TaskFailedException(state))
×
607
    end
608
    close(ref.repl_channel)
×
609
    close(ref.response_channel)
×
610
end
611

612
"""
613
    run_repl(repl::AbstractREPL)
614
    run_repl(repl, consumer = backend->nothing; backend_on_current_task = true)
615

616
    Main function to start the REPL
617

618
    consumer is an optional function that takes a REPLBackend as an argument
619
"""
620
function run_repl(repl::AbstractREPL, @nospecialize(consumer = x -> nothing); backend_on_current_task::Bool = true, backend = REPLBackend())
×
621
    backend_ref = REPLBackendRef(backend)
×
622
    cleanup = @task try
×
623
            destroy(backend_ref, t)
×
624
        catch e
625
            Core.print(Core.stderr, "\nINTERNAL ERROR: ")
×
626
            Core.println(Core.stderr, e)
×
627
            Core.println(Core.stderr, catch_backtrace())
×
628
        end
629
    get_module = () -> Base.active_module(repl)
×
630
    if backend_on_current_task
×
631
        t = @async run_frontend(repl, backend_ref)
×
632
        errormonitor(t)
×
633
        Base._wait2(t, cleanup)
×
634
        start_repl_backend(backend, consumer; get_module)
×
635
    else
636
        t = @async start_repl_backend(backend, consumer; get_module)
×
637
        errormonitor(t)
×
638
        Base._wait2(t, cleanup)
×
639
        run_frontend(repl, backend_ref)
×
640
    end
641
    return backend
×
642
end
643

644
## BasicREPL ##
645

646
mutable struct BasicREPL <: AbstractREPL
647
    terminal::TextTerminal
648
    waserror::Bool
649
    frontend_task::Task
650
    BasicREPL(t) = new(t, false)
×
651
end
652

653
outstream(r::BasicREPL) = r.terminal
×
654
hascolor(r::BasicREPL) = hascolor(r.terminal)
×
655

656
function run_frontend(repl::BasicREPL, backend::REPLBackendRef)
×
657
    repl.frontend_task = current_task()
×
658
    d = REPLDisplay(repl)
×
659
    dopushdisplay = !in(d,Base.Multimedia.displays)
×
660
    dopushdisplay && pushdisplay(d)
×
661
    hit_eof = false
×
662
    while true
×
663
        Base.reseteof(repl.terminal)
×
664
        write(repl.terminal, JULIA_PROMPT)
×
665
        line = ""
×
666
        ast = nothing
×
667
        interrupted = false
×
668
        while true
×
669
            try
×
670
                line *= readline(repl.terminal, keep=true)
×
671
            catch e
672
                if isa(e,InterruptException)
×
673
                    try # raise the debugger if present
×
674
                        ccall(:jl_raise_debugger, Int, ())
×
675
                    catch
×
676
                    end
677
                    line = ""
×
678
                    interrupted = true
×
679
                    break
×
680
                elseif isa(e,EOFError)
×
681
                    hit_eof = true
×
682
                    break
×
683
                else
684
                    rethrow()
×
685
                end
686
            end
687
            ast = Base.parse_input_line(line)
×
688
            (isa(ast,Expr) && ast.head === :incomplete) || break
×
689
        end
×
690
        if !isempty(line)
×
691
            response = eval_with_backend(ast, backend)
×
692
            print_response(repl, response, !ends_with_semicolon(line), false)
×
693
        end
694
        write(repl.terminal, '\n')
×
695
        ((!interrupted && isempty(line)) || hit_eof) && break
×
696
    end
×
697
    # terminate backend
698
    put!(backend.repl_channel, (nothing, -1))
×
699
    dopushdisplay && popdisplay(d)
×
700
    nothing
×
701
end
702

703
## LineEditREPL ##
704

705
mutable struct LineEditREPL <: AbstractREPL
706
    t::TextTerminal
707
    hascolor::Bool
708
    prompt_color::String
709
    input_color::String
710
    answer_color::String
711
    shell_color::String
712
    help_color::String
713
    pkg_color::String
714
    history_file::Bool
715
    in_shell::Bool
716
    in_help::Bool
717
    envcolors::Bool
718
    waserror::Bool
719
    specialdisplay::Union{Nothing,AbstractDisplay}
720
    options::Options
721
    mistate::Union{MIState,Nothing}
722
    last_shown_line_infos::Vector{Tuple{String,Int}}
723
    interface::ModalInterface
724
    backendref::REPLBackendRef
725
    frontend_task::Task
726
    function LineEditREPL(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,pkg_color,history_file,in_shell,in_help,envcolors)
×
727
        opts = Options()
×
728
        opts.hascolor = hascolor
×
729
        if !hascolor
×
730
            opts.beep_colors = [""]
×
731
        end
732
        new(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,pkg_color,history_file,in_shell,
×
733
            in_help,envcolors,false,nothing, opts, nothing, Tuple{String,Int}[])
734
    end
735
end
736
outstream(r::LineEditREPL) = (t = r.t; t isa TTYTerminal ? t.out_stream : t)
×
737
specialdisplay(r::LineEditREPL) = r.specialdisplay
×
738
specialdisplay(r::AbstractREPL) = nothing
×
739
terminal(r::LineEditREPL) = r.t
×
740
hascolor(r::LineEditREPL) = r.hascolor
×
741

742
LineEditREPL(t::TextTerminal, hascolor::Bool, envcolors::Bool=false) =
×
743
    LineEditREPL(t, hascolor,
744
        hascolor ? Base.text_colors[:green] : "",
745
        hascolor ? Base.input_color() : "",
746
        hascolor ? Base.answer_color() : "",
747
        hascolor ? Base.text_colors[:red] : "",
748
        hascolor ? Base.text_colors[:yellow] : "",
749
        hascolor ? Base.text_colors[:blue] : "",
750
        false, false, false, envcolors
751
    )
752

753
mutable struct REPLCompletionProvider <: CompletionProvider
754
    modifiers::LineEdit.Modifiers
755
end
756
REPLCompletionProvider() = REPLCompletionProvider(LineEdit.Modifiers())
×
757

758
mutable struct ShellCompletionProvider <: CompletionProvider end
759
struct LatexCompletions <: CompletionProvider end
760

761
Base.active_module((; mistate)::LineEditREPL) = mistate === nothing ? Main : mistate.active_module
×
762
Base.active_module(::AbstractREPL) = Main
×
763
Base.active_module(d::REPLDisplay) = Base.active_module(d.repl)
×
764

765
setmodifiers!(c::CompletionProvider, m::LineEdit.Modifiers) = nothing
×
766

767
setmodifiers!(c::REPLCompletionProvider, m::LineEdit.Modifiers) = c.modifiers = m
×
768

769
"""
770
    activate(mod::Module=Main)
771

772
Set `mod` as the default contextual module in the REPL,
773
both for evaluating expressions and printing them.
774
"""
775
function activate(mod::Module=Main; interactive_utils::Bool=true)
×
776
    mistate = (Base.active_repl::LineEditREPL).mistate
×
777
    mistate === nothing && return nothing
×
778
    mistate.active_module = mod
×
779
    interactive_utils && Base.load_InteractiveUtils(mod)
×
780
    return nothing
×
781
end
782

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

785
function complete_line(c::REPLCompletionProvider, s::PromptState, mod::Module; hint::Bool=false)
×
786
    partial = beforecursor(s.input_buffer)
×
787
    full = LineEdit.input_string(s)
×
788
    ret, range, should_complete = completions(full, lastindex(partial), mod, c.modifiers.shift, hint)
×
789
    c.modifiers = LineEdit.Modifiers()
×
790
    return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), partial[range], should_complete
×
791
end
792

793
function complete_line(c::ShellCompletionProvider, s::PromptState; hint::Bool=false)
×
794
    # First parse everything up to the current position
795
    partial = beforecursor(s.input_buffer)
×
796
    full = LineEdit.input_string(s)
×
797
    ret, range, should_complete = shell_completions(full, lastindex(partial), hint)
×
798
    return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), partial[range], should_complete
×
799
end
800

801
function complete_line(c::LatexCompletions, s; hint::Bool=false)
×
802
    partial = beforecursor(LineEdit.buffer(s))
×
803
    full = LineEdit.input_string(s)::String
×
804
    ret, range, should_complete = bslash_completions(full, lastindex(partial), hint)[2]
×
805
    return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), partial[range], should_complete
×
806
end
807

808
with_repl_linfo(f, repl) = f(outstream(repl))
×
809
function with_repl_linfo(f, repl::LineEditREPL)
×
810
    linfos = Tuple{String,Int}[]
×
811
    io = IOContext(outstream(repl), :last_shown_line_infos => linfos)
×
812
    f(io)
×
813
    if !isempty(linfos)
×
814
        repl.last_shown_line_infos = linfos
×
815
    end
816
    nothing
×
817
end
818

819
mutable struct REPLHistoryProvider <: HistoryProvider
820
    history::Vector{String}
821
    file_path::String
822
    history_file::Union{Nothing,IO}
823
    start_idx::Int
824
    cur_idx::Int
825
    last_idx::Int
826
    last_buffer::IOBuffer
827
    last_mode::Union{Nothing,Prompt}
828
    mode_mapping::Dict{Symbol,Prompt}
829
    modes::Vector{Symbol}
830
end
831
REPLHistoryProvider(mode_mapping::Dict{Symbol}) =
×
832
    REPLHistoryProvider(String[], "", nothing, 0, 0, -1, IOBuffer(),
833
                        nothing, mode_mapping, UInt8[])
834

835
invalid_history_message(path::String) = """
×
836
Invalid history file ($path) format:
837
If you have a history file left over from an older version of Julia,
838
try renaming or deleting it.
839
Invalid character: """
840

841
munged_history_message(path::String) = """
×
842
Invalid history file ($path) format:
843
An editor may have converted tabs to spaces at line """
844

845
function hist_open_file(hp::REPLHistoryProvider)
×
846
    f = open(hp.file_path, read=true, write=true, create=true)
×
847
    hp.history_file = f
×
848
    seekend(f)
×
849
end
850

851
function hist_from_file(hp::REPLHistoryProvider, path::String)
×
852
    getline(lines, i) = i > length(lines) ? "" : lines[i]
×
853
    file_lines = readlines(path)
×
854
    countlines = 0
×
855
    while true
×
856
        # First parse the metadata that starts with '#' in particular the REPL mode
857
        countlines += 1
×
858
        line = getline(file_lines, countlines)
×
859
        mode = :julia
×
860
        isempty(line) && break
×
861
        line[1] != '#' &&
×
862
            error(invalid_history_message(path), repr(line[1]), " at line ", countlines)
863
        while !isempty(line)
×
864
            startswith(line, '#') || break
×
865
            if startswith(line, "# mode: ")
×
866
                mode = Symbol(SubString(line, 9))
×
867
            end
868
            countlines += 1
×
869
            line = getline(file_lines, countlines)
×
870
        end
×
871
        isempty(line) && break
×
872

873
        # Now parse the code for the current REPL mode
874
        line[1] == ' '  &&
×
875
            error(munged_history_message(path), countlines)
876
        line[1] != '\t' &&
×
877
            error(invalid_history_message(path), repr(line[1]), " at line ", countlines)
878
        lines = String[]
×
879
        while !isempty(line)
×
880
            push!(lines, chomp(SubString(line, 2)))
×
881
            next_line = getline(file_lines, countlines+1)
×
882
            isempty(next_line) && break
×
883
            first(next_line) == ' '  && error(munged_history_message(path), countlines)
×
884
            # A line not starting with a tab means we are done with code for this entry
885
            first(next_line) != '\t' && break
×
886
            countlines += 1
×
887
            line = getline(file_lines, countlines)
×
888
        end
×
889
        push!(hp.modes, mode)
×
890
        push!(hp.history, join(lines, '\n'))
×
891
    end
×
892
    hp.start_idx = length(hp.history)
×
893
    return hp
×
894
end
895

896
function add_history(hist::REPLHistoryProvider, s::PromptState)
×
897
    str = rstrip(String(take!(copy(s.input_buffer))))
×
898
    isempty(strip(str)) && return
×
899
    mode = mode_idx(hist, LineEdit.mode(s))
×
900
    !isempty(hist.history) &&
×
901
        isequal(mode, hist.modes[end]) && str == hist.history[end] && return
902
    push!(hist.modes, mode)
×
903
    push!(hist.history, str)
×
904
    hist.history_file === nothing && return
×
905
    entry = """
×
906
    # time: $(Libc.strftime("%Y-%m-%d %H:%M:%S %Z", time()))
907
    # mode: $mode
908
    $(replace(str, r"^"ms => "\t"))
×
909
    """
910
    # TODO: write-lock history file
911
    try
×
912
        seekend(hist.history_file)
×
913
    catch err
914
        (err isa SystemError) || rethrow()
×
915
        # File handle might get stale after a while, especially under network file systems
916
        # If this doesn't fix it (e.g. when file is deleted), we'll end up rethrowing anyway
917
        hist_open_file(hist)
×
918
    end
919
    print(hist.history_file, entry)
×
920
    flush(hist.history_file)
×
921
    nothing
×
922
end
923

924
function history_move(s::Union{LineEdit.MIState,LineEdit.PrefixSearchState}, hist::REPLHistoryProvider, idx::Int, save_idx::Int = hist.cur_idx)
×
925
    max_idx = length(hist.history) + 1
×
926
    @assert 1 <= hist.cur_idx <= max_idx
×
927
    (1 <= idx <= max_idx) || return :none
×
928
    idx != hist.cur_idx || return :none
×
929

930
    # save the current line
931
    if save_idx == max_idx
×
932
        hist.last_mode = LineEdit.mode(s)
×
933
        hist.last_buffer = copy(LineEdit.buffer(s))
×
934
    else
935
        hist.history[save_idx] = LineEdit.input_string(s)
×
936
        hist.modes[save_idx] = mode_idx(hist, LineEdit.mode(s))
×
937
    end
938

939
    # load the saved line
940
    if idx == max_idx
×
941
        last_buffer = hist.last_buffer
×
942
        LineEdit.transition(s, hist.last_mode) do
×
943
            LineEdit.replace_line(s, last_buffer)
×
944
        end
945
        hist.last_mode = nothing
×
946
        hist.last_buffer = IOBuffer()
×
947
    else
948
        if haskey(hist.mode_mapping, hist.modes[idx])
×
949
            LineEdit.transition(s, hist.mode_mapping[hist.modes[idx]]) do
×
950
                LineEdit.replace_line(s, hist.history[idx])
×
951
            end
952
        else
953
            return :skip
×
954
        end
955
    end
956
    hist.cur_idx = idx
×
957

958
    return :ok
×
959
end
960

961
# REPL History can also transitions modes
962
function LineEdit.accept_result_newmode(hist::REPLHistoryProvider)
×
963
    if 1 <= hist.cur_idx <= length(hist.modes)
×
964
        return hist.mode_mapping[hist.modes[hist.cur_idx]]
×
965
    end
966
    return nothing
×
967
end
968

969
function history_prev(s::LineEdit.MIState, hist::REPLHistoryProvider,
×
970
                      num::Int=1, save_idx::Int = hist.cur_idx)
971
    num <= 0 && return history_next(s, hist, -num, save_idx)
×
972
    hist.last_idx = -1
×
973
    m = history_move(s, hist, hist.cur_idx-num, save_idx)
×
974
    if m === :ok
×
975
        LineEdit.move_input_start(s)
×
976
        LineEdit.reset_key_repeats(s) do
×
977
            LineEdit.move_line_end(s)
×
978
        end
979
        return LineEdit.refresh_line(s)
×
980
    elseif m === :skip
×
981
        return history_prev(s, hist, num+1, save_idx)
×
982
    else
983
        return Terminals.beep(s)
×
984
    end
985
end
986

987
function history_next(s::LineEdit.MIState, hist::REPLHistoryProvider,
×
988
                      num::Int=1, save_idx::Int = hist.cur_idx)
989
    if num == 0
×
990
        Terminals.beep(s)
×
991
        return
×
992
    end
993
    num < 0 && return history_prev(s, hist, -num, save_idx)
×
994
    cur_idx = hist.cur_idx
×
995
    max_idx = length(hist.history) + 1
×
996
    if cur_idx == max_idx && 0 < hist.last_idx
×
997
        # issue #6312
998
        cur_idx = hist.last_idx
×
999
        hist.last_idx = -1
×
1000
    end
1001
    m = history_move(s, hist, cur_idx+num, save_idx)
×
1002
    if m === :ok
×
1003
        LineEdit.move_input_end(s)
×
1004
        return LineEdit.refresh_line(s)
×
1005
    elseif m === :skip
×
1006
        return history_next(s, hist, num+1, save_idx)
×
1007
    else
1008
        return Terminals.beep(s)
×
1009
    end
1010
end
1011

1012
history_first(s::LineEdit.MIState, hist::REPLHistoryProvider) =
×
1013
    history_prev(s, hist, hist.cur_idx - 1 -
1014
                 (hist.cur_idx > hist.start_idx+1 ? hist.start_idx : 0))
1015

1016
history_last(s::LineEdit.MIState, hist::REPLHistoryProvider) =
×
1017
    history_next(s, hist, length(hist.history) - hist.cur_idx + 1)
1018

1019
function history_move_prefix(s::LineEdit.PrefixSearchState,
×
1020
                             hist::REPLHistoryProvider,
1021
                             prefix::AbstractString,
1022
                             backwards::Bool,
1023
                             cur_idx::Int = hist.cur_idx)
1024
    cur_response = String(take!(copy(LineEdit.buffer(s))))
×
1025
    # when searching forward, start at last_idx
1026
    if !backwards && hist.last_idx > 0
×
1027
        cur_idx = hist.last_idx
×
1028
    end
1029
    hist.last_idx = -1
×
1030
    max_idx = length(hist.history)+1
×
1031
    idxs = backwards ? ((cur_idx-1):-1:1) : ((cur_idx+1):1:max_idx)
×
1032
    for idx in idxs
×
1033
        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)))
×
1034
            m = history_move(s, hist, idx)
×
1035
            if m === :ok
×
1036
                if idx == max_idx
×
1037
                    # on resuming the in-progress edit, leave the cursor where the user last had it
1038
                elseif isempty(prefix)
×
1039
                    # on empty prefix search, move cursor to the end
1040
                    LineEdit.move_input_end(s)
×
1041
                else
1042
                    # otherwise, keep cursor at the prefix position as a visual cue
1043
                    seek(LineEdit.buffer(s), sizeof(prefix))
×
1044
                end
1045
                LineEdit.refresh_line(s)
×
1046
                return :ok
×
1047
            elseif m === :skip
×
1048
                return history_move_prefix(s,hist,prefix,backwards,idx)
×
1049
            end
1050
        end
1051
    end
×
1052
    Terminals.beep(s)
×
1053
    nothing
×
1054
end
1055
history_next_prefix(s::LineEdit.PrefixSearchState, hist::REPLHistoryProvider, prefix::AbstractString) =
×
1056
    history_move_prefix(s, hist, prefix, false)
1057
history_prev_prefix(s::LineEdit.PrefixSearchState, hist::REPLHistoryProvider, prefix::AbstractString) =
×
1058
    history_move_prefix(s, hist, prefix, true)
1059

1060
function history_search(hist::REPLHistoryProvider, query_buffer::IOBuffer, response_buffer::IOBuffer,
×
1061
                        backwards::Bool=false, skip_current::Bool=false)
1062

1063
    qpos = position(query_buffer)
×
1064
    qpos > 0 || return true
×
1065
    searchdata = beforecursor(query_buffer)
×
1066
    response_str = String(take!(copy(response_buffer)))
×
1067

1068
    # Alright, first try to see if the current match still works
1069
    a = position(response_buffer) + 1 # position is zero-indexed
×
1070
    # FIXME: I'm pretty sure this is broken since it uses an index
1071
    # into the search data to index into the response string
1072
    b = a + sizeof(searchdata)
×
1073
    b = b ≤ ncodeunits(response_str) ? prevind(response_str, b) : b-1
×
1074
    b = min(lastindex(response_str), b) # ensure that b is valid
×
1075

1076
    searchstart = backwards ? b : a
×
1077
    if searchdata == response_str[a:b]
×
1078
        if skip_current
×
1079
            searchstart = backwards ? prevind(response_str, b) : nextind(response_str, a)
×
1080
        else
1081
            return true
×
1082
        end
1083
    end
1084

1085
    # Start searching
1086
    # First the current response buffer
1087
    if 1 <= searchstart <= lastindex(response_str)
×
1088
        match = backwards ? findprev(searchdata, response_str, searchstart) :
×
1089
                            findnext(searchdata, response_str, searchstart)
1090
        if match !== nothing
×
1091
            seek(response_buffer, first(match) - 1)
×
1092
            return true
×
1093
        end
1094
    end
1095

1096
    # Now search all the other buffers
1097
    idxs = backwards ? ((hist.cur_idx-1):-1:1) : ((hist.cur_idx+1):1:length(hist.history))
×
1098
    for idx in idxs
×
1099
        h = hist.history[idx]
×
1100
        match = backwards ? findlast(searchdata, h) : findfirst(searchdata, h)
×
1101
        if match !== nothing && h != response_str && haskey(hist.mode_mapping, hist.modes[idx])
×
1102
            truncate(response_buffer, 0)
×
1103
            write(response_buffer, h)
×
1104
            seek(response_buffer, first(match) - 1)
×
1105
            hist.cur_idx = idx
×
1106
            return true
×
1107
        end
1108
    end
×
1109

1110
    return false
×
1111
end
1112

1113
function history_reset_state(hist::REPLHistoryProvider)
×
1114
    if hist.cur_idx != length(hist.history) + 1
×
1115
        hist.last_idx = hist.cur_idx
×
1116
        hist.cur_idx = length(hist.history) + 1
×
1117
    end
1118
    nothing
×
1119
end
1120
LineEdit.reset_state(hist::REPLHistoryProvider) = history_reset_state(hist)
×
1121

1122
function return_callback(s)
×
1123
    ast = Base.parse_input_line(String(take!(copy(LineEdit.buffer(s)))), depwarn=false)
×
1124
    return !(isa(ast, Expr) && ast.head === :incomplete)
×
1125
end
1126

1127
find_hist_file() = get(ENV, "JULIA_HISTORY",
×
1128
                       !isempty(DEPOT_PATH) ? joinpath(DEPOT_PATH[1], "logs", "repl_history.jl") :
1129
                       error("DEPOT_PATH is empty and ENV[\"JULIA_HISTORY\"] not set."))
1130

1131
backend(r::AbstractREPL) = r.backendref
×
1132

1133
function eval_with_backend(ast, backend::REPLBackendRef)
×
1134
    put!(backend.repl_channel, (ast, 1))
×
1135
    return take!(backend.response_channel) # (val, iserr)
×
1136
end
1137

1138
function respond(f, repl, main; pass_empty::Bool = false, suppress_on_semicolon::Bool = true)
×
1139
    return function do_respond(s::MIState, buf, ok::Bool)
×
1140
        if !ok
×
1141
            return transition(s, :abort)
×
1142
        end
1143
        line = String(take!(buf)::Vector{UInt8})
×
1144
        if !isempty(line) || pass_empty
×
1145
            reset(repl)
×
1146
            local response
×
1147
            try
×
1148
                ast = Base.invokelatest(f, line)
×
1149
                response = eval_with_backend(ast, backend(repl))
×
1150
            catch
1151
                response = Pair{Any, Bool}(current_exceptions(), true)
×
1152
            end
1153
            hide_output = suppress_on_semicolon && ends_with_semicolon(line)
×
1154
            print_response(repl, response, !hide_output, hascolor(repl))
×
1155
        end
1156
        prepare_next(repl)
×
1157
        reset_state(s)
×
1158
        return s.current_mode.sticky ? true : transition(s, main)
×
1159
    end
1160
end
1161

1162
function reset(repl::LineEditREPL)
×
1163
    raw!(repl.t, false)
×
1164
    hascolor(repl) && print(repl.t, Base.text_colors[:normal])
×
1165
    nothing
×
1166
end
1167

1168
function prepare_next(repl::LineEditREPL)
×
1169
    println(terminal(repl))
×
1170
end
1171

1172
function mode_keymap(julia_prompt::Prompt)
×
1173
    AnyDict(
×
1174
    '\b' => function (s::MIState,o...)
×
1175
        if isempty(s) || position(LineEdit.buffer(s)) == 0
×
1176
            buf = copy(LineEdit.buffer(s))
×
1177
            transition(s, julia_prompt) do
×
1178
                LineEdit.state(s, julia_prompt).input_buffer = buf
×
1179
            end
1180
        else
1181
            LineEdit.edit_backspace(s)
×
1182
        end
1183
    end,
1184
    "^C" => function (s::MIState,o...)
×
1185
        LineEdit.move_input_end(s)
×
1186
        LineEdit.refresh_line(s)
×
1187
        print(LineEdit.terminal(s), "^C\n\n")
×
1188
        transition(s, julia_prompt)
×
1189
        transition(s, :reset)
×
1190
        LineEdit.refresh_line(s)
×
1191
    end)
1192
end
1193

1194
repl_filename(repl, hp::REPLHistoryProvider) = "REPL[$(max(length(hp.history)-hp.start_idx, 1))]"
×
1195
repl_filename(repl, hp) = "REPL"
×
1196

1197
const JL_PROMPT_PASTE = Ref(true)
1198
enable_promptpaste(v::Bool) = JL_PROMPT_PASTE[] = v
×
1199

1200
function contextual_prompt(repl::LineEditREPL, prompt::Union{String,Function})
×
1201
    function ()
×
1202
        mod = Base.active_module(repl)
×
1203
        prefix = mod == Main ? "" : string('(', mod, ") ")
×
1204
        pr = prompt isa String ? prompt : prompt()
×
1205
        prefix * pr
×
1206
    end
1207
end
1208

1209
setup_interface(
×
1210
    repl::LineEditREPL;
1211
    # those keyword arguments may be deprecated eventually in favor of the Options mechanism
1212
    hascolor::Bool = repl.options.hascolor,
1213
    extra_repl_keymap::Any = repl.options.extra_keymap
1214
) = setup_interface(repl, hascolor, extra_repl_keymap)
1215

1216

1217
# This non keyword method can be precompiled which is important
1218
function setup_interface(
×
1219
    repl::LineEditREPL,
1220
    hascolor::Bool,
1221
    extra_repl_keymap::Any, # Union{Dict,Vector{<:Dict}},
1222
)
1223
    # The precompile statement emitter has problem outputting valid syntax for the
1224
    # type of `Union{Dict,Vector{<:Dict}}` (see #28808).
1225
    # This function is however important to precompile for REPL startup time, therefore,
1226
    # make the type Any and just assert that we have the correct type below.
1227
    @assert extra_repl_keymap isa Union{Dict,Vector{<:Dict}}
×
1228

1229
    ###
1230
    #
1231
    # This function returns the main interface that describes the REPL
1232
    # functionality, it is called internally by functions that setup a
1233
    # Terminal-based REPL frontend.
1234
    #
1235
    # See run_frontend(repl::LineEditREPL, backend::REPLBackendRef)
1236
    # for usage
1237
    #
1238
    ###
1239

1240
    ###
1241
    # We setup the interface in two stages.
1242
    # First, we set up all components (prompt,rsearch,shell,help)
1243
    # Second, we create keymaps with appropriate transitions between them
1244
    #   and assign them to the components
1245
    #
1246
    ###
1247

1248
    ############################### Stage I ################################
1249

1250
    # This will provide completions for REPL and help mode
1251
    replc = REPLCompletionProvider()
×
1252

1253
    # Set up the main Julia prompt
1254
    julia_prompt = Prompt(contextual_prompt(repl, JULIA_PROMPT);
×
1255
        # Copy colors from the prompt object
1256
        prompt_prefix = hascolor ? repl.prompt_color : "",
1257
        prompt_suffix = hascolor ?
1258
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1259
        repl = repl,
1260
        complete = replc,
1261
        on_enter = return_callback)
1262

1263
    # Setup help mode
1264
    help_mode = Prompt(contextual_prompt(repl, HELP_PROMPT),
×
1265
        prompt_prefix = hascolor ? repl.help_color : "",
1266
        prompt_suffix = hascolor ?
1267
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1268
        repl = repl,
1269
        complete = replc,
1270
        # When we're done transform the entered line into a call to helpmode function
1271
        on_done = respond(line::String->helpmode(outstream(repl), line, repl.mistate.active_module),
×
1272
                          repl, julia_prompt, pass_empty=true, suppress_on_semicolon=false))
1273

1274

1275
    # Set up shell mode
1276
    shell_mode = Prompt(SHELL_PROMPT;
×
1277
        prompt_prefix = hascolor ? repl.shell_color : "",
1278
        prompt_suffix = hascolor ?
1279
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1280
        repl = repl,
1281
        complete = ShellCompletionProvider(),
1282
        # Transform "foo bar baz" into `foo bar baz` (shell quoting)
1283
        # and pass into Base.repl_cmd for processing (handles `ls` and `cd`
1284
        # special)
1285
        on_done = respond(repl, julia_prompt) do line
1286
            Expr(:call, :(Base.repl_cmd),
×
1287
                :(Base.cmd_gen($(Base.shell_parse(line::String)[1]))),
1288
                outstream(repl))
1289
        end,
1290
        sticky = true)
1291

1292
    # Set up dummy Pkg mode that will be replaced once Pkg is loaded
1293
    # use 6 dots to occupy the same space as the most likely "@v1.xx" env name
1294
    dummy_pkg_mode = Prompt(Pkg_promptf,
×
1295
        prompt_prefix = hascolor ? repl.pkg_color : "",
1296
        prompt_suffix = hascolor ?
1297
        (repl.envcolors ? Base.input_color : repl.input_color) : "",
1298
        repl = repl,
1299
        complete = LineEdit.EmptyCompletionProvider(),
1300
        on_done = respond(line->nothing, repl, julia_prompt),
×
1301
        on_enter = function (s::MIState)
×
1302
                # This is hit when the user tries to execute a command before the real Pkg mode has been
1303
                # switched to. Ok to do this even if Pkg is loading on the other task because of the loading lock.
1304
                REPLExt = load_pkg()
×
1305
                if REPLExt isa Module && isdefined(REPLExt, :PkgCompletionProvider)
×
1306
                    for mode in repl.interface.modes
×
1307
                        if mode isa LineEdit.Prompt && mode.complete isa REPLExt.PkgCompletionProvider
×
1308
                            # pkg mode
1309
                            buf = copy(LineEdit.buffer(s))
×
1310
                            transition(s, mode) do
×
1311
                                LineEdit.state(s, mode).input_buffer = buf
×
1312
                            end
1313
                        end
1314
                    end
×
1315
                end
1316
                return true
×
1317
            end,
1318
        sticky = true)
1319

1320

1321
    ################################# Stage II #############################
1322

1323
    # Setup history
1324
    # We will have a unified history for all REPL modes
1325
    hp = REPLHistoryProvider(Dict{Symbol,Prompt}(:julia => julia_prompt,
×
1326
                                                 :shell => shell_mode,
1327
                                                 :help  => help_mode,
1328
                                                 :pkg  => dummy_pkg_mode))
1329
    if repl.history_file
×
1330
        try
×
1331
            hist_path = find_hist_file()
×
1332
            mkpath(dirname(hist_path))
×
1333
            hp.file_path = hist_path
×
1334
            hist_open_file(hp)
×
1335
            finalizer(replc) do replc
×
1336
                close(hp.history_file)
×
1337
            end
1338
            hist_from_file(hp, hist_path)
×
1339
        catch
1340
            # use REPL.hascolor to avoid using the local variable with the same name
1341
            print_response(repl, Pair{Any, Bool}(current_exceptions(), true), true, REPL.hascolor(repl))
×
1342
            println(outstream(repl))
×
1343
            @info "Disabling history file for this session"
×
1344
            repl.history_file = false
×
1345
        end
1346
    end
1347
    history_reset_state(hp)
×
1348
    julia_prompt.hist = hp
×
1349
    shell_mode.hist = hp
×
1350
    help_mode.hist = hp
×
1351
    dummy_pkg_mode.hist = hp
×
1352

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

1355

1356
    search_prompt, skeymap = LineEdit.setup_search_keymap(hp)
×
1357
    search_prompt.complete = LatexCompletions()
×
1358

1359
    shell_prompt_len = length(SHELL_PROMPT)
×
1360
    help_prompt_len = length(HELP_PROMPT)
×
1361
    jl_prompt_regex = Regex("^In \\[[0-9]+\\]: |^(?:\\(.+\\) )?$JULIA_PROMPT")
×
1362
    pkg_prompt_regex = Regex("^(?:\\(.+\\) )?$PKG_PROMPT")
×
1363

1364
    # Canonicalize user keymap input
1365
    if isa(extra_repl_keymap, Dict)
×
1366
        extra_repl_keymap = AnyDict[extra_repl_keymap]
×
1367
    end
1368

1369
    repl_keymap = AnyDict(
×
1370
        ';' => function (s::MIState,o...)
×
1371
            if isempty(s) || position(LineEdit.buffer(s)) == 0
×
1372
                buf = copy(LineEdit.buffer(s))
×
1373
                transition(s, shell_mode) do
×
1374
                    LineEdit.state(s, shell_mode).input_buffer = buf
×
1375
                end
1376
            else
1377
                edit_insert(s, ';')
×
1378
                LineEdit.check_show_hint(s)
×
1379
            end
1380
        end,
1381
        '?' => function (s::MIState,o...)
×
1382
            if isempty(s) || position(LineEdit.buffer(s)) == 0
×
1383
                buf = copy(LineEdit.buffer(s))
×
1384
                transition(s, help_mode) do
×
1385
                    LineEdit.state(s, help_mode).input_buffer = buf
×
1386
                end
1387
            else
1388
                edit_insert(s, '?')
×
1389
                LineEdit.check_show_hint(s)
×
1390
            end
1391
        end,
1392
        ']' => function (s::MIState,o...)
×
1393
            if isempty(s) || position(LineEdit.buffer(s)) == 0
×
1394
                buf = copy(LineEdit.buffer(s))
×
1395
                transition(s, dummy_pkg_mode) do
×
1396
                    LineEdit.state(s, dummy_pkg_mode).input_buffer = buf
×
1397
                end
1398
                # load Pkg on another thread if available so that typing in the dummy Pkg prompt
1399
                # isn't blocked, but instruct the main REPL task to do the transition via s.async_channel
1400
                t_replswitch = Threads.@spawn begin
×
1401
                    REPLExt = load_pkg()
×
1402
                    if REPLExt isa Module && isdefined(REPLExt, :PkgCompletionProvider)
×
1403
                        put!(s.async_channel,
×
1404
                            function (s::MIState)
×
1405
                                LineEdit.mode(s) === dummy_pkg_mode || return :ok
×
1406
                                for mode in repl.interface.modes
×
1407
                                    if mode isa LineEdit.Prompt && mode.complete isa REPLExt.PkgCompletionProvider
×
1408
                                        buf = copy(LineEdit.buffer(s))
×
1409
                                        transition(s, mode) do
×
1410
                                            LineEdit.state(s, mode).input_buffer = buf
×
1411
                                        end
1412
                                        if !isempty(s)
×
1413
                                            @invokelatest(LineEdit.check_show_hint(s))
×
1414
                                        end
1415
                                        break
×
1416
                                    end
1417
                                end
×
1418
                                return :ok
×
1419
                            end
1420
                        )
1421
                    end
1422
                end
1423
                Base.errormonitor(t_replswitch)
×
1424
            else
1425
                edit_insert(s, ']')
×
1426
                LineEdit.check_show_hint(s)
×
1427
            end
1428
        end,
1429

1430
        # Bracketed Paste Mode
1431
        "\e[200~" => (s::MIState,o...)->begin
×
1432
            input = LineEdit.bracketed_paste(s) # read directly from s until reaching the end-bracketed-paste marker
×
1433
            sbuffer = LineEdit.buffer(s)
×
1434
            curspos = position(sbuffer)
×
1435
            seek(sbuffer, 0)
×
1436
            shouldeval = (bytesavailable(sbuffer) == curspos && !occursin(UInt8('\n'), sbuffer))
×
1437
            seek(sbuffer, curspos)
×
1438
            if curspos == 0
×
1439
                # if pasting at the beginning, strip leading whitespace
1440
                input = lstrip(input)
×
1441
            end
1442
            if !shouldeval
×
1443
                # when pasting in the middle of input, just paste in place
1444
                # don't try to execute all the WIP, since that's rather confusing
1445
                # and is often ill-defined how it should behave
1446
                edit_insert(s, input)
×
1447
                return
×
1448
            end
1449
            LineEdit.push_undo(s)
×
1450
            edit_insert(sbuffer, input)
×
1451
            input = String(take!(sbuffer))
×
1452
            oldpos = firstindex(input)
×
1453
            firstline = true
×
1454
            isprompt_paste = false
×
1455
            curr_prompt_len = 0
×
1456
            pasting_help = false
×
1457

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

1570
        # Open the editor at the location of a stackframe or method
1571
        # This is accessing a contextual variable that gets set in
1572
        # the show_backtrace and show_method_table functions.
1573
        "^Q" => (s::MIState, o...) -> begin
×
1574
            linfos = repl.last_shown_line_infos
×
1575
            str = String(take!(LineEdit.buffer(s)))
×
1576
            n = tryparse(Int, str)
×
1577
            n === nothing && @goto writeback
×
1578
            if n <= 0 || n > length(linfos) || startswith(linfos[n][1], "REPL[")
×
1579
                @goto writeback
×
1580
            end
1581
            try
×
1582
                InteractiveUtils.edit(Base.fixup_stdlib_path(linfos[n][1]), linfos[n][2])
×
1583
            catch ex
1584
                ex isa ProcessFailedException || ex isa Base.IOError || ex isa SystemError || rethrow()
×
1585
                @info "edit failed" _exception=ex
×
1586
            end
1587
            LineEdit.refresh_line(s)
×
1588
            return
×
1589
            @label writeback
×
1590
            write(LineEdit.buffer(s), str)
×
1591
            return
×
1592
        end,
1593
    )
1594

1595
    prefix_prompt, prefix_keymap = LineEdit.setup_prefix_keymap(hp, julia_prompt)
×
1596

1597
    a = Dict{Any,Any}[skeymap, repl_keymap, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
×
1598
    prepend!(a, extra_repl_keymap)
×
1599

1600
    julia_prompt.keymap_dict = LineEdit.keymap(a)
×
1601

1602
    mk = mode_keymap(julia_prompt)
×
1603

1604
    b = Dict{Any,Any}[skeymap, mk, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
×
1605
    prepend!(b, extra_repl_keymap)
×
1606

1607
    shell_mode.keymap_dict = help_mode.keymap_dict = dummy_pkg_mode.keymap_dict = LineEdit.keymap(b)
×
1608

1609
    allprompts = LineEdit.TextInterface[julia_prompt, shell_mode, help_mode, dummy_pkg_mode, search_prompt, prefix_prompt]
×
1610
    return ModalInterface(allprompts)
×
1611
end
1612

1613
function run_frontend(repl::LineEditREPL, backend::REPLBackendRef)
×
1614
    repl.frontend_task = current_task()
×
1615
    d = REPLDisplay(repl)
×
1616
    dopushdisplay = repl.specialdisplay === nothing && !in(d,Base.Multimedia.displays)
×
1617
    dopushdisplay && pushdisplay(d)
×
1618
    if !isdefined(repl,:interface)
×
1619
        interface = repl.interface = setup_interface(repl)
×
1620
    else
1621
        interface = repl.interface
×
1622
    end
1623
    repl.backendref = backend
×
1624
    repl.mistate = LineEdit.init_state(terminal(repl), interface)
×
1625
    run_interface(terminal(repl), interface, repl.mistate)
×
1626
    # Terminate Backend
1627
    put!(backend.repl_channel, (nothing, -1))
×
1628
    dopushdisplay && popdisplay(d)
×
1629
    nothing
×
1630
end
1631

1632
## StreamREPL ##
1633

1634
mutable struct StreamREPL <: AbstractREPL
1635
    stream::IO
1636
    prompt_color::String
1637
    input_color::String
1638
    answer_color::String
1639
    waserror::Bool
1640
    frontend_task::Task
1641
    StreamREPL(stream,pc,ic,ac) = new(stream,pc,ic,ac,false)
×
1642
end
1643
StreamREPL(stream::IO) = StreamREPL(stream, Base.text_colors[:green], Base.input_color(), Base.answer_color())
×
1644
run_repl(stream::IO) = run_repl(StreamREPL(stream))
×
1645

1646
outstream(s::StreamREPL) = s.stream
×
1647
hascolor(s::StreamREPL) = get(s.stream, :color, false)::Bool
×
1648

1649
answer_color(r::LineEditREPL) = r.envcolors ? Base.answer_color() : r.answer_color
×
1650
answer_color(r::StreamREPL) = r.answer_color
×
1651
input_color(r::LineEditREPL) = r.envcolors ? Base.input_color() : r.input_color
×
1652
input_color(r::StreamREPL) = r.input_color
×
1653

1654
let matchend = Dict("\"" => r"\"", "\"\"\"" => r"\"\"\"", "'" => r"'",
1655
    "`" => r"`", "```" => r"```", "#" => r"$"m, "#=" => r"=#|#=")
1656
    global _rm_strings_and_comments
1657
    function _rm_strings_and_comments(code::Union{String,SubString{String}})
×
1658
        buf = IOBuffer(sizehint = sizeof(code))
×
1659
        pos = 1
×
1660
        while true
×
1661
            i = findnext(r"\"(?!\"\")|\"\"\"|'|`(?!``)|```|#(?!=)|#=", code, pos)
×
1662
            isnothing(i) && break
×
1663
            match = SubString(code, i)
×
1664
            j = findnext(matchend[match]::Regex, code, nextind(code, last(i)))
×
1665
            if match == "#=" # possibly nested
×
1666
                nested = 1
×
1667
                while j !== nothing
×
1668
                    nested += SubString(code, j) == "#=" ? +1 : -1
×
1669
                    iszero(nested) && break
×
1670
                    j = findnext(r"=#|#=", code, nextind(code, last(j)))
×
1671
                end
×
1672
            elseif match[1] != '#' # quote match: check non-escaped
×
1673
                while j !== nothing
×
1674
                    notbackslash = findprev(!=('\\'), code, prevind(code, first(j)))::Int
×
1675
                    isodd(first(j) - notbackslash) && break # not escaped
×
1676
                    j = findnext(matchend[match]::Regex, code, nextind(code, first(j)))
×
1677
                end
×
1678
            end
1679
            isnothing(j) && break
×
1680
            if match[1] == '#'
×
1681
                print(buf, SubString(code, pos, prevind(code, first(i))))
×
1682
            else
1683
                print(buf, SubString(code, pos, last(i)), ' ', SubString(code, j))
×
1684
            end
1685
            pos = nextind(code, last(j))
×
1686
        end
×
1687
        print(buf, SubString(code, pos, lastindex(code)))
×
1688
        return String(take!(buf))
×
1689
    end
1690
end
1691

1692
# heuristic function to decide if the presence of a semicolon
1693
# at the end of the expression was intended for suppressing output
1694
ends_with_semicolon(code::AbstractString) = ends_with_semicolon(String(code))
×
1695
ends_with_semicolon(code::Union{String,SubString{String}}) =
×
1696
    contains(_rm_strings_and_comments(code), r";\s*$")
×
1697

1698
function banner(io::IO = stdout; short = false)
×
1699
    if Base.GIT_VERSION_INFO.tagged_commit
×
1700
        commit_string = Base.TAGGED_RELEASE_BANNER
×
1701
    elseif isempty(Base.GIT_VERSION_INFO.commit)
×
1702
        commit_string = ""
×
1703
    else
1704
        days = Int(floor((ccall(:jl_clock_now, Float64, ()) - Base.GIT_VERSION_INFO.fork_master_timestamp) / (60 * 60 * 24)))
×
1705
        days = max(0, days)
×
1706
        unit = days == 1 ? "day" : "days"
×
1707
        distance = Base.GIT_VERSION_INFO.fork_master_distance
×
1708
        commit = Base.GIT_VERSION_INFO.commit_short
×
1709

1710
        if distance == 0
×
1711
            commit_string = "Commit $(commit) ($(days) $(unit) old master)"
×
1712
        else
1713
            branch = Base.GIT_VERSION_INFO.branch
×
1714
            commit_string = "$(branch)/$(commit) (fork: $(distance) commits, $(days) $(unit))"
×
1715
        end
1716
    end
1717

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

1720
    if get(io, :color, false)::Bool
×
1721
        c = Base.text_colors
×
1722
        tx = c[:normal] # text
×
1723
        jl = c[:normal] # julia
×
1724
        d1 = c[:bold] * c[:blue]    # first dot
×
1725
        d2 = c[:bold] * c[:red]     # second dot
×
1726
        d3 = c[:bold] * c[:green]   # third dot
×
1727
        d4 = c[:bold] * c[:magenta] # fourth dot
×
1728

1729
        if short
×
1730
            print(io,"""
×
1731
              $(d3)o$(tx)  | Version $(VERSION)$(commit_date)
1732
             $(d2)o$(tx) $(d4)o$(tx) | $(commit_string)
1733
            """)
1734
        else
1735
            print(io,"""               $(d3)_$(tx)
×
1736
               $(d1)_$(tx)       $(jl)_$(tx) $(d2)_$(d3)(_)$(d4)_$(tx)     |  Documentation: https://docs.julialang.org
1737
              $(d1)(_)$(jl)     | $(d2)(_)$(tx) $(d4)(_)$(tx)    |
1738
               $(jl)_ _   _| |_  __ _$(tx)   |  Type \"?\" for help, \"]?\" for Pkg help.
1739
              $(jl)| | | | | | |/ _` |$(tx)  |
1740
              $(jl)| | |_| | | | (_| |$(tx)  |  Version $(VERSION)$(commit_date)
1741
             $(jl)_/ |\\__'_|_|_|\\__'_|$(tx)  |  $(commit_string)
1742
            $(jl)|__/$(tx)                   |
1743

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

1763
            """)
1764
        end
1765
    end
1766
end
1767

1768
function run_frontend(repl::StreamREPL, backend::REPLBackendRef)
×
1769
    repl.frontend_task = current_task()
×
1770
    have_color = hascolor(repl)
×
1771
    banner(repl.stream)
×
1772
    d = REPLDisplay(repl)
×
1773
    dopushdisplay = !in(d,Base.Multimedia.displays)
×
1774
    dopushdisplay && pushdisplay(d)
×
1775
    while !eof(repl.stream)::Bool
×
1776
        if have_color
×
1777
            print(repl.stream,repl.prompt_color)
×
1778
        end
1779
        print(repl.stream, JULIA_PROMPT)
×
1780
        if have_color
×
1781
            print(repl.stream, input_color(repl))
×
1782
        end
1783
        line = readline(repl.stream, keep=true)
×
1784
        if !isempty(line)
×
1785
            ast = Base.parse_input_line(line)
×
1786
            if have_color
×
1787
                print(repl.stream, Base.color_normal)
×
1788
            end
1789
            response = eval_with_backend(ast, backend)
×
1790
            print_response(repl, response, !ends_with_semicolon(line), have_color)
×
1791
        end
1792
    end
×
1793
    # Terminate Backend
1794
    put!(backend.repl_channel, (nothing, -1))
×
1795
    dopushdisplay && popdisplay(d)
×
1796
    nothing
×
1797
end
1798

1799
module Numbered
1800

1801
using ..REPL
1802

1803
__current_ast_transforms() = Base.active_repl_backend !== nothing ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1804

1805
function repl_eval_counter(hp)
×
1806
    return length(hp.history) - hp.start_idx
×
1807
end
1808

1809
function out_transform(@nospecialize(x), n::Ref{Int})
×
1810
    return Expr(:toplevel, get_usings!([], x)..., quote
×
1811
        let __temp_val_a72df459 = $x
×
1812
            $capture_result($n, __temp_val_a72df459)
×
1813
            __temp_val_a72df459
×
1814
        end
1815
    end)
1816
end
1817

1818
function get_usings!(usings, ex)
×
1819
    ex isa Expr || return usings
×
1820
    # get all `using` and `import` statements which are at the top level
1821
    for (i, arg) in enumerate(ex.args)
×
1822
        if Base.isexpr(arg, :toplevel)
×
1823
            get_usings!(usings, arg)
×
1824
        elseif Base.isexpr(arg, [:using, :import])
×
1825
            push!(usings, popat!(ex.args, i))
×
1826
        end
1827
    end
×
1828
    return usings
×
1829
end
1830

1831
function create_global_out!(mod)
×
1832
    if !isdefinedglobal(mod, :Out)
×
1833
        out = Dict{Int, Any}()
×
1834
        @eval mod begin
×
1835
            const Out = $(out)
×
1836
            export Out
×
1837
        end
1838
        return out
×
1839
    end
1840
    return getglobal(mod, Out)
×
1841
end
1842

1843
function capture_result(n::Ref{Int}, @nospecialize(x))
×
1844
    n = n[]
×
1845
    mod = Base.MainInclude
×
1846
    # TODO: This invokelatest is only required due to backdated constants
1847
    # and should be removed after
1848
    out = isdefinedglobal(mod, :Out) ? invokelatest(getglobal, mod, :Out) : invokelatest(create_global_out!, mod)
×
1849
    if x !== out && x !== nothing # remove this?
×
1850
        out[n] = x
×
1851
    end
1852
    nothing
×
1853
end
1854

1855
function set_prompt(repl::LineEditREPL, n::Ref{Int})
×
1856
    julia_prompt = repl.interface.modes[1]
×
1857
    julia_prompt.prompt = function()
×
1858
        n[] = repl_eval_counter(julia_prompt.hist)+1
×
1859
        string("In [", n[], "]: ")
×
1860
    end
1861
    nothing
×
1862
end
1863

1864
function set_output_prefix(repl::LineEditREPL, n::Ref{Int})
×
1865
    julia_prompt = repl.interface.modes[1]
×
1866
    if REPL.hascolor(repl)
×
1867
        julia_prompt.output_prefix_prefix = Base.text_colors[:red]
×
1868
    end
1869
    julia_prompt.output_prefix = () -> string("Out[", n[], "]: ")
×
1870
    nothing
×
1871
end
1872

1873
function __current_ast_transforms(backend)
×
1874
    if backend === nothing
×
1875
        Base.active_repl_backend !== nothing ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1876
    else
1877
        backend.ast_transforms
×
1878
    end
1879
end
1880

1881
function numbered_prompt!(repl::LineEditREPL=Base.active_repl::LineEditREPL, backend=nothing)
×
1882
    n = Ref{Int}(0)
×
1883
    set_prompt(repl, n)
×
1884
    set_output_prefix(repl, n)
×
1885
    push!(__current_ast_transforms(backend), @nospecialize(ast) -> out_transform(ast, n))
×
1886
    return
×
1887
end
1888

1889
"""
1890
    Out[n]
1891

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

1895
See also [`ans`](@ref).
1896
"""
1897
Base.MainInclude.Out
1898

1899
end
1900

1901
import .Numbered.numbered_prompt!
1902

1903
# this assignment won't survive precompilation,
1904
# but will stick if REPL is baked into a sysimg.
1905
# Needs to occur after this module is finished.
1906
Base.REPL_MODULE_REF[] = REPL
1907

1908
if Base.generating_output()
1909
    include("precompile.jl")
1910
end
1911

1912
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

© 2025 Coveralls, Inc