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

JuliaLang / julia / #37745

11 Apr 2024 03:54AM UTC coverage: 87.241% (+5.8%) from 81.402%
#37745

push

local

web-flow
Fix comparison base for line table compression (#54032)

I'm not entirely sure what the original intent of this statement was,
but the effect ends up being that some codeloc entries end up negative
in the compressed representation, but the code always assumes unsigned
integers, so things roundtripped badly, leading to badly corrupted stack
traces. I guess this might have been a rebase mistake,
since the same line exists (correctly) a few lines prior. Fixes #54031.

75950 of 87058 relevant lines covered (87.24%)

15852930.37 hits per line

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

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

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

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

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

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

112
import Base:
113
    AbstractDisplay,
114
    display,
115
    show,
116
    AnyDict,
117
    ==
118

119
_displaysize(io::IO) = displaysize(io)::Tuple{Int,Int}
32✔
120

121
include("Terminals.jl")
122
using .Terminals
123

124
abstract type AbstractREPL end
125

126
include("options.jl")
127

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

148
include("REPLCompletions.jl")
149
using .REPLCompletions
150

151
include("TerminalMenus/TerminalMenus.jl")
152
include("docview.jl")
153

154
@nospecialize # use only declared type signatures
155

156
answer_color(::AbstractREPL) = ""
×
157

158
const JULIA_PROMPT = "julia> "
159
const PKG_PROMPT = "pkg> "
160
const SHELL_PROMPT = "shell> "
161
const HELP_PROMPT = "help?> "
162

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

175
    REPLBackend(repl_channel, response_channel, in_eval, ast_transforms=copy(repl_ast_transforms)) =
52✔
176
        new(repl_channel, response_channel, in_eval, ast_transforms)
177
end
178
REPLBackend() = REPLBackend(Channel(1), Channel(1), false)
26✔
179

180
"""
181
    softscope(ex)
182

183
Return a modified version of the parsed expression `ex` that uses
184
the REPL's "soft" scoping rules for global syntax blocks.
185
"""
186
function softscope(@nospecialize ex)
376✔
187
    if ex isa Expr
376✔
188
        h = ex.head
241✔
189
        if h === :toplevel
241✔
190
            ex′ = Expr(h)
137✔
191
            map!(softscope, resize!(ex′.args, length(ex.args)), ex.args)
137✔
192
            return ex′
137✔
193
        elseif h in (:meta, :import, :using, :export, :module, :error, :incomplete, :thunk)
104✔
194
            return ex
2✔
195
        elseif h === :global && all(x->isa(x, Symbol), ex.args)
103✔
196
            return ex
1✔
197
        else
198
            return Expr(:block, Expr(:softscope, true), ex)
101✔
199
        end
200
    end
201
    return ex
135✔
202
end
203

204
# Temporary alias until Documenter updates
205
const softscope! = softscope
206

207
const repl_ast_transforms = Any[softscope] # defaults for new REPL backends
208

209
# Allows an external package to add hooks into the code loading.
210
# The hook should take a Vector{Symbol} of package names and
211
# return true if all packages could be installed, false if not
212
# to e.g. install packages on demand
213
const install_packages_hooks = Any[]
214

215
# N.B.: Any functions starting with __repl_entry cut off backtraces when printing in the REPL.
216
# We need to do this for both the actual eval and macroexpand, since the latter can cause custom macro
217
# code to run (and error).
218
__repl_entry_lower_with_loc(mod::Module, @nospecialize(ast), toplevel_file::Ref{Ptr{UInt8}}, toplevel_line::Ref{Cint}) =
217✔
219
    ccall(:jl_expand_with_loc, Any, (Any, Any, Ptr{UInt8}, Cint), ast, mod, toplevel_file[], toplevel_line[])
220
__repl_entry_eval_expanded_with_loc(mod::Module, @nospecialize(ast), toplevel_file::Ref{Ptr{UInt8}}, toplevel_line::Ref{Cint}) =
216✔
221
    ccall(:jl_toplevel_eval_flex, Any, (Any, Any, Cint, Cint, Ptr{Ptr{UInt8}}, Ptr{Cint}), mod, ast, 1, 1, toplevel_file, toplevel_line)
222

223
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))
345✔
224
    if !isexpr(ast, :toplevel)
453✔
225
        ast = __repl_entry_lower_with_loc(mod, ast, toplevel_file, toplevel_line)
217✔
226
        check_for_missing_packages_and_run_hooks(ast)
217✔
227
        return __repl_entry_eval_expanded_with_loc(mod, ast, toplevel_file, toplevel_line)
216✔
228
    end
229
    local value=nothing
128✔
230
    for i = 1:length(ast.args)
128✔
231
        value = toplevel_eval_with_hooks(mod, ast.args[i], toplevel_file, toplevel_line)
237✔
232
    end
339✔
233
    return value
121✔
234
end
235

236
function eval_user_input(@nospecialize(ast), backend::REPLBackend, mod::Module)
108✔
237
    lasterr = nothing
×
238
    Base.sigatomic_begin()
108✔
239
    while true
113✔
240
        try
113✔
241
            Base.sigatomic_end()
113✔
242
            if lasterr !== nothing
113✔
243
                put!(backend.response_channel, Pair{Any, Bool}(lasterr, true))
5✔
244
            else
245
                backend.in_eval = true
108✔
246
                for xf in backend.ast_transforms
108✔
247
                    ast = Base.invokelatest(xf, ast)
126✔
248
                end
126✔
249
                value = toplevel_eval_with_hooks(mod, ast)
108✔
250
                backend.in_eval = false
102✔
251
                setglobal!(Base.MainInclude, :ans, value)
102✔
252
                put!(backend.response_channel, Pair{Any, Bool}(value, false))
102✔
253
            end
254
            break
112✔
255
        catch err
256
            if lasterr !== nothing
5✔
257
                println("SYSTEM ERROR: Failed to report error to REPL frontend")
×
258
                println(err)
×
259
            end
260
            lasterr = current_exceptions()
5✔
261
        end
262
    end
5✔
263
    Base.sigatomic_end()
107✔
264
    nothing
265
end
266

267
function check_for_missing_packages_and_run_hooks(ast)
217✔
268
    isa(ast, Expr) || return
327✔
269
    mods = modules_to_be_loaded(ast)
107✔
270
    filter!(mod -> isnothing(Base.identify_package(String(mod))), mods) # keep missing modules
112✔
271
    if !isempty(mods)
107✔
272
        isempty(install_packages_hooks) && Base.require_stdlib(Base.PkgId(Base.UUID("44cfe95a-1eb2-52ea-b672-e2afdf69b78f"), "Pkg"))
1✔
273
        for f in install_packages_hooks
1✔
274
            Base.invokelatest(f, mods) && return
1✔
275
        end
×
276
    end
277
end
278

279
function _modules_to_be_loaded!(ast::Expr, mods::Vector{Symbol})
664✔
280
    ast.head === :quote && return mods # don't search if it's not going to be run during this eval
664✔
281
    if ast.head === :using || ast.head === :import
1,312✔
282
        for arg in ast.args
20✔
283
            arg = arg::Expr
24✔
284
            arg1 = first(arg.args)
24✔
285
            if arg1 isa Symbol # i.e. `Foo`
24✔
286
                if arg1 != :. # don't include local imports
21✔
287
                    push!(mods, arg1)
20✔
288
                end
289
            else # i.e. `Foo: bar`
290
                push!(mods, first((arg1::Expr).args))
3✔
291
            end
292
        end
24✔
293
    end
294
    if ast.head !== :thunk
664✔
295
        for arg in ast.args
554✔
296
            if isexpr(arg, (:block, :if, :using, :import))
1,338✔
297
                _modules_to_be_loaded!(arg, mods)
33✔
298
            end
299
        end
1,338✔
300
    else
301
        code = ast.args[1]
110✔
302
        for arg in code.code
110✔
303
            isa(arg, Expr) || continue
882✔
304
            _modules_to_be_loaded!(arg, mods)
502✔
305
        end
882✔
306
    end
307
end
308

309
function modules_to_be_loaded(ast::Expr, mods::Vector{Symbol} = Symbol[])
22✔
310
    _modules_to_be_loaded!(ast, mods)
258✔
311
    filter!(mod::Symbol -> !in(mod, (:Base, :Main, :Core)), mods) # Exclude special non-package modules
152✔
312
    return unique(mods)
129✔
313
end
314

315
"""
316
    start_repl_backend(repl_channel::Channel, response_channel::Channel)
317

318
    Starts loop for REPL backend
319
    Returns a REPLBackend with backend_task assigned
320

321
    Deprecated since sync / async behavior cannot be selected
322
"""
323
function start_repl_backend(repl_channel::Channel{Any}, response_channel::Channel{Any}
×
324
                            ; get_module::Function = ()->Main)
325
    # Maintain legacy behavior of asynchronous backend
326
    backend = REPLBackend(repl_channel, response_channel, false)
×
327
    # Assignment will be made twice, but will be immediately available
328
    backend.backend_task = @async start_repl_backend(backend; get_module)
×
329
    return backend
×
330
end
331

