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

JuliaLang / julia / #38019

28 Feb 2025 07:25AM UTC coverage: 23.0% (+2.7%) from 20.258%
#38019

push

local

web-flow
Compiler: avoid type instability in access to `PartialStruct` field (#57553)

The `Base.getproperty` method for `PartialStruct` has a type assert to
ensure type stable access of the field `undef`. However Compiler.jl has
`Compiler.getproperty === Core.getfield`.

Introduce a getter for this field of `PartialStruct` into Compiler.jl
and use it.

I guess this should improve compiler performance, and it should help
avoid spurious invalidation.

11249 of 48909 relevant lines covered (23.0%)

126698.24 hits per line

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

0.37
/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
            bpart = Base.lookup_binding_partition(ex.world, GlobalRef(scope, var))
×
37
            kind = Base.binding_kind(bpart)
×
38
            if kind === Base.BINDING_KIND_GLOBAL || kind === Base.BINDING_KIND_UNDEF_CONST || kind == Base.BINDING_KIND_DECLARED
×
39
                print(io, "\nSuggestion: add an appropriate import or assignment. This global was declared but not assigned.")
×
40
            elseif kind === Base.BINDING_KIND_FAILED
×
41
                print(io, "\nHint: It looks like two or more modules export different ",
×
42
                "bindings with this name, resulting in ambiguity. Try explicitly ",
43
                "importing it from a particular module, or qualifying the name ",
44
                "with the module it should come from.")
45
            elseif kind === Base.BINDING_KIND_GUARD
×
46
                print(io, "\nSuggestion: check for spelling errors or missing imports.")
×
47
            elseif Base.is_some_imported(kind)
×
48
                print(io, "\nSuggestion: this global was defined as `$(Base.partition_restriction(bpart).globalref)` but not assigned a value.")
×
49
            end
50
        elseif scope === :static_parameter
×
51
            print(io, "\nSuggestion: run Test.detect_unbound_args to detect method arguments that do not fully constrain a type parameter.")
×
52
        elseif scope === :local
×
53
            print(io, "\nSuggestion: check for an assignment to a local variable that shadows a global of the same name.")
×
54
        end
55
    else
56
        scope = undef
×
57
    end
58
    if scope !== Base
×
59
        warned = _UndefVarError_warnfor(io, [Base], var)
×
60

61
        if !warned
×
62
            modules_to_check = (m for m in Base.loaded_modules_order
×
63
                                if m !== Core && m !== Base && m !== Main && m !== scope)
64
            warned |= _UndefVarError_warnfor(io, modules_to_check, var)
×
65
        end
66

67
        warned || _UndefVarError_warnfor(io, [Core, Main], var)
×
68
    end
69
    return nothing
×
70
end
71

72
function _UndefVarError_warnfor(io::IO, modules, var::Symbol)
×
73
    active_mod = Base.active_module()
×
74

75
    warned = false
×
76
    # collect modules which export or make public the variable by
77
    # the module in which the variable is defined
78
    to_warn_about = Dict{Module, Vector{Module}}()
×
79
    for m in modules
×
80
        # only include in info if binding has a value and is exported or public
81
        if !Base.isdefined(m, var) || (!Base.isexported(m, var) && !Base.ispublic(m, var))
×
82
            continue
×
83
        end
84
        warned = true
×
85

86
        # handle case where the undefined variable is the name of a loaded module
87
        if Symbol(m) == var && !isdefined(active_mod, var)
×
88
            print(io, "\nHint: $m is loaded but not imported in the active module $active_mod.")
×
89
            continue
×
90
        end
91

92
        binding_m = Base.binding_module(m, var)
×
93
        if !haskey(to_warn_about, binding_m)
×
94
            to_warn_about[binding_m] = [m]
×
95
        else
96
            push!(to_warn_about[binding_m], m)
×
97
        end
98
    end
×
99

100
    for (binding_m, modules) in pairs(to_warn_about)
×
101
        print(io, "\nHint: a global variable of this name also exists in ", binding_m, ".")
×
102
        for m in modules
×
103
            m == binding_m && continue
×
104
            how_available = if Base.isexported(m, var)
×
105
                "exported by"
×
106
            elseif Base.ispublic(m, var)
×
107
                "declared public in"
×
108
            end
109
            print(io, "\n    - Also $how_available $m")
×
110
            if !isdefined(active_mod, nameof(m)) || (getproperty(active_mod, nameof(m)) !== m)
×
111
                print(io, " (loaded but not imported in $active_mod)")
×
112
            end
113
            print(io, ".")
×
114
        end
×
115
    end
×
116
    return warned
×
117
end
118

119
function __init__()
1✔
120
    Base.REPL_MODULE_REF[] = REPL
1✔
121
    Base.Experimental.register_error_hint(UndefVarError_hint, UndefVarError)
1✔
122
    return nothing
1✔
123
end
124

125
using Base.Meta, Sockets, StyledStrings
126
using JuliaSyntaxHighlighting
127
import InteractiveUtils
128

129
export
130
    AbstractREPL,
131
    BasicREPL,
132
    LineEditREPL,
133
    StreamREPL
134

135
public TerminalMenus
136

137
import Base:
138
    AbstractDisplay,
139
    display,
140
    show,
141
    AnyDict,
142
    ==
143

144
_displaysize(io::IO) = displaysize(io)::Tuple{Int,Int}
×
145

146
include("Terminals.jl")
147
using .Terminals
148

149
abstract type AbstractREPL end
150

151
include("options.jl")
152

153
include("LineEdit.jl")
154
using .LineEdit
155
import .LineEdit:
156
    CompletionProvider,
157
    HistoryProvider,
158
    add_history,
159
    complete_line,
160
    history_next,
161
    history_next_prefix,
162
    history_prev,
163
    history_prev_prefix,
164
    history_first,
165
    history_last,
166
    history_search,
167
    setmodifiers!,
168
    terminal,
169
    MIState,
170
    PromptState,
171
    mode_idx
172

173
include("REPLCompletions.jl")
174
using .REPLCompletions
175

176
include("TerminalMenus/TerminalMenus.jl")
177
include("docview.jl")
178

179
include("Pkg_beforeload.jl")
180

181
@nospecialize # use only declared type signatures
182

183
answer_color(::AbstractREPL) = ""
×
184

185
const JULIA_PROMPT = "julia> "
186
const PKG_PROMPT = "pkg> "
187
const SHELL_PROMPT = "shell> "
188
const HELP_PROMPT = "help?> "
189

190
mutable struct REPLBackend
191
    "channel for AST"
192
    repl_channel::Channel{Any}
193
    "channel for results: (value, iserror)"
194
    response_channel::Channel{Any}
195
    "flag indicating the state of this backend"
196
    in_eval::Bool
197
    "transformation functions to apply before evaluating expressions"
198
    ast_transforms::Vector{Any}
199
    "current backend task"
200
    backend_task::Task
201

202
    REPLBackend(repl_channel, response_channel, in_eval, ast_transforms=copy(repl_ast_transforms)) =
×
203
        new(repl_channel, response_channel, in_eval, ast_transforms)
204
end
205
REPLBackend() = REPLBackend(Channel(1), Channel(1), false)
×
206

207
"""
208
    softscope(ex)
209

210
Return a modified version of the parsed expression `ex` that uses
211
the REPL's "soft" scoping rules for global syntax blocks.
212
"""
213
function softscope(@nospecialize ex)
×
214
    if ex isa Expr
×
215
        h = ex.head
×
216
        if h === :toplevel
×
217
            ex′ = Expr(h)
×
218
            map!(softscope, resize!(ex′.args, length(ex.args)), ex.args)
×
219
            return ex′
×
220
        elseif h in (:meta, :import, :using, :export, :module, :error, :incomplete, :thunk)
×
221
            return ex
×
222
        elseif h === :global && all(x->isa(x, Symbol), ex.args)
×
223
            return ex
×
224
        else
225
            return Expr(:block, Expr(:softscope, true), ex)
×
226
        end
227
    end
228
    return ex
×
229
end
230

231
# Temporary alias until Documenter updates
232
const softscope! = softscope
233

234
function print_qualified_access_warning(mod::Module, owner::Module, name::Symbol)
×
235
    @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
×
236
end
237

238
function has_ancestor(query::Module, target::Module)
×
239
    query == target && return true
×
240
    while true
×
241
        next = parentmodule(query)
×
242
        next == target && return true
×
243
        next == query && return false
×
244
        query = next
×
245
    end
×
246
end
247

248
retrieve_modules(::Module, ::Any) = (nothing,)
×
249
function retrieve_modules(current_module::Module, mod_name::Symbol)
×
250
    mod = try
×
251
        getproperty(current_module, mod_name)
×
252
    catch
253
        return (nothing,)
×
254
    end
255
    return (mod isa Module ? mod : nothing,)
×
256
end
257
retrieve_modules(current_module::Module, mod_name::QuoteNode) = retrieve_modules(current_module, mod_name.value)
×
258
function retrieve_modules(current_module::Module, mod_expr::Expr)
×
259
    if Meta.isexpr(mod_expr, :., 2)
×
260
        current_module = retrieve_modules(current_module, mod_expr.args[1])[1]
×
261
        current_module === nothing && return (nothing,)
×
262
        return (current_module, retrieve_modules(current_module, mod_expr.args[2])...)
×
263
    else
264
        return (nothing,)
×
265
    end
266
end
267

268
add_locals!(locals, ast::Any) = nothing
×
269
function add_locals!(locals, ast::Expr)
×
270
    for arg in ast.args
×
271
        add_locals!(locals, arg)
×
272
    end
×
273
    return nothing
×
274
end
275
function add_locals!(locals, ast::Symbol)
×
276
    push!(locals, ast)
×
277
    return nothing
×
278
end
279

280
function collect_names_to_warn!(warnings, locals, current_module::Module, ast)
×
281
    ast isa Expr || return
×
282

283
    # don't recurse through module definitions
284
    ast.head === :module && return
×
285

286
    if Meta.isexpr(ast, :., 2)
×
287
        mod_name, name_being_accessed = ast.args
×
288
        # retrieve the (possibly-nested) module being named here
289
        mods = retrieve_modules(current_module, mod_name)
×
290
        all(x -> x isa Module, mods) || return
×
291
        outer_mod = first(mods)
×
292
        mod = last(mods)
×
293
        if name_being_accessed isa QuoteNode
×
294
            name_being_accessed = name_being_accessed.value
×
295
        end
296
        name_being_accessed isa Symbol || return
×
297
        owner = try
×
298
            which(mod, name_being_accessed)
×
299
        catch
300
            return
×
301
        end
302
        # if `owner` is a submodule of `mod`, then don't warn. E.g. the name `parse` is present in the module `JSON`
303
        # but is owned by `JSON.Parser`; we don't warn if it is accessed as `JSON.parse`.
304
        has_ancestor(owner, mod) && return
×
305
        # Don't warn if the name is public in the module we are accessing it
306
        Base.ispublic(mod, name_being_accessed) && return
×
307
        # Don't warn if accessing names defined in Core from Base if they are present in Base (e.g. `Base.throw`).
308
        mod === Base && Base.ispublic(Core, name_being_accessed) && return
×
309
        push!(warnings, (; outer_mod, mod, owner, name_being_accessed))
×
310
        # no recursion
311
        return
×
312
    elseif Meta.isexpr(ast, :(=), 2)
×
313
        lhs, rhs = ast.args
×
314
        # any symbols we find on the LHS we will count as local. This can potentially be overzealous,
315
        # but we want to avoid false positives (unnecessary warnings) more than false negatives.
316
        add_locals!(locals, lhs)
×
317
        # we'll recurse into the RHS only
318
        return collect_names_to_warn!(warnings, locals, current_module, rhs)
×
319
    elseif Meta.isexpr(ast, :function) && length(ast.args) >= 1
×
320

321
        if Meta.isexpr(ast.args[1], :call, 2)
×
322
            func_name, func_args = ast.args[1].args
×
323
            # here we have a function definition and are inspecting it's arguments for local variables.
324
            # we will error on the conservative side by adding all symbols we find (regardless if they are local variables or possibly-global default values)
325
            add_locals!(locals, func_args)
×
326
        end
327
        # fall through to general recursion
328
    end
329

330
    for arg in ast.args
×
331
        collect_names_to_warn!(warnings, locals, current_module, arg)
×
332
    end
×
333

334
    return nothing
×
335
end
336

337
function collect_qualified_access_warnings(current_mod, ast)
×
338
    warnings = Set()
×
339
    locals = Set{Symbol}()
×
340
    collect_names_to_warn!(warnings, locals, current_mod, ast)
×
341
    filter!(warnings) do (; outer_mod)
×
342
        nameof(outer_mod) ∉ locals
×
343
    end
344
    return warnings
×
345
end
346

347
function warn_on_non_owning_accesses(current_mod, ast)
×
348
    warnings = collect_qualified_access_warnings(current_mod, ast)
×
349
    for (; outer_mod, mod, owner, name_being_accessed) in warnings
×
350
        print_qualified_access_warning(mod, owner, name_being_accessed)
×
351
    end
×
352
    return ast
×
353
end
354
warn_on_non_owning_accesses(ast) = warn_on_non_owning_accesses(Base.active_module(), ast)
×
355

356
const repl_ast_transforms = Any[softscope, warn_on_non_owning_accesses] # defaults for new REPL backends
357

358
# Allows an external package to add hooks into the code loading.
359
# The hook should take a Vector{Symbol} of package names and
360
# return true if all packages could be installed, false if not
361
# to e.g. install packages on demand
362
const install_packages_hooks = Any[]
363

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

372
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))
×
373
    if !isexpr(ast, :toplevel)
