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

JuliaLang / julia / #37884

26 Aug 2024 10:27PM UTC coverage: 84.78% (-2.6%) from 87.33%
#37884

push

local

web-flow
Fix cong implementation to be properly random and not just cycling. (#55509)

This was found by @IanButterworth. It unfortunately has a small
performance regression due to actually using all the rng bits

73819 of 87071 relevant lines covered (84.78%)

15743344.65 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_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
    if isdefined(ex, :scope)
×
34
        scope = ex.scope
×
35
        if scope isa Module
×
36
            bnd = ccall(:jl_get_module_binding, Any, (Any, Any, Cint), scope, var, true)::Core.Binding
×
37
            if isdefined(bnd, :owner)
×
38
                owner = bnd.owner
×
39
                if owner === bnd
×
40
                    print(io, "\nSuggestion: add an appropriate import or assignment. This global was declared but not assigned.")
×
41
                end
42
            else
43
                owner = ccall(:jl_binding_owner, Ptr{Cvoid}, (Any, Any), scope, var)
×
44
                if C_NULL == owner
×
45
                    # No global of this name exists in this module.
46
                    # This is the common case, so do not print that information.
47
                    # It could be the binding was exported by two modules, which we can detect
48
                    # by the `usingfailed` flag in the binding:
49
                    if isdefined(bnd, :flags) && Bool(bnd.flags >> 4 & 1) # magic location of the `usingfailed` flag
×
50
                        print(io, "\nHint: It looks like two or more modules export different ",
×
51
                              "bindings with this name, resulting in ambiguity. Try explicitly ",
52
                              "importing it from a particular module, or qualifying the name ",
53
                              "with the module it should come from.")
54
                    else
55
                        print(io, "\nSuggestion: check for spelling errors or missing imports.")
×
56
                    end
57
                    owner = bnd
×
58
                else
59
                    owner = unsafe_pointer_to_objref(owner)::Core.Binding
×
60
                end
61
            end
62
            if owner !== bnd
×
63
                # this could use jl_binding_dbgmodule for the exported location in the message too
64
                print(io, "\nSuggestion: this global was defined as `$(owner.globalref)` but not assigned a value.")
×
65
            end
66
        elseif scope === :static_parameter
×
67
            print(io, "\nSuggestion: run Test.detect_unbound_args to detect method arguments that do not fully constrain a type parameter.")
×
68
        elseif scope === :local
×
69
            print(io, "\nSuggestion: check for an assignment to a local variable that shadows a global of the same name.")
×
70
        end
71
    else
72
        scope = undef
×
73
    end
74
    if scope !== Base && !_UndefVarError_warnfor(io, Base, var)
×
75
        warned = false
×
76
        for m in Base.loaded_modules_order
×
77
            m === Core && continue
×
78
            m === Base && continue
×
79
            m === Main && continue
×
80
            m === scope && continue
×
81
            warned |= _UndefVarError_warnfor(io, m, var)
×
82
        end
×
83
        warned ||
×
84
            _UndefVarError_warnfor(io, Core, var) ||
85
            _UndefVarError_warnfor(io, Main, var)
86
    end
87
    return nothing
×
88
end
89

90
function _UndefVarError_warnfor(io::IO, m::Module, var::Symbol)
×
91
    Base.isbindingresolved(m, var) || return false
×
92
    (Base.isexported(m, var) || Base.ispublic(m, var)) || return false
×
93
    print(io, "\nHint: a global variable of this name also exists in $m.")
×
94
    return true
×
95
end
96

97
function __init__()
2✔
98
    Base.REPL_MODULE_REF[] = REPL
2✔
99
    Base.Experimental.register_error_hint(UndefVarError_hint, UndefVarError)
2✔
100
    return nothing
2✔
101
end
102

103
using Base.Meta, Sockets, StyledStrings
104
using JuliaSyntaxHighlighting
105
import InteractiveUtils
106

107
export
108
    AbstractREPL,
109
    BasicREPL,
110
    LineEditREPL,
111
    StreamREPL
112

113
public TerminalMenus
114

115
import Base:
116
    AbstractDisplay,
117
    display,
118
    show,
119
    AnyDict,
120
    ==
121

122
_displaysize(io::IO) = displaysize(io)::Tuple{Int,Int}
×
123

124
include("Terminals.jl")
125
using .Terminals
126

127
abstract type AbstractREPL end
128

129
include("options.jl")
130

131
include("LineEdit.jl")
132
using .LineEdit
133
import .LineEdit:
134
    CompletionProvider,
135
    HistoryProvider,
136
    add_history,
137
    complete_line,
138
    history_next,
139
    history_next_prefix,
140
    history_prev,
141
    history_prev_prefix,
142
    history_first,
143
    history_last,
144
    history_search,
145
    setmodifiers!,
146
    terminal,
147
    MIState,
148
    PromptState,
149
    mode_idx
150

151
include("REPLCompletions.jl")
152
using .REPLCompletions
153

154
include("TerminalMenus/TerminalMenus.jl")
155
include("docview.jl")
156

157
include("Pkg_beforeload.jl")
158

159
@nospecialize # use only declared type signatures
160

161
answer_color(::AbstractREPL) = ""
×
162

163
const JULIA_PROMPT = "julia> "
164
const PKG_PROMPT = "pkg> "
165
const SHELL_PROMPT = "shell> "
166
const HELP_PROMPT = "help?> "
167

168
mutable struct REPLBackend
169
    "channel for AST"
170
    repl_channel::Channel{Any}
171
    "channel for results: (value, iserror)"
172
    response_channel::Channel{Any}
173
    "flag indicating the state of this backend"
174
    in_eval::Bool
175
    "transformation functions to apply before evaluating expressions"
176
    ast_transforms::Vector{Any}
177
    "current backend task"
178
    backend_task::Task
179

180
    REPLBackend(repl_channel, response_channel, in_eval, ast_transforms=copy(repl_ast_transforms)) =
×
181
        new(repl_channel, response_channel, in_eval, ast_transforms)
182
end
183
REPLBackend() = REPLBackend(Channel(1), Channel(1), false)
×
184

185
"""
186
    softscope(ex)
187

188
Return a modified version of the parsed expression `ex` that uses
189
the REPL's "soft" scoping rules for global syntax blocks.
190
"""
191
function softscope(@nospecialize ex)
×
192
    if ex isa Expr
×
193
        h = ex.head
×
194
        if h === :toplevel
×
195
            ex′ = Expr(h)
×
196
            map!(softscope, resize!(ex′.args, length(ex.args)), ex.args)
×
197
            return ex′
×
198
        elseif h in (:meta, :import, :using, :export, :module, :error, :incomplete, :thunk)
×
199
            return ex
×
200
        elseif h === :global && all(x->isa(x, Symbol), ex.args)
×
201
            return ex
×
202
        else
203
            return Expr(:block, Expr(:softscope, true), ex)
×
204
        end
205
    end
206
    return ex
×
207
end
208

209
# Temporary alias until Documenter updates
210
const softscope! = softscope
211

212
function print_qualified_access_warning(mod::Module, owner::Module, name::Symbol)
×
213
    @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
×
214
end
215

216
function has_ancestor(query::Module, target::Module)
×
217
    query == target && return true
×
218
    while true
×
219
        next = parentmodule(query)
×
220
        next == target && return true
×
221
        next == query && return false
×
222
        query = next
×
223
    end
×
224
end
225

226
retrieve_modules(::Module, ::Any) = (nothing,)
×
227
function retrieve_modules(current_module::Module, mod_name::Symbol)
×
228
    mod = try
×
229
        getproperty(current_module, mod_name)
×
230
    catch
231
        return (nothing,)
×
232
    end
233
    return (mod isa Module ? mod : nothing,)
×
234
end
235
retrieve_modules(current_module::Module, mod_name::QuoteNode) = retrieve_modules(current_module, mod_name.value)
×
236
function retrieve_modules(current_module::Module, mod_expr::Expr)
×
237
    if Meta.isexpr(mod_expr, :., 2)
×
238
        current_module = retrieve_modules(current_module, mod_expr.args[1])[1]
×
239
        current_module === nothing && return (nothing,)
×
240
        return (current_module, retrieve_modules(current_module, mod_expr.args[2])...)
×
241
    else
242
        return (nothing,)
×
243
    end
244
end
245

246
add_locals!(locals, ast::Any) = nothing
×
247
function add_locals!(locals, ast::Expr)
×
248
    for arg in ast.args
×
249
        add_locals!(locals, arg)
×
250
    end
×
251
    return nothing
×
252
end
253
function add_locals!(locals, ast::Symbol)
×
254
    push!(locals, ast)
×
255
    return nothing
×
256
end
257

258
function collect_names_to_warn!(warnings, locals, current_module::Module, ast)
×
259
    ast isa Expr || return
×
260

261
    # don't recurse through module definitions
262
    ast.head === :module && return
×
263

264
    if Meta.isexpr(ast, :., 2)
×
265
        mod_name, name_being_accessed = ast.args
×
266
        # retrieve the (possibly-nested) module being named here
267
        mods = retrieve_modules(current_module, mod_name)
×
268
        all(x -> x isa Module, mods) || return
×
269
        outer_mod = first(mods)
×
270
        mod = last(mods)
×
271
        if name_being_accessed isa QuoteNode
×
272
            name_being_accessed = name_being_accessed.value
×
273
        end
274
        name_being_accessed isa Symbol || return
×
275
        owner = try
×
276
            which(mod, name_being_accessed)
×
277
        catch
278
            return
×
279
        end
280
        # if `owner` is a submodule of `mod`, then don't warn. E.g. the name `parse` is present in the module `JSON`
281
        # but is owned by `JSON.Parser`; we don't warn if it is accessed as `JSON.parse`.
282
        has_ancestor(owner, mod) && return
×
283
        # Don't warn if the name is public in the module we are accessing it
284
        Base.ispublic(mod, name_being_accessed) && return
×
285
        # Don't warn if accessing names defined in Core from Base if they are present in Base (e.g. `Base.throw`).
286
        mod === Base && Base.ispublic(Core, name_being_accessed) && return
×
287
        push!(warnings, (; outer_mod, mod, owner, name_being_accessed))
×
288
        # no recursion
289
        return
×
290
    elseif Meta.isexpr(ast, :(=), 2)
×
291
        lhs, rhs = ast.args
×
292
        # any symbols we find on the LHS we will count as local. This can potentially be overzealous,
293
        # but we want to avoid false positives (unnecessary warnings) more than false negatives.
294
        add_locals!(locals, lhs)
×
295
        # we'll recurse into the RHS only
296
        return collect_names_to_warn!(warnings, locals, current_module, rhs)
×
297
    elseif Meta.isexpr(ast, :function) && length(ast.args) >= 1
×
298

299
        if Meta.isexpr(ast.args[1], :call, 2)
×
300
            func_name, func_args = ast.args[1].args
×
301
            # here we have a function definition and are inspecting it's arguments for local variables.
302
            # we will error on the conservative side by adding all symbols we find (regardless if they are local variables or possibly-global default values)
303
            add_locals!(locals, func_args)
×
304
        end
305
        # fall through to general recursion
306
    end
307

308
    for arg in ast.args
×
309
        collect_names_to_warn!(warnings, locals, current_module, arg)
×
310
    end
×
311

312
    return nothing
×
313
end
314

315
function collect_qualified_access_warnings(current_mod, ast)
×
316
    warnings = Set()
×
317
    locals = Set{Symbol}()
×
318
    collect_names_to_warn!(warnings, locals, current_mod, ast)
×
319
    filter!(warnings) do (; outer_mod)
×
320
        nameof(outer_mod) ∉ locals
×
321
    end
322
    return warnings
×
323
end
324

325
function warn_on_non_owning_accesses(current_mod, ast)
×
326
    warnings = collect_qualified_access_warnings(current_mod, ast)
×
327
    for (; outer_mod, mod, owner, name_being_accessed) in warnings
×
328
        print_qualified_access_warning(mod, owner, name_being_accessed)
×
329
    end
×
330
    return ast
×
331
end
332
warn_on_non_owning_accesses(ast) = warn_on_non_owning_accesses(Base.active_module(), ast)
×
333

334
const repl_ast_transforms = Any[softscope, warn_on_non_owning_accesses] # defaults for new REPL backends
335

336
# Allows an external package to add hooks into the code loading.
337
# The hook should take a Vector{Symbol} of package names and
338
# return true if all packages could be installed, false if not
339
# to e.g. install packages on demand
340
const install_packages_hooks = Any[]
341

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

350
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))
×
351
    if !isexpr(ast, :toplevel)