332
"""
333
    start_repl_backend(backend::REPLBackend)
334

335
    Call directly to run backend loop on current Task.
336
    Use @async for run backend on new Task.
337

338
    Does not return backend until loop is finished.
339
"""
340
function start_repl_backend(backend::REPLBackend,  @nospecialize(consumer = x -> nothing); get_module::Function = ()->Main)
56✔
341
    backend.backend_task = Base.current_task()
26✔
342
    consumer(backend)
26✔
343
    repl_backend_loop(backend, get_module)
26✔
344
    return backend
25✔
345
end
346

347
function repl_backend_loop(backend::REPLBackend, get_module::Function)
26✔
348
    # include looks at this to determine the relative include path
349
    # nothing means cwd
350
    while true
×
351
        tls = task_local_storage()
133✔
352
        tls[:SOURCE_PATH] = nothing
133✔
353
        ast, show_value = take!(backend.repl_channel)
133✔
354
        if show_value == -1
133✔
355
            # exit flag
356
            break
25✔
357
        end
358
        eval_user_input(ast, backend, get_module())
108✔
359
    end
107✔
360
    return nothing
25✔
361
end
362

363
struct REPLDisplay{Repl<:AbstractREPL} <: AbstractDisplay
364
    repl::Repl
35✔
365
end
366

367
function display(d::REPLDisplay, mime::MIME"text/plain", x)
63✔
368
    x = Ref{Any}(x)
63✔
369
    with_repl_linfo(d.repl) do io
63✔
370
        io = IOContext(io, :limit => true, :module => active_module(d)::Module)
123✔
371
        if d.repl isa LineEditREPL
3✔
372
            mistate = d.repl.mistate
60✔
373
            mode = LineEdit.mode(mistate)
60✔
374
            if mode isa LineEdit.Prompt
60✔
375
                LineEdit.write_output_prefix(io, mode, get(io, :color, false)::Bool)
240✔
376
            end
377
        end
378
        get(io, :color, false)::Bool && write(io, answer_color(d.repl))
69✔
379
        if isdefined(d.repl, :options) && isdefined(d.repl.options, :iocontext)
3✔
380
            # this can override the :limit property set initially
381
            io = foldl(IOContext, d.repl.options.iocontext, init=io)
60✔
382
        end
383
        show(io, mime, x[])
63✔
384
        println(io)
62✔
385
    end
386
    return nothing
62✔
387
end
388
display(d::REPLDisplay, x) = display(d, MIME("text/plain"), x)
63✔
389

390
function print_response(repl::AbstractREPL, response, show_value::Bool, have_color::Bool)
104✔
391
    repl.waserror = response[2]
104✔
392
    with_repl_linfo(repl) do io
104✔
393
        io = IOContext(io, :module => active_module(repl)::Module)
205✔
394
        print_response(io, response, show_value, have_color, specialdisplay(repl))
104✔
395
    end
396
    return nothing
104✔
397
end
398

399
function repl_display_error(errio::IO, @nospecialize errval)
9✔
400
    # this will be set to true if types in the stacktrace are truncated
401
    limitflag = Ref(false)
9✔
402
    errio = IOContext(errio, :stacktrace_types_limited => limitflag)
9✔
403
    Base.invokelatest(Base.display_error, errio, errval)
9✔
404
    if limitflag[]
7✔
405
        print(errio, "Some type information was truncated. Use `show(err)` to see complete types.")
2✔
406
        println(errio)
2✔
407
    end
408
    return nothing
7✔
409
end
410

411
function print_response(errio::IO, response, show_value::Bool, have_color::Bool, specialdisplay::Union{AbstractDisplay,Nothing}=nothing)
105✔
412
    Base.sigatomic_begin()
105✔
413
    val, iserr = response
105✔
414
    while true
106✔
415
        try
106✔
416
            Base.sigatomic_end()
106✔
417
            if iserr
106✔
418
                val = Base.scrub_repl_backtrace(val)
8✔
419
                Base.istrivialerror(val) || setglobal!(Base.MainInclude, :err, val)
13✔
420
                repl_display_error(errio, val)
8✔
421
            else
422
                if val !== nothing && show_value
98✔
423
                    try
63✔
424
                        if specialdisplay === nothing
63✔
425
                            Base.invokelatest(display, val)
49✔
426
                        else
427
                            Base.invokelatest(display, specialdisplay, val)
62✔
428
                        end
429
                    catch
430
                        println(errio, "Error showing value of type ", typeof(val), ":")
1✔
431
                        rethrow()
1✔
432
                    end
433
                end
434
            end
435
            break
106✔
436
        catch ex
437
            if iserr
2✔
438
                println(errio) # an error during printing is likely to leave us mid-line
1✔
439
                println(errio, "SYSTEM (REPL): showing an error caused an error")
1✔
440
                try
1✔
441
                    excs = Base.scrub_repl_backtrace(current_exceptions())
1✔
442
                    setglobal!(Base.MainInclude, :err, excs)
1✔
443
                    repl_display_error(errio, excs)
2✔
444
                catch e
445
                    # at this point, only print the name of the type as a Symbol to
446
                    # minimize the possibility of further errors.
447
                    println(errio)
1✔
448
                    println(errio, "SYSTEM (REPL): caught exception of type ", typeof(e).name.name,
1✔
449
                            " while trying to handle a nested exception; giving up")
450
                end
451
                break
1✔
452
            end
453
            val = current_exceptions()
1✔
454
            iserr = true
1✔
455
        end
456
    end
1✔
457
    Base.sigatomic_end()
105✔
458
    nothing
459
end
460

461
# A reference to a backend that is not mutable
462
struct REPLBackendRef
463
    repl_channel::Channel{Any}
24✔
464
    response_channel::Channel{Any}
465
end
466
REPLBackendRef(backend::REPLBackend) = REPLBackendRef(backend.repl_channel, backend.response_channel)
24✔
467

468
function destroy(ref::REPLBackendRef, state::Task)
23✔
469
    if istaskfailed(state)
23✔
470
        close(ref.repl_channel, TaskFailedException(state))
×
471
        close(ref.response_channel, TaskFailedException(state))
×
472
    end
473
    close(ref.repl_channel)
23✔
474
    close(ref.response_channel)
23✔
475
end
476

477
"""
478
    run_repl(repl::AbstractREPL)
479
    run_repl(repl, consumer = backend->nothing; backend_on_current_task = true)
480

481
    Main function to start the REPL
482

483
    consumer is an optional function that takes a REPLBackend as an argument
484
"""
485
function run_repl(repl::AbstractREPL, @nospecialize(consumer = x -> nothing); backend_on_current_task::Bool = true, backend = REPLBackend())
71✔
486
    backend_ref = REPLBackendRef(backend)
24✔
487
    cleanup = @task try
47✔
488
            destroy(backend_ref, t)
23✔
489
        catch e
490
            Core.print(Core.stderr, "\nINTERNAL ERROR: ")
×
491
            Core.println(Core.stderr, e)
×
492
            Core.println(Core.stderr, catch_backtrace())
×
493
        end
494
    get_module = () -> active_module(repl)
128✔
495
    if backend_on_current_task
24✔
496
        t = @async run_frontend(repl, backend_ref)
48✔
497
        errormonitor(t)
24✔
498
        Base._wait2(t, cleanup)
24✔
499
        start_repl_backend(backend, consumer; get_module)
24✔
500
    else
501
        t = @async start_repl_backend(backend, consumer; get_module)
×
502
        errormonitor(t)
×
503
        Base._wait2(t, cleanup)
×
504
        run_frontend(repl, backend_ref)
×
505
    end
506
    return backend
23✔
507
end
508

509
## BasicREPL ##
510

511
mutable struct BasicREPL <: AbstractREPL
512
    terminal::TextTerminal
513
    waserror::Bool
514
    frontend_task::Task
515
    BasicREPL(t) = new(t, false)
3✔
516
end
517

518
outstream(r::BasicREPL) = r.terminal
6✔
519
hascolor(r::BasicREPL) = hascolor(r.terminal)
×
520

521
function run_frontend(repl::BasicREPL, backend::REPLBackendRef)
3✔
522
    repl.frontend_task = current_task()
3✔
523
    d = REPLDisplay(repl)
3✔
524
    dopushdisplay = !in(d,Base.Multimedia.displays)
6✔
525
    dopushdisplay && pushdisplay(d)
3✔
526
    hit_eof = false
3✔
527
    while true
6✔
528
        Base.reseteof(repl.terminal)
6✔
529
        write(repl.terminal, JULIA_PROMPT)
6✔
530
        line = ""
6✔
531
        ast = nothing
6✔
532
        interrupted = false
6✔
533
        while true
6✔
534
            try
6✔
535
                line *= readline(repl.terminal, keep=true)
6✔
536
            catch e
537
                if isa(e,InterruptException)
×
538
                    try # raise the debugger if present
×
539
                        ccall(:jl_raise_debugger, Int, ())
×
540
                    catch
×
541
                    end
542
                    line = ""
×
543
                    interrupted = true
×
544
                    break
×
545
                elseif isa(e,EOFError)
×
546
                    hit_eof = true
×
547
                    break
×
548
                else
549
                    rethrow()
×
550
                end
551
            end
552
            ast = Base.parse_input_line(line)
12✔
553
            (isa(ast,Expr) && ast.head === :incomplete) || break
12✔
554
        end
×
555
        if !isempty(line)
6✔
556
            response = eval_with_backend(ast, backend)