×
374
        ast = invokelatest(__repl_entry_lower_with_loc, mod, ast, toplevel_file, toplevel_line)
×
375
        check_for_missing_packages_and_run_hooks(ast)
×
376
        return invokelatest(__repl_entry_eval_expanded_with_loc, mod, ast, toplevel_file, toplevel_line)
×
377
    end
378
    local value=nothing
×
379
    for i = 1:length(ast.args)
×
380
        value = toplevel_eval_with_hooks(mod, ast.args[i], toplevel_file, toplevel_line)
×
381
    end
×
382
    return value
×
383
end
384

385
function eval_user_input(@nospecialize(ast), backend::REPLBackend, mod::Module)
×
386
    lasterr = nothing
×
387
    Base.sigatomic_begin()
×
388
    while true
×
389
        try
×
390
            Base.sigatomic_end()
×
391
            if lasterr !== nothing
×
392
                put!(backend.response_channel, Pair{Any, Bool}(lasterr, true))
×
393
            else
394
                backend.in_eval = true
×
395
                for xf in backend.ast_transforms
×
396
                    ast = Base.invokelatest(xf, ast)
×
397
                end
×
398
                value = toplevel_eval_with_hooks(mod, ast)
×
399
                backend.in_eval = false
×
400
                setglobal!(Base.MainInclude, :ans, value)
×
401
                put!(backend.response_channel, Pair{Any, Bool}(value, false))
×
402
            end
403
            break
×
404
        catch err
405
            if lasterr !== nothing
×
406
                println("SYSTEM ERROR: Failed to report error to REPL frontend")
×
407
                println(err)
×
408
            end
409
            lasterr = current_exceptions()
×
410
        end
411
    end
×
412
    Base.sigatomic_end()
×
413
    nothing
×
414
end
415

416
function check_for_missing_packages_and_run_hooks(ast)
×
417
    isa(ast, Expr) || return
×
418
    mods = modules_to_be_loaded(ast)
×
419
    filter!(mod -> isnothing(Base.identify_package(String(mod))), mods) # keep missing modules
×
420
    if !isempty(mods)
×
421
        isempty(install_packages_hooks) && load_pkg()
×
422
        for f in install_packages_hooks
×
423
            Base.invokelatest(f, mods) && return
×
424
        end
×
425
    end
426
end
427

428
function _modules_to_be_loaded!(ast::Expr, mods::Vector{Symbol})
×
429
    ast.head === :quote && return mods # don't search if it's not going to be run during this eval
×
430
    if ast.head === :using || ast.head === :import
×
431
        for arg in ast.args
×
432
            arg = arg::Expr
×
433
            arg1 = first(arg.args)
×
434
            if arg1 isa Symbol # i.e. `Foo`
×
435
                if arg1 != :. # don't include local import `import .Foo`
×
436
                    push!(mods, arg1)
×
437
                end
438
            else # i.e. `Foo: bar`
439
                sym = first((arg1::Expr).args)::Symbol
×
440
                if sym != :. # don't include local import `import .Foo: a`
×
441
                    push!(mods, sym)
×
442
                end
443
            end
444
        end
×
445
    end
446
    if ast.head !== :thunk
×
447
        for arg in ast.args
×
448
            if isexpr(arg, (:block, :if, :using, :import))
×
449
                _modules_to_be_loaded!(arg, mods)
×
450
            end
451
        end
×
452
    else
453
        code = ast.args[1]
×
454
        for arg in code.code
×
455
            isa(arg, Expr) || continue
×
456
            _modules_to_be_loaded!(arg, mods)
×
457
        end
×
458
    end
459
end
460

461
function modules_to_be_loaded(ast::Expr, mods::Vector{Symbol} = Symbol[])
×
462
    _modules_to_be_loaded!(ast, mods)
×
463
    filter!(mod::Symbol -> !in(mod, (:Base, :Main, :Core)), mods) # Exclude special non-package modules
×
464
    return unique(mods)
×
465
end
466

467
"""
468
    start_repl_backend(repl_channel::Channel, response_channel::Channel)
469

470
    Starts loop for REPL backend
471
    Returns a REPLBackend with backend_task assigned
472

473
    Deprecated since sync / async behavior cannot be selected
474
"""
475
function start_repl_backend(repl_channel::Channel{Any}, response_channel::Channel{Any}
×
476
                            ; get_module::Function = ()->Main)
477
    # Maintain legacy behavior of asynchronous backend
478
    backend = REPLBackend(repl_channel, response_channel, false)
×
479
    # Assignment will be made twice, but will be immediately available
480
    backend.backend_task = @async start_repl_backend(backend; get_module)
×
481
    return backend
×
482
end
483

484
"""
485
    start_repl_backend(backend::REPLBackend)
486

487
    Call directly to run backend loop on current Task.
488
    Use @async for run backend on new Task.
489

490
    Does not return backend until loop is finished.
491
"""
492
function start_repl_backend(backend::REPLBackend,  @nospecialize(consumer = x -> nothing); get_module::Function = ()->Main)
×
493
    backend.backend_task = Base.current_task()
×
494
    consumer(backend)
×
495
    repl_backend_loop(backend, get_module)
×
496
    return backend
×
497
end
498

499
function repl_backend_loop(backend::REPLBackend, get_module::Function)
×
500
    # include looks at this to determine the relative include path
501
    # nothing means cwd
502
    while true
×
503
        tls = task_local_storage()
×
504
        tls[:SOURCE_PATH] = nothing
×
505
        ast, show_value = take!(backend.repl_channel)
×
506
        if show_value == -1
×
507
            # exit flag
508
            break
×
509
        end
510
        eval_user_input(ast, backend, get_module())
×
511
    end
×
512
    return nothing
×
513
end
514

515
SHOW_MAXIMUM_BYTES::Int = 1_048_576
516

517
# Limit printing during REPL display
518
mutable struct LimitIO{IO_t <: IO} <: IO
519
    io::IO_t
520
    maxbytes::Int
521
    n::Int # max bytes to write
522
end
523
LimitIO(io::IO, maxbytes) = LimitIO(io, maxbytes, 0)
×
524

525
struct LimitIOException <: Exception
526
    maxbytes::Int
527
end
528

529
function Base.showerror(io::IO, e::LimitIOException)
×
530
    print(io, "$LimitIOException: aborted printing after attempting to print more than $(Base.format_bytes(e.maxbytes)) within a `LimitIO`.")