×
352
        ast = __repl_entry_lower_with_loc(mod, ast, toplevel_file, toplevel_line)
×
353
        check_for_missing_packages_and_run_hooks(ast)
×
354
        return __repl_entry_eval_expanded_with_loc(mod, ast, toplevel_file, toplevel_line)
×
355
    end
356
    local value=nothing
×
357
    for i = 1:length(ast.args)
×
358
        value = toplevel_eval_with_hooks(mod, ast.args[i], toplevel_file, toplevel_line)
×
359
    end
×
360
    return value
×
361
end
362

363
function eval_user_input(@nospecialize(ast), backend::REPLBackend, mod::Module)
×
364
    lasterr = nothing
×
365
    Base.sigatomic_begin()
×
366
    while true
×
367
        try
×
368
            Base.sigatomic_end()
×
369
            if lasterr !== nothing
×
370
                put!(backend.response_channel, Pair{Any, Bool}(lasterr, true))
×
371
            else
372
                backend.in_eval = true
×
373
                for xf in backend.ast_transforms
×
374
                    ast = Base.invokelatest(xf, ast)
×
375
                end
×
376
                value = toplevel_eval_with_hooks(mod, ast)
×
377
                backend.in_eval = false
×
378
                setglobal!(Base.MainInclude, :ans, value)