4✔
557
            print_response(repl, response, !ends_with_semicolon(line), false)
3✔
558
        end
559
        write(repl.terminal, '\n')
5✔
560
        ((!interrupted && isempty(line)) || hit_eof) && break
8✔
561
    end
3✔
562
    # terminate backend
563
    put!(backend.repl_channel, (nothing, -1))
2✔
564
    dopushdisplay && popdisplay(d)
2✔
565
    nothing
566
end
567

568
## LineEditREPL ##
569

570
mutable struct LineEditREPL <: AbstractREPL
571
    t::TextTerminal
572
    hascolor::Bool
573
    prompt_color::String
574
    input_color::String
575
    answer_color::String
576
    shell_color::String
577
    help_color::String
578
    history_file::Bool
579
    in_shell::Bool
580
    in_help::Bool
581
    envcolors::Bool
582
    waserror::Bool
583
    specialdisplay::Union{Nothing,AbstractDisplay}
584
    options::Options
585
    mistate::Union{MIState,Nothing}
586
    last_shown_line_infos::Vector{Tuple{String,Int}}
587
    interface::ModalInterface
588
    backendref::REPLBackendRef
589
    frontend_task::Task
590
    function LineEditREPL(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,history_file,in_shell,in_help,envcolors)
591
        opts = Options()
28✔
592
        opts.hascolor = hascolor
28✔
593
        if !hascolor
28✔
594
            opts.beep_colors = [""]
×
595
        end
596
        new(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,history_file,in_shell,
28✔
597
            in_help,envcolors,false,nothing, opts, nothing, Tuple{String,Int}[])
598
    end
599
end
600
outstream(r::LineEditREPL) = (t = r.t; t isa TTYTerminal ? t.out_stream : t)
171✔
601
specialdisplay(r::LineEditREPL) = r.specialdisplay
101✔
602
specialdisplay(r::AbstractREPL) = nothing
×
603
terminal(r::LineEditREPL) = r.t
158✔
604
hascolor(r::LineEditREPL) = r.hascolor
203✔
605

606
LineEditREPL(t::TextTerminal, hascolor::Bool, envcolors::Bool=false) =
56✔
607
    LineEditREPL(t, hascolor,
608
        hascolor ? Base.text_colors[:green] : "",
609
        hascolor ? Base.input_color() : "",
610
        hascolor ? Base.answer_color() : "",
611
        hascolor ? Base.text_colors[:red] : "",
612
        hascolor ? Base.text_colors[:yellow] : "",
613
        false, false, false, envcolors
614
    )
615

616
mutable struct REPLCompletionProvider <: CompletionProvider
617
    modifiers::LineEdit.Modifiers
25✔
618
end
619
REPLCompletionProvider() = REPLCompletionProvider(LineEdit.Modifiers())
25✔
620

621
mutable struct ShellCompletionProvider <: CompletionProvider end
25✔
622
struct LatexCompletions <: CompletionProvider end
623

624
function active_module() # this method is also called from Base
22,759✔
625
    isdefined(Base, :active_repl) || return Main
45,518✔
626
    return active_module(Base.active_repl::AbstractREPL)
×
627
end
628
active_module((; mistate)::LineEditREPL) = mistate === nothing ? Main : mistate.active_module
4,841✔
629
active_module(::AbstractREPL) = Main
10✔
630
active_module(d::REPLDisplay) = active_module(d.repl)
123✔
631

632
setmodifiers!(c::CompletionProvider, m::LineEdit.Modifiers) = nothing
×
633

634
setmodifiers!(c::REPLCompletionProvider, m::LineEdit.Modifiers) = c.modifiers = m
×
635

636
"""
637
    activate(mod::Module=Main)
638

639
Set `mod` as the default contextual module in the REPL,
640
both for evaluating expressions and printing them.
641
"""
642
function activate(mod::Module=Main)
×
643
    mistate = (Base.active_repl::LineEditREPL).mistate
×
644
    mistate === nothing && return nothing
×
645
    mistate.active_module = mod
×
646
    Base.load_InteractiveUtils(mod)
×
647
    return nothing
×
648
end
649

650
beforecursor(buf::IOBuffer) = String(buf.data[1:buf.ptr-1])
4,432✔
651

652
function complete_line(c::REPLCompletionProvider, s::PromptState, mod::Module)
1,762✔
653
    partial = beforecursor(s.input_buffer)
3,524✔
654
    full = LineEdit.input_string(s)
3,524✔
655
    ret, range, should_complete = completions(full, lastindex(partial), mod, c.modifiers.shift)
3,524✔
656
    c.modifiers = LineEdit.Modifiers()
1,762✔
657
    return unique!(map(completion_text, ret)), partial[range], should_complete
1,762✔
658
end
659

660
function complete_line(c::ShellCompletionProvider, s::PromptState)
422✔
661
    # First parse everything up to the current position
662
    partial = beforecursor(s.input_buffer)
844✔
663
    full = LineEdit.input_string(s)
844✔
664
    ret, range, should_complete = shell_completions(full, lastindex(partial))
844✔
665
    return unique!(map(completion_text, ret)), partial[range], should_complete
422✔
666
end
667

668
function complete_line(c::LatexCompletions, s)
×
669
    partial = beforecursor(LineEdit.buffer(s))
×
670
    full = LineEdit.input_string(s)::String
×
671
    ret, range, should_complete = bslash_completions(full, lastindex(partial))[2]
×
672
    return unique!(map(completion_text, ret)), partial[range], should_complete
×
673
end
674

675
with_repl_linfo(f, repl) = f(outstream(repl))
6✔
676
function with_repl_linfo(f, repl::LineEditREPL)
161✔
677
    linfos = Tuple{String,Int}[]
161✔
678
    io = IOContext(outstream(repl), :last_shown_line_infos => linfos)
161✔
679
    f(io)
161✔
680
    if !isempty(linfos)
160✔
681
        repl.last_shown_line_infos = linfos
6✔
682
    end
683
    nothing
684
end
685

686
mutable struct REPLHistoryProvider <: HistoryProvider
687
    history::Vector{String}
30✔
688
    file_path::String
689
    history_file::Union{Nothing,IO}
690
    start_idx::Int
691
    cur_idx::Int
692
    last_idx::Int
693
    last_buffer::IOBuffer
694
    last_mode::Union{Nothing,Prompt}
695
    mode_mapping::Dict{Symbol,Prompt}
696
    modes::Vector{Symbol}
697
end
698
REPLHistoryProvider(mode_mapping::Dict{Symbol}) =
30✔
699
    REPLHistoryProvider(String[], "", nothing, 0, 0, -1, IOBuffer(),
700
                        nothing, mode_mapping, UInt8[])
701

702
invalid_history_message(path::String) = """
×
703
Invalid history file ($path) format:
704
If you have a history file left over from an older version of Julia,
705
try renaming or deleting it.
706
Invalid character: """
707

708
munged_history_message(path::String) = """
×
709
Invalid history file ($path) format:
710
An editor may have converted tabs to spaces at line """
711

712
function hist_open_file(hp::REPLHistoryProvider)
713
    f = open(hp.file_path, read=true, write=true, create=true)
4✔
714
    hp.history_file = f
4✔
715
    seekend(f)
4✔
716
end
717

718
function hist_from_file(hp::REPLHistoryProvider, path::String)
8✔
719
    getline(lines, i) = i > length(lines) ? "" : lines[i]
276✔
720
    file_lines = readlines(path)
8✔
721
    countlines = 0
×
722
    while true
42✔
723
        # First parse the metadata that starts with '#' in particular the REPL mode
724
        countlines += 1
42✔
725
        line = getline(file_lines, countlines)
76✔
726
        mode = :julia
×
727
        isempty(line) && break
42✔
728
        line[1] != '#' &&
68✔
729
            error(invalid_history_message(path), repr(line[1]), " at line ", countlines)
730
        while !isempty(line)
102✔
731
            startswith(line, '#') || break
204✔
732
            if startswith(line, "# mode: ")
68✔
733
                mode = Symbol(SubString(line, 9))
68✔
734
            end
735
            countlines += 1
68✔
736
            line = getline(file_lines, countlines)
136✔
737
        end
68✔
738
        isempty(line) && break
34✔
739

740
        # Now parse the code for the current REPL mode
741
        line[1] == ' '  &&
68✔
742
            error(munged_history_message(path), countlines)
743
        line[1] != '\t' &&
68✔
744
            error(invalid_history_message(path), repr(line[1]), " at line ", countlines)
745
        lines = String[]
34✔
746
        while !isempty(line)
34✔
747
            push!(lines, chomp(SubString(line, 2)))
68✔
748
            next_line = getline(file_lines, countlines+1)
64✔
749
            isempty(next_line) && break
34✔
750
            first(next_line) == ' '  && error(munged_history_message(path), countlines)
60✔
751
            # A line not starting with a tab means we are done with code for this entry
752
            first(next_line) != '\t' && break
60✔
753
            countlines += 1
×
754
            line = getline(file_lines, countlines)
×
755
        end
×
756
        push!(hp.modes, mode)
34✔
757
        push!(hp.history, join(lines, '\n'))
34✔
758
    end
34✔
759
    hp.start_idx = length(hp.history)
8✔
760
    return hp
8✔
761
end
762