×
531
end
532

533
function Base.write(io::LimitIO, v::UInt8)
×
534
    io.n > io.maxbytes && throw(LimitIOException(io.maxbytes))
×
535
    n_bytes = write(io.io, v)
×
536
    io.n += n_bytes
×
537
    return n_bytes
×
538
end
539

540
# Semantically, we only need to override `Base.write`, but we also
541
# override `unsafe_write` for performance.
542
function Base.unsafe_write(limiter::LimitIO, p::Ptr{UInt8}, nb::UInt)
×
543
    # already exceeded? throw
544
    limiter.n > limiter.maxbytes && throw(LimitIOException(limiter.maxbytes))
×
545
    remaining = limiter.maxbytes - limiter.n # >= 0
×
546

547
    # Not enough bytes left; we will print up to the limit, then throw
548
    if remaining < nb
×
549
        if remaining > 0
×
550
            Base.unsafe_write(limiter.io, p, remaining)
×
551
        end
552
        throw(LimitIOException(limiter.maxbytes))
×
553
    end
554

555
    # We won't hit the limit so we'll write the full `nb` bytes
556
    bytes_written = Base.unsafe_write(limiter.io, p, nb)::Union{Int,UInt}
×
557
    limiter.n += bytes_written
×
558
    return bytes_written
×
559
end
560

561
struct REPLDisplay{Repl<:AbstractREPL} <: AbstractDisplay
562
    repl::Repl
563
end
564

565
function show_limited(io::IO, mime::MIME, x)
×
566
    try
×
567
        # We wrap in a LimitIO to limit the amount of printing.
568
        # We unpack `IOContext`s, since we will pass the properties on the outside.
569
        inner = io isa IOContext ? io.io : io
×
570
        wrapped_limiter = IOContext(LimitIO(inner, SHOW_MAXIMUM_BYTES), io)
×
571
        # `show_repl` to allow the hook with special syntax highlighting
572
        show_repl(wrapped_limiter, mime, x)
×
573
    catch e
574
        e isa LimitIOException || rethrow()
×
575
        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)
×
576
    end
577
end
578

579
function display(d::REPLDisplay, mime::MIME"text/plain", x)
×
580
    x = Ref{Any}(x)
×
581
    with_repl_linfo(d.repl) do io
×
582
        io = IOContext(io, :limit => true, :module => Base.active_module(d)::Module)
×
583
        if d.repl isa LineEditREPL
×
584
            mistate = d.repl.mistate
×
585
            mode = LineEdit.mode(mistate)
×
586
            if mode isa LineEdit.Prompt
×
587
                LineEdit.write_output_prefix(io, mode, get(io, :color, false)::Bool)
×
588
            end
589
        end
590
        get(io, :color, false)::Bool && write(io, answer_color(d.repl))
×
591
        if isdefined(d.repl, :options) && isdefined(d.repl.options, :iocontext)
×
592
            # this can override the :limit property set initially
593
            io = foldl(IOContext, d.repl.options.iocontext, init=io)
×
594
        end
595
        show_limited(io, mime, x[])
×
596
        println(io)
×
597
    end
598
    return nothing
×
599
end
600

601
display(d::REPLDisplay, x) = display(d, MIME("text/plain"), x)
×
602

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

605
show_repl(io::IO, ::MIME"text/plain", ex::Expr) =
×
606
    print(io, JuliaSyntaxHighlighting.highlight(
607
        sprint(show, ex, context=IOContext(io, :color => false))))
608

609
function print_response(repl::AbstractREPL, response, show_value::Bool, have_color::Bool)
×
610
    repl.waserror = response[2]
×
611
    with_repl_linfo(repl) do io
×
612
        io = IOContext(io, :module => Base.active_module(repl)::Module)
×
613
        print_response(io, response, show_value, have_color, specialdisplay(repl))
×
614
    end
615
    return nothing
×
616
end
617

618
function repl_display_error(errio::IO, @nospecialize errval)
×
619
    # this will be set to true if types in the stacktrace are truncated
620
    limitflag = Ref(false)
×
621
    errio = IOContext(errio, :stacktrace_types_limited => limitflag)
×
622
    Base.invokelatest(Base.display_error, errio, errval)
×
623
    if limitflag[]
×
624
        print(errio, "Some type information was truncated. Use `show(err)` to see complete types.")
×
625
        println(errio)
×
626
    end
627
    return nothing
×
628
end
629

630
function print_response(errio::IO, response, show_value::Bool, have_color::Bool, specialdisplay::Union{AbstractDisplay,Nothing}=nothing)
×
631
    Base.sigatomic_begin()
×
632
    val, iserr = response
×
633
    while true
×
634
        try
×
635
            Base.sigatomic_end()
×
636
            if iserr
×
637
                val = Base.scrub_repl_backtrace(val)
×
638
                Base.istrivialerror(val) || setglobal!(Base.MainInclude, :err, val)
×
639
                repl_display_error(errio, val)
×
640
            else
641
                if val !== nothing && show_value
×
642
                    try
×
643
                        if specialdisplay === nothing
×
644
                            Base.invokelatest(display, val)
×
645
                        else
646
                            Base.invokelatest(display, specialdisplay, val)
×
647
                        end
648
                    catch
649
                        println(errio, "Error showing value of type ", typeof(val), ":")
×
650
                        rethrow()
×
651
                    end
652
                end
653
            end
654
            break
×
655
        catch ex
656
            if iserr
×
657
                println(errio) # an error during printing is likely to leave us mid-line
×
658
                println(errio, "SYSTEM (REPL): showing an error caused an error")
×
659
                try
×
660
                    excs = Base.scrub_repl_backtrace(current_exceptions())
×
661
                    setglobal!(Base.MainInclude, :err, excs)
×
662
                    repl_display_error(errio, excs)
×
663
                catch e
664
                    # at this point, only print the name of the type as a Symbol to
665
                    # minimize the possibility of further errors.
666
                    println(errio)
×
667
                    println(errio, "SYSTEM (REPL): caught exception of type ", typeof(e).name.name,
×
668
                            " while trying to handle a nested exception; giving up")
669
                end
670
                break
×
671
            end
672
            val = current_exceptions()
×
673
            iserr = true
×
674
        end
675
    end
×
676
    Base.sigatomic_end()
×
677
    nothing
×
678
end
679

680
# A reference to a backend that is not mutable
681
struct REPLBackendRef
682
    repl_channel::Channel{Any}
683
    response_channel::Channel{Any}
684
end
685
REPLBackendRef(backend::REPLBackend) = REPLBackendRef(backend.repl_channel, backend.response_channel)
×
686

687
function destroy(ref::REPLBackendRef, state::Task)
×
688
    if istaskfailed(state)
×
689
        close(ref.repl_channel, TaskFailedException(state))
×
690
        close(ref.response_channel, TaskFailedException(state))
×
691
    end
692
    close(ref.repl_channel)
×
693
    close(ref.response_channel)
×
694
end
695

696
"""
697
    run_repl(repl::AbstractREPL)
698
    run_repl(repl, consumer = backend->nothing; backend_on_current_task = true)
699

700
    Main function to start the REPL
701

702
    consumer is an optional function that takes a REPLBackend as an argument
703
"""
704
function run_repl(repl::AbstractREPL, @nospecialize(consumer = x -> nothing); backend_on_current_task::Bool = true, backend = REPLBackend())
×
705
    backend_ref = REPLBackendRef(backend)
×
706
    cleanup = @task try
×
707
            destroy(backend_ref, t)
×
708
        catch e
709
            Core.print(Core.stderr, "\nINTERNAL ERROR: ")
×
710
            Core.println(Core.stderr, e)
×
711
            Core.println(Core.stderr, catch_backtrace())
×
712
        end
713
    get_module = () -> Base.active_module(repl)
×
714
    if backend_on_current_task
×
715
        t = @async run_frontend(repl, backend_ref)
×
716
        errormonitor(t)
×
717
        Base._wait2(t, cleanup)
×
718
        start_repl_backend(backend, consumer; get_module)
×
719
    else
720
        t = @async start_repl_backend(backend, consumer; get_module)
×
721
        errormonitor(t)
×
722
        Base._wait2(t, cleanup)
×
723
        run_frontend(repl, backend_ref)
×
724
    end
725
    return backend
×
726
end
727

728
## BasicREPL ##
729

730
mutable struct BasicREPL <: AbstractREPL
731
    terminal::TextTerminal
732
    waserror::Bool
733
    frontend_task::Task
734
    BasicREPL(t) = new(t, false)
×
735
end
736

737
outstream(r::BasicREPL) = r.terminal
×
738
hascolor(r::BasicREPL) = hascolor(r.terminal)
×
739

740
function run_frontend(repl::BasicREPL, backend::REPLBackendRef)
×
741
    repl.frontend_task = current_task()
×
742
    d = REPLDisplay(repl)
×
743
    dopushdisplay = !in(d,Base.Multimedia.displays)
×
744
    dopushdisplay && pushdisplay(d)
×
745
    hit_eof = false
×
746
    while true
×
747
        Base.reseteof(repl.terminal)
×
748
        write(repl.terminal, JULIA_PROMPT)
×
749
        line = ""
×
750
        ast = nothing
×
751
        interrupted = false
×
752
        while true
×
753
            try
×
754
                line *= readline(repl.terminal, keep=true)
×
755
            catch e
756
                if isa(e,InterruptException)
×
757
                    try # raise the debugger if present
×
758
                        ccall(:jl_raise_debugger, Int, ())
×
759
                    catch
×
760
                    end
761
                    line = ""
×
762
                    interrupted = true
×
763
                    break
×
764
                elseif isa(e,EOFError)
×
765
                    hit_eof = true
×
766
                    break
×
767
                else
768
                    rethrow()
×
769
                end
770
            end
771
            ast = Base.parse_input_line(line)
×
772
            (isa(ast,Expr) && ast.head === :incomplete) || break
×
773
        end
×
774
        if !isempty(line)
×
775
            response = eval_with_backend(ast, backend)