×
379
                put!(backend.response_channel, Pair{Any, Bool}(value, false))
×
380
            end
381
            break
×
382
        catch err
383
            if lasterr !== nothing
×
384
                println("SYSTEM ERROR: Failed to report error to REPL frontend")
×
385
                println(err)
×
386
            end
387
            lasterr = current_exceptions()
×
388
        end
389
    end
×
390
    Base.sigatomic_end()
×
391
    nothing
×
392
end
393

394
function check_for_missing_packages_and_run_hooks(ast)
×
395
    isa(ast, Expr) || return
×
396
    mods = modules_to_be_loaded(ast)
×
397
    filter!(mod -> isnothing(Base.identify_package(String(mod))), mods) # keep missing modules
×
398
    if !isempty(mods)
×
399
        isempty(install_packages_hooks) && load_pkg()
×
400
        for f in install_packages_hooks
×
401
            Base.invokelatest(f, mods) && return
×
402
        end
×
403
    end
404
end
405

406
function _modules_to_be_loaded!(ast::Expr, mods::Vector{Symbol})
×
407
    ast.head === :quote && return mods # don't search if it's not going to be run during this eval
×
408
    if ast.head === :using || ast.head === :import
×
409
        for arg in ast.args
×
410
            arg = arg::Expr
×
411
            arg1 = first(arg.args)