763
function add_history(hist::REPLHistoryProvider, s::PromptState)
119✔
764
    str = rstrip(String(take!(copy(s.input_buffer))))
223✔
765
    isempty(strip(str)) && return
119✔
766
    mode = mode_idx(hist, LineEdit.mode(s))
101✔
767
    !isempty(hist.history) &&
101✔
768
        isequal(mode, hist.modes[end]) && str == hist.history[end] && return
769
    push!(hist.modes, mode)
95✔
770
    push!(hist.history, str)
95✔
771
    hist.history_file === nothing && return
95✔
772
    entry = """
16✔
773
    # time: $(Libc.strftime("%Y-%m-%d %H:%M:%S %Z", time()))
774
    # mode: $mode
775
    $(replace(str, r"^"ms => "\t"))
776
    """
777
    # TODO: write-lock history file
778
    try
16✔
779
        seekend(hist.history_file)
16✔
780
    catch err
781
        (err isa SystemError) || rethrow()
×
782
        # File handle might get stale after a while, especially under network file systems
783
        # If this doesn't fix it (e.g. when file is deleted), we'll end up rethrowing anyway
784
        hist_open_file(hist)
×
785
    end
786
    print(hist.history_file, entry)
32✔
787
    flush(hist.history_file)
16✔
788
    nothing
789
end
790

791
function history_move(s::Union{LineEdit.MIState,LineEdit.PrefixSearchState}, hist::REPLHistoryProvider, idx::Int, save_idx::Int = hist.cur_idx)
96✔
792
    max_idx = length(hist.history) + 1
136✔
793
    @assert 1 <= hist.cur_idx <= max_idx
96✔
794
    (1 <= idx <= max_idx) || return :none
98✔
795
    idx != hist.cur_idx || return :none
94✔
796

797
    # save the current line
798
    if save_idx == max_idx
94✔
799
        hist.last_mode = LineEdit.mode(s)
46✔
800
        hist.last_buffer = copy(LineEdit.buffer(s))
46✔
801
    else
802
        hist.history[save_idx] = LineEdit.input_string(s)
84✔
803
        hist.modes[save_idx] = mode_idx(hist, LineEdit.mode(s))
63✔
804
    end
805

806
    # load the saved line
807
    if idx == max_idx
94✔
808
        last_buffer = hist.last_buffer
10✔
809
        LineEdit.transition(s, hist.last_mode) do
10✔
810
            LineEdit.replace_line(s, last_buffer)
10✔
811
        end
812
        hist.last_mode = nothing
10✔
813
        hist.last_buffer = IOBuffer()
10✔
814
    else
815
        if haskey(hist.mode_mapping, hist.modes[idx])
168✔
816
            LineEdit.transition(s, hist.mode_mapping[hist.modes[idx]]) do
68✔
817
                LineEdit.replace_line(s, hist.history[idx])
68✔
818
            end
819
        else
820
            return :skip
16✔
821
        end
822
    end
823
    hist.cur_idx = idx
78✔
824

825
    return :ok
78✔
826
end
827

828
# REPL History can also transitions modes
829
function LineEdit.accept_result_newmode(hist::REPLHistoryProvider)
27✔
830
    if 1 <= hist.cur_idx <= length(hist.modes)
27✔
831
        return hist.mode_mapping[hist.modes[hist.cur_idx]]
23✔
832
    end
833
    return nothing
4✔
834
end
835

836
function history_prev(s::LineEdit.MIState, hist::REPLHistoryProvider,
34✔
837
                      num::Int=1, save_idx::Int = hist.cur_idx)
838
    num <= 0 && return history_next(s, hist, -num, save_idx)
58✔
839
    hist.last_idx = -1
32✔
840
    m = history_move(s, hist, hist.cur_idx-num, save_idx)
32✔
841
    if m === :ok
32✔
842
        LineEdit.move_input_start(s)
24✔
843
        LineEdit.reset_key_repeats(s) do
24✔
844
            LineEdit.move_line_end(s)
24✔
845
        end
846
        return LineEdit.refresh_line(s)
24✔
847
    elseif m === :skip
8✔
848
        return history_prev(s, hist, num+1, save_idx)
8✔
849
    else
850
        return Terminals.beep(s)
×
851
    end
852
end
853

854
function history_next(s::LineEdit.MIState, hist::REPLHistoryProvider,
26✔
855
                      num::Int=1, save_idx::Int = hist.cur_idx)
856
    if num == 0
44✔
857
        Terminals.beep(s)
×
858
        return
×
859
    end
860
    num < 0 && return history_prev(s, hist, -num, save_idx)
26✔
861
    cur_idx = hist.cur_idx
24✔
862
    max_idx = length(hist.history) + 1
24✔
863
    if cur_idx == max_idx && 0 < hist.last_idx
24✔
864
        # issue #6312
865
        cur_idx = hist.last_idx
×
866
        hist.last_idx = -1
×
867
    end
868
    m = history_move(s, hist, cur_idx+num, save_idx)
24✔
869
    if m === :ok
24✔
870
        LineEdit.move_input_end(s)
16✔
871
        return LineEdit.refresh_line(s)
16✔
872
    elseif m === :skip
8✔
873
        return history_next(s, hist, num+1, save_idx)
6✔
874
    else
875
        return Terminals.beep(s)
2✔
876
    end
877
end
878

879
history_first(s::LineEdit.MIState, hist::REPLHistoryProvider) =
6✔
880
    history_prev(s, hist, hist.cur_idx - 1 -
881
                 (hist.cur_idx > hist.start_idx+1 ? hist.start_idx : 0))
882

883
history_last(s::LineEdit.MIState, hist::REPLHistoryProvider) =
4✔
884
    history_next(s, hist, length(hist.history) - hist.cur_idx + 1)
885

886
function history_move_prefix(s::LineEdit.PrefixSearchState,
38✔
887
                             hist::REPLHistoryProvider,
888
                             prefix::AbstractString,
889
                             backwards::Bool,
890
                             cur_idx::Int = hist.cur_idx)
891
    cur_response = String(take!(copy(LineEdit.buffer(s))))
101✔
892
    # when searching forward, start at last_idx
893
    if !backwards && hist.last_idx > 0
38✔
894
        cur_idx = hist.last_idx
1✔
895
    end
896
    hist.last_idx = -1
38✔
897
    max_idx = length(hist.history)+1
38✔
898
    idxs = backwards ? ((cur_idx-1):-1:1) : ((cur_idx+1):1:max_idx)
65✔
899
    for idx in idxs
38✔
900
        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)))
192✔
901
            m = history_move(s, hist, idx)
36✔
902
            if m === :ok
36✔
903
                if idx == max_idx
34✔
904
                    # on resuming the in-progress edit, leave the cursor where the user last had it
905
                elseif isempty(prefix)
30✔
906
                    # on empty prefix search, move cursor to the end
907
                    LineEdit.move_input_end(s)
14✔
908
                else
909
                    # otherwise, keep cursor at the prefix position as a visual cue
910
                    seek(LineEdit.buffer(s), sizeof(prefix))
16✔
911
                end
912
                LineEdit.refresh_line(s)
34✔
913
                return :ok
34✔
914
            elseif m === :skip
2✔
915
                return history_move_prefix(s,hist,prefix,backwards,idx)
2✔
916
            end
917
        end
918
    end
62✔
919
    Terminals.beep(s)
×
920
    nothing
921
end
922
history_next_prefix(s::LineEdit.PrefixSearchState, hist::REPLHistoryProvider, prefix::AbstractString) =
5✔
923
    history_move_prefix(s, hist, prefix, false)
924
history_prev_prefix(s::LineEdit.PrefixSearchState, hist::REPLHistoryProvider, prefix::AbstractString) =
31✔
925
    history_move_prefix(s, hist, prefix, true)
926

927
function history_search(hist::REPLHistoryProvider, query_buffer::IOBuffer, response_buffer::IOBuffer,
28✔
928
                        backwards::Bool=false, skip_current::Bool=false)
929

930
    qpos = position(query_buffer)
28✔
931
    qpos > 0 || return true
28✔
932
    searchdata = beforecursor(query_buffer)
56✔
933
    response_str = String(take!(copy(response_buffer)))
50✔
934

935
    # Alright, first try to see if the current match still works
936
    a = position(response_buffer) + 1 # position is zero-indexed
28✔
937
    # FIXME: I'm pretty sure this is broken since it uses an index
938
    # into the search data to index into the response string
939
    b = a + sizeof(searchdata)
28✔
940
    b = b ≤ ncodeunits(response_str) ? prevind(response_str, b) : b-1
48✔
941
    b = min(lastindex(response_str), b) # ensure that b is valid
50✔
942

943
    searchstart = backwards ? b : a
28✔
944
    if searchdata == response_str[a:b]
44✔
945
        if skip_current
10✔
946
            searchstart = backwards ? prevind(response_str, b) : nextind(response_str, a)
4✔
947
        else
948
            return true
6✔
949
        end
950
    end
951

952
    # Start searching
953
    # First the current response buffer
954
    if 1 <= searchstart <= lastindex(response_str)
36✔
955
        match = backwards ? findprev(searchdata, response_str, searchstart) :
14✔
956
                            findnext(searchdata, response_str, searchstart)
957
        if match !== nothing
14✔
958
            seek(response_buffer, first(match) - 1)