×
776
            print_response(repl, response, !ends_with_semicolon(line), false)
×
777
        end
778
        write(repl.terminal, '\n')
×
779
        ((!interrupted && isempty(line)) || hit_eof) && break
×
780
    end
×
781
    # terminate backend
782
    put!(backend.repl_channel, (nothing, -1))
×
783
    dopushdisplay && popdisplay(d)
×
784
    nothing
×
785
end
786

787
## LineEditREPL ##
788

789
mutable struct LineEditREPL <: AbstractREPL
790
    t::TextTerminal
791
    hascolor::Bool
792
    prompt_color::String
793
    input_color::String
794
    answer_color::String
795
    shell_color::String
796
    help_color::String
797
    pkg_color::String
798
    history_file::Bool
799
    in_shell::Bool
800
    in_help::Bool
801
    envcolors::Bool
802
    waserror::Bool
803
    specialdisplay::Union{Nothing,AbstractDisplay}
804
    options::Options
805
    mistate::Union{MIState,Nothing}
806
    last_shown_line_infos::Vector{Tuple{String,Int}}
807
    interface::ModalInterface
808
    backendref::REPLBackendRef
809
    frontend_task::Task
810
    function LineEditREPL(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,pkg_color,history_file,in_shell,in_help,envcolors)
×
811
        opts = Options()
×
812
        opts.hascolor = hascolor
×
813
        if !hascolor
×
814
            opts.beep_colors = [""]
×
815
        end
816
        new(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,pkg_color,history_file,in_shell,
×
817
            in_help,envcolors,false,nothing, opts, nothing, Tuple{String,Int}[])
818
    end
819
end
820
outstream(r::LineEditREPL) = (t = r.t; t isa TTYTerminal ? t.out_stream : t)
×
821
specialdisplay(r::LineEditREPL) = r.specialdisplay
×
822
specialdisplay(r::AbstractREPL) = nothing
×
823
terminal(r::LineEditREPL) = r.t
×
824
hascolor(r::LineEditREPL) = r.hascolor
×
825

826
LineEditREPL(t::TextTerminal, hascolor::Bool, envcolors::Bool=false) =
×
827
    LineEditREPL(t, hascolor,
828
        hascolor ? Base.text_colors[:green] : "",
829
        hascolor ? Base.input_color() : "",
830
        hascolor ? Base.answer_color() : "",
831
        hascolor ? Base.text_colors[:red] : "",
832
        hascolor ? Base.text_colors[:yellow] : "",
833
        hascolor ? Base.text_colors[:blue] : "",
834
        false, false, false, envcolors
835
    )
836

837
mutable struct REPLCompletionProvider <: CompletionProvider
838
    modifiers::LineEdit.Modifiers
839
end
840
REPLCompletionProvider() = REPLCompletionProvider(LineEdit.Modifiers())
×
841

842
mutable struct ShellCompletionProvider <: CompletionProvider end
843
struct LatexCompletions <: CompletionProvider end
844

845
Base.active_module((; mistate)::LineEditREPL) = mistate === nothing ? Main : mistate.active_module
×
846
Base.active_module(::AbstractREPL) = Main
×
847
Base.active_module(d::REPLDisplay) = Base.active_module(d.repl)
×
848

849
setmodifiers!(c::CompletionProvider, m::LineEdit.Modifiers) = nothing
×
850

851
setmodifiers!(c::REPLCompletionProvider, m::LineEdit.Modifiers) = c.modifiers = m
×
852

853
"""
854
    activate(mod::Module=Main)
855

856
Set `mod` as the default contextual module in the REPL,
857
both for evaluating expressions and printing them.
858
"""
859
function activate(mod::Module=Main; interactive_utils::Bool=true)
×
860
    mistate = (Base.active_repl::LineEditREPL).mistate
×
861
    mistate === nothing && return nothing
×
862
    mistate.active_module = mod
×
863
    interactive_utils && Base.load_InteractiveUtils(mod)
×
864
    return nothing
×
865
end
866

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

869
function complete_line(c::REPLCompletionProvider, s::PromptState, mod::Module; hint::Bool=false)
×
870
    partial = beforecursor(s.input_buffer)
×
871
    full = LineEdit.input_string(s)
×
872
    ret, range, should_complete = completions(full, lastindex(partial), mod, c.modifiers.shift, hint)
×
873
    c.modifiers = LineEdit.Modifiers()
×
874
    return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), partial[range], should_complete
×
875
end
876

877
function complete_line(c::ShellCompletionProvider, s::PromptState; hint::Bool=false)
×
878
    # First parse everything up to the current position
879
    partial = beforecursor(s.input_buffer)
×
880
    full = LineEdit.input_string(s)
×
881
    ret, range, should_complete = shell_completions(full, lastindex(partial), hint)
×
882
    return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), partial[range], should_complete
×
883
end
884

885
function complete_line(c::LatexCompletions, s; hint::Bool=false)
×
886
    partial = beforecursor(LineEdit.buffer(s))
×
887
    full = LineEdit.input_string(s)::String
×
888
    ret, range, should_complete = bslash_completions(full, lastindex(partial), hint)[2]
×
889
    return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), partial[range], should_complete
×
890
end
891

892
with_repl_linfo(f, repl) = f(outstream(repl))
×
893
function with_repl_linfo(f, repl::LineEditREPL)
×
894
    linfos = Tuple{String,Int}[]
×
895
    io = IOContext(outstream(repl), :last_shown_line_infos => linfos)
×
896
    f(io)
×
897
    if !isempty(linfos)
×
898
        repl.last_shown_line_infos = linfos
×
899
    end
900
    nothing
×
901
end
902

903
mutable struct REPLHistoryProvider <: HistoryProvider
904
    history::Vector{String}
905
    file_path::String
906
    history_file::Union{Nothing,IO}
907
    start_idx::Int
908
    cur_idx::Int
909
    last_idx::Int
910
    last_buffer::IOBuffer
911
    last_mode::Union{Nothing,Prompt}
912
    mode_mapping::Dict{Symbol,Prompt}
913
    modes::Vector{Symbol}
914
end
915
REPLHistoryProvider(mode_mapping::Dict{Symbol}) =
×
916
    REPLHistoryProvider(String[], "", nothing, 0, 0, -1, IOBuffer(),
917
                        nothing, mode_mapping, UInt8[])
918

919
invalid_history_message(path::String) = """
×
920
Invalid history file ($path) format:
921
If you have a history file left over from an older version of Julia,
922
try renaming or deleting it.
923
Invalid character: """
924

925
munged_history_message(path::String) = """
×
926
Invalid history file ($path) format:
927
An editor may have converted tabs to spaces at line """
928

929
function hist_open_file(hp::REPLHistoryProvider)
×
930
    f = open(hp.file_path, read=true, write=true, create=true)
×
931
    hp.history_file = f
×
932
    seekend(f)
×
933
end
934

935
function hist_from_file(hp::REPLHistoryProvider, path::String)
×
936
    getline(lines, i) = i > length(lines) ? "" : lines[i]
×
937
    file_lines = readlines(path)
×
938
    countlines = 0
×
939
    while true
×
940
        # First parse the metadata that starts with '#' in particular the REPL mode
941
        countlines += 1
×
942
        line = getline(file_lines, countlines)
×
943
        mode = :julia
×
944
        isempty(line) && break
×
945
        line[1] != '#' &&
×
946
            error(invalid_history_message(path), repr(line[1]), " at line ", countlines)
947
        while !isempty(line)
×
948
            startswith(line, '#') || break
×
949
            if startswith(line, "# mode: ")
×
950
                mode = Symbol(SubString(line, 9))
×
951
            end
952
            countlines += 1
×
953
            line = getline(file_lines, countlines)
×
954
        end
×
955
        isempty(line) && break
×
956

957
        # Now parse the code for the current REPL mode
958
        line[1] == ' '  &&
×
959
            error(munged_history_message(path), countlines)
960
        line[1] != '\t' &&
×
961
            error(invalid_history_message(path), repr(line[1]), " at line ", countlines)
962
        lines = String[]
×
963
        while !isempty(line)
×
964
            push!(lines, chomp(SubString(line, 2)))
×
965
            next_line = getline(file_lines, countlines+1)
×
966
            isempty(next_line) && break
×
967
            first(next_line) == ' '  && error(munged_history_message(path), countlines)
×
968
            # A line not starting with a tab means we are done with code for this entry
969
            first(next_line) != '\t' && break
×
970
            countlines += 1
×
971
            line = getline(file_lines, countlines)
×
972
        end
×
973
        push!(hp.modes, mode)
×
974
        push!(hp.history, join(lines, '\n'))
×
975
    end
×
976
    hp.start_idx = length(hp.history)
×
977
    return hp
×
978
end
979

980
function add_history(hist::REPLHistoryProvider, s::PromptState)
×
981
    str = rstrip(String(take!(copy(s.input_buffer))))
×
982
    isempty(strip(str)) && return
×
983
    mode = mode_idx(hist, LineEdit.mode(s))
×
984
    !isempty(hist.history) &&
×
985
        isequal(mode, hist.modes[end]) && str == hist.history[end] && return
986
    push!(hist.modes, mode)
×
987
    push!(hist.history, str)
×
988
    hist.history_file === nothing && return
×
989
    entry = """
×
990
    # time: $(Libc.strftime("%Y-%m-%d %H:%M:%S %Z", time()))
991
    # mode: $mode
992
    $(replace(str, r"^"ms => "\t"))
×
993
    """
994
    # TODO: write-lock history file
995
    try
×
996
        seekend(hist.history_file)
×
997
    catch err
998
        (err isa SystemError) || rethrow()
×
999
        # File handle might get stale after a while, especially under network file systems
1000
        # If this doesn't fix it (e.g. when file is deleted), we'll end up rethrowing anyway
1001
        hist_open_file(hist)
×
1002
    end
1003
    print(hist.history_file, entry)
×
1004
    flush(hist.history_file)
×
1005
    nothing
×
1006
end
1007

