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

JuliaLang / julia / #37643

05 Oct 2023 03:14PM UTC coverage: 86.883% (+0.05%) from 86.835%
#37643

push

local

web-flow
Use a simple error when reporting sysimg load failures. (#51598)

`jl_errorexception_type` is undefined at the point we (fail to) load a
sysimg.

73424 of 84509 relevant lines covered (86.88%)

11771585.16 hits per line

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

80.57
/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
const PRECOMPILE_STATEMENTS = Vector{String}()
18

19
function __init__()
11✔
20
    Base.REPL_MODULE_REF[] = REPL
11✔
21
    # We can encounter the situation where the sub-ordinate process used
22
    # during precompilation of REPL, can load a valid cache-file.
23
    # We need to replay the statements such that the parent process
24
    # can also include those. See JuliaLang/julia#51532
25
    if Base.JLOptions().trace_compile !== C_NULL && !isempty(PRECOMPILE_STATEMENTS)
11✔
26
        for statement in PRECOMPILE_STATEMENTS
11✔
27
            ccall(:jl_write_precompile_statement, Cvoid, (Cstring,), statement)
7,436✔
28
        end
7,436✔
29
    else
30
        empty!(PRECOMPILE_STATEMENTS)
×
31
    end
32
end
33

34
Base.Experimental.@optlevel 1
35
Base.Experimental.@max_methods 1
36

37
using Base.Meta, Sockets
38
import InteractiveUtils
39

40
export
41
    AbstractREPL,
42
    BasicREPL,
43
    LineEditREPL,
44
    StreamREPL
45

46
import Base:
47
    AbstractDisplay,
48
    display,
49
    show,
50
    AnyDict,
51
    ==
52

53
_displaysize(io::IO) = displaysize(io)::Tuple{Int,Int}
29✔
54

55
include("Terminals.jl")
56
using .Terminals
57

58
abstract type AbstractREPL end
59

60
include("options.jl")
61

62
include("LineEdit.jl")
63
using .LineEdit
64
import ..LineEdit:
65
    CompletionProvider,
66
    HistoryProvider,
67
    add_history,
68
    complete_line,
69
    history_next,
70
    history_next_prefix,
71
    history_prev,
72
    history_prev_prefix,
73
    history_first,
74
    history_last,
75
    history_search,
76
    accept_result,
77
    setmodifiers!,
78
    terminal,
79
    MIState,
80
    PromptState,
81
    TextInterface,
82
    mode_idx
83

84
include("REPLCompletions.jl")
85
using .REPLCompletions
86

87
include("TerminalMenus/TerminalMenus.jl")
88
include("docview.jl")
89

90
@nospecialize # use only declared type signatures
91

92
answer_color(::AbstractREPL) = ""
×
93

94
const JULIA_PROMPT = "julia> "
95
const PKG_PROMPT = "pkg> "
96
const SHELL_PROMPT = "shell> "
97
const HELP_PROMPT = "help?> "
98

99
mutable struct REPLBackend
100
    "channel for AST"
101
    repl_channel::Channel{Any}
102
    "channel for results: (value, iserror)"
103
    response_channel::Channel{Any}
104
    "flag indicating the state of this backend"
105
    in_eval::Bool
106
    "transformation functions to apply before evaluating expressions"
107
    ast_transforms::Vector{Any}
108
    "current backend task"
109
    backend_task::Task
110

111
    REPLBackend(repl_channel, response_channel, in_eval, ast_transforms=copy(repl_ast_transforms)) =
48✔
112
        new(repl_channel, response_channel, in_eval, ast_transforms)
113
end
114
REPLBackend() = REPLBackend(Channel(1), Channel(1), false)
24✔
115

116
"""
117
    softscope(ex)
118

119
Return a modified version of the parsed expression `ex` that uses
120
the REPL's "soft" scoping rules for global syntax blocks.
121
"""
122
function softscope(@nospecialize ex)
364✔
123
    if ex isa Expr
364✔
124
        h = ex.head
235✔
125
        if h === :toplevel
235✔
126
            ex′ = Expr(h)
133✔
127
            map!(softscope, resize!(ex′.args, length(ex.args)), ex.args)
133✔
128
            return ex′
133✔
129
        elseif h in (:meta, :import, :using, :export, :module, :error, :incomplete, :thunk)
102✔
130
            return ex
2✔
131
        elseif h === :global && all(x->isa(x, Symbol), ex.args)
102✔
132
            return ex
1✔
133
        else
134
            return Expr(:block, Expr(:softscope, true), ex)
99✔
135
        end
136
    end
137
    return ex
129✔
138
end
139

140
# Temporary alias until Documenter updates
141
const softscope! = softscope
142

143
const repl_ast_transforms = Any[softscope] # defaults for new REPL backends
144

145
# Allows an external package to add hooks into the code loading.
146
# The hook should take a Vector{Symbol} of package names and
147
# return true if all packages could be installed, false if not
148
# to e.g. install packages on demand
149
const install_packages_hooks = Any[]
150

151
function eval_user_input(@nospecialize(ast), backend::REPLBackend, mod::Module)
104✔
152
    lasterr = nothing
×
153
    Base.sigatomic_begin()
104✔
154
    while true
107✔
155
        try
107✔
156
            Base.sigatomic_end()
107✔
157
            if lasterr !== nothing
107✔
158
                put!(backend.response_channel, Pair{Any, Bool}(lasterr, true))
3✔
159
            else
160
                backend.in_eval = true
104✔
161
                if !isempty(install_packages_hooks)
104✔
162
                    check_for_missing_packages_and_run_hooks(ast)
×
163
                end
164
                for xf in backend.ast_transforms
104✔
165
                    ast = Base.invokelatest(xf, ast)
122✔
166
                end
226✔
167
                value = Core.eval(mod, ast)
104✔
168
                backend.in_eval = false
100✔
169
                setglobal!(Base.MainInclude, :ans, value)
100✔
170
                put!(backend.response_channel, Pair{Any, Bool}(value, false))
100✔
171
            end
172
            break
106✔
173
        catch err
174
            if lasterr !== nothing
3✔
175
                println("SYSTEM ERROR: Failed to report error to REPL frontend")
×
176
                println(err)
×
177
            end
178
            lasterr = current_exceptions()
3✔
179
        end
180
    end
3✔
181
    Base.sigatomic_end()
103✔
182
    nothing
103✔
183
end
184

185
function check_for_missing_packages_and_run_hooks(ast)
×
186
    isa(ast, Expr) || return
×
187
    mods = modules_to_be_loaded(ast)
×
188
    filter!(mod -> isnothing(Base.identify_package(String(mod))), mods) # keep missing modules
×
189
    if !isempty(mods)
×
190
        for f in install_packages_hooks
×
191
            Base.invokelatest(f, mods) && return
×
192
        end
×
193
    end
194
end
195

196
function modules_to_be_loaded(ast::Expr, mods::Vector{Symbol} = Symbol[])
73✔
197
    ast.head === :quote && return mods # don't search if it's not going to be run during this eval
73✔
198
    if ast.head === :using || ast.head === :import
88✔
199
        for arg in ast.args
17✔
200
            arg = arg::Expr
21✔
201
            arg1 = first(arg.args)
21✔
202
            if arg1 isa Symbol # i.e. `Foo`
21✔
203
                if arg1 != :. # don't include local imports
18✔
204
                    push!(mods, arg1)
17✔
205
                end
206
            else # i.e. `Foo: bar`
207
                push!(mods, first((arg1::Expr).args))
3✔
208
            end
209
        end
38✔
210
    end
211
    for arg in ast.args
51✔
212
        if isexpr(arg, (:block, :if, :using, :import))
130✔
213
            modules_to_be_loaded(arg, mods)
29✔
214
        end
215
    end
144✔
216
    filter!(mod -> !in(String(mod), ["Base", "Main", "Core"]), mods) # Exclude special non-package modules
101✔
217
    return unique(mods)
51✔
218
end
219

220
"""
221
    start_repl_backend(repl_channel::Channel, response_channel::Channel)
222

223
    Starts loop for REPL backend
224
    Returns a REPLBackend with backend_task assigned
225

226
    Deprecated since sync / async behavior cannot be selected
227
"""
228
function start_repl_backend(repl_channel::Channel{Any}, response_channel::Channel{Any}
×
229
                            ; get_module::Function = ()->Main)
230
    # Maintain legacy behavior of asynchronous backend
231
    backend = REPLBackend(repl_channel, response_channel, false)
×
232
    # Assignment will be made twice, but will be immediately available
233
    backend.backend_task = @async start_repl_backend(backend; get_module)
×
234
    return backend
×
235
end
236

237
"""
238
    start_repl_backend(backend::REPLBackend)
239

240
    Call directly to run backend loop on current Task.
241
    Use @async for run backend on new Task.
242

243
    Does not return backend until loop is finished.
244
"""
245
function start_repl_backend(backend::REPLBackend,  @nospecialize(consumer = x -> nothing); get_module::Function = ()->Main)
56✔
246
    backend.backend_task = Base.current_task()
24✔
247
    consumer(backend)
24✔
248
    repl_backend_loop(backend, get_module)
24✔
249
    return backend
23✔
250
end
251

252
function repl_backend_loop(backend::REPLBackend, get_module::Function)
24✔
253
    # include looks at this to determine the relative include path
254
    # nothing means cwd
255
    while true
×
256
        tls = task_local_storage()
127✔
257
        tls[:SOURCE_PATH] = nothing
127✔
258
        ast, show_value = take!(backend.repl_channel)
127✔
259
        if show_value == -1
127✔
260
            # exit flag
261
            break
23✔
262
        end
263
        eval_user_input(ast, backend, get_module())
104✔
264
    end
103✔
265
    return nothing
23✔
266
end
267

268
struct REPLDisplay{Repl<:AbstractREPL} <: AbstractDisplay
269
    repl::Repl
33✔
270
end
271

272
function display(d::REPLDisplay, mime::MIME"text/plain", x)
61✔
273
    x = Ref{Any}(x)
61✔
274
    with_repl_linfo(d.repl) do io
61✔
275
        io = IOContext(io, :limit => true, :module => active_module(d)::Module)
119✔
276
        if d.repl isa LineEditREPL
3✔
277
            mistate = d.repl.mistate
58✔
278
            mode = LineEdit.mode(mistate)
58✔
279
            if mode isa LineEdit.Prompt
58✔
280
                LineEdit.write_output_prefix(io, mode, get(io, :color, false)::Bool)
232✔
281
            end
282
        end
283
        get(io, :color, false)::Bool && write(io, answer_color(d.repl))
67✔
284
        if isdefined(d.repl, :options) && isdefined(d.repl.options, :iocontext)
3✔
285
            # this can override the :limit property set initially
286
            io = foldl(IOContext, d.repl.options.iocontext, init=io)
58✔
287
        end
288
        show(io, mime, x[])
61✔
289
        println(io)
60✔
290
    end
291
    return nothing
60✔
292
end
293
display(d::REPLDisplay, x) = display(d, MIME("text/plain"), x)
61✔
294

295
function print_response(repl::AbstractREPL, response, show_value::Bool, have_color::Bool)
100✔
296
    repl.waserror = response[2]
100✔
297
    with_repl_linfo(repl) do io
100✔
298
        io = IOContext(io, :module => active_module(repl)::Module)
197✔
299
        print_response(io, response, show_value, have_color, specialdisplay(repl))
100✔
300
    end
301
    return nothing
100✔
302
end
303

304
function repl_display_error(errio::IO, @nospecialize errval)
7✔
305
    # this will be set to true if types in the stacktrace are truncated
306
    limitflag = Ref(false)
7✔
307
    errio = IOContext(errio, :stacktrace_types_limited => limitflag)
7✔
308
    Base.invokelatest(Base.display_error, errio, errval)
7✔
309
    if limitflag[]
5✔
310
        print(errio, "Some type information was truncated. Use `show(err)` to see complete types.")
2✔
311
        println(errio)
2✔
312
    end
313
    return nothing
5✔
314
end
315

316
function print_response(errio::IO, response, show_value::Bool, have_color::Bool, specialdisplay::Union{AbstractDisplay,Nothing}=nothing)
101✔
317
    Base.sigatomic_begin()
101✔
318
    val, iserr = response
101✔
319
    while true
102✔
320
        try
102✔
321
            Base.sigatomic_end()
102✔
322
            if iserr
102✔
323
                val = Base.scrub_repl_backtrace(val)
6✔
324
                Base.istrivialerror(val) || setglobal!(Base.MainInclude, :err, val)
10✔
325
                repl_display_error(errio, val)
6✔
326
            else
327
                if val !== nothing && show_value
96✔
328
                    try
61✔
329
                        if specialdisplay === nothing
61✔
330
                            Base.invokelatest(display, val)
47✔
331
                        else
332
                            Base.invokelatest(display, specialdisplay, val)
60✔
333
                        end
334
                    catch
335
                        println(errio, "Error showing value of type ", typeof(val), ":")
1✔
336
                        rethrow()
1✔
337
                    end
338
                end
339
            end
340
            break
102✔
341
        catch ex
342
            if iserr
2✔
343
                println(errio) # an error during printing is likely to leave us mid-line
1✔
344
                println(errio, "SYSTEM (REPL): showing an error caused an error")
1✔
345
                try
1✔
346
                    excs = Base.scrub_repl_backtrace(current_exceptions())
1✔
347
                    setglobal!(Base.MainInclude, :err, excs)
1✔
348
                    repl_display_error(errio, excs)
2✔
349
                catch e
350
                    # at this point, only print the name of the type as a Symbol to
351
                    # minimize the possibility of further errors.
352
                    println(errio)
1✔
353
                    println(errio, "SYSTEM (REPL): caught exception of type ", typeof(e).name.name,
1✔
354
                            " while trying to handle a nested exception; giving up")
355
                end
356
                break
1✔
357
            end
358
            val = current_exceptions()
1✔
359
            iserr = true
1✔
360
        end
361
    end
1✔
362
    Base.sigatomic_end()
101✔
363
    nothing
101✔
364
end
365

366
# A reference to a backend that is not mutable
367
struct REPLBackendRef
368
    repl_channel::Channel{Any}
22✔
369
    response_channel::Channel{Any}
370
end
371
REPLBackendRef(backend::REPLBackend) = REPLBackendRef(backend.repl_channel, backend.response_channel)
22✔
372

373
function destroy(ref::REPLBackendRef, state::Task)
21✔
374
    if istaskfailed(state)
21✔
375
        close(ref.repl_channel, TaskFailedException(state))
×
376
        close(ref.response_channel, TaskFailedException(state))
×
377
    end
378
    close(ref.repl_channel)
21✔
379
    close(ref.response_channel)
21✔
380
end
381

382
"""
383
    run_repl(repl::AbstractREPL)
384
    run_repl(repl, consumer = backend->nothing; backend_on_current_task = true)
385

386
    Main function to start the REPL
387

388
    consumer is an optional function that takes a REPLBackend as an argument
389
"""
390
function run_repl(repl::AbstractREPL, @nospecialize(consumer = x -> nothing); backend_on_current_task::Bool = true, backend = REPLBackend())
86✔
391
    backend_ref = REPLBackendRef(backend)
22✔
392
    cleanup = @task try
43✔
393
            destroy(backend_ref, t)
21✔
394
        catch e
395
            Core.print(Core.stderr, "\nINTERNAL ERROR: ")
×
396
            Core.println(Core.stderr, e)
×
397
            Core.println(Core.stderr, catch_backtrace())
×
398
        end
399
    get_module = () -> active_module(repl)
122✔
400
    if backend_on_current_task
22✔
401
        t = @async run_frontend(repl, backend_ref)
44✔
402
        errormonitor(t)
22✔
403
        Base._wait2(t, cleanup)
22✔
404
        start_repl_backend(backend, consumer; get_module)
22✔
405
    else
406
        t = @async start_repl_backend(backend, consumer; get_module)
×
407
        errormonitor(t)
×
408
        Base._wait2(t, cleanup)
×
409
        run_frontend(repl, backend_ref)
×
410
    end
411
    return backend
21✔
412
end
413

414
## BasicREPL ##
415

416
mutable struct BasicREPL <: AbstractREPL
417
    terminal::TextTerminal
418
    waserror::Bool
419
    frontend_task::Task
420
    BasicREPL(t) = new(t, false)
3✔
421
end
422

423
outstream(r::BasicREPL) = r.terminal
6✔
424
hascolor(r::BasicREPL) = hascolor(r.terminal)
×
425

426
function run_frontend(repl::BasicREPL, backend::REPLBackendRef)
3✔
427
    repl.frontend_task = current_task()
3✔
428
    d = REPLDisplay(repl)
3✔
429
    dopushdisplay = !in(d,Base.Multimedia.displays)
6✔
430
    dopushdisplay && pushdisplay(d)
3✔
431
    hit_eof = false
3✔
432
    while true
6✔
433
        Base.reseteof(repl.terminal)
6✔
434
        write(repl.terminal, JULIA_PROMPT)
6✔
435
        line = ""
6✔
436
        ast = nothing
6✔
437
        interrupted = false
6✔
438
        while true
6✔
439
            try
6✔
440
                line *= readline(repl.terminal, keep=true)
6✔
441
            catch e
442
                if isa(e,InterruptException)
×
443
                    try # raise the debugger if present
×
444
                        ccall(:jl_raise_debugger, Int, ())
×
445
                    catch
×
446
                    end
447
                    line = ""
×
448
                    interrupted = true
×
449
                    break
×
450
                elseif isa(e,EOFError)
×
451
                    hit_eof = true
×
452
                    break
×
453
                else
454
                    rethrow()
×
455
                end
456
            end
457
            ast = Base.parse_input_line(line)
12✔
458
            (isa(ast,Expr) && ast.head === :incomplete) || break
12✔
459
        end
×
460
        if !isempty(line)
6✔
461
            response = eval_with_backend(ast, backend)
4✔
462
            print_response(repl, response, !ends_with_semicolon(line), false)
3✔
463
        end
464
        write(repl.terminal, '\n')
5✔
465
        ((!interrupted && isempty(line)) || hit_eof) && break
8✔
466
    end
3✔
467
    # terminate backend
468
    put!(backend.repl_channel, (nothing, -1))
2✔
469
    dopushdisplay && popdisplay(d)
2✔
470
    nothing
2✔
471
end
472

473
## LineEditREPL ##
474

475
mutable struct LineEditREPL <: AbstractREPL
476
    t::TextTerminal
477
    hascolor::Bool
478
    prompt_color::String
479
    input_color::String
480
    answer_color::String
481
    shell_color::String
482
    help_color::String
483
    history_file::Bool
484
    in_shell::Bool
485
    in_help::Bool
486
    envcolors::Bool
487
    waserror::Bool
488
    specialdisplay::Union{Nothing,AbstractDisplay}
489
    options::Options
490
    mistate::Union{MIState,Nothing}
491
    last_shown_line_infos::Vector{Tuple{String,Int}}
492
    interface::ModalInterface
493
    backendref::REPLBackendRef
494
    frontend_task::Task
495
    function LineEditREPL(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,history_file,in_shell,in_help,envcolors)
26✔
496
        opts = Options()
26✔
497
        opts.hascolor = hascolor
26✔
498
        if !hascolor
26✔
499
            opts.beep_colors = [""]
×
500
        end
501
        new(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,history_file,in_shell,
26✔
502
            in_help,envcolors,false,nothing, opts, nothing, Tuple{String,Int}[])
503
    end
504
end
505
outstream(r::LineEditREPL) = (t = r.t; t isa TTYTerminal ? t.out_stream : t)
495✔
506
specialdisplay(r::LineEditREPL) = r.specialdisplay
97✔
507
specialdisplay(r::AbstractREPL) = nothing
3✔
508
terminal(r::LineEditREPL) = r.t
150✔
509
hascolor(r::LineEditREPL) = r.hascolor
195✔
510

511
LineEditREPL(t::TextTerminal, hascolor::Bool, envcolors::Bool=false) =
52✔
512
    LineEditREPL(t, hascolor,
513
        hascolor ? Base.text_colors[:green] : "",
514
        hascolor ? Base.input_color() : "",
515
        hascolor ? Base.answer_color() : "",
516
        hascolor ? Base.text_colors[:red] : "",
517
        hascolor ? Base.text_colors[:yellow] : "",
518
        false, false, false, envcolors
519
    )
520

521
mutable struct REPLCompletionProvider <: CompletionProvider
522
    modifiers::LineEdit.Modifiers
23✔
523
end
524
REPLCompletionProvider() = REPLCompletionProvider(LineEdit.Modifiers())
23✔
525

526
mutable struct ShellCompletionProvider <: CompletionProvider end
23✔
527
struct LatexCompletions <: CompletionProvider end
528

529
function active_module() # this method is also called from Base
7,720✔
530
    isdefined(Base, :active_repl) || return Main
15,440✔
531
    return active_module(Base.active_repl::AbstractREPL)
×
532
end
533
active_module((; mistate)::LineEditREPL) = mistate === nothing ? Main : mistate.active_module
4,663✔
534
active_module(::AbstractREPL) = Main
10✔
535
active_module(d::REPLDisplay) = active_module(d.repl)
119✔
536

537
setmodifiers!(c::CompletionProvider, m::LineEdit.Modifiers) = nothing
×
538

539
setmodifiers!(c::REPLCompletionProvider, m::LineEdit.Modifiers) = c.modifiers = m
×
540

541
"""
542
    activate(mod::Module=Main)
543

544
Set `mod` as the default contextual module in the REPL,
545
both for evaluating expressions and printing them.
546
"""
547
function activate(mod::Module=Main)
×
548
    mistate = (Base.active_repl::LineEditREPL).mistate
×
549
    mistate === nothing && return nothing
×
550
    mistate.active_module = mod
×
551
    Base.load_InteractiveUtils(mod)
×
552
    return nothing
×
553
end
554

555
beforecursor(buf::IOBuffer) = String(buf.data[1:buf.ptr-1])
2,149✔
556

557
function complete_line(c::REPLCompletionProvider, s::PromptState, mod::Module)
1,681✔
558
    partial = beforecursor(s.input_buffer)
1,681✔
559
    full = LineEdit.input_string(s)
1,681✔
560
    ret, range, should_complete = completions(full, lastindex(partial), mod, c.modifiers.shift)
1,681✔
561
    c.modifiers = LineEdit.Modifiers()
1,681✔
562
    return unique!(map(completion_text, ret)), partial[range], should_complete
1,681✔
563
end
564

565
function complete_line(c::ShellCompletionProvider, s::PromptState)
436✔
566
    # First parse everything up to the current position
567
    partial = beforecursor(s.input_buffer)
436✔
568
    full = LineEdit.input_string(s)
436✔
569
    ret, range, should_complete = shell_completions(full, lastindex(partial))
436✔
570
    return unique!(map(completion_text, ret)), partial[range], should_complete
436✔
571
end
572

573
function complete_line(c::LatexCompletions, s)
×
574
    partial = beforecursor(LineEdit.buffer(s))
×
575
    full = LineEdit.input_string(s)::String
×
576
    ret, range, should_complete = bslash_completions(full, lastindex(partial))[2]
×
577
    return unique!(map(completion_text, ret)), partial[range], should_complete
×
578
end
579

580
with_repl_linfo(f, repl) = f(outstream(repl))
6✔
581
function with_repl_linfo(f, repl::LineEditREPL)
155✔
582
    linfos = Tuple{String,Int}[]
155✔
583
    io = IOContext(outstream(repl), :last_shown_line_infos => linfos)
310✔
584
    f(io)
155✔
585
    if !isempty(linfos)
154✔
586
        repl.last_shown_line_infos = linfos
5✔
587
    end
588
    nothing
154✔
589
end
590

591
mutable struct REPLHistoryProvider <: HistoryProvider
592
    history::Vector{String}
28✔
593
    file_path::String
594
    history_file::Union{Nothing,IO}
595
    start_idx::Int
596
    cur_idx::Int
597
    last_idx::Int
598
    last_buffer::IOBuffer
599
    last_mode::Union{Nothing,Prompt}
600
    mode_mapping::Dict{Symbol,Prompt}
601
    modes::Vector{Symbol}
602
end
603
REPLHistoryProvider(mode_mapping::Dict{Symbol}) =
28✔
604
    REPLHistoryProvider(String[], "", nothing, 0, 0, -1, IOBuffer(),
605
                        nothing, mode_mapping, UInt8[])
606

607
invalid_history_message(path::String) = """
×
608
Invalid history file ($path) format:
609
If you have a history file left over from an older version of Julia,
610
try renaming or deleting it.
611
Invalid character: """
612

613
munged_history_message(path::String) = """
×
614
Invalid history file ($path) format:
615
An editor may have converted tabs to spaces at line """
616

617
function hist_open_file(hp::REPLHistoryProvider)
×
618
    f = open(hp.file_path, read=true, write=true, create=true)
4✔
619
    hp.history_file = f
4✔
620
    seekend(f)
4✔
621
end
622

623
function hist_from_file(hp::REPLHistoryProvider, path::String)
8✔
624
    getline(lines, i) = i > length(lines) ? "" : lines[i]
276✔
625
    file_lines = readlines(path)
8✔
626
    countlines = 0
×
627
    while true
42✔
628
        # First parse the metadata that starts with '#' in particular the REPL mode
629
        countlines += 1
42✔
630
        line = getline(file_lines, countlines)
76✔
631
        mode = :julia
×
632
        isempty(line) && break
42✔
633
        line[1] != '#' &&
68✔
634
            error(invalid_history_message(path), repr(line[1]), " at line ", countlines)
635
        while !isempty(line)
102✔
636
            startswith(line, '#') || break
102✔
637
            if startswith(line, "# mode: ")
136✔
638
                mode = Symbol(SubString(line, 9))
34✔
639
            end
640
            countlines += 1
68✔
641
            line = getline(file_lines, countlines)
136✔
642
        end
68✔
643
        isempty(line) && break
34✔
644

645
        # Now parse the code for the current REPL mode
646
        line[1] == ' '  &&
68✔
647
            error(munged_history_message(path), countlines)
648
        line[1] != '\t' &&
68✔
649
            error(invalid_history_message(path), repr(line[1]), " at line ", countlines)
650
        lines = String[]
34✔
651
        while !isempty(line)
34✔
652
            push!(lines, chomp(SubString(line, 2)))
34✔
653
            next_line = getline(file_lines, countlines+1)
64✔
654
            isempty(next_line) && break
34✔
655
            first(next_line) == ' '  && error(munged_history_message(path), countlines)
30✔
656
            # A line not starting with a tab means we are done with code for this entry
657
            first(next_line) != '\t' && break
30✔
658
            countlines += 1
×
659
            line = getline(file_lines, countlines)
×
660
        end
×
661
        push!(hp.modes, mode)
34✔
662
        push!(hp.history, join(lines, '\n'))
34✔
663
    end
34✔
664
    hp.start_idx = length(hp.history)
8✔
665
    return hp
8✔
666
end
667

668
function add_history(hist::REPLHistoryProvider, s::PromptState)
115✔
669
    str = rstrip(String(take!(copy(s.input_buffer))))
115✔
670
    isempty(strip(str)) && return
115✔
671
    mode = mode_idx(hist, LineEdit.mode(s))
97✔
672
    !isempty(hist.history) &&
158✔
673
        isequal(mode, hist.modes[end]) && str == hist.history[end] && return
674
    push!(hist.modes, mode)
91✔
675
    push!(hist.history, str)
91✔
676
    hist.history_file === nothing && return
91✔
677
    entry = """
16✔
678
    # time: $(Libc.strftime("%Y-%m-%d %H:%M:%S %Z", time()))
679
    # mode: $mode
680
    $(replace(str, r"^"ms => "\t"))
681
    """
682
    # TODO: write-lock history file
683
    try
16✔
684
        seekend(hist.history_file)
16✔
685
    catch err
686
        (err isa SystemError) || rethrow()
×
687
        # File handle might get stale after a while, especially under network file systems
688
        # If this doesn't fix it (e.g. when file is deleted), we'll end up rethrowing anyway
689
        hist_open_file(hist)
×
690
    end
691
    print(hist.history_file, entry)
32✔
692
    flush(hist.history_file)
16✔
693
    nothing
16✔
694
end
695

696
function history_move(s::Union{LineEdit.MIState,LineEdit.PrefixSearchState}, hist::REPLHistoryProvider, idx::Int, save_idx::Int = hist.cur_idx)
100✔
697
    max_idx = length(hist.history) + 1
136✔
698
    @assert 1 <= hist.cur_idx <= max_idx
96✔
699
    (1 <= idx <= max_idx) || return :none
98✔
700
    idx != hist.cur_idx || return :none
94✔
701

702
    # save the current line
703
    if save_idx == max_idx
94✔
704
        hist.last_mode = LineEdit.mode(s)
46✔
705
        hist.last_buffer = copy(LineEdit.buffer(s))
46✔
706
    else
707
        hist.history[save_idx] = LineEdit.input_string(s)
84✔
708
        hist.modes[save_idx] = mode_idx(hist, LineEdit.mode(s))
63✔
709
    end
710

711
    # load the saved line
712
    if idx == max_idx
94✔
713
        last_buffer = hist.last_buffer
10✔
714
        LineEdit.transition(s, hist.last_mode) do
10✔
715
            LineEdit.replace_line(s, last_buffer)
10✔
716
        end
717
        hist.last_mode = nothing
10✔
718
        hist.last_buffer = IOBuffer()
10✔
719
    else
720
        if haskey(hist.mode_mapping, hist.modes[idx])
84✔
721
            LineEdit.transition(s, hist.mode_mapping[hist.modes[idx]]) do
68✔
722
                LineEdit.replace_line(s, hist.history[idx])
68✔
723
            end
724
        else
725
            return :skip
16✔
726
        end
727
    end
728
    hist.cur_idx = idx
78✔
729

730
    return :ok
78✔
731
end
732

733
# REPL History can also transitions modes
734
function LineEdit.accept_result_newmode(hist::REPLHistoryProvider)
27✔
735
    if 1 <= hist.cur_idx <= length(hist.modes)
27✔
736
        return hist.mode_mapping[hist.modes[hist.cur_idx]]
23✔
737
    end
738
    return nothing
4✔
739
end
740

741
function history_prev(s::LineEdit.MIState, hist::REPLHistoryProvider,
58✔
742
                      num::Int=1, save_idx::Int = hist.cur_idx)
743
    num <= 0 && return history_next(s, hist, -num, save_idx)
58✔
744
    hist.last_idx = -1
32✔
745
    m = history_move(s, hist, hist.cur_idx-num, save_idx)
32✔
746
    if m === :ok
32✔
747
        LineEdit.move_input_start(s)
24✔
748
        LineEdit.reset_key_repeats(s) do
24✔
749
            LineEdit.move_line_end(s)
24✔
750
        end
751
        return LineEdit.refresh_line(s)
24✔
752
    elseif m === :skip
8✔
753
        return history_prev(s, hist, num+1, save_idx)
8✔
754
    else
755
        return Terminals.beep(s)
×
756
    end
757
end
758

759
function history_next(s::LineEdit.MIState, hist::REPLHistoryProvider,
44✔
760
                      num::Int=1, save_idx::Int = hist.cur_idx)
761
    if num == 0
44✔
762
        Terminals.beep(s)
×
763
        return
×
764
    end
765
    num < 0 && return history_prev(s, hist, -num, save_idx)
26✔
766
    cur_idx = hist.cur_idx
24✔
767
    max_idx = length(hist.history) + 1
24✔
768
    if cur_idx == max_idx && 0 < hist.last_idx
24✔
769
        # issue #6312
770
        cur_idx = hist.last_idx
×
771
        hist.last_idx = -1
×
772
    end
773
    m = history_move(s, hist, cur_idx+num, save_idx)
24✔
774
    if m === :ok
24✔
775
        LineEdit.move_input_end(s)
16✔
776
        return LineEdit.refresh_line(s)
16✔
777
    elseif m === :skip
8✔
778
        return history_next(s, hist, num+1, save_idx)
6✔
779
    else
780
        return Terminals.beep(s)
2✔
781
    end
782
end
783

784
history_first(s::LineEdit.MIState, hist::REPLHistoryProvider) =
6✔
785
    history_prev(s, hist, hist.cur_idx - 1 -
786
                 (hist.cur_idx > hist.start_idx+1 ? hist.start_idx : 0))
787

788
history_last(s::LineEdit.MIState, hist::REPLHistoryProvider) =
4✔
789
    history_next(s, hist, length(hist.history) - hist.cur_idx + 1)
790

791
function history_move_prefix(s::LineEdit.PrefixSearchState,
62✔
792
                             hist::REPLHistoryProvider,
793
                             prefix::AbstractString,
794
                             backwards::Bool,
795
                             cur_idx::Int = hist.cur_idx)
796
    cur_response = String(take!(copy(LineEdit.buffer(s))))
74✔
797
    # when searching forward, start at last_idx
798
    if !backwards && hist.last_idx > 0
38✔
799
        cur_idx = hist.last_idx
1✔
800
    end
801
    hist.last_idx = -1
38✔
802
    max_idx = length(hist.history)+1
38✔
803
    idxs = backwards ? ((cur_idx-1):-1:1) : ((cur_idx+1):1:max_idx)
65✔
804
    for idx in idxs
74✔
805
        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✔
806
            m = history_move(s, hist, idx)
36✔
807
            if m === :ok
36✔
808
                if idx == max_idx
38✔
809
                    # on resuming the in-progress edit, leave the cursor where the user last had it
810
                elseif isempty(prefix)
30✔
811
                    # on empty prefix search, move cursor to the end
812
                    LineEdit.move_input_end(s)
14✔
813
                else
814
                    # otherwise, keep cursor at the prefix position as a visual cue
815
                    seek(LineEdit.buffer(s), sizeof(prefix))
16✔
816
                end
817
                LineEdit.refresh_line(s)
34✔
818
                return :ok
34✔
819
            elseif m === :skip
2✔
820
                return history_move_prefix(s,hist,prefix,backwards,idx)
2✔
821
            end
822
        end
823
    end
124✔
824
    Terminals.beep(s)
×
825
    nothing
2✔
826
end
827
history_next_prefix(s::LineEdit.PrefixSearchState, hist::REPLHistoryProvider, prefix::AbstractString) =
5✔
828
    history_move_prefix(s, hist, prefix, false)
829
history_prev_prefix(s::LineEdit.PrefixSearchState, hist::REPLHistoryProvider, prefix::AbstractString) =
31✔
830
    history_move_prefix(s, hist, prefix, true)
831

832
function history_search(hist::REPLHistoryProvider, query_buffer::IOBuffer, response_buffer::IOBuffer,
28✔
833
                        backwards::Bool=false, skip_current::Bool=false)
834

835
    qpos = position(query_buffer)
28✔
836
    qpos > 0 || return true
28✔
837
    searchdata = beforecursor(query_buffer)
28✔
838
    response_str = String(take!(copy(response_buffer)))
28✔
839

840
    # Alright, first try to see if the current match still works
841
    a = position(response_buffer) + 1 # position is zero-indexed
28✔
842
    # FIXME: I'm pretty sure this is broken since it uses an index
843
    # into the search data to index into the response string
844
    b = a + sizeof(searchdata)
28✔
845
    b = b ≤ ncodeunits(response_str) ? prevind(response_str, b) : b-1
48✔
846
    b = min(lastindex(response_str), b) # ensure that b is valid
28✔
847

848
    searchstart = backwards ? b : a
28✔
849
    if searchdata == response_str[a:b]
44✔
850
        if skip_current
10✔
851
            searchstart = backwards ? prevind(response_str, b) : nextind(response_str, a)
4✔
852
        else
853
            return true
6✔
854
        end
855
    end
856

857
    # Start searching
858
    # First the current response buffer
859
    if 1 <= searchstart <= lastindex(response_str)
22✔
860
        match = backwards ? findprev(searchdata, response_str, searchstart) :
14✔
861
                            findnext(searchdata, response_str, searchstart)
862
        if match !== nothing
14✔
863
            seek(response_buffer, first(match) - 1)
6✔
864
            return true
6✔
865
        end
866
    end
867

868
    # Now search all the other buffers
869
    idxs = backwards ? ((hist.cur_idx-1):-1:1) : ((hist.cur_idx+1):1:length(hist.history))
32✔
870
    for idx in idxs
32✔
871
        h = hist.history[idx]
40✔
872
        match = backwards ? findlast(searchdata, h) : findfirst(searchdata, h)
40✔
873
        if match !== nothing && h != response_str && haskey(hist.mode_mapping, hist.modes[idx])
40✔
874
            truncate(response_buffer, 0)
12✔
875
            write(response_buffer, h)
12✔
876
            seek(response_buffer, first(match) - 1)
12✔
877
            hist.cur_idx = idx
12✔
878
            return true
12✔
879
        end
880
    end
52✔
881

882
    return false
4✔
883
end
884

885
function history_reset_state(hist::REPLHistoryProvider)
7✔
886
    if hist.cur_idx != length(hist.history) + 1
266✔
887
        hist.last_idx = hist.cur_idx
127✔
888
        hist.cur_idx = length(hist.history) + 1
127✔
889
    end
890
    nothing
266✔
891
end
892
LineEdit.reset_state(hist::REPLHistoryProvider) = history_reset_state(hist)
236✔
893

894
function return_callback(s)
94✔
895
    ast = Base.parse_input_line(String(take!(copy(LineEdit.buffer(s)))), depwarn=false)
94✔
896
    return !(isa(ast, Expr) && ast.head === :incomplete)
94✔
897
end
898

899
find_hist_file() = get(ENV, "JULIA_HISTORY",
8✔
900
                       !isempty(DEPOT_PATH) ? joinpath(DEPOT_PATH[1], "logs", "repl_history.jl") :
901
                       error("DEPOT_PATH is empty and and ENV[\"JULIA_HISTORY\"] not set."))
902

903
backend(r::AbstractREPL) = r.backendref
96✔
904

905
function eval_with_backend(ast, backend::REPLBackendRef)
100✔
906
    put!(backend.repl_channel, (ast, 1))
100✔
907
    return take!(backend.response_channel) # (val, iserr)
100✔
908
end
909

910
function respond(f, repl, main; pass_empty::Bool = false, suppress_on_semicolon::Bool = true)
69✔
911
    return function do_respond(s::MIState, buf, ok::Bool)
200✔
912
        if !ok
131✔
913
            return transition(s, :abort)
19✔
914
        end
915
        line = String(take!(buf)::Vector{UInt8})
112✔
916
        if !isempty(line) || pass_empty
127✔
917
            reset(repl)
97✔
918
            local response
×
919
            try
97✔
920
                ast = Base.invokelatest(f, line)
97✔
921
                response = eval_with_backend(ast, backend(repl))
97✔
922
            catch
923
                response = Pair{Any, Bool}(current_exceptions(), true)
1✔
924
            end
925
            hide_output = suppress_on_semicolon && ends_with_semicolon(line)
97✔
926
            print_response(repl, response, !hide_output, hascolor(repl))
97✔
927
        end
928
        prepare_next(repl)
112✔
929
        reset_state(s)
112✔
930
        return s.current_mode.sticky ? true : transition(s, main)
112✔
931
    end
932
end
933

934
function reset(repl::LineEditREPL)
97✔
935
    raw!(repl.t, false)
97✔
936
    hascolor(repl) && print(repl.t, Base.text_colors[:normal])
97✔
937
    nothing
97✔
938
end
939

940
function prepare_next(repl::LineEditREPL)
112✔
941
    println(terminal(repl))
112✔
942
end
943

944
function mode_keymap(julia_prompt::Prompt)
2✔
945
    AnyDict(
25✔
946
    '\b' => function (s::MIState,o...)
7✔
947
        if isempty(s) || position(LineEdit.buffer(s)) == 0
7✔
948
            buf = copy(LineEdit.buffer(s))
7✔
949
            transition(s, julia_prompt) do
7✔
950
                LineEdit.state(s, julia_prompt).input_buffer = buf
7✔
951
            end
952
        else
953
            LineEdit.edit_backspace(s)
×
954
        end
955
    end,
956
    "^C" => function (s::MIState,o...)
957
        LineEdit.move_input_end(s)
958
        LineEdit.refresh_line(s)
959
        print(LineEdit.terminal(s), "^C\n\n")
960
        transition(s, julia_prompt)
961
        transition(s, :reset)
962
        LineEdit.refresh_line(s)
963
    end)
964
end
965

966
repl_filename(repl, hp::REPLHistoryProvider) = "REPL[$(max(length(hp.history)-hp.start_idx, 1))]"
86✔
967
repl_filename(repl, hp) = "REPL"
×
968

969
const JL_PROMPT_PASTE = Ref(true)
970
enable_promptpaste(v::Bool) = JL_PROMPT_PASTE[] = v
×
971

972
function contextual_prompt(repl::LineEditREPL, prompt::Union{String,Function})
×
973
    function ()
2,130✔
974
        mod = active_module(repl)
4,161✔
975
        prefix = mod == Main ? "" : string('(', mod, ") ")
2,116✔
976
        pr = prompt isa String ? prompt : prompt()
2,084✔
977
        prefix * pr
2,084✔
978
    end
979
end
980

981
setup_interface(
65✔
982
    repl::LineEditREPL;
983
    # those keyword arguments may be deprecated eventually in favor of the Options mechanism
984
    hascolor::Bool = repl.options.hascolor,
985
    extra_repl_keymap::Any = repl.options.extra_keymap
986
) = setup_interface(repl, hascolor, extra_repl_keymap)
987

988
# This non keyword method can be precompiled which is important
989
function setup_interface(
23✔
990
    repl::LineEditREPL,
991
    hascolor::Bool,
992
    extra_repl_keymap::Any, # Union{Dict,Vector{<:Dict}},
993
)
994
    # The precompile statement emitter has problem outputting valid syntax for the
995
    # type of `Union{Dict,Vector{<:Dict}}` (see #28808).
996
    # This function is however important to precompile for REPL startup time, therefore,
997
    # make the type Any and just assert that we have the correct type below.
998
    @assert extra_repl_keymap isa Union{Dict,Vector{<:Dict}}
23✔
999

1000
    ###
1001
    #
1002
    # This function returns the main interface that describes the REPL
1003
    # functionality, it is called internally by functions that setup a
1004
    # Terminal-based REPL frontend.
1005
    #
1006
    # See run_frontend(repl::LineEditREPL, backend::REPLBackendRef)
1007
    # for usage
1008
    #
1009
    ###
1010

1011
    ###
1012
    # We setup the interface in two stages.
1013
    # First, we set up all components (prompt,rsearch,shell,help)
1014
    # Second, we create keymaps with appropriate transitions between them
1015
    #   and assign them to the components
1016
    #
1017
    ###
1018

1019
    ############################### Stage I ################################
1020

1021
    # This will provide completions for REPL and help mode
1022
    replc = REPLCompletionProvider()
23✔
1023

1024
    # Set up the main Julia prompt
1025
    julia_prompt = Prompt(contextual_prompt(repl, JULIA_PROMPT);
46✔
1026
        # Copy colors from the prompt object
1027
        prompt_prefix = hascolor ? repl.prompt_color : "",
1028
        prompt_suffix = hascolor ?
1029
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1030
        repl = repl,
1031
        complete = replc,
1032
        on_enter = return_callback)
1033

1034
    # Setup help mode
1035
    help_mode = Prompt(contextual_prompt(repl, "help?> "),
46✔
1036
        prompt_prefix = hascolor ? repl.help_color : "",
1037
        prompt_suffix = hascolor ?
1038
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1039
        repl = repl,
1040
        complete = replc,
1041
        # When we're done transform the entered line into a call to helpmode function
1042
        on_done = respond(line::String->helpmode(outstream(repl), line, repl.mistate.active_module),
2✔
1043
                          repl, julia_prompt, pass_empty=true, suppress_on_semicolon=false))
1044

1045

1046
    # Set up shell mode
1047
    shell_mode = Prompt(SHELL_PROMPT;
46✔
1048
        prompt_prefix = hascolor ? repl.shell_color : "",
1049
        prompt_suffix = hascolor ?
1050
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1051
        repl = repl,
1052
        complete = ShellCompletionProvider(),
1053
        # Transform "foo bar baz" into `foo bar baz` (shell quoting)
1054
        # and pass into Base.repl_cmd for processing (handles `ls` and `cd`
1055
        # special)
1056
        on_done = respond(repl, julia_prompt) do line
1057
            Expr(:call, :(Base.repl_cmd),
9✔
1058
                :(Base.cmd_gen($(Base.shell_parse(line::String)[1]))),
1059
                outstream(repl))
1060
        end,
1061
        sticky = true)
1062

1063

1064
    ################################# Stage II #############################
1065

1066
    # Setup history
1067
    # We will have a unified history for all REPL modes
1068
    hp = REPLHistoryProvider(Dict{Symbol,Prompt}(:julia => julia_prompt,
23✔
1069
                                                 :shell => shell_mode,
1070
                                                 :help  => help_mode))
1071
    if repl.history_file
23✔
1072
        try
4✔
1073
            hist_path = find_hist_file()
8✔
1074
            mkpath(dirname(hist_path))
4✔
1075
            hp.file_path = hist_path
4✔
1076
            hist_open_file(hp)
4✔
1077
            finalizer(replc) do replc
4✔
1078
                close(hp.history_file)
4✔
1079
            end
1080
            hist_from_file(hp, hist_path)
4✔
1081
        catch
1082
            # use REPL.hascolor to avoid using the local variable with the same name
1083
            print_response(repl, Pair{Any, Bool}(current_exceptions(), true), true, REPL.hascolor(repl))
×
1084
            println(outstream(repl))
×
1085
            @info "Disabling history file for this session"
×
1086
            repl.history_file = false
×
1087
        end
1088
    end
1089
    history_reset_state(hp)
23✔
1090
    julia_prompt.hist = hp
23✔
1091
    shell_mode.hist = hp
23✔
1092
    help_mode.hist = hp
23✔
1093

1094
    julia_prompt.on_done = respond(x->Base.parse_input_line(x,filename=repl_filename(repl,hp)), repl, julia_prompt)
109✔
1095

1096

1097
    search_prompt, skeymap = LineEdit.setup_search_keymap(hp)
23✔
1098
    search_prompt.complete = LatexCompletions()
23✔
1099

1100
    shell_prompt_len = length(SHELL_PROMPT)
×
1101
    help_prompt_len = length(HELP_PROMPT)
×
1102
    jl_prompt_regex = r"^In \[[0-9]+\]: |^(?:\(.+\) )?julia> "
×
1103
    pkg_prompt_regex = r"^(?:\(.+\) )?pkg> "
×
1104

1105
    # Canonicalize user keymap input
1106
    if isa(extra_repl_keymap, Dict)
23✔
1107
        extra_repl_keymap = AnyDict[extra_repl_keymap]
×
1108
    end
1109

1110
    repl_keymap = AnyDict(
23✔
1111
        ';' => function (s::MIState,o...)
53✔
1112
            if isempty(s) || position(LineEdit.buffer(s)) == 0
99✔
1113
                buf = copy(LineEdit.buffer(s))
7✔
1114
                transition(s, shell_mode) do
7✔
1115
                    LineEdit.state(s, shell_mode).input_buffer = buf
7✔
1116
                end
1117
            else
1118
                edit_insert(s, ';')
46✔
1119
            end
1120
        end,
1121
        '?' => function (s::MIState,o...)
1✔
1122
            if isempty(s) || position(LineEdit.buffer(s)) == 0
1✔
1123
                buf = copy(LineEdit.buffer(s))
1✔
1124
                transition(s, help_mode) do
1✔
1125
                    LineEdit.state(s, help_mode).input_buffer = buf
1✔
1126
                end
1127
            else
1128
                edit_insert(s, '?')
×
1129
            end
1130
        end,
1131
        ']' => function (s::MIState,o...)
3✔
1132
            if isempty(s) || position(LineEdit.buffer(s)) == 0
6✔
1133
                pkgid = Base.PkgId(Base.UUID("44cfe95a-1eb2-52ea-b672-e2afdf69b78f"), "Pkg")
×
1134
                if Base.locate_package(pkgid) !== nothing # Only try load Pkg if we can find it
×
1135
                    Pkg = Base.require(pkgid)
×
1136
                    # Pkg should have loaded its REPL mode by now, let's find it so we can transition to it.
1137
                    pkg_mode = nothing
×
1138
                    for mode in repl.interface.modes
×
1139
                        if mode isa LineEdit.Prompt && mode.complete isa Pkg.REPLMode.PkgCompletionProvider
×
1140
                            pkg_mode = mode
×
1141
                            break
×
1142
                        end
1143
                    end
×
1144
                    # TODO: Cache the `pkg_mode`?
1145
                    if pkg_mode !== nothing
×
1146
                        buf = copy(LineEdit.buffer(s))
×
1147
                        transition(s, pkg_mode) do
×
1148
                            LineEdit.state(s, pkg_mode).input_buffer = buf
1149
                        end
1150
                        return
×
1151
                    end
1152
                end
1153
            end
1154
            edit_insert(s, ']')
3✔
1155
        end,
1156

1157
        # Bracketed Paste Mode
1158
        "\e[200~" => (s::MIState,o...)->begin
8✔
1159
            input = LineEdit.bracketed_paste(s) # read directly from s until reaching the end-bracketed-paste marker
8✔
1160
            sbuffer = LineEdit.buffer(s)
8✔
1161
            curspos = position(sbuffer)
8✔
1162
            seek(sbuffer, 0)
8✔
1163
            shouldeval = (bytesavailable(sbuffer) == curspos && !occursin(UInt8('\n'), sbuffer))
8✔
1164
            seek(sbuffer, curspos)
8✔
1165
            if curspos == 0
8✔
1166
                # if pasting at the beginning, strip leading whitespace
1167
                input = lstrip(input)
7✔
1168
            end
1169
            if !shouldeval
8✔
1170
                # when pasting in the middle of input, just paste in place
1171
                # don't try to execute all the WIP, since that's rather confusing
1172
                # and is often ill-defined how it should behave
1173
                edit_insert(s, input)
×
1174
                return
×
1175
            end
1176
            LineEdit.push_undo(s)
8✔
1177
            edit_insert(sbuffer, input)
9✔
1178
            input = String(take!(sbuffer))
8✔
1179
            oldpos = firstindex(input)
×
1180
            firstline = true
×
1181
            isprompt_paste = false
×
1182
            curr_prompt_len = 0
×
1183
            pasting_help = false
×
1184

1185
            while oldpos <= lastindex(input) # loop until all lines have been executed
25✔
1186
                if JL_PROMPT_PASTE[]
24✔
1187
                    # Check if the next statement starts with a prompt i.e. "julia> ", in that case
1188
                    # skip it. But first skip whitespace unless pasting in a docstring which may have
1189
                    # indented prompt examples that we don't want to execute
1190
                    while input[oldpos] in (pasting_help ? ('\n') : ('\n', ' ', '\t'))
66✔
1191
                        oldpos = nextind(input, oldpos)
14✔
1192
                        oldpos >= sizeof(input) && return
14✔
1193
                    end
14✔
1194
                    substr = SubString(input, oldpos)
24✔
1195
                    # Check if input line starts with "julia> ", remove it if we are in prompt paste mode
1196
                    if (firstline || isprompt_paste) && startswith(substr, jl_prompt_regex)
24✔
1197
                        detected_jl_prompt = match(jl_prompt_regex, substr).match
22✔
1198
                        isprompt_paste = true
×
1199
                        curr_prompt_len = sizeof(detected_jl_prompt)
11✔
1200
                        oldpos += curr_prompt_len
11✔
1201
                        transition(s, julia_prompt)
11✔
1202
                        pasting_help = false
11✔
1203
                    # Check if input line starts with "pkg> " or "(...) pkg> ", remove it if we are in prompt paste mode and switch mode
1204
                    elseif (firstline || isprompt_paste) && startswith(substr, pkg_prompt_regex)
13✔
1205
                        detected_pkg_prompt = match(pkg_prompt_regex, substr).match
×
1206
                        isprompt_paste = true
×
1207
                        curr_prompt_len = sizeof(detected_pkg_prompt)
×
1208
                        oldpos += curr_prompt_len
×
1209
                        Base.active_repl.interface.modes[1].keymap_dict[']'](s, o...)
×
1210
                        pasting_help = false
×
1211
                    # Check if input line starts with "shell> ", remove it if we are in prompt paste mode and switch mode
1212
                    elseif (firstline || isprompt_paste) && startswith(substr, SHELL_PROMPT)
24✔
1213
                        isprompt_paste = true
×
1214
                        oldpos += shell_prompt_len
2✔
1215
                        curr_prompt_len = shell_prompt_len
2✔
1216
                        transition(s, shell_mode)
2✔
1217
                        pasting_help = false
2✔
1218
                    # Check if input line starts with "help?> ", remove it if we are in prompt paste mode and switch mode
1219
                    elseif (firstline || isprompt_paste) && startswith(substr, HELP_PROMPT)
20✔
1220
                        isprompt_paste = true
×
1221
                        oldpos += help_prompt_len
1✔
1222
                        curr_prompt_len = help_prompt_len
1✔
1223
                        transition(s, help_mode)
1✔
1224
                        pasting_help = true
1✔
1225
                    # If we are prompt pasting and current statement does not begin with a mode prefix, skip to next line
1226
                    elseif isprompt_paste
10✔
1227
                        while input[oldpos] != '\n'
316✔
1228
                            oldpos = nextind(input, oldpos)
151✔
1229
                            oldpos >= sizeof(input) && return
151✔
1230
                        end
149✔
1231
                        continue
7✔
1232
                    end
1233
                end
1234
                dump_tail = false
15✔
1235
                nl_pos = findfirst('\n', input[oldpos:end])
30✔
1236
                if s.current_mode == julia_prompt
15✔
1237
                    ast, pos = Meta.parse(input, oldpos, raise=false, depwarn=false)
12✔
1238
                    if (isa(ast, Expr) && (ast.head === :error || ast.head === :incomplete)) ||
22✔
1239
                            (pos > ncodeunits(input) && !endswith(input, '\n'))
1240
                        # remaining text is incomplete (an error, or parser ran to the end but didn't stop with a newline):
1241
                        # Insert all the remaining text as one line (might be empty)
1242
                        dump_tail = true
12✔
1243
                    end
1244
                elseif isnothing(nl_pos) # no newline at end, so just dump the tail into the prompt and don't execute
6✔
1245
                    dump_tail = true
×
1246
                elseif s.current_mode == shell_mode # handle multiline shell commands
3✔
1247
                    lines = split(input[oldpos:end], '\n')
4✔
1248
                    pos = oldpos + sizeof(lines[1]) + 1
2✔
1249
                    if length(lines) > 1
2✔
1250
                        for line in lines[2:end]
2✔
1251
                            # to be recognized as a multiline shell command, the lines must be indented to the
1252
                            # same prompt position
1253
                            if !startswith(line, ' '^curr_prompt_len)
5✔
1254
                                break
2✔
1255
                            end
1256
                            pos += sizeof(line) + 1
1✔
1257
                        end
3✔
1258
                    end
1259
                else
1260
                    pos = oldpos + nl_pos
1✔
1261
                end
1262
                if dump_tail
15✔
1263
                    tail = input[oldpos:end]
10✔
1264
                    if !firstline
5✔
1265
                        # strip leading whitespace, but only if it was the result of executing something
1266
                        # (avoids modifying the user's current leading wip line)
1267
                        tail = lstrip(tail)
1✔
1268
                    end
1269
                    if isprompt_paste # remove indentation spaces corresponding to the prompt
5✔
1270
                        tail = replace(tail, r"^"m * ' '^curr_prompt_len => "")
7✔
1271
                    end
1272
                    LineEdit.replace_line(s, tail, true)
10✔
1273
                    LineEdit.refresh_line(s)
5✔
1274
                    break
5✔
1275
                end
1276
                # get the line and strip leading and trailing whitespace
1277
                line = strip(input[oldpos:prevind(input, pos)])
20✔
1278
                if !isempty(line)
10✔
1279
                    if isprompt_paste # remove indentation spaces corresponding to the prompt
10✔
1280
                        line = replace(line, r"^"m * ' '^curr_prompt_len => "")
10✔
1281
                    end
1282
                    # put the line on the screen and history
1283
                    LineEdit.replace_line(s, line)
20✔
1284
                    LineEdit.commit_line(s)
10✔
1285
                    # execute the statement
1286
                    terminal = LineEdit.terminal(s) # This is slightly ugly but ok for now
10✔
1287
                    raw!(terminal, false) && disable_bracketed_paste(terminal)
10✔
1288
                    LineEdit.mode(s).on_done(s, LineEdit.buffer(s), true)
10✔
1289
                    raw!(terminal, true) && enable_bracketed_paste(terminal)
10✔
1290
                    LineEdit.push_undo(s) # when the last line is incomplete
10✔
1291
                end
1292
                oldpos = pos
10✔
1293
                firstline = false
×
1294
            end
23✔
1295
        end,
1296

1297
        # Open the editor at the location of a stackframe or method
1298
        # This is accessing a contextual variable that gets set in
1299
        # the show_backtrace and show_method_table functions.
1300
        "^Q" => (s::MIState, o...) -> begin
1301
            linfos = repl.last_shown_line_infos
1302
            str = String(take!(LineEdit.buffer(s)))
1303
            n = tryparse(Int, str)
1304
            n === nothing && @goto writeback
1305
            if n <= 0 || n > length(linfos) || startswith(linfos[n][1], "REPL[")
1306
                @goto writeback
1307
            end
1308
            try
1309
                InteractiveUtils.edit(Base.fixup_stdlib_path(linfos[n][1]), linfos[n][2])
1310
            catch ex
1311
                ex isa ProcessFailedException || ex isa Base.IOError || ex isa SystemError || rethrow()
1312
                @info "edit failed" _exception=ex
1313
            end
1314
            LineEdit.refresh_line(s)
1315
            return
1316
            @label writeback
1317
            write(LineEdit.buffer(s), str)
1318
            return
1319
        end,
1320
    )
1321

1322
    prefix_prompt, prefix_keymap = LineEdit.setup_prefix_keymap(hp, julia_prompt)
23✔
1323

1324
    a = Dict{Any,Any}[skeymap, repl_keymap, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
138✔
1325
    prepend!(a, extra_repl_keymap)
23✔
1326

1327
    julia_prompt.keymap_dict = LineEdit.keymap(a)
23✔
1328

1329
    mk = mode_keymap(julia_prompt)
23✔
1330

1331
    b = Dict{Any,Any}[skeymap, mk, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
138✔
1332
    prepend!(b, extra_repl_keymap)
23✔
1333

1334
    shell_mode.keymap_dict = help_mode.keymap_dict = LineEdit.keymap(b)
23✔
1335

1336
    allprompts = LineEdit.TextInterface[julia_prompt, shell_mode, help_mode, search_prompt, prefix_prompt]
23✔
1337
    return ModalInterface(allprompts)
23✔
1338
end
1339

1340
function run_frontend(repl::LineEditREPL, backend::REPLBackendRef)
19✔
1341
    repl.frontend_task = current_task()
19✔
1342
    d = REPLDisplay(repl)
19✔
1343
    dopushdisplay = repl.specialdisplay === nothing && !in(d,Base.Multimedia.displays)
32✔
1344
    dopushdisplay && pushdisplay(d)
19✔
1345
    if !isdefined(repl,:interface)
19✔
1346
        interface = repl.interface = setup_interface(repl)
22✔
1347
    else
1348
        interface = repl.interface
8✔
1349
    end
1350
    repl.backendref = backend
19✔
1351
    repl.mistate = LineEdit.init_state(terminal(repl), interface)
19✔
1352
    run_interface(terminal(repl), interface, repl.mistate)
19✔
1353
    # Terminate Backend
1354
    put!(backend.repl_channel, (nothing, -1))
19✔
1355
    dopushdisplay && popdisplay(d)
19✔
1356
    nothing
19✔
1357
end
1358

1359
## StreamREPL ##
1360

1361
mutable struct StreamREPL <: AbstractREPL
1362
    stream::IO
1363
    prompt_color::String
1364
    input_color::String
1365
    answer_color::String
1366
    waserror::Bool
1367
    frontend_task::Task
1368
    StreamREPL(stream,pc,ic,ac) = new(stream,pc,ic,ac,false)
×
1369
end
1370
StreamREPL(stream::IO) = StreamREPL(stream, Base.text_colors[:green], Base.input_color(), Base.answer_color())
×
1371
run_repl(stream::IO) = run_repl(StreamREPL(stream))
×
1372

1373
outstream(s::StreamREPL) = s.stream
×
1374
hascolor(s::StreamREPL) = get(s.stream, :color, false)::Bool
×
1375

1376
answer_color(r::LineEditREPL) = r.envcolors ? Base.answer_color() : r.answer_color
×
1377
answer_color(r::StreamREPL) = r.answer_color
×
1378
input_color(r::LineEditREPL) = r.envcolors ? Base.input_color() : r.input_color
×
1379
input_color(r::StreamREPL) = r.input_color
×
1380

1381
let matchend = Dict("\"" => r"\"", "\"\"\"" => r"\"\"\"", "'" => r"'",
1382
    "`" => r"`", "```" => r"```", "#" => r"$"m, "#=" => r"=#|#=")
1383
    global _rm_strings_and_comments
1384
    function _rm_strings_and_comments(code::Union{String,SubString{String}})
122✔
1385
        buf = IOBuffer(sizehint = sizeof(code))
244✔
1386
        pos = 1
×
1387
        while true
161✔
1388
            i = findnext(r"\"(?!\"\")|\"\"\"|'|`(?!``)|```|#(?!=)|#=", code, pos)
322✔
1389
            isnothing(i) && break
206✔
1390
            match = SubString(code, i)
45✔
1391
            j = findnext(matchend[match]::Regex, code, nextind(code, last(i)))
90✔
1392
            if match == "#=" # possibly nested
85✔
1393
                nested = 1
×
1394
                while j !== nothing
11✔
1395
                    nested += SubString(code, j) == "#=" ? +1 : -1
10✔
1396
                    iszero(nested) && break
10✔
1397
                    j = findnext(r"=#|#=", code, nextind(code, last(j)))
12✔
1398
                end
11✔
1399
            elseif match[1] != '#' # quote match: check non-escaped
40✔
1400
                while j !== nothing
37✔
1401
                    notbackslash = findprev(!=('\\'), code, prevind(code, first(j)))::Int
64✔
1402
                    isodd(first(j) - notbackslash) && break # not escaped
32✔
1403
                    j = findnext(matchend[match]::Regex, code, nextind(code, first(j)))
14✔
1404
                end
7✔
1405
            end
1406
            isnothing(j) && break
84✔
1407
            if match[1] == '#'
39✔
1408
                print(buf, SubString(code, pos, prevind(code, first(i))))
14✔
1409
            else
1410
                print(buf, SubString(code, pos, last(i)), ' ', SubString(code, j))
25✔
1411
            end
1412
            pos = nextind(code, last(j))
78✔
1413
        end
39✔
1414
        print(buf, SubString(code, pos, lastindex(code)))
122✔
1415
        return String(take!(buf))
122✔
1416
    end
1417
end
1418

1419
# heuristic function to decide if the presence of a semicolon
1420
# at the end of the expression was intended for suppressing output
1421
ends_with_semicolon(code::AbstractString) = ends_with_semicolon(String(code))
×
1422
ends_with_semicolon(code::Union{String,SubString{String}}) =
122✔
1423
    contains(_rm_strings_and_comments(code), r";\s*$")
1424

1425
function run_frontend(repl::StreamREPL, backend::REPLBackendRef)
×
1426
    repl.frontend_task = current_task()
×
1427
    have_color = hascolor(repl)
×
1428
    Base.banner(repl.stream)
×
1429
    d = REPLDisplay(repl)
×
1430
    dopushdisplay = !in(d,Base.Multimedia.displays)
×
1431
    dopushdisplay && pushdisplay(d)
×
1432
    while !eof(repl.stream)::Bool
×
1433
        if have_color
×
1434
            print(repl.stream,repl.prompt_color)
×
1435
        end
1436
        print(repl.stream, "julia> ")
×
1437
        if have_color
×
1438
            print(repl.stream, input_color(repl))
×
1439
        end
1440
        line = readline(repl.stream, keep=true)
×
1441
        if !isempty(line)
×
1442
            ast = Base.parse_input_line(line)
×
1443
            if have_color
×
1444
                print(repl.stream, Base.color_normal)
×
1445
            end
1446
            response = eval_with_backend(ast, backend)
×
1447
            print_response(repl, response, !ends_with_semicolon(line), have_color)
×
1448
        end
1449
    end
×
1450
    # Terminate Backend
1451
    put!(backend.repl_channel, (nothing, -1))
×
1452
    dopushdisplay && popdisplay(d)
×
1453
    nothing
×
1454
end
1455

1456
module Numbered
1457

1458
using ..REPL
1459

1460
__current_ast_transforms() = isdefined(Base, :active_repl_backend) ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1461

1462
function repl_eval_counter(hp)
813✔
1463
    return length(hp.history) - hp.start_idx
813✔
1464
end
1465

1466
function out_transform(@nospecialize(x), n::Ref{Int})
16✔
1467
    return Expr(:toplevel, get_usings!([], x)..., quote
16✔
1468
        let __temp_val_a72df459 = $x
1469
            $capture_result($n, __temp_val_a72df459)
1470
            __temp_val_a72df459
1471
        end
1472
    end)
1473
end
1474

1475
function get_usings!(usings, ex)
25✔
1476
    ex isa Expr || return usings
25✔
1477
    # get all `using` and `import` statements which are at the top level
1478
    for (i, arg) in enumerate(ex.args)
50✔
1479
        if Base.isexpr(arg, :toplevel)
73✔
1480
            get_usings!(usings, arg)
9✔
1481
        elseif Base.isexpr(arg, [:using, :import])
64✔
1482
            push!(usings, popat!(ex.args, i))
2✔
1483
        end
1484
    end
71✔
1485
    return usings
25✔
1486
end
1487

1488
function capture_result(n::Ref{Int}, @nospecialize(x))
16✔
1489
    n = n[]
16✔
1490
    mod = Base.MainInclude
16✔
1491
    if !isdefined(mod, :Out)
16✔
1492
        @eval mod global Out
1✔
1493
        @eval mod export Out
1✔
1494
        setglobal!(mod, :Out, Dict{Int, Any}())
1✔
1495
    end
1496
    if x !== getglobal(mod, :Out) && x !== nothing # remove this?
16✔
1497
        getglobal(mod, :Out)[n] = x
14✔
1498
    end
1499
    nothing
16✔
1500
end
1501

1502
function set_prompt(repl::LineEditREPL, n::Ref{Int})
1✔
1503
    julia_prompt = repl.interface.modes[1]
1✔
1504
    julia_prompt.prompt = function()
814✔
1505
        n[] = repl_eval_counter(julia_prompt.hist)+1
813✔
1506
        string("In [", n[], "]: ")
813✔
1507
    end
1508
    nothing
1✔
1509
end
1510

1511
function set_output_prefix(repl::LineEditREPL, n::Ref{Int})
1✔
1512
    julia_prompt = repl.interface.modes[1]
1✔
1513
    if REPL.hascolor(repl)
1✔
1514
        julia_prompt.output_prefix_prefix = Base.text_colors[:red]
1✔
1515
    end
1516
    julia_prompt.output_prefix = () -> string("Out[", n[], "]: ")
15✔
1517
    nothing
1✔
1518
end
1519

1520
function __current_ast_transforms(backend)
1✔
1521
    if backend === nothing
1✔
1522
        isdefined(Base, :active_repl_backend) ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1523
    else
1524
        backend.ast_transforms
1✔
1525
    end
1526
end
1527

1528

1529
function numbered_prompt!(repl::LineEditREPL=Base.active_repl, backend=nothing)
1✔
1530
    n = Ref{Int}(0)
1✔
1531
    set_prompt(repl, n)
1✔
1532
    set_output_prefix(repl, n)
1✔
1533
    push!(__current_ast_transforms(backend), @nospecialize(ast) -> out_transform(ast, n))
17✔
1534
    return
1✔
1535
end
1536

1537
"""
1538
    Out[n]
1539

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

1543
See also [`ans`](@ref).
1544
"""
1545
Base.MainInclude.Out
1546

1547
end
1548

1549
import .Numbered.numbered_prompt!
1550

1551
# this assignment won't survive precompilation,
1552
# but will stick if REPL is baked into a sysimg.
1553
# Needs to occur after this module is finished.
1554
Base.REPL_MODULE_REF[] = REPL
1555

1556
if Base.generating_output()
1557
    include("precompile.jl")
1558
end
1559

1560
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