×
412
            if arg1 isa Symbol # i.e. `Foo`
×
413
                if arg1 != :. # don't include local import `import .Foo`
×
414
                    push!(mods, arg1)
×
415
                end
416
            else # i.e. `Foo: bar`
417
                sym = first((arg1::Expr).args)::Symbol
×
418
                if sym != :. # don't include local import `import .Foo: a`
×
419
                    push!(mods, sym)
×
420
                end
421
            end
422
        end
×
423
    end
424
    if ast.head !== :thunk
×
425
        for arg in ast.args
×
426
            if isexpr(arg, (:block, :if, :using, :import))
×
427
                _modules_to_be_loaded!(arg, mods)
×
428
            end
429
        end
×
430
    else
431
        code = ast.args[1]
×
432
        for arg in code.code
×
433
            isa(arg, Expr) || continue
×
434
            _modules_to_be_loaded!(arg, mods)
×
435
        end
×
436
    end
437
end
438

439
function modules_to_be_loaded(ast::Expr, mods::Vector{Symbol} = Symbol[])
×
440
    _modules_to_be_loaded!(ast, mods)
×
441
    filter!(mod::Symbol -> !in(mod, (:Base, :Main, :Core)), mods) # Exclude special non-package modules
×
442
    return unique(mods)
×
443
end
444