1008
function history_move(s::Union{LineEdit.MIState,LineEdit.PrefixSearchState}, hist::REPLHistoryProvider, idx::Int, save_idx::Int = hist.cur_idx)
×
1009
    max_idx = length(hist.history) + 1
×
1010
    @assert 1 <= hist.cur_idx <= max_idx
×
1011
    (1 <= idx <= max_idx) || return :none
×
1012
    idx != hist.cur_idx || return :none
×
1013

1014
    # save the current line
1015
    if save_idx == max_idx
×
1016
        hist.last_mode = LineEdit.mode(s)
×
1017
        hist.last_buffer = copy(LineEdit.buffer(s))
×
1018
    else
1019
        hist.history[save_idx] = LineEdit.input_string(s)
×
1020
        hist.modes[save_idx] = mode_idx(hist, LineEdit.mode(s))
×
1021
    end
1022

1023
    # load the saved line
1024
    if idx == max_idx
×
1025
        last_buffer = hist.last_buffer
×
1026
        LineEdit.transition(s, hist.last_mode) do
×
1027
            LineEdit.replace_line(s, last_buffer)
×
1028
        end
1029
        hist.last_mode = nothing
×
1030
        hist.last_buffer = IOBuffer()
×
1031
    else
1032
        if haskey(hist.mode_mapping, hist.modes[idx])
×
1033
            LineEdit.transition(s, hist.mode_mapping[hist.modes[idx]]) do
×
1034
                LineEdit.replace_line(s, hist.history[idx])
×
1035
            end
1036
        else
1037
            return :skip
×
1038
        end
1039
    end
1040
    hist.cur_idx = idx
×
1041

1042
    return :ok
×
1043
end
1044

1045
# REPL History can also transitions modes
1046
function LineEdit.accept_result_newmode(hist::REPLHistoryProvider)
×
1047
    if 1 <= hist.cur_idx <= length(hist.modes)
×
1048
        return hist.mode_mapping[hist.modes[hist.cur_idx]]
×
1049
    end
1050
    return nothing
×
1051
end
1052

1053
function history_prev(s::LineEdit.MIState, hist::REPLHistoryProvider,
×
1054
                      num::Int=1, save_idx::Int = hist.cur_idx)
1055
    num <= 0 && return history_next(s, hist, -num, save_idx)
×
1056
    hist.last_idx = -1
×
1057
    m = history_move(s, hist, hist.cur_idx-num, save_idx)
×
1058
    if m === :ok
×
1059
        LineEdit.move_input_start(s)
×
1060
        LineEdit.reset_key_repeats(s) do
×
1061
            LineEdit.move_line_end(s)
×
1062
        end
1063
        return LineEdit.refresh_line(s)
×
1064
    elseif m === :skip
×
1065
        return history_prev(s, hist, num+1, save_idx)
×
1066
    else
1067
        return Terminals.beep(s)
×
1068
    end
1069
end
1070

1071
function history_next(s::LineEdit.MIState, hist::REPLHistoryProvider,
×
1072
                      num::Int=1, save_idx::Int = hist.cur_idx)
1073
    if num == 0
×
1074
        Terminals.beep(s)
×
1075
        return
×
1076
    end
1077
    num < 0 && return history_prev(s, hist, -num, save_idx)
×
1078
    cur_idx = hist.cur_idx
×
1079
    max_idx = length(hist.history) + 1
×
1080
    if cur_idx == max_idx && 0 < hist.last_idx
×
1081
        # issue #6312
1082
        cur_idx = hist.last_idx
×
1083
        hist.last_idx = -1
×
1084
    end
1085
    m = history_move(s, hist, cur_idx+num, save_idx)
×
1086
    if m === :ok
×
1087
        LineEdit.move_input_end(s)
×
1088
        return LineEdit.refresh_line(s)
×
1089
    elseif m === :skip
×
1090
        return history_next(s, hist, num+1, save_idx)
×
1091
    else
1092
        return Terminals.beep(s)
×
1093
    end
1094
end
1095

1096
history_first(s::LineEdit.MIState, hist::REPLHistoryProvider) =
×
1097
    history_prev(s, hist, hist.cur_idx - 1 -
1098
                 (hist.cur_idx > hist.start_idx+1 ? hist.start_idx : 0))
1099

1100
history_last(s::LineEdit.MIState, hist::REPLHistoryProvider) =
×
1101
    history_next(s, hist, length(hist.history) - hist.cur_idx + 1)
1102

1103
function history_move_prefix(s::LineEdit.PrefixSearchState,
×
1104
                             hist::REPLHistoryProvider,
1105
                             prefix::AbstractString,
1106
                             backwards::Bool,
1107
                             cur_idx::Int = hist.cur_idx)
1108
    cur_response = String(take!(copy(LineEdit.buffer(s))))
×
1109
    # when searching forward, start at last_idx
1110
    if !backwards && hist.last_idx > 0
×
1111
        cur_idx = hist.last_idx
×
1112
    end
1113
    hist.last_idx = -1
×
1114
    max_idx = length(hist.history)+1
×
1115
    idxs = backwards ? ((cur_idx-1):-1:1) : ((cur_idx+1):1:max_idx)
×
1116
    for idx in idxs
×
1117
        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)))
×
1118
            m = history_move(s, hist, idx)
×
1119
            if m === :ok
×
1120
                if idx == max_idx
×
1121
                    # on resuming the in-progress edit, leave the cursor where the user last had it
1122
                elseif isempty(prefix)
×
1123
                    # on empty prefix search, move cursor to the end
1124
                    LineEdit.move_input_end(s)
×
1125
                else
1126
                    # otherwise, keep cursor at the prefix position as a visual cue
1127
                    seek(LineEdit.buffer(s), sizeof(prefix))
×
1128
                end
1129
                LineEdit.refresh_line(s)
×
1130
                return :ok
×
1131
            elseif m === :skip
×
1132
                return history_move_prefix(s,hist,prefix,backwards,idx)
×
1133
            end
1134
        end
1135
    end
×
1136
    Terminals.beep(s)
×
1137
    nothing
×
1138
end
1139
history_next_prefix(s::LineEdit.PrefixSearchState, hist::REPLHistoryProvider, prefix::AbstractString) =
×
1140
    history_move_prefix(s, hist, prefix, false)
1141
history_prev_prefix(s::LineEdit.PrefixSearchState, hist::REPLHistoryProvider, prefix::AbstractString) =
×
1142
    history_move_prefix(s, hist, prefix, true)
1143

1144
function history_search(hist::REPLHistoryProvider, query_buffer::IOBuffer, response_buffer::IOBuffer,
×
1145
                        backwards::Bool=false, skip_current::Bool=false)
1146

1147
    qpos = position(query_buffer)
×
1148
    qpos > 0 || return true
×
1149
    searchdata = beforecursor(query_buffer)
×
1150
    response_str = String(take!(copy(response_buffer)))
×
1151

1152
    # Alright, first try to see if the current match still works
1153
    a = position(response_buffer) + 1 # position is zero-indexed
×
1154
    # FIXME: I'm pretty sure this is broken since it uses an index
1155
    # into the search data to index into the response string
1156
    b = a + sizeof(searchdata)
×
1157
    b = b ≤ ncodeunits(response_str) ? prevind(response_str, b) : b-1
×
1158
    b = min(lastindex(response_str), b) # ensure that b is valid
×
1159

1160
    searchstart = backwards ? b : a
×
1161
    if searchdata == response_str[a:b]
×
1162
        if skip_current
×
1163
            searchstart = backwards ? prevind(response_str, b) : nextind(response_str, a)
×
1164
        else
1165
            return true
×
1166
        end
1167
    end
1168

1169
    # Start searching
1170
    # First the current response buffer
1171
    if 1 <= searchstart <= lastindex(response_str)
×
1172
        match = backwards ? findprev(searchdata, response_str, searchstart) :
×
1173
                            findnext(searchdata, response_str, searchstart)
1174
        if match !== nothing
×
1175
            seek(response_buffer, first(match) - 1)
×
1176
            return true
×
1177
        end
1178
    end
1179

1180
    # Now search all the other buffers
1181
    idxs = backwards ? ((hist.cur_idx-1):-1:1) : ((hist.cur_idx+1):1:length(hist.history))
×
1182
    for idx in idxs
×
1183
        h = hist.history[idx]
×
1184
        match = backwards ? findlast(searchdata, h) : findfirst(searchdata, h)
×
1185
        if match !== nothing && h != response_str && haskey(hist.mode_mapping, hist.modes[idx])
×
1186
            truncate(response_buffer, 0)
×
1187
            write(response_buffer, h)
×
1188
            seek(response_buffer, first(match) - 1)
×
1189
            hist.cur_idx = idx
×
1190
            return true
×
1191
        end
1192
    end
×
1193

1194
    return false
×
1195
end
1196

1197
function history_reset_state(hist::REPLHistoryProvider)
×
1198
    if hist.cur_idx != length(hist.history) + 1
×
1199
        hist.last_idx = hist.cur_idx
×
1200
        hist.cur_idx = length(hist.history) + 1
×
1201
    end
1202
    nothing
×
1203
end
1204
LineEdit.reset_state(hist::REPLHistoryProvider) = history_reset_state(hist)
×
1205

1206
function return_callback(s)
×
1207
    ast = Base.parse_input_line(String(take!(copy(LineEdit.buffer(s)))), depwarn=false)
×
1208
    return !(isa(ast, Expr) && ast.head === :incomplete)
×
1209
end
1210

1211
find_hist_file() = get(ENV, "JULIA_HISTORY",
×
1212
                       !isempty(DEPOT_PATH) ? joinpath(DEPOT_PATH[1], "logs", "repl_history.jl") :
1213
                       error("DEPOT_PATH is empty and ENV[\"JULIA_HISTORY\"] not set."))
1214

1215
backend(r::AbstractREPL) = r.backendref
×
1216

1217
function eval_with_backend(ast, backend::REPLBackendRef)
×
1218
    put!(backend.repl_channel, (ast, 1))
×
1219
    return take!(backend.response_channel) # (val, iserr)
×
1220
end
1221