6✔
959
            return true
6✔
960
        end
961
    end
962

963
    # Now search all the other buffers
964
    idxs = backwards ? ((hist.cur_idx-1):-1:1) : ((hist.cur_idx+1):1:length(hist.history))
32✔
965
    for idx in idxs
16✔
966
        h = hist.history[idx]
40✔
967
        match = backwards ? findlast(searchdata, h) : findfirst(searchdata, h)
80✔
968
        if match !== nothing && h != response_str && haskey(hist.mode_mapping, hist.modes[idx])
54✔
969
            truncate(response_buffer, 0)
12✔
970
            write(response_buffer, h)
12✔
971
            seek(response_buffer, first(match) - 1)
12✔
972
            hist.cur_idx = idx
12✔
973
            return true
12✔
974
        end
975
    end
52✔
976

977
    return false
4✔
978
end
979

980
function history_reset_state(hist::REPLHistoryProvider)
981
    if hist.cur_idx != length(hist.history) + 1
276✔
982
        hist.last_idx = hist.cur_idx
133✔
983
        hist.cur_idx = length(hist.history) + 1
133✔
984
    end
985
    nothing
986
end
987
LineEdit.reset_state(hist::REPLHistoryProvider) = history_reset_state(hist)
244✔
988

989
function return_callback(s)
98✔
990
    ast = Base.parse_input_line(String(take!(copy(LineEdit.buffer(s)))), depwarn=false)
181✔
991
    return !(isa(ast, Expr) && ast.head === :incomplete)
98✔
992
end
993

994
find_hist_file() = get(ENV, "JULIA_HISTORY",
8✔
995
                       !isempty(DEPOT_PATH) ? joinpath(DEPOT_PATH[1], "logs", "repl_history.jl") :
996
                       error("DEPOT_PATH is empty and ENV[\"JULIA_HISTORY\"] not set."))
997

998
backend(r::AbstractREPL) = r.backendref
100✔
999

1000
function eval_with_backend(ast, backend::REPLBackendRef)
104✔
1001
    put!(backend.repl_channel, (ast, 1))
104✔
1002
    return take!(backend.response_channel) # (val, iserr)
104✔
1003
end
1004

1005
function respond(f, repl, main; pass_empty::Bool = false, suppress_on_semicolon::Bool = true)
75✔
1006
    return function do_respond(s::MIState, buf, ok::Bool)
212✔
1007
        if !ok
137✔
1008
            return transition(s, :abort)
21✔
1009
        end
1010
        line = String(take!(buf)::Vector{UInt8})
217✔
1011
        if !isempty(line) || pass_empty
131✔
1012
            reset(repl)
101✔
1013
            local response
1014
            try
101✔
1015
                ast = Base.invokelatest(f, line)
101✔
1016
                response = eval_with_backend(ast, backend(repl))
100✔
1017
            catch
1018
                response = Pair{Any, Bool}(current_exceptions(), true)
1✔
1019
            end
1020
            hide_output = suppress_on_semicolon && ends_with_semicolon(line)
101✔
1021
            print_response(repl, response, !hide_output, hascolor(repl))
101✔
1022
        end
1023
        prepare_next(repl)
116✔
1024
        reset_state(s)
116✔
1025
        return s.current_mode.sticky ? true : transition(s, main)
116✔
1026
    end
1027
end
1028

1029
function reset(repl::LineEditREPL)
101✔
1030
    raw!(repl.t, false)
101✔
1031
    hascolor(repl) && print(repl.t, Base.text_colors[:normal])
101✔
1032
    nothing
1033
end
1034

1035
function prepare_next(repl::LineEditREPL)
116✔
1036
    println(terminal(repl))
116✔
1037
end
1038

1039
function mode_keymap(julia_prompt::Prompt)
1040
    AnyDict(
27✔
1041
    '\b' => function (s::MIState,o...)
7✔
1042
        if isempty(s) || position(LineEdit.buffer(s)) == 0
7✔
1043
            buf = copy(LineEdit.buffer(s))
7✔
1044
            transition(s, julia_prompt) do
7✔
1045
                LineEdit.state(s, julia_prompt).input_buffer = buf
7✔
1046
            end
1047
        else
1048
            LineEdit.edit_backspace(s)
×
1049
        end
1050
    end,
1051
    "^C" => function (s::MIState,o...)
1052
        LineEdit.move_input_end(s)
1053
        LineEdit.refresh_line(s)
1054
        print(LineEdit.terminal(s), "^C\n\n")
1055
        transition(s, julia_prompt)
1056
        transition(s, :reset)
1057
        LineEdit.refresh_line(s)
1058
    end)
1059
end
1060

1061
repl_filename(repl, hp::REPLHistoryProvider) = "REPL[$(max(length(hp.history)-hp.start_idx, 1))]"
90✔
1062
repl_filename(repl, hp) = "REPL"
×
1063

1064
const JL_PROMPT_PASTE = Ref(true)
1065
enable_promptpaste(v::Bool) = JL_PROMPT_PASTE[] = v
×
1066

1067
function contextual_prompt(repl::LineEditREPL, prompt::Union{String,Function})
1068
    function ()
2,213✔
1069
        mod = active_module(repl)
4,319✔
1070
        prefix = mod == Main ? "" : string('(', mod, ") ")
2,195✔
1071
        pr = prompt isa String ? prompt : prompt()
2,163✔
1072
        prefix * pr
2,163✔
1073
    end
1074
end
1075

1076
setup_interface(
71✔
1077
    repl::LineEditREPL;
1078
    # those keyword arguments may be deprecated eventually in favor of the Options mechanism
1079
    hascolor::Bool = repl.options.hascolor,
1080
    extra_repl_keymap::Any = repl.options.extra_keymap
1081
) = setup_interface(repl, hascolor, extra_repl_keymap)
1082

1083
# This non keyword method can be precompiled which is important
1084
function setup_interface(
25✔
1085
    repl::LineEditREPL,
1086
    hascolor::Bool,
1087
    extra_repl_keymap::Any, # Union{Dict,Vector{<:Dict}},
1088
)
1089
    # The precompile statement emitter has problem outputting valid syntax for the
1090
    # type of `Union{Dict,Vector{<:Dict}}` (see #28808).
1091
    # This function is however important to precompile for REPL startup time, therefore,
1092
    # make the type Any and just assert that we have the correct type below.
1093
    @assert extra_repl_keymap isa Union{Dict,Vector{<:Dict}}
25✔
1094

1095
    ###
1096
    #
1097
    # This function returns the main interface that describes the REPL
1098
    # functionality, it is called internally by functions that setup a
1099
    # Terminal-based REPL frontend.
1100
    #
1101
    # See run_frontend(repl::LineEditREPL, backend::REPLBackendRef)
1102
    # for usage
1103
    #
1104
    ###
1105

1106
    ###
1107
    # We setup the interface in two stages.
1108
    # First, we set up all components (prompt,rsearch,shell,help)
1109
    # Second, we create keymaps with appropriate transitions between them
1110
    #   and assign them to the components
1111
    #
1112
    ###
1113

1114
    ############################### Stage I ################################
1115

1116
    # This will provide completions for REPL and help mode
1117
    replc = REPLCompletionProvider()
25✔
1118

1119
    # Set up the main Julia prompt
1120
    julia_prompt = Prompt(contextual_prompt(repl, JULIA_PROMPT);
50✔
1121
        # Copy colors from the prompt object
1122
        prompt_prefix = hascolor ? repl.prompt_color : "",
1123
        prompt_suffix = hascolor ?
1124
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1125
        repl = repl,
1126
        complete = replc,
1127
        on_enter = return_callback)
1128

1129
    # Setup help mode
1130
    help_mode = Prompt(contextual_prompt(repl, "help?> "),
50✔
1131
        prompt_prefix = hascolor ? repl.help_color : "",
1132
        prompt_suffix = hascolor ?
1133
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1134
        repl = repl,
1135
        complete = replc,
1136
        # When we're done transform the entered line into a call to helpmode function
1137
        on_done = respond(line::String->helpmode(outstream(repl), line, repl.mistate.active_module),
2✔
1138
                          repl, julia_prompt, pass_empty=true, suppress_on_semicolon=false))
1139

1140

1141
    # Set up shell mode
1142
    shell_mode = Prompt(SHELL_PROMPT;
50✔
1143
        prompt_prefix = hascolor ? repl.shell_color : "",
1144
        prompt_suffix = hascolor ?
1145
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1146
        repl = repl,
1147
        complete = ShellCompletionProvider(),
1148
        # Transform "foo bar baz" into `foo bar baz` (shell quoting)
1149
        # and pass into Base.repl_cmd for processing (handles `ls` and `cd`
1150
        # special)
1151
        on_done = respond(repl, julia_prompt) do line
1152
            Expr(:call, :(Base.repl_cmd),
9✔
1153
                :(Base.cmd_gen($(Base.shell_parse(line::String)[1]))),
1154
                outstream(repl))
1155
        end,
1156
        sticky = true)
1157

1158

1159
    ################################# Stage II #############################
1160

1161
    # Setup history
1162
    # We will have a unified history for all REPL modes
1163
    hp = REPLHistoryProvider(Dict{Symbol,Prompt}(:julia => julia_prompt,
25✔
1164
                                                 :shell => shell_mode,
1165
                                                 :help  => help_mode))