445
"""
446
    start_repl_backend(repl_channel::Channel, response_channel::Channel)
447

448
    Starts loop for REPL backend
449
    Returns a REPLBackend with backend_task assigned
450

451
    Deprecated since sync / async behavior cannot be selected
452
"""
453
function start_repl_backend(repl_channel::Channel{Any}, response_channel::Channel{Any}
×
454
                            ; get_module::Function = ()->Main)
455
    # Maintain legacy behavior of asynchronous backend
456
    backend = REPLBackend(repl_channel, response_channel, false)
×
457
    # Assignment will be made twice, but will be immediately available
458
    backend.backend_task = @async start_repl_backend(backend; get_module)
×
459
    return backend
×
460
end
461

462
"""
463
    start_repl_backend(backend::REPLBackend)
464

465
    Call directly to run backend loop on current Task.
466
    Use @async for run backend on new Task.
467

468
    Does not return backend until loop is finished.
469
"""
470
function start_repl_backend(backend::REPLBackend,  @nospecialize(consumer = x -> nothing); get_module::Function = ()->Main)
×
471
    backend.backend_task = Base.current_task()
×
472
    consumer(backend)
×
473
    repl_backend_loop(backend, get_module)
×
474
    return backend
×
475
end
476

477
function repl_backend_loop(backend::REPLBackend, get_module::Function)
×
478
    # include looks at this to determine the relative include path
479
    # nothing means cwd
480
    while true
×
481
        tls = task_local_storage()
×
482
        tls[:SOURCE_PATH] = nothing
×
483
        ast, show_value = take!(backend.repl_channel)
×
484
        if show_value == -1
×
485
            # exit flag
486
            break
×
487
        end
488
        eval_user_input(ast, backend, get_module())
×
489
    end
×
490
    return nothing
×
491
end
492

493
struct REPLDisplay{Repl<:AbstractREPL} <: AbstractDisplay
494
    repl::Repl
495
end
496

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

519
display(d::REPLDisplay, x) = display(d, MIME("text/plain"), x)
×
520

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

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

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

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

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

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

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

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

618
    Main function to start the REPL
619

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

646
## BasicREPL ##
647

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

655
outstream(r::BasicREPL) = r.terminal
×
656
hascolor(r::BasicREPL) = hascolor(r.terminal)
×
657

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

705
## LineEditREPL ##
706

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

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

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

760
mutable struct ShellCompletionProvider <: CompletionProvider end
761
struct LatexCompletions <: CompletionProvider end
762

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

767
setmodifiers!(c::CompletionProvider, m::LineEdit.Modifiers) = nothing
×
768

769
setmodifiers!(c::REPLCompletionProvider, m::LineEdit.Modifiers) = c.modifiers = m
×
770

771
"""
772
    activate(mod::Module=Main)
773

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

960
    return :ok
×
961
end
962

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

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

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

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

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

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

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

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

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

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

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

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

1112
    return false
×
1113
end
1114

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

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

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

1133
backend(r::AbstractREPL) = r.backendref
×
1134

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

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

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

1170
function prepare_next(repl::LineEditREPL)
×
1171
    println(terminal(repl))
×
1172
end
1173

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

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

1199
const JL_PROMPT_PASTE = Ref(true)
1200
enable_promptpaste(v::Bool) = JL_PROMPT_PASTE[] = v
×
1201

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

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

1218

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

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

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

1250
    ############################### Stage I ################################
1251

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

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

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

1276

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

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

1322

1323
    ################################# Stage II #############################
1324

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

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

1357

1358
    search_prompt, skeymap = LineEdit.setup_search_keymap(hp)
×
1359
    search_prompt.complete = LatexCompletions()
×
1360

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

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

1371
    repl_keymap = AnyDict(
×
1372
        ';' => function (s::MIState,o...)
×
1373
            if isempty(s) || position(LineEdit.buffer(s)) == 0
×
1374
                buf = copy(LineEdit.buffer(s))
×
1375
                transition(s, shell_mode) do
×
1376
                    LineEdit.state(s, shell_mode).input_buffer = buf
×
1377
                end
1378
            else
1379
                edit_insert(s, ';')
×
1380
            end
1381
        end,
1382
        '?' => function (s::MIState,o...)
×
1383
            if isempty(s) || position(LineEdit.buffer(s)) == 0
×
1384
                buf = copy(LineEdit.buffer(s))
×
1385
                transition(s, help_mode) do
×
1386
                    LineEdit.state(s, help_mode).input_buffer = buf
×
1387
                end
1388
            else
1389
                edit_insert(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) && @invokelatest(LineEdit.check_for_hint(s))
×
1413
                                            @invokelatest(LineEdit.refresh_line(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
            end
1427
        end,
1428

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

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

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

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

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

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

1601
    mk = mode_keymap(julia_prompt)
×
1602

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

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

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

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

1631
## StreamREPL ##
1632

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

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

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

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

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

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

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

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

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

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

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

1762
            """)
1763
        end
1764
    end
1765
end
1766

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

1798
module Numbered
1799

1800
using ..REPL
1801

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

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

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

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

1830
function capture_result(n::Ref{Int}, @nospecialize(x))
×
1831
    n = n[]
×
1832
    mod = Base.MainInclude
×
1833
    if !isdefined(mod, :Out)
×
1834
        @eval mod global Out
×
1835
        @eval mod export Out
×
1836
        setglobal!(mod, :Out, Dict{Int, Any}())
×
1837
    end
1838
    if x !== getglobal(mod, :Out) && x !== nothing # remove this?
×
1839
        getglobal(mod, :Out)[n] = x
×
1840
    end
1841
    nothing
×
1842
end
1843

1844
function set_prompt(repl::LineEditREPL, n::Ref{Int})
×
1845
    julia_prompt = repl.interface.modes[1]
×
1846
    julia_prompt.prompt = function()
×
1847
        n[] = repl_eval_counter(julia_prompt.hist)+1
×
1848
        string("In [", n[], "]: ")
×
1849
    end
1850
    nothing
×
1851
end
1852

1853
function set_output_prefix(repl::LineEditREPL, n::Ref{Int})
×
1854
    julia_prompt = repl.interface.modes[1]
×
1855
    if REPL.hascolor(repl)
×
1856
        julia_prompt.output_prefix_prefix = Base.text_colors[:red]
×
1857
    end
1858
    julia_prompt.output_prefix = () -> string("Out[", n[], "]: ")
×
1859
    nothing
×
1860
end
1861

1862
function __current_ast_transforms(backend)
×
1863
    if backend === nothing
×
1864
        Base.active_repl_backend !== nothing ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1865
    else
1866
        backend.ast_transforms
×
1867
    end
1868
end
1869

1870
function numbered_prompt!(repl::LineEditREPL=Base.active_repl::LineEditREPL, backend=nothing)
×
1871
    n = Ref{Int}(0)
×
1872
    set_prompt(repl, n)
×
1873
    set_output_prefix(repl, n)
×
1874
    push!(__current_ast_transforms(backend), @nospecialize(ast) -> out_transform(ast, n))
×
1875
    return
×
1876
end
1877

1878
"""
1879
    Out[n]
1880

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

1884
See also [`ans`](@ref).
1885
"""
1886
Base.MainInclude.Out
1887

1888
end
1889

1890
import .Numbered.numbered_prompt!
1891

1892
# this assignment won't survive precompilation,
1893
# but will stick if REPL is baked into a sysimg.
1894
# Needs to occur after this module is finished.
1895
Base.REPL_MODULE_REF[] = REPL
1896

1897
if Base.generating_output()
1898
    include("precompile.jl")
1899
end
1900

1901
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