1222
function respond(f, repl, main; pass_empty::Bool = false, suppress_on_semicolon::Bool = true)
×
1223
    return function do_respond(s::MIState, buf, ok::Bool)
×
1224
        if !ok
×
1225
            return transition(s, :abort)
×
1226
        end
1227
        line = String(take!(buf)::Vector{UInt8})
×
1228
        if !isempty(line) || pass_empty
×
1229
            reset(repl)
×
1230
            local response
×
1231
            try
×
1232
                ast = Base.invokelatest(f, line)
×
1233
                response = eval_with_backend(ast, backend(repl))
×
1234
            catch
1235
                response = Pair{Any, Bool}(current_exceptions(), true)
×
1236
            end
1237
            hide_output = suppress_on_semicolon && ends_with_semicolon(line)
×
1238
            print_response(repl, response, !hide_output, hascolor(repl))
×
1239
        end
1240
        prepare_next(repl)
×
1241
        reset_state(s)
×
1242
        return s.current_mode.sticky ? true : transition(s, main)
×
1243
    end
1244
end
1245

1246
function reset(repl::LineEditREPL)
×
1247
    raw!(repl.t, false)
×
1248
    hascolor(repl) && print(repl.t, Base.text_colors[:normal])
×
1249
    nothing
×
1250
end
1251

1252
function prepare_next(repl::LineEditREPL)
×
1253
    println(terminal(repl))
×
1254
end
1255

1256
function mode_keymap(julia_prompt::Prompt)
×
1257
    AnyDict(
×
1258
    '\b' => function (s::MIState,o...)
×
1259
        if isempty(s) || position(LineEdit.buffer(s)) == 0
×
1260
            buf = copy(LineEdit.buffer(s))
×
1261
            transition(s, julia_prompt) do
×
1262
                LineEdit.state(s, julia_prompt).input_buffer = buf
×
1263
            end
1264
        else
1265
            LineEdit.edit_backspace(s)
×
1266
        end
1267
    end,
1268
    "^C" => function (s::MIState,o...)
×
1269
        LineEdit.move_input_end(s)
×
1270
        LineEdit.refresh_line(s)
×
1271
        print(LineEdit.terminal(s), "^C\n\n")
×
1272
        transition(s, julia_prompt)
×
1273
        transition(s, :reset)
×
1274
        LineEdit.refresh_line(s)
×
1275
    end)
1276
end
1277

1278
repl_filename(repl, hp::REPLHistoryProvider) = "REPL[$(max(length(hp.history)-hp.start_idx, 1))]"
×
1279
repl_filename(repl, hp) = "REPL"
×
1280

1281
const JL_PROMPT_PASTE = Ref(true)
1282
enable_promptpaste(v::Bool) = JL_PROMPT_PASTE[] = v
×
1283

1284
function contextual_prompt(repl::LineEditREPL, prompt::Union{String,Function})
×
1285
    function ()
×
1286
        mod = Base.active_module(repl)
×
1287
        prefix = mod == Main ? "" : string('(', mod, ") ")
×
1288
        pr = prompt isa String ? prompt : prompt()
×
1289
        prefix * pr
×
1290
    end
1291
end
1292

1293
setup_interface(
×
1294
    repl::LineEditREPL;
1295
    # those keyword arguments may be deprecated eventually in favor of the Options mechanism
1296
    hascolor::Bool = repl.options.hascolor,
1297
    extra_repl_keymap::Any = repl.options.extra_keymap
1298
) = setup_interface(repl, hascolor, extra_repl_keymap)
1299

1300

1301
# This non keyword method can be precompiled which is important
1302
function setup_interface(
×
1303
    repl::LineEditREPL,
1304
    hascolor::Bool,
1305
    extra_repl_keymap::Any, # Union{Dict,Vector{<:Dict}},
1306
)
1307
    # The precompile statement emitter has problem outputting valid syntax for the
1308
    # type of `Union{Dict,Vector{<:Dict}}` (see #28808).
1309
    # This function is however important to precompile for REPL startup time, therefore,
1310
    # make the type Any and just assert that we have the correct type below.
1311
    @assert extra_repl_keymap isa Union{Dict,Vector{<:Dict}}
×
1312

1313
    ###
1314
    #
1315
    # This function returns the main interface that describes the REPL
1316
    # functionality, it is called internally by functions that setup a
1317
    # Terminal-based REPL frontend.
1318
    #
1319
    # See run_frontend(repl::LineEditREPL, backend::REPLBackendRef)
1320
    # for usage
1321
    #
1322
    ###
1323

1324
    ###
1325
    # We setup the interface in two stages.
1326
    # First, we set up all components (prompt,rsearch,shell,help)
1327
    # Second, we create keymaps with appropriate transitions between them
1328
    #   and assign them to the components
1329
    #
1330
    ###
1331

1332
    ############################### Stage I ################################
1333

1334
    # This will provide completions for REPL and help mode
1335
    replc = REPLCompletionProvider()
×
1336

1337
    # Set up the main Julia prompt
1338
    julia_prompt = Prompt(contextual_prompt(repl, JULIA_PROMPT);
×
1339
        # Copy colors from the prompt object
1340
        prompt_prefix = hascolor ? repl.prompt_color : "",
1341
        prompt_suffix = hascolor ?
1342
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1343
        repl = repl,
1344
        complete = replc,
1345
        on_enter = return_callback)
1346

1347
    # Setup help mode
1348
    help_mode = Prompt(contextual_prompt(repl, HELP_PROMPT),
×
1349
        prompt_prefix = hascolor ? repl.help_color : "",
1350
        prompt_suffix = hascolor ?
1351
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1352
        repl = repl,
1353
        complete = replc,
1354
        # When we're done transform the entered line into a call to helpmode function
1355
        on_done = respond(line::String->helpmode(outstream(repl), line, repl.mistate.active_module),
×
1356
                          repl, julia_prompt, pass_empty=true, suppress_on_semicolon=false))
1357

1358

1359
    # Set up shell mode
1360
    shell_mode = Prompt(SHELL_PROMPT;
×
1361
        prompt_prefix = hascolor ? repl.shell_color : "",
1362
        prompt_suffix = hascolor ?
1363
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1364
        repl = repl,
1365
        complete = ShellCompletionProvider(),
1366
        # Transform "foo bar baz" into `foo bar baz` (shell quoting)
1367
        # and pass into Base.repl_cmd for processing (handles `ls` and `cd`
1368
        # special)
1369
        on_done = respond(repl, julia_prompt) do line
1370
            Expr(:call, :(Base.repl_cmd),
×
1371
                :(Base.cmd_gen($(Base.shell_parse(line::String)[1]))),
1372
                outstream(repl))
1373
        end,
1374
        sticky = true)
1375

1376
    # Set up dummy Pkg mode that will be replaced once Pkg is loaded
1377
    # use 6 dots to occupy the same space as the most likely "@v1.xx" env name
1378
    dummy_pkg_mode = Prompt(Pkg_promptf,
×
1379
        prompt_prefix = hascolor ? repl.pkg_color : "",
1380
        prompt_suffix = hascolor ?
1381
        (repl.envcolors ? Base.input_color : repl.input_color) : "",
1382
        repl = repl,
1383
        complete = LineEdit.EmptyCompletionProvider(),
1384
        on_done = respond(line->nothing, repl, julia_prompt),
×
1385
        on_enter = function (s::MIState)
×
1386
                # This is hit when the user tries to execute a command before the real Pkg mode has been
1387
                # switched to. Ok to do this even if Pkg is loading on the other task because of the loading lock.
1388
                REPLExt = load_pkg()
×
1389
                if REPLExt isa Module && isdefined(REPLExt, :PkgCompletionProvider)
×
1390
                    for mode in repl.interface.modes
×
1391
                        if mode isa LineEdit.Prompt && mode.complete isa REPLExt.PkgCompletionProvider
×
1392
                            # pkg mode
1393
                            buf = copy(LineEdit.buffer(s))
×
1394
                            transition(s, mode) do
×
1395
                                LineEdit.state(s, mode).input_buffer = buf
×
1396
                            end
1397
                        end
1398
                    end
×
1399
                end
1400
                return true
×
1401
            end,
1402
        sticky = true)
1403

1404

1405
    ################################# Stage II #############################
1406

1407
    # Setup history
1408
    # We will have a unified history for all REPL modes
1409
    hp = REPLHistoryProvider(Dict{Symbol,Prompt}(:julia => julia_prompt,
×
1410
                                                 :shell => shell_mode,
1411
                                                 :help  => help_mode,
1412
                                                 :pkg  => dummy_pkg_mode))
1413
    if repl.history_file
×
1414
        try
×
1415
            hist_path = find_hist_file()
×
1416
            mkpath(dirname(hist_path))
×
1417
            hp.file_path = hist_path
×
1418
            hist_open_file(hp)
×
1419
            finalizer(replc) do replc
×
1420
                close(hp.history_file)
×
1421
            end
1422
            hist_from_file(hp, hist_path)
×
1423
        catch
1424
            # use REPL.hascolor to avoid using the local variable with the same name
1425
            print_response(repl, Pair{Any, Bool}(current_exceptions(), true), true, REPL.hascolor(repl))
×
1426
            println(outstream(repl))
×
1427
            @info "Disabling history file for this session"
×
1428
            repl.history_file = false
×
1429
        end
1430
    end
1431
    history_reset_state(hp)
×
1432
    julia_prompt.hist = hp
×
1433
    shell_mode.hist = hp
×
1434
    help_mode.hist = hp
×
1435
    dummy_pkg_mode.hist = hp
×
1436

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

1439

1440
    search_prompt, skeymap = LineEdit.setup_search_keymap(hp)
×
1441
    search_prompt.complete = LatexCompletions()
×
1442

1443
    shell_prompt_len = length(SHELL_PROMPT)
×
1444
    help_prompt_len = length(HELP_PROMPT)
×
1445
    jl_prompt_regex = Regex("^In \\[[0-9]+\\]: |^(?:\\(.+\\) )?$JULIA_PROMPT")
×
1446
    pkg_prompt_regex = Regex("^(?:\\(.+\\) )?$PKG_PROMPT")
×
1447

1448
    # Canonicalize user keymap input
1449
    if isa(extra_repl_keymap, Dict)
×
1450
        extra_repl_keymap = AnyDict[extra_repl_keymap]
×
1451
    end
1452

1453
    repl_keymap = AnyDict(
×
1454
        ';' => function (s::MIState,o...)
×
1455
            if isempty(s) || position(LineEdit.buffer(s)) == 0
×
1456
                buf = copy(LineEdit.buffer(s))
×
1457
                transition(s, shell_mode) do
×
1458
                    LineEdit.state(s, shell_mode).input_buffer = buf
×
1459
                end
1460
            else
1461
                edit_insert(s, ';')
×
1462
                LineEdit.check_show_hint(s)
×
1463
            end
1464
        end,
1465
        '?' => function (s::MIState,o...)
×
1466
            if isempty(s) || position(LineEdit.buffer(s)) == 0
×
1467
                buf = copy(LineEdit.buffer(s))
×
1468
                transition(s, help_mode) do
×
1469
                    LineEdit.state(s, help_mode).input_buffer = buf
×
1470
                end
1471
            else
1472
                edit_insert(s, '?')
×
1473
                LineEdit.check_show_hint(s)
×
1474
            end
1475
        end,
1476
        ']' => function (s::MIState,o...)
×
1477
            if isempty(s) || position(LineEdit.buffer(s)) == 0
×
1478
                buf = copy(LineEdit.buffer(s))
×
1479
                transition(s, dummy_pkg_mode) do
×
1480
                    LineEdit.state(s, dummy_pkg_mode).input_buffer = buf
×
1481
                end
1482
                # load Pkg on another thread if available so that typing in the dummy Pkg prompt
1483
                # isn't blocked, but instruct the main REPL task to do the transition via s.async_channel
1484
                t_replswitch = Threads.@spawn begin
×
1485
                    REPLExt = load_pkg()
×
1486
                    if REPLExt isa Module && isdefined(REPLExt, :PkgCompletionProvider)
×
1487
                        put!(s.async_channel,
×
1488
                            function (s::MIState)
×
1489
                                LineEdit.mode(s) === dummy_pkg_mode || return :ok
×
1490
                                for mode in repl.interface.modes
×
1491
                                    if mode isa LineEdit.Prompt && mode.complete isa REPLExt.PkgCompletionProvider
×
1492
                                        buf = copy(LineEdit.buffer(s))
×
1493
                                        transition(s, mode) do
×
1494
                                            LineEdit.state(s, mode).input_buffer = buf
×
1495
                                        end
1496
                                        if !isempty(s)
×
1497
                                            @invokelatest(LineEdit.check_show_hint(s))
×
1498
                                        end
1499
                                        break
×
1500
                                    end
1501
                                end
×
1502
                                return :ok
×
1503
                            end
1504
                        )
1505
                    end
1506
                end
1507
                Base.errormonitor(t_replswitch)
×
1508
            else
1509
                edit_insert(s, ']')
×
1510
                LineEdit.check_show_hint(s)
×
1511
            end
1512
        end,
1513

1514
        # Bracketed Paste Mode
1515
        "\e[200~" => (s::MIState,o...)->begin
×
1516
            input = LineEdit.bracketed_paste(s) # read directly from s until reaching the end-bracketed-paste marker
×
1517
            sbuffer = LineEdit.buffer(s)
×
1518
            curspos = position(sbuffer)
×
1519
            seek(sbuffer, 0)
×
1520
            shouldeval = (bytesavailable(sbuffer) == curspos && !occursin(UInt8('\n'), sbuffer))
×
1521
            seek(sbuffer, curspos)
×
1522
            if curspos == 0
×
1523
                # if pasting at the beginning, strip leading whitespace
1524
                input = lstrip(input)
×
1525
            end
1526
            if !shouldeval
×
1527
                # when pasting in the middle of input, just paste in place
1528
                # don't try to execute all the WIP, since that's rather confusing
1529
                # and is often ill-defined how it should behave
1530
                edit_insert(s, input)
×
1531
                return
×
1532
            end
1533
            LineEdit.push_undo(s)
×
1534
            edit_insert(sbuffer, input)
×
1535
            input = String(take!(sbuffer))
×
1536
            oldpos = firstindex(input)
×
1537
            firstline = true
×
1538
            isprompt_paste = false
×
1539
            curr_prompt_len = 0
×
1540
            pasting_help = false
×
1541

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

1654
        # Open the editor at the location of a stackframe or method
1655
        # This is accessing a contextual variable that gets set in
1656
        # the show_backtrace and show_method_table functions.
1657
        "^Q" => (s::MIState, o...) -> begin
×
1658
            linfos = repl.last_shown_line_infos
×
1659
            str = String(take!(LineEdit.buffer(s)))
×
1660
            n = tryparse(Int, str)
×
1661
            n === nothing && @goto writeback
×
1662
            if n <= 0 || n > length(linfos) || startswith(linfos[n][1], "REPL[")
×
1663
                @goto writeback
×
1664
            end
1665
            try
×
1666
                InteractiveUtils.edit(Base.fixup_stdlib_path(linfos[n][1]), linfos[n][2])
×
1667
            catch ex
1668
                ex isa ProcessFailedException || ex isa Base.IOError || ex isa SystemError || rethrow()
×
1669
                @info "edit failed" _exception=ex
×
1670
            end
1671
            LineEdit.refresh_line(s)
×
1672
            return
×
1673
            @label writeback
×
1674
            write(LineEdit.buffer(s), str)
×
1675
            return
×
1676
        end,
1677
    )
1678

1679
    prefix_prompt, prefix_keymap = LineEdit.setup_prefix_keymap(hp, julia_prompt)
×
1680