1166
    if repl.history_file
25✔
1167
        try
4✔
1168
            hist_path = find_hist_file()
8✔
1169
            mkpath(dirname(hist_path))
4✔
1170
            hp.file_path = hist_path
4✔
1171
            hist_open_file(hp)
4✔
1172
            finalizer(replc) do replc
4✔
1173
                close(hp.history_file)
4✔
1174
            end
1175
            hist_from_file(hp, hist_path)
4✔
1176
        catch
1177
            # use REPL.hascolor to avoid using the local variable with the same name
1178
            print_response(repl, Pair{Any, Bool}(current_exceptions(), true), true, REPL.hascolor(repl))
×
1179
            println(outstream(repl))
×
1180
            @info "Disabling history file for this session"
×
1181
            repl.history_file = false
×
1182
        end
1183
    end
1184
    history_reset_state(hp)
25✔
1185
    julia_prompt.hist = hp
25✔
1186
    shell_mode.hist = hp
25✔
1187
    help_mode.hist = hp
25✔
1188

1189
    julia_prompt.on_done = respond(x->Base.parse_input_line(x,filename=repl_filename(repl,hp)), repl, julia_prompt)
115✔
1190

1191

1192
    search_prompt, skeymap = LineEdit.setup_search_keymap(hp)
25✔
1193
    search_prompt.complete = LatexCompletions()
25✔
1194

1195
    shell_prompt_len = length(SHELL_PROMPT)
×
1196
    help_prompt_len = length(HELP_PROMPT)
×
1197
    jl_prompt_regex = r"^In \[[0-9]+\]: |^(?:\(.+\) )?julia> "
×
1198
    pkg_prompt_regex = r"^(?:\(.+\) )?pkg> "
×
1199

1200
    # Canonicalize user keymap input
1201
    if isa(extra_repl_keymap, Dict)
25✔
1202
        extra_repl_keymap = AnyDict[extra_repl_keymap]
×
1203
    end
1204

1205
    repl_keymap = AnyDict(
25✔
1206
        ';' => function (s::MIState,o...)
55✔
1207
            if isempty(s) || position(LineEdit.buffer(s)) == 0
103✔
1208
                buf = copy(LineEdit.buffer(s))
7✔
1209
                transition(s, shell_mode) do
7✔
1210
                    LineEdit.state(s, shell_mode).input_buffer = buf
7✔
1211
                end
1212
            else
1213
                edit_insert(s, ';')
48✔
1214
            end
1215
        end,
1216
        '?' => function (s::MIState,o...)
1✔
1217
            if isempty(s) || position(LineEdit.buffer(s)) == 0
1✔
1218
                buf = copy(LineEdit.buffer(s))
1✔
1219
                transition(s, help_mode) do
1✔
1220
                    LineEdit.state(s, help_mode).input_buffer = buf
1✔
1221
                end
1222
            else
1223
                edit_insert(s, '?')
×
1224
            end
1225
        end,
1226
        ']' => function (s::MIState,o...)
3✔
1227
            if isempty(s) || position(LineEdit.buffer(s)) == 0
6✔
1228
                pkgid = Base.PkgId(Base.UUID("44cfe95a-1eb2-52ea-b672-e2afdf69b78f"), "Pkg")
×
1229
                REPLExt = Base.require_stdlib(pkgid, "REPLExt")
×
1230
                pkg_mode = nothing
×
1231
                if REPLExt isa Module && isdefined(REPLExt, :PkgCompletionProvider)
×
1232
                    for mode in repl.interface.modes
×
1233
                        if mode isa LineEdit.Prompt && mode.complete isa REPLExt.PkgCompletionProvider
×
1234
                            pkg_mode = mode
×
1235
                            break
×
1236
                        end
1237
                    end
×
1238
                end
1239
                # TODO: Cache the `pkg_mode`?
1240
                if pkg_mode !== nothing
×
1241
                    buf = copy(LineEdit.buffer(s))
×
1242
                    transition(s, pkg_mode) do
×
1243
                        LineEdit.state(s, pkg_mode).input_buffer = buf
1244
                    end
1245
                    return
×
1246
                end
1247
            end
1248
            edit_insert(s, ']')
3✔
1249
        end,
1250

1251
        # Bracketed Paste Mode
1252
        "\e[200~" => (s::MIState,o...)->begin
8✔
1253
            input = LineEdit.bracketed_paste(s) # read directly from s until reaching the end-bracketed-paste marker
8✔
1254
            sbuffer = LineEdit.buffer(s)
8✔
1255
            curspos = position(sbuffer)
8✔
1256
            seek(sbuffer, 0)
8✔
1257
            shouldeval = (bytesavailable(sbuffer) == curspos && !occursin(UInt8('\n'), sbuffer))
8✔
1258
            seek(sbuffer, curspos)
8✔
1259
            if curspos == 0
8✔
1260
                # if pasting at the beginning, strip leading whitespace
1261
                input = lstrip(input)
7✔
1262
            end
1263
            if !shouldeval
8✔
1264
                # when pasting in the middle of input, just paste in place
1265
                # don't try to execute all the WIP, since that's rather confusing
1266
                # and is often ill-defined how it should behave
1267
                edit_insert(s, input)
×
1268
                return
×
1269
            end
1270
            LineEdit.push_undo(s)
8✔
1271
            edit_insert(sbuffer, input)
8✔
1272
            input = String(take!(sbuffer))
16✔
1273
            oldpos = firstindex(input)
×
1274
            firstline = true
×
1275
            isprompt_paste = false
×
1276
            curr_prompt_len = 0
×
1277
            pasting_help = false
×
1278

1279
            while oldpos <= lastindex(input) # loop until all lines have been executed
50✔
1280
                if JL_PROMPT_PASTE[]
24✔
1281
                    # Check if the next statement starts with a prompt i.e. "julia> ", in that case
1282
                    # skip it. But first skip whitespace unless pasting in a docstring which may have
1283
                    # indented prompt examples that we don't want to execute
1284
                    while input[oldpos] in (pasting_help ? ('\n') : ('\n', ' ', '\t'))
66✔
1285
                        oldpos = nextind(input, oldpos)
28✔
1286
                        oldpos >= sizeof(input) && return
14✔
1287
                    end
14✔
1288
                    substr = SubString(input, oldpos)
48✔
1289
                    # Check if input line starts with "julia> ", remove it if we are in prompt paste mode
1290
                    if (firstline || isprompt_paste) && startswith(substr, jl_prompt_regex)
24✔
1291
                        detected_jl_prompt = match(jl_prompt_regex, substr).match
11✔
1292
                        isprompt_paste = true
×
1293
                        curr_prompt_len = sizeof(detected_jl_prompt)
11✔
1294
                        oldpos += curr_prompt_len
11✔
1295
                        transition(s, julia_prompt)
11✔
1296
                        pasting_help = false
×
1297
                    # Check if input line starts with "pkg> " or "(...) pkg> ", remove it if we are in prompt paste mode and switch mode
1298
                    elseif (firstline || isprompt_paste) && startswith(substr, pkg_prompt_regex)
13✔
1299
                        detected_pkg_prompt = match(pkg_prompt_regex, substr).match
×
1300
                        isprompt_paste = true
×
1301
                        curr_prompt_len = sizeof(detected_pkg_prompt)
×
1302
                        oldpos += curr_prompt_len
×
1303
                        Base.active_repl.interface.modes[1].keymap_dict[']'](s, o...)
×
1304
                        pasting_help = false
×
1305
                    # Check if input line starts with "shell> ", remove it if we are in prompt paste mode and switch mode
1306
                    elseif (firstline || isprompt_paste) && startswith(substr, SHELL_PROMPT)
13✔
1307
                        isprompt_paste = true
×
1308
                        oldpos += shell_prompt_len
2✔
1309
                        curr_prompt_len = shell_prompt_len
2✔
1310
                        transition(s, shell_mode)
2✔
1311
                        pasting_help = false
×
1312
                    # Check if input line starts with "help?> ", remove it if we are in prompt paste mode and switch mode
1313
                    elseif (firstline || isprompt_paste) && startswith(substr, HELP_PROMPT)
11✔
1314
                        isprompt_paste = true
×
1315
                        oldpos += help_prompt_len
1✔
1316
                        curr_prompt_len = help_prompt_len
1✔
1317
                        transition(s, help_mode)
1✔
1318
                        pasting_help = true
×
1319
                    # If we are prompt pasting and current statement does not begin with a mode prefix, skip to next line
1320
                    elseif isprompt_paste
10✔
1321
                        while input[oldpos] != '\n'
316✔
1322
                            oldpos = nextind(input, oldpos)
302✔
1323
                            oldpos >= sizeof(input) && return
151✔
1324
                        end
149✔
1325
                        continue
7✔
1326
                    end
1327
                end
1328
                dump_tail = false
15✔
1329
                nl_pos = findfirst('\n', input[oldpos:end])
30✔
1330
                if s.current_mode == julia_prompt
15✔
1331
                    ast, pos = Meta.parse(input, oldpos, raise=false, depwarn=false)
12✔
1332
                    if (isa(ast, Expr) && (ast.head === :error || ast.head === :incomplete)) ||
22✔
1333
                            (pos > ncodeunits(input) && !endswith(input, '\n'))
1334
                        # remaining text is incomplete (an error, or parser ran to the end but didn't stop with a newline):
1335
                        # Insert all the remaining text as one line (might be empty)
1336
                        dump_tail = true
×
1337
                    end
1338
                elseif isnothing(nl_pos) # no newline at end, so just dump the tail into the prompt and don't execute
6✔
1339
                    dump_tail = true
×
1340
                elseif s.current_mode == shell_mode # handle multiline shell commands
3✔
1341
                    lines = split(input[oldpos:end], '\n')
4✔
1342
                    pos = oldpos + sizeof(lines[1]) + 1
2✔
1343
                    if length(lines) > 1
2✔
1344
                        for line in lines[2:end]
2✔
1345
                            # to be recognized as a multiline shell command, the lines must be indented to the
1346
                            # same prompt position
1347
                            if !startswith(line, ' '^curr_prompt_len)
3✔
1348
                                break
2✔
1349
                            end
1350
                            pos += sizeof(line) + 1
1✔
1351
                        end
1✔
1352
                    end
1353
                else
1354
                    pos = oldpos + nl_pos
1✔
1355
                end
1356
                if dump_tail
15✔
1357
                    tail = input[oldpos:end]
10✔
1358
                    if !firstline
5✔
1359
                        # strip leading whitespace, but only if it was the result of executing something
1360
                        # (avoids modifying the user's current leading wip line)
1361
                        tail = lstrip(tail)
1✔
1362
                    end
1363
                    if isprompt_paste # remove indentation spaces corresponding to the prompt
5✔
1364
                        tail = replace(tail, r"^"m * ' '^curr_prompt_len => "")
7✔
1365
                    end
1366
                    LineEdit.replace_line(s, tail, true)
10✔
1367
                    LineEdit.refresh_line(s)
5✔
1368
                    break
5✔
1369
                end
1370
                # get the line and strip leading and trailing whitespace
1371
                line = strip(input[oldpos:prevind(input, pos)])
20✔
1372
                if !isempty(line)
10✔
1373
                    if isprompt_paste # remove indentation spaces corresponding to the prompt
10✔
1374
                        line = replace(line, r"^"m * ' '^curr_prompt_len => "")
10✔
1375
                    end
1376
                    # put the line on the screen and history
1377
                    LineEdit.replace_line(s, line)
20✔
1378
                    LineEdit.commit_line(s)
10✔
1379
                    # execute the statement
1380
                    terminal = LineEdit.terminal(s) # This is slightly ugly but ok for now
10✔
1381
                    raw!(terminal, false) && disable_bracketed_paste(terminal)
10✔
1382
                    @invokelatest LineEdit.mode(s).on_done(s, LineEdit.buffer(s), true)
10✔
1383
                    raw!(terminal, true) && enable_bracketed_paste(terminal)
10✔
1384
                    LineEdit.push_undo(s) # when the last line is incomplete
10✔
1385
                end
1386
                oldpos = pos
10✔
1387
                firstline = false
×
1388
            end
17✔
1389
        end,
1390

1391
        # Open the editor at the location of a stackframe or method
1392
        # This is accessing a contextual variable that gets set in
1393
        # the show_backtrace and show_method_table functions.
1394
        "^Q" => (s::MIState, o...) -> begin
1395
            linfos = repl.last_shown_line_infos
1396
            str = String(take!(LineEdit.buffer(s)))
1397
            n = tryparse(Int, str)
1398
            n === nothing && @goto writeback
1399
            if n <= 0 || n > length(linfos) || startswith(linfos[n][1], "REPL[")
1400
                @goto writeback
1401
            end
1402
            try
1403
                InteractiveUtils.edit(Base.fixup_stdlib_path(linfos[n][1]), linfos[n][2])
1404
            catch ex
1405
                ex isa ProcessFailedException || ex isa Base.IOError || ex isa SystemError || rethrow()
1406
                @info "edit failed" _exception=ex
1407
            end
1408
            LineEdit.refresh_line(s)
1409
            return
1410
            @label writeback
1411
            write(LineEdit.buffer(s), str)
1412
            return
1413
        end,
1414
    )
