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

JuliaLang / julia / #38055

29 Apr 2025 05:47AM UTC coverage: 25.833% (+0.1%) from 25.721%
#38055

push

local

web-flow
Change `mod(x, Inf)` semantics to align with C when `x` is finite (#47102)

fix #46694

---------

Co-authored-by: Dilum Aluthge <dilum@aluthge.com>
Co-authored-by: Lilith Orion Hafner <lilithhafner@gmail.com>
Co-authored-by: Oscar Smith <oscardssmith@gmail.com>
Co-authored-by: Mosè Giordano <765740+giordano@users.noreply.github.com>

1 of 2 new or added lines in 1 file covered. (50.0%)

999 existing lines in 7 files now uncovered.

12948 of 50122 relevant lines covered (25.83%)

1075884.19 hits per line

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

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

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

6
Example minimal code
7

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

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

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

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

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

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

52
public TerminalMenus
53

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

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

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

66
abstract type AbstractREPL end
67

68
include("options.jl")
69

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

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

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

97
include("Pkg_beforeload.jl")
98

99
@nospecialize # use only declared type signatures
100

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

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

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

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

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

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

141
"""
142
    softscope(ex)
143

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

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

168
function print_qualified_access_warning(mod::Module, owner::Module, name::Symbol)
×
169
    @warn string(name, " is defined in ", owner, " and is not public in ", mod) maxlog = 1 _id = string("repl-warning-", mod, "-", owner, "-", name) _line = nothing _file = nothing _module = nothing
×
170
end
171

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

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

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

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

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

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

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

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

268
    return nothing
×
269
end
270

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

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

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

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

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

306
function toplevel_eval_with_hooks(mod::Module, @nospecialize(ast), toplevel_file=Ref{Ptr{UInt8}}(Base.unsafe_convert(Ptr{UInt8}, :REPL)), toplevel_line=Ref{Cint}(1))
×
307
    if !isexpr(ast, :toplevel)
×
308
        ast = invokelatest(__repl_entry_lower_with_loc, mod, ast, toplevel_file, toplevel_line)
×
309
        check_for_missing_packages_and_run_hooks(ast)
×
310
        return invokelatest(__repl_entry_eval_expanded_with_loc, mod, ast, toplevel_file, toplevel_line)
×
311
    end
312
    local value=nothing
×
313
    for i = 1:length(ast.args)
×
314
        value = toplevel_eval_with_hooks(mod, ast.args[i], toplevel_file, toplevel_line)
×
315
    end
×
316
    return value
×
317
end
318

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

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

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

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

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

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

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

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

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

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

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

465
SHOW_MAXIMUM_BYTES::Int = 1_048_576
466

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

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

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

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

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

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

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

511
struct REPLDisplay{Repl<:AbstractREPL} <: AbstractDisplay
512
    repl::Repl
513
end
514

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

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

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

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

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

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

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

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

634

635

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

640
    Main function to start the REPL
641

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

668
## BasicREPL ##
669

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

677
outstream(r::BasicREPL) = r.terminal
×
678
hascolor(r::BasicREPL) = hascolor(r.terminal)
×
679

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

727
## LineEditREPL ##
728

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

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

777
mutable struct REPLCompletionProvider <: CompletionProvider
778
    modifiers::LineEdit.Modifiers
779
end
780
REPLCompletionProvider() = REPLCompletionProvider(LineEdit.Modifiers())
×
781

782
mutable struct ShellCompletionProvider <: CompletionProvider end
783
struct LatexCompletions <: CompletionProvider end
784

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

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

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

793
"""
794
    activate(mod::Module=Main)
795

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

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

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

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

UNCOV
820
function complete_line(c::ShellCompletionProvider, s::PromptState; hint::Bool=false)
×
UNCOV
821
    full = LineEdit.input_string(s)
×
822
    ret, range, should_complete = shell_completions(full, thisind(full, position(s)), hint)
×
823
    range = to_region(full, range)
×
824
    return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), range, should_complete
×
825
end
826

UNCOV
827
function complete_line(c::LatexCompletions, s; hint::Bool=false)
×
UNCOV
828
    full = LineEdit.input_string(s)::String
×
829
    ret, range, should_complete = bslash_completions(full, thisind(full, position(s)), hint)[2]
×
830
    range = to_region(full, range)
×
831
    return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), range, should_complete
×
832
end
833

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

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

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

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

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

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

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

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

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

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

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

990
    return :ok
×
991
end
992

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

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

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

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

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

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

1092
function history_search(hist::REPLHistoryProvider, query_buffer::IOBuffer, response_buffer::IOBuffer,
×
1093
                        backwards::Bool=false, skip_current::Bool=false)
1094

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

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

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

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

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

1142
    return false
×
1143
end
1144

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

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

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

1163
backend(r::AbstractREPL) = hasproperty(r, :backendref) ? r.backendref : nothing
×
1164

1165

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

1184

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

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

1215
function prepare_next(repl::LineEditREPL)
×
1216
    println(terminal(repl))
×
1217
end
1218

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

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

1244
const JL_PROMPT_PASTE = Ref(true)
1245
enable_promptpaste(v::Bool) = JL_PROMPT_PASTE[] = v
×
1246

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

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

1263

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

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

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

1295
    ############################### Stage I ################################
1296

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

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

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

1321

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

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

1367

1368
    ################################# Stage II #############################
1369

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

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

1402

1403
    search_prompt, skeymap = LineEdit.setup_search_keymap(hp)
×
1404
    search_prompt.complete = LatexCompletions()
×
1405

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

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

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

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

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

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

1642
    prefix_prompt, prefix_keymap = LineEdit.setup_prefix_keymap(hp, julia_prompt)
×
1643

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

1647
    julia_prompt.keymap_dict = LineEdit.keymap(a)
×
1648

1649
    mk = mode_keymap(julia_prompt)
×
1650

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

UNCOV
1654
    shell_mode.keymap_dict = help_mode.keymap_dict = dummy_pkg_mode.keymap_dict = LineEdit.keymap(b)
×
1655

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

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

1679
## StreamREPL ##
1680

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

1693
outstream(s::StreamREPL) = s.stream
×
1694
hascolor(s::StreamREPL) = get(s.stream, :color, false)::Bool
×
1695

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

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

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

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

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

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

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

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

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

1810
            """)
1811
        end
1812
    end
1813
end
1814

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

1846
module Numbered
1847

1848
using ..REPL
1849

UNCOV
1850
__current_ast_transforms() = Base.active_repl_backend !== nothing ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1851

1852
function repl_eval_counter(hp)
×
1853
    return length(hp.history) - hp.start_idx
×
1854
end
1855

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

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

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

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

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

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

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

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

1936
"""
1937
    Out[n]
1938

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

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

1946
end
1947

1948
import .Numbered.numbered_prompt!
1949

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

1955
if Base.generating_output()
1956
    include("precompile.jl")
1957
end
1958

1959
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