1681
    a = Dict{Any,Any}[skeymap, repl_keymap, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
×
1682
    prepend!(a, extra_repl_keymap)
×
1683

1684
    julia_prompt.keymap_dict = LineEdit.keymap(a)
×
1685

1686
    mk = mode_keymap(julia_prompt)
×
1687

1688
    b = Dict{Any,Any}[skeymap, mk, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
×
1689
    prepend!(b, extra_repl_keymap)
×
1690

1691
    shell_mode.keymap_dict = help_mode.keymap_dict = dummy_pkg_mode.keymap_dict = LineEdit.keymap(b)
×
1692

1693
    allprompts = LineEdit.TextInterface[julia_prompt, shell_mode, help_mode, dummy_pkg_mode, search_prompt, prefix_prompt]
×
1694
    return ModalInterface(allprompts)
×
1695
end
1696

1697
function run_frontend(repl::LineEditREPL, backend::REPLBackendRef)
×
1698
    repl.frontend_task = current_task()
×
1699
    d = REPLDisplay(repl)
×
1700
    dopushdisplay = repl.specialdisplay === nothing && !in(d,Base.Multimedia.displays)
×
1701
    dopushdisplay && pushdisplay(d)
×
1702
    if !isdefined(repl,:interface)
×
1703
        interface = repl.interface = setup_interface(repl)
×
1704
    else
1705
        interface = repl.interface
×
1706
    end
1707
    repl.backendref = backend
×
1708
    repl.mistate = LineEdit.init_state(terminal(repl), interface)
×
1709
    run_interface(terminal(repl), interface, repl.mistate)
×
1710
    # Terminate Backend
1711
    put!(backend.repl_channel, (nothing, -1))
×
1712
    dopushdisplay && popdisplay(d)
×
1713
    nothing
×
1714
end
1715

1716
## StreamREPL ##
1717

1718
mutable struct StreamREPL <: AbstractREPL
1719
    stream::IO
1720
    prompt_color::String
1721
    input_color::String
1722
    answer_color::String
1723
    waserror::Bool
1724
    frontend_task::Task
1725
    StreamREPL(stream,pc,ic,ac) = new(stream,pc,ic,ac,false)
×
1726
end
1727
StreamREPL(stream::IO) = StreamREPL(stream, Base.text_colors[:green], Base.input_color(), Base.answer_color())
×
1728
run_repl(stream::IO) = run_repl(StreamREPL(stream))
×
1729

1730
outstream(s::StreamREPL) = s.stream
×
1731
hascolor(s::StreamREPL) = get(s.stream, :color, false)::Bool
×
1732

1733
answer_color(r::LineEditREPL) = r.envcolors ? Base.answer_color() : r.answer_color
×
1734
answer_color(r::StreamREPL) = r.answer_color
×
1735
input_color(r::LineEditREPL) = r.envcolors ? Base.input_color() : r.input_color
×
1736
input_color(r::StreamREPL) = r.input_color
×
1737

1738
let matchend = Dict("\"" => r"\"", "\"\"\"" => r"\"\"\"", "'" => r"'",
1739
    "`" => r"`", "```" => r"```", "#" => r"$"m, "#=" => r"=#|#=")
1740
    global _rm_strings_and_comments
1741
    function _rm_strings_and_comments(code::Union{String,SubString{String}})
×
1742
        buf = IOBuffer(sizehint = sizeof(code))
×
1743
        pos = 1
×
1744
        while true
×
1745
            i = findnext(r"\"(?!\"\")|\"\"\"|'|`(?!``)|```|#(?!=)|#=", code, pos)
×
1746
            isnothing(i) && break
×
1747
            match = SubString(code, i)
×
1748
            j = findnext(matchend[match]::Regex, code, nextind(code, last(i)))
×
1749
            if match == "#=" # possibly nested
×
1750
                nested = 1
×
1751
                while j !== nothing
×
1752
                    nested += SubString(code, j) == "#=" ? +1 : -1
×
1753
                    iszero(nested) && break
×
1754
                    j = findnext(r"=#|#=", code, nextind(code, last(j)))
×
1755
                end
×
1756
            elseif match[1] != '#' # quote match: check non-escaped
×
1757
                while j !== nothing
×
1758
                    notbackslash = findprev(!=('\\'), code, prevind(code, first(j)))::Int
×
1759
                    isodd(first(j) - notbackslash) && break # not escaped
×
1760
                    j = findnext(matchend[match]::Regex, code, nextind(code, first(j)))
×
1761
                end
×
1762
            end
1763
            isnothing(j) && break
×
1764
            if match[1] == '#'
×
1765
                print(buf, SubString(code, pos, prevind(code, first(i))))
×
1766
            else
1767
                print(buf, SubString(code, pos, last(i)), ' ', SubString(code, j))
×
1768
            end
1769
            pos = nextind(code, last(j))
×
1770
        end
×
1771
        print(buf, SubString(code, pos, lastindex(code)))
×
1772
        return String(take!(buf))
×
1773
    end
1774
end
1775

1776
# heuristic function to decide if the presence of a semicolon
1777
# at the end of the expression was intended for suppressing output
1778
ends_with_semicolon(code::AbstractString) = ends_with_semicolon(String(code))
×
1779
ends_with_semicolon(code::Union{String,SubString{String}}) =
×
1780
    contains(_rm_strings_and_comments(code), r";\s*$")
×
1781

1782
function banner(io::IO = stdout; short = false)
×
1783
    if Base.GIT_VERSION_INFO.tagged_commit
×
1784
        commit_string = Base.TAGGED_RELEASE_BANNER
×
1785
    elseif isempty(Base.GIT_VERSION_INFO.commit)
×
1786
        commit_string = ""
×
1787
    else
1788
        days = Int(floor((ccall(:jl_clock_now, Float64, ()) - Base.GIT_VERSION_INFO.fork_master_timestamp) / (60 * 60 * 24)))
×
1789
        days = max(0, days)
×
1790
        unit = days == 1 ? "day" : "days"
×
1791
        distance = Base.GIT_VERSION_INFO.fork_master_distance
×
1792
        commit = Base.GIT_VERSION_INFO.commit_short
×
1793

1794
        if distance == 0
×
1795
            commit_string = "Commit $(commit) ($(days) $(unit) old master)"
×
1796
        else
1797
            branch = Base.GIT_VERSION_INFO.branch
×
1798
            commit_string = "$(branch)/$(commit) (fork: $(distance) commits, $(days) $(unit))"
×
1799
        end
1800
    end
1801

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

1804
    if get(io, :color, false)::Bool
×
1805
        c = Base.text_colors
×
1806
        tx = c[:normal] # text
×
1807
        jl = c[:normal] # julia
×
1808
        d1 = c[:bold] * c[:blue]    # first dot
×
1809
        d2 = c[:bold] * c[:red]     # second dot
×
1810
        d3 = c[:bold] * c[:green]   # third dot
×
1811
        d4 = c[:bold] * c[:magenta] # fourth dot
×
1812

1813
        if short
×
1814
            print(io,"""
×
1815
              $(d3)o$(tx)  | Version $(VERSION)$(commit_date)
1816
             $(d2)o$(tx) $(d4)o$(tx) | $(commit_string)
1817
            """)
1818
        else
1819
            print(io,"""               $(d3)_$(tx)
×
1820
               $(d1)_$(tx)       $(jl)_$(tx) $(d2)_$(d3)(_)$(d4)_$(tx)     |  Documentation: https://docs.julialang.org
1821
              $(d1)(_)$(jl)     | $(d2)(_)$(tx) $(d4)(_)$(tx)    |
1822
               $(jl)_ _   _| |_  __ _$(tx)   |  Type \"?\" for help, \"]?\" for Pkg help.
1823
              $(jl)| | | | | | |/ _` |$(tx)  |
1824
              $(jl)| | |_| | | | (_| |$(tx)  |  Version $(VERSION)$(commit_date)
1825
             $(jl)_/ |\\__'_|_|_|\\__'_|$(tx)  |  $(commit_string)
1826
            $(jl)|__/$(tx)                   |
1827

1828
            """)
1829
        end
1830
    else
1831
        if short
×
1832
            print(io,"""
×
1833
              o  |  Version $(VERSION)$(commit_date)
1834
             o o |  $(commit_string)
1835
            """)
1836
        else
1837
            print(io,"""
×
1838
                           _
1839
               _       _ _(_)_     |  Documentation: https://docs.julialang.org
1840
              (_)     | (_) (_)    |
1841
               _ _   _| |_  __ _   |  Type \"?\" for help, \"]?\" for Pkg help.
1842
              | | | | | | |/ _` |  |
1843
              | | |_| | | | (_| |  |  Version $(VERSION)$(commit_date)
1844
             _/ |\\__'_|_|_|\\__'_|  |  $(commit_string)
1845
            |__/                   |
1846

1847
            """)
1848
        end
1849
    end
1850
end
1851

1852
function run_frontend(repl::StreamREPL, backend::REPLBackendRef)
×
1853
    repl.frontend_task = current_task()
×
1854
    have_color = hascolor(repl)
×
1855
    banner(repl.stream)
×
1856
    d = REPLDisplay(repl)
×
1857
    dopushdisplay = !in(d,Base.Multimedia.displays)
×
1858
    dopushdisplay && pushdisplay(d)
×
1859
    while !eof(repl.stream)::Bool
×
1860
        if have_color
×
1861
            print(repl.stream,repl.prompt_color)
×
1862
        end
1863
        print(repl.stream, JULIA_PROMPT)
×
1864
        if have_color
×
1865
            print(repl.stream, input_color(repl))
×
1866
        end
1867
        line = readline(repl.stream, keep=true)
×
1868
        if !isempty(line)
×
1869
            ast = Base.parse_input_line(line)
×
1870
            if have_color
×
1871
                print(repl.stream, Base.color_normal)
×
1872
            end
1873
            response = eval_with_backend(ast, backend)
×
1874
            print_response(repl, response, !ends_with_semicolon(line), have_color)
×
1875
        end
1876
    end
×
1877
    # Terminate Backend
1878
    put!(backend.repl_channel, (nothing, -1))
×
1879
    dopushdisplay && popdisplay(d)
×
1880
    nothing
×
1881
end
1882

1883
module Numbered
1884

1885
using ..REPL
1886

1887
__current_ast_transforms() = Base.active_repl_backend !== nothing ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1888

1889
function repl_eval_counter(hp)
×
1890
    return length(hp.history) - hp.start_idx
×
1891
end
1892

1893
function out_transform(@nospecialize(x), n::Ref{Int})
×
1894
    return Expr(:toplevel, get_usings!([], x)..., quote
×
1895
        let __temp_val_a72df459 = $x
×
1896
            $capture_result($n, __temp_val_a72df459)
×
1897
            __temp_val_a72df459
×
1898
        end
1899
    end)
1900
end
1901

1902
function get_usings!(usings, ex)
×
1903
    ex isa Expr || return usings
×
1904
    # get all `using` and `import` statements which are at the top level
1905
    for (i, arg) in enumerate(ex.args)
×
1906
        if Base.isexpr(arg, :toplevel)
×
1907
            get_usings!(usings, arg)
×
1908
        elseif Base.isexpr(arg, [:using, :import])
×
1909
            push!(usings, popat!(ex.args, i))
×
1910
        end
1911
    end
×
1912
    return usings
×
1913
end
1914

1915
function create_global_out!(mod)
×
1916
    if !isdefinedglobal(mod, :Out)
×
1917
        out = Dict{Int, Any}()
×
1918
        @eval mod begin
×
1919
            const Out = $(out)
×
1920
            export Out
×
1921
        end
1922
        return out
×
1923
    end
1924
    return getglobal(mod, Out)
×
1925
end
1926

1927
function capture_result(n::Ref{Int}, @nospecialize(x))
×
1928
    n = n[]
×
1929
    mod = Base.MainInclude
×
1930
    # TODO: This invokelatest is only required due to backdated constants
1931
    # and should be removed after
1932
    out = isdefinedglobal(mod, :Out) ? invokelatest(getglobal, mod, :Out) : invokelatest(create_global_out!, mod)
×
1933
    if x !== out && x !== nothing # remove this?
×
1934
        out[n] = x
×
1935
    end
1936
    nothing
×
1937
end
1938

1939
function set_prompt(repl::LineEditREPL, n::Ref{Int})
×
1940
    julia_prompt = repl.interface.modes[1]
×
1941
    julia_prompt.prompt = function()
×
1942
        n[] = repl_eval_counter(julia_prompt.hist)+1
×
1943
        string("In [", n[], "]: ")
×
1944
    end
1945
    nothing
×
1946
end
1947

1948
function set_output_prefix(repl::LineEditREPL, n::Ref{Int})
×
1949
    julia_prompt = repl.interface.modes[1]
×
1950
    if REPL.hascolor(repl)
×
1951
        julia_prompt.output_prefix_prefix = Base.text_colors[:red]
×
1952
    end
1953
    julia_prompt.output_prefix = () -> string("Out[", n[], "]: ")
×
1954
    nothing
×
1955
end
1956

1957
function __current_ast_transforms(backend)
×
1958
    if backend === nothing
×
1959
        Base.active_repl_backend !== nothing ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1960
    else
1961
        backend.ast_transforms
×
1962
    end
1963
end
1964

1965
function numbered_prompt!(repl::LineEditREPL=Base.active_repl::LineEditREPL, backend=nothing)
×
1966
    n = Ref{Int}(0)
×
1967
    set_prompt(repl, n)
×
1968
    set_output_prefix(repl, n)
×
1969
    push!(__current_ast_transforms(backend), @nospecialize(ast) -> out_transform(ast, n))
×
1970
    return
×
1971
end
1972

1973
"""
1974
    Out[n]
1975

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

1979
See also [`ans`](@ref).
1980
"""
1981
Base.MainInclude.Out
1982

1983
end
1984

1985
import .Numbered.numbered_prompt!
1986

1987
# this assignment won't survive precompilation,
1988
# but will stick if REPL is baked into a sysimg.
1989
# Needs to occur after this module is finished.
1990
Base.REPL_MODULE_REF[] = REPL
1991

1992
if Base.generating_output()
1993
    include("precompile.jl")
1994
end
1995

1996
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