1415

1416
    prefix_prompt, prefix_keymap = LineEdit.setup_prefix_keymap(hp, julia_prompt)
25✔
1417

1418
    a = Dict{Any,Any}[skeymap, repl_keymap, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
150✔
1419
    prepend!(a, extra_repl_keymap)
25✔
1420

1421
    julia_prompt.keymap_dict = LineEdit.keymap(a)
25✔
1422

1423
    mk = mode_keymap(julia_prompt)
25✔
1424

1425
    b = Dict{Any,Any}[skeymap, mk, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
150✔
1426
    prepend!(b, extra_repl_keymap)
25✔
1427

1428
    shell_mode.keymap_dict = help_mode.keymap_dict = LineEdit.keymap(b)
25✔
1429

1430
    allprompts = LineEdit.TextInterface[julia_prompt, shell_mode, help_mode, search_prompt, prefix_prompt]
25✔
1431
    return ModalInterface(allprompts)
25✔
1432
end
1433

1434
function run_frontend(repl::LineEditREPL, backend::REPLBackendRef)
21✔
1435
    repl.frontend_task = current_task()
21✔
1436
    d = REPLDisplay(repl)
21✔
1437
    dopushdisplay = repl.specialdisplay === nothing && !in(d,Base.Multimedia.displays)
36✔
1438
    dopushdisplay && pushdisplay(d)
21✔
1439
    if !isdefined(repl,:interface)
21✔
1440
        interface = repl.interface = setup_interface(repl)
26✔
1441
    else
1442
        interface = repl.interface
8✔
1443
    end
1444
    repl.backendref = backend
21✔
1445
    repl.mistate = LineEdit.init_state(terminal(repl), interface)
21✔
1446
    run_interface(terminal(repl), interface, repl.mistate)
21✔
1447
    # Terminate Backend
1448
    put!(backend.repl_channel, (nothing, -1))
21✔
1449
    dopushdisplay && popdisplay(d)
21✔
1450
    nothing
1451
end
1452

1453
## StreamREPL ##
1454

1455
mutable struct StreamREPL <: AbstractREPL
1456
    stream::IO
1457
    prompt_color::String
1458
    input_color::String
1459
    answer_color::String
1460
    waserror::Bool
1461
    frontend_task::Task
1462
    StreamREPL(stream,pc,ic,ac) = new(stream,pc,ic,ac,false)
×
1463
end
1464
StreamREPL(stream::IO) = StreamREPL(stream, Base.text_colors[:green], Base.input_color(), Base.answer_color())
×
1465
run_repl(stream::IO) = run_repl(StreamREPL(stream))
×
1466

1467
outstream(s::StreamREPL) = s.stream
×
1468
hascolor(s::StreamREPL) = get(s.stream, :color, false)::Bool
×
1469

1470
answer_color(r::LineEditREPL) = r.envcolors ? Base.answer_color() : r.answer_color
×
1471
answer_color(r::StreamREPL) = r.answer_color
×
1472
input_color(r::LineEditREPL) = r.envcolors ? Base.input_color() : r.input_color
×
1473
input_color(r::StreamREPL) = r.input_color
×
1474

1475
let matchend = Dict("\"" => r"\"", "\"\"\"" => r"\"\"\"", "'" => r"'",
1476
    "`" => r"`", "```" => r"```", "#" => r"$"m, "#=" => r"=#|#=")
1477
    global _rm_strings_and_comments
1478
    function _rm_strings_and_comments(code::Union{String,SubString{String}})
126✔
1479
        buf = IOBuffer(sizehint = sizeof(code))
252✔
1480
        pos = 1
×
1481
        while true
166✔
1482
            i = findnext(r"\"(?!\"\")|\"\"\"|'|`(?!``)|```|#(?!=)|#=", code, pos)
332✔
1483
            isnothing(i) && break
212✔
1484
            match = SubString(code, i)
46✔
1485
            j = findnext(matchend[match]::Regex, code, nextind(code, last(i)))
92✔
1486
            if match == "#=" # possibly nested
46✔
1487
                nested = 1
×
1488
                while j !== nothing
11✔
1489
                    nested += SubString(code, j) == "#=" ? +1 : -1
10✔
1490
                    iszero(nested) && break
10✔
1491
                    j = findnext(r"=#|#=", code, nextind(code, last(j)))
12✔
1492
                end
6✔
1493
            elseif match[1] != '#' # quote match: check non-escaped
82✔
1494
                while j !== nothing
38✔
1495
                    notbackslash = findprev(!=('\\'), code, prevind(code, first(j)))::Int
66✔
1496
                    isodd(first(j) - notbackslash) && break # not escaped
33✔
1497
                    j = findnext(matchend[match]::Regex, code, nextind(code, first(j)))
14✔
1498
                end
7✔
1499
            end
1500
            isnothing(j) && break
86✔
1501
            if match[1] == '#'
80✔
1502
                print(buf, SubString(code, pos, prevind(code, first(i))))
14✔
1503
            else
1504
                print(buf, SubString(code, pos, last(i)), ' ', SubString(code, j))
26✔
1505
            end
1506
            pos = nextind(code, last(j))
40✔
1507
        end
40✔
1508
        print(buf, SubString(code, pos, lastindex(code)))
126✔
1509
        return String(take!(buf))
126✔
1510
    end
1511
end
1512

1513
# heuristic function to decide if the presence of a semicolon
1514
# at the end of the expression was intended for suppressing output
1515
ends_with_semicolon(code::AbstractString) = ends_with_semicolon(String(code))
×
1516
ends_with_semicolon(code::Union{String,SubString{String}}) =
126✔
1517
    contains(_rm_strings_and_comments(code), r";\s*$")
1518

1519
function banner(io::IO = stdout; short = false)
4✔
1520
    if Base.GIT_VERSION_INFO.tagged_commit
×
1521
        commit_string = Base.TAGGED_RELEASE_BANNER
×
1522
    elseif isempty(Base.GIT_VERSION_INFO.commit)
×
1523
        commit_string = ""
×
1524
    else
1525
        days = Int(floor((ccall(:jl_clock_now, Float64, ()) - Base.GIT_VERSION_INFO.fork_master_timestamp) / (60 * 60 * 24)))
2✔
1526
        days = max(0, days)
2✔
1527
        unit = days == 1 ? "day" : "days"
2✔
1528
        distance = Base.GIT_VERSION_INFO.fork_master_distance
×
1529
        commit = Base.GIT_VERSION_INFO.commit_short
2✔
1530

1531
        if distance == 0
×
1532
            commit_string = "Commit $(commit) ($(days) $(unit) old master)"
2✔
1533
        else
1534
            branch = Base.GIT_VERSION_INFO.branch
×
1535
            commit_string = "$(branch)/$(commit) (fork: $(distance) commits, $(days) $(unit))"
×
1536
        end
1537
    end
1538

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

1541
    if get(io, :color, false)::Bool
2✔
1542
        c = Base.text_colors
×
1543
        tx = c[:normal] # text
×
1544
        jl = c[:normal] # julia
×
1545
        d1 = c[:bold] * c[:blue]    # first dot
×
1546
        d2 = c[:bold] * c[:red]     # second dot
×
1547
        d3 = c[:bold] * c[:green]   # third dot
×
1548
        d4 = c[:bold] * c[:magenta] # fourth dot
×
1549

1550
        if short
×
1551
            print(io,"""
×
1552
              $(d3)o$(tx)  | Version $(VERSION)$(commit_date)
1553
             $(d2)o$(tx) $(d4)o$(tx) | $(commit_string)
1554
            """)
1555
        else
1556
            print(io,"""               $(d3)_$(tx)
×
1557
               $(d1)_$(tx)       $(jl)_$(tx) $(d2)_$(d3)(_)$(d4)_$(tx)     |  Documentation: https://docs.julialang.org
1558
              $(d1)(_)$(jl)     | $(d2)(_)$(tx) $(d4)(_)$(tx)    |
1559
               $(jl)_ _   _| |_  __ _$(tx)   |  Type \"?\" for help, \"]?\" for Pkg help.
1560
              $(jl)| | | | | | |/ _` |$(tx)  |
1561
              $(jl)| | |_| | | | (_| |$(tx)  |  Version $(VERSION)$(commit_date)
1562
             $(jl)_/ |\\__'_|_|_|\\__'_|$(tx)  |  $(commit_string)
1563
            $(jl)|__/$(tx)                   |
1564

1565
            """)
1566
        end
1567
    else
1568
        if short
2✔
1569
            print(io,"""
1✔
1570
              o  |  Version $(VERSION)$(commit_date)
1571
             o o |  $(commit_string)
1572
            """)
1573
        else
1574
            print(io,"""
1✔
1575
                           _
1576
               _       _ _(_)_     |  Documentation: https://docs.julialang.org
1577
              (_)     | (_) (_)    |
1578
               _ _   _| |_  __ _   |  Type \"?\" for help, \"]?\" for Pkg help.
1579
              | | | | | | |/ _` |  |
1580
              | | |_| | | | (_| |  |  Version $(VERSION)$(commit_date)
1581
             _/ |\\__'_|_|_|\\__'_|  |  $(commit_string)
1582
            |__/                   |
1583

1584
            """)
1585
        end
1586
    end
1587
end
1588

1589
function run_frontend(repl::StreamREPL, backend::REPLBackendRef)
×
1590
    repl.frontend_task = current_task()
×
1591
    have_color = hascolor(repl)
×
1592
    banner(repl.stream)
×
1593
    d = REPLDisplay(repl)
×
1594
    dopushdisplay = !in(d,Base.Multimedia.displays)
×
1595
    dopushdisplay && pushdisplay(d)
×
1596
    while !eof(repl.stream)::Bool
×
1597
        if have_color
×
1598
            print(repl.stream,repl.prompt_color)
×
1599
        end
1600
        print(repl.stream, "julia> ")
×
1601
        if have_color
×
1602
            print(repl.stream, input_color(repl))
×
1603
        end
1604
        line = readline(repl.stream, keep=true)
×
1605
        if !isempty(line)
×
1606
            ast = Base.parse_input_line(line)
×
1607
            if have_color
×
1608
                print(repl.stream, Base.color_normal)
×
1609
            end
1610
            response = eval_with_backend(ast, backend)
×
1611
            print_response(repl, response, !ends_with_semicolon(line), have_color)
×
1612
        end
1613
    end
×
1614
    # Terminate Backend
1615
    put!(backend.repl_channel, (nothing, -1))
×
1616
    dopushdisplay && popdisplay(d)
×
1617
    nothing
×
1618
end
1619

1620
module Numbered
1621

1622
using ..REPL
1623

1624
__current_ast_transforms() = isdefined(Base, :active_repl_backend) ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1625

1626
function repl_eval_counter(hp)
789✔
1627
    return length(hp.history) - hp.start_idx
789✔
1628
end
1629

1630
function out_transform(@nospecialize(x), n::Ref{Int})
16✔
1631
    return Expr(:toplevel, get_usings!([], x)..., quote
16✔
1632
        let __temp_val_a72df459 = $x
1633
            $capture_result($n, __temp_val_a72df459)
1634
            __temp_val_a72df459
1635
        end
1636
    end)
1637
end
1638

1639
function get_usings!(usings, ex)
25✔
1640
    ex isa Expr || return usings
25✔
1641
    # get all `using` and `import` statements which are at the top level
1642
    for (i, arg) in enumerate(ex.args)
25✔
1643
        if Base.isexpr(arg, :toplevel)
48✔
1644
            get_usings!(usings, arg)
9✔
1645
        elseif Base.isexpr(arg, [:using, :import])
64✔
1646
            push!(usings, popat!(ex.args, i))
2✔
1647
        end
1648
    end
71✔
1649
    return usings
25✔
1650
end
1651

1652
function capture_result(n::Ref{Int}, @nospecialize(x))
16✔
1653
    n = n[]
16✔
1654
    mod = Base.MainInclude
16✔
1655
    if !isdefined(mod, :Out)
16✔
1656
        @eval mod global Out
1✔
1657
        @eval mod export Out
1✔
1658
        setglobal!(mod, :Out, Dict{Int, Any}())
1✔
1659
    end
1660
    if x !== getglobal(mod, :Out) && x !== nothing # remove this?
16✔
1661
        getglobal(mod, :Out)[n] = x
14✔
1662
    end
1663
    nothing
16✔
1664
end
1665

1666
function set_prompt(repl::LineEditREPL, n::Ref{Int})
1✔
1667
    julia_prompt = repl.interface.modes[1]
1✔
1668
    julia_prompt.prompt = function()
790✔
1669
        n[] = repl_eval_counter(julia_prompt.hist)+1
789✔
1670
        string("In [", n[], "]: ")
789✔
1671
    end
1672
    nothing
1✔
1673
end
1674

1675
function set_output_prefix(repl::LineEditREPL, n::Ref{Int})
1✔
1676
    julia_prompt = repl.interface.modes[1]
1✔
1677
    if REPL.hascolor(repl)
1✔
1678
        julia_prompt.output_prefix_prefix = Base.text_colors[:red]
1✔
1679
    end
1680
    julia_prompt.output_prefix = () -> string("Out[", n[], "]: ")
15✔
1681
    nothing
1✔
1682
end
1683

1684
function __current_ast_transforms(backend)
1685
    if backend === nothing
1✔
1686
        isdefined(Base, :active_repl_backend) ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1687
    else
1688
        backend.ast_transforms
1✔
1689
    end
1690
end
1691

1692
function numbered_prompt!(repl::LineEditREPL=Base.active_repl, backend=nothing)
1693
    n = Ref{Int}(0)
1✔
1694
    set_prompt(repl, n)
1✔
1695
    set_output_prefix(repl, n)
1✔
1696
    push!(__current_ast_transforms(backend), @nospecialize(ast) -> out_transform(ast, n))
17✔
1697
    return
1✔
1698
end
1699

1700
"""
1701
    Out[n]
1702

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

1706
See also [`ans`](@ref).
1707
"""
1708
Base.MainInclude.Out
1709

1710
end
1711

1712
import .Numbered.numbered_prompt!
1713

1714
# this assignment won't survive precompilation,
1715
# but will stick if REPL is baked into a sysimg.
1716
# Needs to occur after this module is finished.
1717
Base.REPL_MODULE_REF[] = REPL
1718

1719
if Base.generating_output()
1720
    include("precompile.jl")
1721
end
1722

1723
end # module
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc