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

JuliaLang / julia / #37430

pending completion
#37430

push

local

web-flow
Use `sum` in `count` for constant folding of type based predicates (#48454)

* Use `sum` in `count` for constant folding for type based predicates.

* Use existing `_bool` functionality for type assertion

---------

Co-authored-by: Sukera <Seelengrab@users.noreply.github.com>

1 of 1 new or added line in 1 file covered. (100.0%)

69787 of 75048 relevant lines covered (92.99%)

34159588.81 hits per line

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

40.0
/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
    import REPL
9
    term = REPL.Terminals.TTYTerminal("dumb", stdin, stdout, stderr)
10
    repl = REPL.LineEditREPL(term, true)
11
    REPL.run_repl(repl)
12
    ```
13
"""
14
module REPL
15

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

19
using Base.Meta, Sockets
20
import InteractiveUtils
21

22
export
23
    AbstractREPL,
24
    BasicREPL,
25
    LineEditREPL,
26
    StreamREPL
27

28
import Base:
29
    AbstractDisplay,
30
    display,
31
    show,
32
    AnyDict,
33
    ==
34

35
_displaysize(io::IO) = displaysize(io)::Tuple{Int,Int}
36

37
include("Terminals.jl")
38
using .Terminals
39

40
abstract type AbstractREPL end
41

42
include("options.jl")
43

44
include("LineEdit.jl")
45
using .LineEdit
46
import ..LineEdit:
47
    CompletionProvider,
48
    HistoryProvider,
49
    add_history,
50
    complete_line,
51
    history_next,
52
    history_next_prefix,
53
    history_prev,
54
    history_prev_prefix,
55
    history_first,
56
    history_last,
57
    history_search,
58
    accept_result,
59
    setmodifiers!,
60
    terminal,
61
    MIState,
62
    PromptState,
63
    TextInterface,
64
    mode_idx
65

66
include("REPLCompletions.jl")
67
using .REPLCompletions
68

69
include("TerminalMenus/TerminalMenus.jl")
70
include("docview.jl")
71

72
@nospecialize # use only declared type signatures
73

74
function __init__()
435✔
75
    Base.REPL_MODULE_REF[] = REPL
435✔
76
end
77

78
answer_color(::AbstractREPL) = ""
79

80
const JULIA_PROMPT = "julia> "
81
const PKG_PROMPT = "pkg> "
82
const SHELL_PROMPT = "shell> "
83
const HELP_PROMPT = "help?> "
84

85
mutable struct REPLBackend
86
    "channel for AST"
87
    repl_channel::Channel{Any}
88
    "channel for results: (value, iserror)"
89
    response_channel::Channel{Any}
90
    "flag indicating the state of this backend"
91
    in_eval::Bool
92
    "transformation functions to apply before evaluating expressions"
93
    ast_transforms::Vector{Any}
94
    "current backend task"
95
    backend_task::Task
96

97
    REPLBackend(repl_channel, response_channel, in_eval, ast_transforms=copy(repl_ast_transforms)) =
98
        new(repl_channel, response_channel, in_eval, ast_transforms)
99
end
100
REPLBackend() = REPLBackend(Channel(1), Channel(1), false)
101

102
"""
103
    softscope(ex)
104

105
Return a modified version of the parsed expression `ex` that uses
106
the REPL's "soft" scoping rules for global syntax blocks.
107
"""
108
function softscope(@nospecialize ex)
109
    if ex isa Expr
110
        h = ex.head
111
        if h === :toplevel
112
            ex′ = Expr(h)
113
            map!(softscope, resize!(ex′.args, length(ex.args)), ex.args)
114
            return ex′
115
        elseif h in (:meta, :import, :using, :export, :module, :error, :incomplete, :thunk)
116
            return ex
117
        elseif h === :global && all(x->isa(x, Symbol), ex.args)
118
            return ex
119
        else
120
            return Expr(:block, Expr(:softscope, true), ex)
121
        end
122
    end
123
    return ex
124
end
125

126
# Temporary alias until Documenter updates
127
const softscope! = softscope
128

129
const repl_ast_transforms = Any[softscope] # defaults for new REPL backends
130

131
# Allows an external package to add hooks into the code loading.
132
# The hook should take a Vector{Symbol} of package names and
133
# return true if all packages could be installed, false if not
134
# to e.g. install packages on demand
135
const install_packages_hooks = Any[]
136

137
function eval_user_input(@nospecialize(ast), backend::REPLBackend, mod::Module)
138
    lasterr = nothing
139
    Base.sigatomic_begin()
140
    while true
141
        try
142
            Base.sigatomic_end()
143
            if lasterr !== nothing
144
                put!(backend.response_channel, Pair{Any, Bool}(lasterr, true))
145
            else
146
                backend.in_eval = true
147
                if !isempty(install_packages_hooks)
148
                    check_for_missing_packages_and_run_hooks(ast)
149
                end
150
                for xf in backend.ast_transforms
151
                    ast = Base.invokelatest(xf, ast)
152
                end
153
                value = Core.eval(mod, ast)
154
                backend.in_eval = false
155
                setglobal!(mod, :ans, value)
156
                put!(backend.response_channel, Pair{Any, Bool}(value, false))
157
            end
158
            break
159
        catch err
160
            if lasterr !== nothing
161
                println("SYSTEM ERROR: Failed to report error to REPL frontend")
162
                println(err)
163
            end
164
            lasterr = current_exceptions()
165
        end
166
    end
167
    Base.sigatomic_end()
168
    nothing
169
end
170

171
function check_for_missing_packages_and_run_hooks(ast)
172
    isa(ast, Expr) || return
173
    mods = modules_to_be_loaded(ast)
174
    filter!(mod -> isnothing(Base.identify_package(String(mod))), mods) # keep missing modules
175
    if !isempty(mods)
176
        for f in install_packages_hooks
177
            Base.invokelatest(f, mods) && return
178
        end
179
    end
180
end
181

182
function modules_to_be_loaded(ast::Expr, mods::Vector{Symbol} = Symbol[])
183
    ast.head === :quote && return mods # don't search if it's not going to be run during this eval
184
    if ast.head === :using || ast.head === :import
185
        for arg in ast.args
186
            arg = arg::Expr
187
            arg1 = first(arg.args)
188
            if arg1 isa Symbol # i.e. `Foo`
189
                if arg1 != :. # don't include local imports
190
                    push!(mods, arg1)
191
                end
192
            else # i.e. `Foo: bar`
193
                push!(mods, first((arg1::Expr).args))
194
            end
195
        end
196
    end
197
    for arg in ast.args
198
        if isexpr(arg, (:block, :if, :using, :import))
199
            modules_to_be_loaded(arg, mods)
200
        end
201
    end
202
    filter!(mod -> !in(String(mod), ["Base", "Main", "Core"]), mods) # Exclude special non-package modules
203
    return unique(mods)
204
end
205

206
"""
207
    start_repl_backend(repl_channel::Channel, response_channel::Channel)
208

209
    Starts loop for REPL backend
210
    Returns a REPLBackend with backend_task assigned
211

212
    Deprecated since sync / async behavior cannot be selected
213
"""
214
function start_repl_backend(repl_channel::Channel{Any}, response_channel::Channel{Any}
215
                            ; get_module::Function = ()->Main)
216
    # Maintain legacy behavior of asynchronous backend
217
    backend = REPLBackend(repl_channel, response_channel, false)
218
    # Assignment will be made twice, but will be immediately available
219
    backend.backend_task = @async start_repl_backend(backend; get_module)
220
    return backend
221
end
222

223
"""
224
    start_repl_backend(backend::REPLBackend)
225

226
    Call directly to run backend loop on current Task.
227
    Use @async for run backend on new Task.
228

229
    Does not return backend until loop is finished.
230
"""
231
function start_repl_backend(backend::REPLBackend,  @nospecialize(consumer = x -> nothing); get_module::Function = ()->Main)
232
    backend.backend_task = Base.current_task()
233
    consumer(backend)
234
    repl_backend_loop(backend, get_module)
235
    return backend
236
end
237

238
function repl_backend_loop(backend::REPLBackend, get_module::Function)
239
    # include looks at this to determine the relative include path
240
    # nothing means cwd
241
    while true
242
        tls = task_local_storage()
243
        tls[:SOURCE_PATH] = nothing
244
        ast, show_value = take!(backend.repl_channel)
245
        if show_value == -1
246
            # exit flag
247
            break
248
        end
249
        eval_user_input(ast, backend, get_module())
250
    end
251
    return nothing
252
end
253

254
struct REPLDisplay{R<:AbstractREPL} <: AbstractDisplay
255
    repl::R
256
end
257

258
==(a::REPLDisplay, b::REPLDisplay) = a.repl === b.repl
259

260
function display(d::REPLDisplay, mime::MIME"text/plain", x)
×
261
    x = Ref{Any}(x)
×
262
    with_repl_linfo(d.repl) do io
×
263
        io = IOContext(io, :limit => true, :module => active_module(d)::Module)
264
        if d.repl isa LineEditREPL
265
            mistate = d.repl.mistate
266
            mode = LineEdit.mode(mistate)
267
            LineEdit.write_output_prefix(io, mode, get(io, :color, false)::Bool)
268
        end
269
        get(io, :color, false)::Bool && write(io, answer_color(d.repl))
270
        if isdefined(d.repl, :options) && isdefined(d.repl.options, :iocontext)
271
            # this can override the :limit property set initially
272
            io = foldl(IOContext, d.repl.options.iocontext, init=io)
273
        end
274
        show(io, mime, x[])
275
        println(io)
276
    end
277
    return nothing
×
278
end
279
display(d::REPLDisplay, x) = display(d, MIME("text/plain"), x)
×
280

281
function print_response(repl::AbstractREPL, response, show_value::Bool, have_color::Bool)
282
    repl.waserror = response[2]
283
    with_repl_linfo(repl) do io
284
        io = IOContext(io, :module => active_module(repl)::Module)
285
        print_response(io, response, show_value, have_color, specialdisplay(repl))
286
    end
287
    return nothing
288
end
289
function print_response(errio::IO, response, show_value::Bool, have_color::Bool, specialdisplay::Union{AbstractDisplay,Nothing}=nothing)
290
    Base.sigatomic_begin()
291
    val, iserr = response
292
    while true
293
        try
294
            Base.sigatomic_end()
295
            if iserr
296
                val = Base.scrub_repl_backtrace(val)
297
                Base.istrivialerror(val) || setglobal!(Main, :err, val)
298
                Base.invokelatest(Base.display_error, errio, val)
299
            else
300
                if val !== nothing && show_value
301
                    try
302
                        if specialdisplay === nothing
303
                            Base.invokelatest(display, val)
304
                        else
305
                            Base.invokelatest(display, specialdisplay, val)
306
                        end
307
                    catch
308
                        println(errio, "Error showing value of type ", typeof(val), ":")
309
                        rethrow()
310
                    end
311
                end
312
            end
313
            break
314
        catch ex
315
            if iserr
316
                println(errio) # an error during printing is likely to leave us mid-line
317
                println(errio, "SYSTEM (REPL): showing an error caused an error")
318
                try
319
                    excs = Base.scrub_repl_backtrace(current_exceptions())
320
                    setglobal!(Main, :err, excs)
321
                    Base.invokelatest(Base.display_error, errio, excs)
322
                catch e
323
                    # at this point, only print the name of the type as a Symbol to
324
                    # minimize the possibility of further errors.
325
                    println(errio)
326
                    println(errio, "SYSTEM (REPL): caught exception of type ", typeof(e).name.name,
327
                            " while trying to handle a nested exception; giving up")
328
                end
329
                break
330
            end
331
            val = current_exceptions()
332
            iserr = true
333
        end
334
    end
335
    Base.sigatomic_end()
336
    nothing
337
end
338

339
# A reference to a backend that is not mutable
340
struct REPLBackendRef
341
    repl_channel::Channel{Any}
342
    response_channel::Channel{Any}
343
end
344
REPLBackendRef(backend::REPLBackend) = REPLBackendRef(backend.repl_channel, backend.response_channel)
345

346
function destroy(ref::REPLBackendRef, state::Task)
347
    if istaskfailed(state)
348
        close(ref.repl_channel, TaskFailedException(state))
349
        close(ref.response_channel, TaskFailedException(state))
350
    end
351
    close(ref.repl_channel)
352
    close(ref.response_channel)
353
end
354

355
"""
356
    run_repl(repl::AbstractREPL)
357
    run_repl(repl, consumer = backend->nothing; backend_on_current_task = true)
358

359
    Main function to start the REPL
360

361
    consumer is an optional function that takes a REPLBackend as an argument
362
"""
363
function run_repl(repl::AbstractREPL, @nospecialize(consumer = x -> nothing); backend_on_current_task::Bool = true, backend = REPLBackend())
364
    backend_ref = REPLBackendRef(backend)
365
    cleanup = @task try
366
            destroy(backend_ref, t)
367
        catch e
368
            Core.print(Core.stderr, "\nINTERNAL ERROR: ")
369
            Core.println(Core.stderr, e)
370
            Core.println(Core.stderr, catch_backtrace())
371
        end
372
    get_module = () -> active_module(repl)
373
    if backend_on_current_task
374
        t = @async run_frontend(repl, backend_ref)
375
        errormonitor(t)
376
        Base._wait2(t, cleanup)
377
        start_repl_backend(backend, consumer; get_module)
378
    else
379
        t = @async start_repl_backend(backend, consumer; get_module)
380
        errormonitor(t)
381
        Base._wait2(t, cleanup)
382
        run_frontend(repl, backend_ref)
383
    end
384
    return backend
385
end
386

387
## BasicREPL ##
388

389
mutable struct BasicREPL <: AbstractREPL
390
    terminal::TextTerminal
391
    waserror::Bool
392
    frontend_task::Task
393
    BasicREPL(t) = new(t, false)
394
end
395

396
outstream(r::BasicREPL) = r.terminal
397
hascolor(r::BasicREPL) = hascolor(r.terminal)
398

399
function run_frontend(repl::BasicREPL, backend::REPLBackendRef)
400
    repl.frontend_task = current_task()
401
    d = REPLDisplay(repl)
402
    dopushdisplay = !in(d,Base.Multimedia.displays)
403
    dopushdisplay && pushdisplay(d)
404
    hit_eof = false
405
    while true
406
        Base.reseteof(repl.terminal)
407
        write(repl.terminal, JULIA_PROMPT)
408
        line = ""
409
        ast = nothing
410
        interrupted = false
411
        while true
412
            try
413
                line *= readline(repl.terminal, keep=true)
414
            catch e
415
                if isa(e,InterruptException)
416
                    try # raise the debugger if present
417
                        ccall(:jl_raise_debugger, Int, ())
418
                    catch
419
                    end
420
                    line = ""
421
                    interrupted = true
422
                    break
423
                elseif isa(e,EOFError)
424
                    hit_eof = true
425
                    break
426
                else
427
                    rethrow()
428
                end
429
            end
430
            ast = Base.parse_input_line(line)
431
            (isa(ast,Expr) && ast.head === :incomplete) || break
432
        end
433
        if !isempty(line)
434
            response = eval_with_backend(ast, backend)
435
            print_response(repl, response, !ends_with_semicolon(line), false)
436
        end
437
        write(repl.terminal, '\n')
438
        ((!interrupted && isempty(line)) || hit_eof) && break
439
    end
440
    # terminate backend
441
    put!(backend.repl_channel, (nothing, -1))
442
    dopushdisplay && popdisplay(d)
443
    nothing
444
end
445

446
## LineEditREPL ##
447

448
mutable struct LineEditREPL <: AbstractREPL
449
    t::TextTerminal
450
    hascolor::Bool
451
    prompt_color::String
452
    input_color::String
453
    answer_color::String
454
    shell_color::String
455
    help_color::String
456
    history_file::Bool
457
    in_shell::Bool
458
    in_help::Bool
459
    envcolors::Bool
460
    waserror::Bool
461
    specialdisplay::Union{Nothing,AbstractDisplay}
462
    options::Options
463
    mistate::Union{MIState,Nothing}
464
    last_shown_line_infos::Vector{Tuple{String,Int}}
465
    interface::ModalInterface
466
    backendref::REPLBackendRef
467
    frontend_task::Task
468
    function LineEditREPL(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,history_file,in_shell,in_help,envcolors)
469
        opts = Options()
470
        opts.hascolor = hascolor
471
        if !hascolor
472
            opts.beep_colors = [""]
473
        end
474
        new(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,history_file,in_shell,
475
            in_help,envcolors,false,nothing, opts, nothing, Tuple{String,Int}[])
476
    end
477
end
478
outstream(r::LineEditREPL) = (t = r.t; t isa TTYTerminal ? t.out_stream : t)
479
specialdisplay(r::LineEditREPL) = r.specialdisplay
480
specialdisplay(r::AbstractREPL) = nothing
481
terminal(r::LineEditREPL) = r.t
482
hascolor(r::LineEditREPL) = r.hascolor
483

484
LineEditREPL(t::TextTerminal, hascolor::Bool, envcolors::Bool=false) =
485
    LineEditREPL(t, hascolor,
486
        hascolor ? Base.text_colors[:green] : "",
487
        hascolor ? Base.input_color() : "",
488
        hascolor ? Base.answer_color() : "",
489
        hascolor ? Base.text_colors[:red] : "",
490
        hascolor ? Base.text_colors[:yellow] : "",
491
        false, false, false, envcolors
492
    )
493

494
mutable struct REPLCompletionProvider <: CompletionProvider
495
    modifiers::LineEdit.Modifiers
496
end
497
REPLCompletionProvider() = REPLCompletionProvider(LineEdit.Modifiers())
498

499
mutable struct ShellCompletionProvider <: CompletionProvider end
500
struct LatexCompletions <: CompletionProvider end
501

502
function active_module() # this method is also called from Base
59,047✔
503
    isdefined(Base, :active_repl) || return Main
118,094✔
504
    return active_module(Base.active_repl::AbstractREPL)
×
505
end
506
active_module((; mistate)::LineEditREPL) = mistate === nothing ? Main : mistate.active_module
507
active_module(::AbstractREPL) = Main
508
active_module(d::REPLDisplay) = active_module(d.repl)
509

510
setmodifiers!(c::REPLCompletionProvider, m::LineEdit.Modifiers) = c.modifiers = m
511

512
"""
513
    activate(mod::Module=Main)
514

515
Set `mod` as the default contextual module in the REPL,
516
both for evaluating expressions and printing them.
517
"""
518
function activate(mod::Module=Main)
519
    mistate = (Base.active_repl::LineEditREPL).mistate
520
    mistate === nothing && return nothing
521
    mistate.active_module = mod
522
    Base.load_InteractiveUtils(mod)
523
    return nothing
524
end
525

526
beforecursor(buf::IOBuffer) = String(buf.data[1:buf.ptr-1])
527

528
function complete_line(c::REPLCompletionProvider, s::PromptState, mod::Module)
529
    partial = beforecursor(s.input_buffer)
530
    full = LineEdit.input_string(s)
531
    ret, range, should_complete = completions(full, lastindex(partial), mod, c.modifiers.shift)
532
    c.modifiers = LineEdit.Modifiers()
533
    return unique!(map(completion_text, ret)), partial[range], should_complete
534
end
535

536
function complete_line(c::ShellCompletionProvider, s::PromptState)
537
    # First parse everything up to the current position
538
    partial = beforecursor(s.input_buffer)
539
    full = LineEdit.input_string(s)
540
    ret, range, should_complete = shell_completions(full, lastindex(partial))
541
    return unique!(map(completion_text, ret)), partial[range], should_complete
542
end
543

544
function complete_line(c::LatexCompletions, s)
545
    partial = beforecursor(LineEdit.buffer(s))
546
    full = LineEdit.input_string(s)::String
547
    ret, range, should_complete = bslash_completions(full, lastindex(partial))[2]
548
    return unique!(map(completion_text, ret)), partial[range], should_complete
549
end
550

551
with_repl_linfo(f, repl) = f(outstream(repl))
552
function with_repl_linfo(f, repl::LineEditREPL)
553
    linfos = Tuple{String,Int}[]
554
    io = IOContext(outstream(repl), :last_shown_line_infos => linfos)
555
    f(io)
556
    if !isempty(linfos)
557
        repl.last_shown_line_infos = linfos
558
    end
559
    nothing
560
end
561

562
mutable struct REPLHistoryProvider <: HistoryProvider
563
    history::Vector{String}
564
    file_path::String
565
    history_file::Union{Nothing,IO}
566
    start_idx::Int
567
    cur_idx::Int
568
    last_idx::Int
569
    last_buffer::IOBuffer
570
    last_mode::Union{Nothing,Prompt}
571
    mode_mapping::Dict{Symbol,Prompt}
572
    modes::Vector{Symbol}
573
end
574
REPLHistoryProvider(mode_mapping::Dict{Symbol}) =
575
    REPLHistoryProvider(String[], "", nothing, 0, 0, -1, IOBuffer(),
576
                        nothing, mode_mapping, UInt8[])
577

578
invalid_history_message(path::String) = """
579
Invalid history file ($path) format:
580
If you have a history file left over from an older version of Julia,
581
try renaming or deleting it.
582
Invalid character: """
583

584
munged_history_message(path::String) = """
585
Invalid history file ($path) format:
586
An editor may have converted tabs to spaces at line """
587

588
function hist_open_file(hp::REPLHistoryProvider)
589
    f = open(hp.file_path, read=true, write=true, create=true)
590
    hp.history_file = f
591
    seekend(f)
592
end
593

594
function hist_from_file(hp::REPLHistoryProvider, path::String)
595
    getline(lines, i) = i > length(lines) ? "" : lines[i]
596
    file_lines = readlines(path)
597
    countlines = 0
598
    while true
599
        # First parse the metadata that starts with '#' in particular the REPL mode
600
        countlines += 1
601
        line = getline(file_lines, countlines)
602
        mode = :julia
603
        isempty(line) && break
604
        line[1] != '#' &&
605
            error(invalid_history_message(path), repr(line[1]), " at line ", countlines)
606
        while !isempty(line)
607
            startswith(line, '#') || break
608
            if startswith(line, "# mode: ")
609
                mode = Symbol(SubString(line, 9))
610
            end
611
            countlines += 1
612
            line = getline(file_lines, countlines)
613
        end
614
        isempty(line) && break
615

616
        # Now parse the code for the current REPL mode
617
        line[1] == ' '  &&
618
            error(munged_history_message(path), countlines)
619
        line[1] != '\t' &&
620
            error(invalid_history_message(path), repr(line[1]), " at line ", countlines)
621
        lines = String[]
622
        while !isempty(line)
623
            push!(lines, chomp(SubString(line, 2)))
624
            next_line = getline(file_lines, countlines+1)
625
            isempty(next_line) && break
626
            first(next_line) == ' '  && error(munged_history_message(path), countlines)
627
            # A line not starting with a tab means we are done with code for this entry
628
            first(next_line) != '\t' && break
629
            countlines += 1
630
            line = getline(file_lines, countlines)
631
        end
632
        push!(hp.modes, mode)
633
        push!(hp.history, join(lines, '\n'))
634
    end
635
    hp.start_idx = length(hp.history)
636
    return hp
637
end
638

639
function add_history(hist::REPLHistoryProvider, s::PromptState)
640
    str = rstrip(String(take!(copy(s.input_buffer))))
641
    isempty(strip(str)) && return
642
    mode = mode_idx(hist, LineEdit.mode(s))
643
    !isempty(hist.history) &&
644
        isequal(mode, hist.modes[end]) && str == hist.history[end] && return
645
    push!(hist.modes, mode)
646
    push!(hist.history, str)
647
    hist.history_file === nothing && return
648
    entry = """
649
    # time: $(Libc.strftime("%Y-%m-%d %H:%M:%S %Z", time()))
650
    # mode: $mode
651
    $(replace(str, r"^"ms => "\t"))
652
    """
653
    # TODO: write-lock history file
654
    try
655
        seekend(hist.history_file)
656
    catch err
657
        (err isa SystemError) || rethrow()
658
        # File handle might get stale after a while, especially under network file systems
659
        # If this doesn't fix it (e.g. when file is deleted), we'll end up rethrowing anyway
660
        hist_open_file(hist)
661
    end
662
    print(hist.history_file, entry)
663
    flush(hist.history_file)
664
    nothing
665
end
666

667
function history_move(s::Union{LineEdit.MIState,LineEdit.PrefixSearchState}, hist::REPLHistoryProvider, idx::Int, save_idx::Int = hist.cur_idx)
668
    max_idx = length(hist.history) + 1
669
    @assert 1 <= hist.cur_idx <= max_idx
670
    (1 <= idx <= max_idx) || return :none
671
    idx != hist.cur_idx || return :none
672

673
    # save the current line
674
    if save_idx == max_idx
675
        hist.last_mode = LineEdit.mode(s)
676
        hist.last_buffer = copy(LineEdit.buffer(s))
677
    else
678
        hist.history[save_idx] = LineEdit.input_string(s)
679
        hist.modes[save_idx] = mode_idx(hist, LineEdit.mode(s))
680
    end
681

682
    # load the saved line
683
    if idx == max_idx
684
        last_buffer = hist.last_buffer
685
        LineEdit.transition(s, hist.last_mode) do
686
            LineEdit.replace_line(s, last_buffer)
687
        end
688
        hist.last_mode = nothing
689
        hist.last_buffer = IOBuffer()
690
    else
691
        if haskey(hist.mode_mapping, hist.modes[idx])
692
            LineEdit.transition(s, hist.mode_mapping[hist.modes[idx]]) do
693
                LineEdit.replace_line(s, hist.history[idx])
694
            end
695
        else
696
            return :skip
697
        end
698
    end
699
    hist.cur_idx = idx
700

701
    return :ok
702
end
703

704
# REPL History can also transitions modes
705
function LineEdit.accept_result_newmode(hist::REPLHistoryProvider)
706
    if 1 <= hist.cur_idx <= length(hist.modes)
707
        return hist.mode_mapping[hist.modes[hist.cur_idx]]
708
    end
709
    return nothing
710
end
711

712
function history_prev(s::LineEdit.MIState, hist::REPLHistoryProvider,
713
                      num::Int=1, save_idx::Int = hist.cur_idx)
714
    num <= 0 && return history_next(s, hist, -num, save_idx)
715
    hist.last_idx = -1
716
    m = history_move(s, hist, hist.cur_idx-num, save_idx)
717
    if m === :ok
718
        LineEdit.move_input_start(s)
719
        LineEdit.reset_key_repeats(s) do
720
            LineEdit.move_line_end(s)
721
        end
722
        return LineEdit.refresh_line(s)
723
    elseif m === :skip
724
        return history_prev(s, hist, num+1, save_idx)
725
    else
726
        return Terminals.beep(s)
727
    end
728
end
729

730
function history_next(s::LineEdit.MIState, hist::REPLHistoryProvider,
731
                      num::Int=1, save_idx::Int = hist.cur_idx)
732
    if num == 0
733
        Terminals.beep(s)
734
        return
735
    end
736
    num < 0 && return history_prev(s, hist, -num, save_idx)
737
    cur_idx = hist.cur_idx
738
    max_idx = length(hist.history) + 1
739
    if cur_idx == max_idx && 0 < hist.last_idx
740
        # issue #6312
741
        cur_idx = hist.last_idx
742
        hist.last_idx = -1
743
    end
744
    m = history_move(s, hist, cur_idx+num, save_idx)
745
    if m === :ok
746
        LineEdit.move_input_end(s)
747
        return LineEdit.refresh_line(s)
748
    elseif m === :skip
749
        return history_next(s, hist, num+1, save_idx)
750
    else
751
        return Terminals.beep(s)
752
    end
753
end
754

755
history_first(s::LineEdit.MIState, hist::REPLHistoryProvider) =
756
    history_prev(s, hist, hist.cur_idx - 1 -
757
                 (hist.cur_idx > hist.start_idx+1 ? hist.start_idx : 0))
758

759
history_last(s::LineEdit.MIState, hist::REPLHistoryProvider) =
760
    history_next(s, hist, length(hist.history) - hist.cur_idx + 1)
761

762
function history_move_prefix(s::LineEdit.PrefixSearchState,
763
                             hist::REPLHistoryProvider,
764
                             prefix::AbstractString,
765
                             backwards::Bool,
766
                             cur_idx::Int = hist.cur_idx)
767
    cur_response = String(take!(copy(LineEdit.buffer(s))))
768
    # when searching forward, start at last_idx
769
    if !backwards && hist.last_idx > 0
770
        cur_idx = hist.last_idx
771
    end
772
    hist.last_idx = -1
773
    max_idx = length(hist.history)+1
774
    idxs = backwards ? ((cur_idx-1):-1:1) : ((cur_idx+1):1:max_idx)
775
    for idx in idxs
776
        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)))
777
            m = history_move(s, hist, idx)
778
            if m === :ok
779
                if idx == max_idx
780
                    # on resuming the in-progress edit, leave the cursor where the user last had it
781
                elseif isempty(prefix)
782
                    # on empty prefix search, move cursor to the end
783
                    LineEdit.move_input_end(s)
784
                else
785
                    # otherwise, keep cursor at the prefix position as a visual cue
786
                    seek(LineEdit.buffer(s), sizeof(prefix))
787
                end
788
                LineEdit.refresh_line(s)
789
                return :ok
790
            elseif m === :skip
791
                return history_move_prefix(s,hist,prefix,backwards,idx)
792
            end
793
        end
794
    end
795
    Terminals.beep(s)
796
    nothing
797
end
798
history_next_prefix(s::LineEdit.PrefixSearchState, hist::REPLHistoryProvider, prefix::AbstractString) =
799
    history_move_prefix(s, hist, prefix, false)
800
history_prev_prefix(s::LineEdit.PrefixSearchState, hist::REPLHistoryProvider, prefix::AbstractString) =
801
    history_move_prefix(s, hist, prefix, true)
802

803
function history_search(hist::REPLHistoryProvider, query_buffer::IOBuffer, response_buffer::IOBuffer,
804
                        backwards::Bool=false, skip_current::Bool=false)
805

806
    qpos = position(query_buffer)
807
    qpos > 0 || return true
808
    searchdata = beforecursor(query_buffer)
809
    response_str = String(take!(copy(response_buffer)))
810

811
    # Alright, first try to see if the current match still works
812
    a = position(response_buffer) + 1 # position is zero-indexed
813
    # FIXME: I'm pretty sure this is broken since it uses an index
814
    # into the search data to index into the response string
815
    b = a + sizeof(searchdata)
816
    b = b ≤ ncodeunits(response_str) ? prevind(response_str, b) : b-1
817
    b = min(lastindex(response_str), b) # ensure that b is valid
818

819
    searchstart = backwards ? b : a
820
    if searchdata == response_str[a:b]
821
        if skip_current
822
            searchstart = backwards ? prevind(response_str, b) : nextind(response_str, a)
823
        else
824
            return true
825
        end
826
    end
827

828
    # Start searching
829
    # First the current response buffer
830
    if 1 <= searchstart <= lastindex(response_str)
831
        match = backwards ? findprev(searchdata, response_str, searchstart) :
832
                            findnext(searchdata, response_str, searchstart)
833
        if match !== nothing
834
            seek(response_buffer, first(match) - 1)
835
            return true
836
        end
837
    end
838

839
    # Now search all the other buffers
840
    idxs = backwards ? ((hist.cur_idx-1):-1:1) : ((hist.cur_idx+1):1:length(hist.history))
841
    for idx in idxs
842
        h = hist.history[idx]
843
        match = backwards ? findlast(searchdata, h) : findfirst(searchdata, h)
844
        if match !== nothing && h != response_str && haskey(hist.mode_mapping, hist.modes[idx])
845
            truncate(response_buffer, 0)
846
            write(response_buffer, h)
847
            seek(response_buffer, first(match) - 1)
848
            hist.cur_idx = idx
849
            return true
850
        end
851
    end
852

853
    return false
854
end
855

856
function history_reset_state(hist::REPLHistoryProvider)
857
    if hist.cur_idx != length(hist.history) + 1
858
        hist.last_idx = hist.cur_idx
859
        hist.cur_idx = length(hist.history) + 1
860
    end
861
    nothing
862
end
863
LineEdit.reset_state(hist::REPLHistoryProvider) = history_reset_state(hist)
864

865
function return_callback(s)
866
    ast = Base.parse_input_line(String(take!(copy(LineEdit.buffer(s)))), depwarn=false)
867
    return !(isa(ast, Expr) && ast.head === :incomplete)
868
end
869

870
find_hist_file() = get(ENV, "JULIA_HISTORY",
871
                       !isempty(DEPOT_PATH) ? joinpath(DEPOT_PATH[1], "logs", "repl_history.jl") :
872
                       error("DEPOT_PATH is empty and and ENV[\"JULIA_HISTORY\"] not set."))
873

874
backend(r::AbstractREPL) = r.backendref
875

876
function eval_with_backend(ast, backend::REPLBackendRef)
877
    put!(backend.repl_channel, (ast, 1))
878
    return take!(backend.response_channel) # (val, iserr)
879
end
880

881
function respond(f, repl, main; pass_empty::Bool = false, suppress_on_semicolon::Bool = true)
882
    return function do_respond(s::MIState, buf, ok::Bool)
883
        if !ok
884
            return transition(s, :abort)
885
        end
886
        line = String(take!(buf)::Vector{UInt8})
887
        if !isempty(line) || pass_empty
888
            reset(repl)
889
            local response
890
            try
891
                ast = Base.invokelatest(f, line)
892
                response = eval_with_backend(ast, backend(repl))
893
            catch
894
                response = Pair{Any, Bool}(current_exceptions(), true)
895
            end
896
            hide_output = suppress_on_semicolon && ends_with_semicolon(line)
897
            print_response(repl, response, !hide_output, hascolor(repl))
898
        end
899
        prepare_next(repl)
900
        reset_state(s)
901
        return s.current_mode.sticky ? true : transition(s, main)
902
    end
903
end
904

905
function reset(repl::LineEditREPL)
906
    raw!(repl.t, false)
907
    hascolor(repl) && print(repl.t, Base.text_colors[:normal])
908
    nothing
909
end
910

911
function prepare_next(repl::LineEditREPL)
912
    println(terminal(repl))
913
end
914

915
function mode_keymap(julia_prompt::Prompt)
916
    AnyDict(
917
    '\b' => function (s::MIState,o...)
918
        if isempty(s) || position(LineEdit.buffer(s)) == 0
919
            buf = copy(LineEdit.buffer(s))
920
            transition(s, julia_prompt) do
921
                LineEdit.state(s, julia_prompt).input_buffer = buf
922
            end
923
        else
924
            LineEdit.edit_backspace(s)
925
        end
926
    end,
927
    "^C" => function (s::MIState,o...)
928
        LineEdit.move_input_end(s)
929
        LineEdit.refresh_line(s)
930
        print(LineEdit.terminal(s), "^C\n\n")
931
        transition(s, julia_prompt)
932
        transition(s, :reset)
933
        LineEdit.refresh_line(s)
934
    end)
935
end
936

937
repl_filename(repl, hp::REPLHistoryProvider) = "REPL[$(max(length(hp.history)-hp.start_idx, 1))]"
938
repl_filename(repl, hp) = "REPL"
939

940
const JL_PROMPT_PASTE = Ref(true)
941
enable_promptpaste(v::Bool) = JL_PROMPT_PASTE[] = v
942

943
function contextual_prompt(repl::LineEditREPL, prompt::Union{String,Function})
944
    function ()
945
        mod = active_module(repl)
946
        prefix = mod == Main ? "" : string('(', mod, ") ")
947
        pr = prompt isa String ? prompt : prompt()
948
        prefix * pr
949
    end
950
end
951

952
setup_interface(
953
    repl::LineEditREPL;
954
    # those keyword arguments may be deprecated eventually in favor of the Options mechanism
955
    hascolor::Bool = repl.options.hascolor,
956
    extra_repl_keymap::Any = repl.options.extra_keymap
957
) = setup_interface(repl, hascolor, extra_repl_keymap)
958

959
# This non keyword method can be precompiled which is important
960
function setup_interface(
961
    repl::LineEditREPL,
962
    hascolor::Bool,
963
    extra_repl_keymap::Any, # Union{Dict,Vector{<:Dict}},
964
)
965
    # The precompile statement emitter has problem outputting valid syntax for the
966
    # type of `Union{Dict,Vector{<:Dict}}` (see #28808).
967
    # This function is however important to precompile for REPL startup time, therefore,
968
    # make the type Any and just assert that we have the correct type below.
969
    @assert extra_repl_keymap isa Union{Dict,Vector{<:Dict}}
970

971
    ###
972
    #
973
    # This function returns the main interface that describes the REPL
974
    # functionality, it is called internally by functions that setup a
975
    # Terminal-based REPL frontend.
976
    #
977
    # See run_frontend(repl::LineEditREPL, backend::REPLBackendRef)
978
    # for usage
979
    #
980
    ###
981

982
    ###
983
    # We setup the interface in two stages.
984
    # First, we set up all components (prompt,rsearch,shell,help)
985
    # Second, we create keymaps with appropriate transitions between them
986
    #   and assign them to the components
987
    #
988
    ###
989

990
    ############################### Stage I ################################
991

992
    # This will provide completions for REPL and help mode
993
    replc = REPLCompletionProvider()
994

995
    # Set up the main Julia prompt
996
    julia_prompt = Prompt(contextual_prompt(repl, JULIA_PROMPT);
997
        # Copy colors from the prompt object
998
        prompt_prefix = hascolor ? repl.prompt_color : "",
999
        prompt_suffix = hascolor ?
1000
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1001
        repl = repl,
1002
        complete = replc,
1003
        on_enter = return_callback)
1004

1005
    # Setup help mode
1006
    help_mode = Prompt(contextual_prompt(repl, "help?> "),
1007
        prompt_prefix = hascolor ? repl.help_color : "",
1008
        prompt_suffix = hascolor ?
1009
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1010
        repl = repl,
1011
        complete = replc,
1012
        # When we're done transform the entered line into a call to helpmode function
1013
        on_done = respond(line::String->helpmode(outstream(repl), line, repl.mistate.active_module),
1014
                          repl, julia_prompt, pass_empty=true, suppress_on_semicolon=false))
1015

1016

1017
    # Set up shell mode
1018
    shell_mode = Prompt(SHELL_PROMPT;
1019
        prompt_prefix = hascolor ? repl.shell_color : "",
1020
        prompt_suffix = hascolor ?
1021
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1022
        repl = repl,
1023
        complete = ShellCompletionProvider(),
1024
        # Transform "foo bar baz" into `foo bar baz` (shell quoting)
1025
        # and pass into Base.repl_cmd for processing (handles `ls` and `cd`
1026
        # special)
1027
        on_done = respond(repl, julia_prompt) do line
1028
            Expr(:call, :(Base.repl_cmd),
1029
                :(Base.cmd_gen($(Base.shell_parse(line::String)[1]))),
1030
                outstream(repl))
1031
        end,
1032
        sticky = true)
1033

1034

1035
    ################################# Stage II #############################
1036

1037
    # Setup history
1038
    # We will have a unified history for all REPL modes
1039
    hp = REPLHistoryProvider(Dict{Symbol,Prompt}(:julia => julia_prompt,
1040
                                                 :shell => shell_mode,
1041
                                                 :help  => help_mode))
1042
    if repl.history_file
1043
        try
1044
            hist_path = find_hist_file()
1045
            mkpath(dirname(hist_path))
1046
            hp.file_path = hist_path
1047
            hist_open_file(hp)
1048
            finalizer(replc) do replc
1049
                close(hp.history_file)
1050
            end
1051
            hist_from_file(hp, hist_path)
1052
        catch
1053
            # use REPL.hascolor to avoid using the local variable with the same name
1054
            print_response(repl, Pair{Any, Bool}(current_exceptions(), true), true, REPL.hascolor(repl))
1055
            println(outstream(repl))
1056
            @info "Disabling history file for this session"
1057
            repl.history_file = false
1058
        end
1059
    end
1060
    history_reset_state(hp)
1061
    julia_prompt.hist = hp
1062
    shell_mode.hist = hp
1063
    help_mode.hist = hp
1064

1065
    julia_prompt.on_done = respond(x->Base.parse_input_line(x,filename=repl_filename(repl,hp)), repl, julia_prompt)
1066

1067

1068
    search_prompt, skeymap = LineEdit.setup_search_keymap(hp)
1069
    search_prompt.complete = LatexCompletions()
1070

1071
    shell_prompt_len = length(SHELL_PROMPT)
1072
    help_prompt_len = length(HELP_PROMPT)
1073
    jl_prompt_regex = r"^In \[[0-9]+\]: |^(?:\(.+\) )?julia> "
1074
    pkg_prompt_regex = r"^(?:\(.+\) )?pkg> "
1075

1076
    # Canonicalize user keymap input
1077
    if isa(extra_repl_keymap, Dict)
1078
        extra_repl_keymap = AnyDict[extra_repl_keymap]
1079
    end
1080

1081
    repl_keymap = AnyDict(
1082
        ';' => function (s::MIState,o...)
1083
            if isempty(s) || position(LineEdit.buffer(s)) == 0
1084
                buf = copy(LineEdit.buffer(s))
1085
                transition(s, shell_mode) do
1086
                    LineEdit.state(s, shell_mode).input_buffer = buf
1087
                end
1088
            else
1089
                edit_insert(s, ';')
1090
            end
1091
        end,
1092
        '?' => function (s::MIState,o...)
1093
            if isempty(s) || position(LineEdit.buffer(s)) == 0
1094
                buf = copy(LineEdit.buffer(s))
1095
                transition(s, help_mode) do
1096
                    LineEdit.state(s, help_mode).input_buffer = buf
1097
                end
1098
            else
1099
                edit_insert(s, '?')
1100
            end
1101
        end,
1102

1103
        # Bracketed Paste Mode
1104
        "\e[200~" => (s::MIState,o...)->begin
1105
            input = LineEdit.bracketed_paste(s) # read directly from s until reaching the end-bracketed-paste marker
1106
            sbuffer = LineEdit.buffer(s)
1107
            curspos = position(sbuffer)
1108
            seek(sbuffer, 0)
1109
            shouldeval = (bytesavailable(sbuffer) == curspos && !occursin(UInt8('\n'), sbuffer))
1110
            seek(sbuffer, curspos)
1111
            if curspos == 0
1112
                # if pasting at the beginning, strip leading whitespace
1113
                input = lstrip(input)
1114
            end
1115
            if !shouldeval
1116
                # when pasting in the middle of input, just paste in place
1117
                # don't try to execute all the WIP, since that's rather confusing
1118
                # and is often ill-defined how it should behave
1119
                edit_insert(s, input)
1120
                return
1121
            end
1122
            LineEdit.push_undo(s)
1123
            edit_insert(sbuffer, input)
1124
            input = String(take!(sbuffer))
1125
            oldpos = firstindex(input)
1126
            firstline = true
1127
            isprompt_paste = false
1128
            curr_prompt_len = 0
1129
            pasting_help = false
1130

1131
            while oldpos <= lastindex(input) # loop until all lines have been executed
1132
                if JL_PROMPT_PASTE[]
1133
                    # Check if the next statement starts with a prompt i.e. "julia> ", in that case
1134
                    # skip it. But first skip whitespace unless pasting in a docstring which may have
1135
                    # indented prompt examples that we don't want to execute
1136
                    while input[oldpos] in (pasting_help ? ('\n') : ('\n', ' ', '\t'))
1137
                        oldpos = nextind(input, oldpos)
1138
                        oldpos >= sizeof(input) && return
1139
                    end
1140
                    substr = SubString(input, oldpos)
1141
                    # Check if input line starts with "julia> ", remove it if we are in prompt paste mode
1142
                    if (firstline || isprompt_paste) && startswith(substr, jl_prompt_regex)
1143
                        detected_jl_prompt = match(jl_prompt_regex, substr).match
1144
                        isprompt_paste = true
1145
                        curr_prompt_len = sizeof(detected_jl_prompt)
1146
                        oldpos += curr_prompt_len
1147
                        transition(s, julia_prompt)
1148
                        pasting_help = false
1149
                    # Check if input line starts with "pkg> " or "(...) pkg> ", remove it if we are in prompt paste mode and switch mode
1150
                    elseif (firstline || isprompt_paste) && startswith(substr, pkg_prompt_regex)
1151
                        detected_pkg_prompt = match(pkg_prompt_regex, substr).match
1152
                        isprompt_paste = true
1153
                        curr_prompt_len = sizeof(detected_pkg_prompt)
1154
                        oldpos += curr_prompt_len
1155
                        Base.active_repl.interface.modes[1].keymap_dict[']'](s, o...)
1156
                        pasting_help = false
1157
                    # Check if input line starts with "shell> ", remove it if we are in prompt paste mode and switch mode
1158
                    elseif (firstline || isprompt_paste) && startswith(substr, SHELL_PROMPT)
1159
                        isprompt_paste = true
1160
                        oldpos += shell_prompt_len
1161
                        curr_prompt_len = shell_prompt_len
1162
                        transition(s, shell_mode)
1163
                        pasting_help = false
1164
                    # Check if input line starts with "help?> ", remove it if we are in prompt paste mode and switch mode
1165
                    elseif (firstline || isprompt_paste) && startswith(substr, HELP_PROMPT)
1166
                        isprompt_paste = true
1167
                        oldpos += help_prompt_len
1168
                        curr_prompt_len = help_prompt_len
1169
                        transition(s, help_mode)
1170
                        pasting_help = true
1171
                    # If we are prompt pasting and current statement does not begin with a mode prefix, skip to next line
1172
                    elseif isprompt_paste
1173
                        while input[oldpos] != '\n'
1174
                            oldpos = nextind(input, oldpos)
1175
                            oldpos >= sizeof(input) && return
1176
                        end
1177
                        continue
1178
                    end
1179
                end
1180
                dump_tail = false
1181
                nl_pos = findfirst('\n', input[oldpos:end])
1182
                if s.current_mode == julia_prompt
1183
                    ast, pos = Meta.parse(input, oldpos, raise=false, depwarn=false)
1184
                    if (isa(ast, Expr) && (ast.head === :error || ast.head === :incomplete)) ||
1185
                            (pos > ncodeunits(input) && !endswith(input, '\n'))
1186
                        # remaining text is incomplete (an error, or parser ran to the end but didn't stop with a newline):
1187
                        # Insert all the remaining text as one line (might be empty)
1188
                        dump_tail = true
1189
                    end
1190
                elseif isnothing(nl_pos) # no newline at end, so just dump the tail into the prompt and don't execute
1191
                    dump_tail = true
1192
                elseif s.current_mode == shell_mode # handle multiline shell commands
1193
                    lines = split(input[oldpos:end], '\n')
1194
                    pos = oldpos + sizeof(lines[1]) + 1
1195
                    if length(lines) > 1
1196
                        for line in lines[2:end]
1197
                            # to be recognized as a multiline shell command, the lines must be indented to the
1198
                            # same prompt position
1199
                            if !startswith(line, ' '^curr_prompt_len)
1200
                                break
1201
                            end
1202
                            pos += sizeof(line) + 1
1203
                        end
1204
                    end
1205
                else
1206
                    pos = oldpos + nl_pos
1207
                end
1208
                if dump_tail
1209
                    tail = input[oldpos:end]
1210
                    if !firstline
1211
                        # strip leading whitespace, but only if it was the result of executing something
1212
                        # (avoids modifying the user's current leading wip line)
1213
                        tail = lstrip(tail)
1214
                    end
1215
                    if isprompt_paste # remove indentation spaces corresponding to the prompt
1216
                        tail = replace(tail, r"^"m * ' '^curr_prompt_len => "")
1217
                    end
1218
                    LineEdit.replace_line(s, tail, true)
1219
                    LineEdit.refresh_line(s)
1220
                    break
1221
                end
1222
                # get the line and strip leading and trailing whitespace
1223
                line = strip(input[oldpos:prevind(input, pos)])
1224
                if !isempty(line)
1225
                    if isprompt_paste # remove indentation spaces corresponding to the prompt
1226
                        line = replace(line, r"^"m * ' '^curr_prompt_len => "")
1227
                    end
1228
                    # put the line on the screen and history
1229
                    LineEdit.replace_line(s, line)
1230
                    LineEdit.commit_line(s)
1231
                    # execute the statement
1232
                    terminal = LineEdit.terminal(s) # This is slightly ugly but ok for now
1233
                    raw!(terminal, false) && disable_bracketed_paste(terminal)
1234
                    LineEdit.mode(s).on_done(s, LineEdit.buffer(s), true)
1235
                    raw!(terminal, true) && enable_bracketed_paste(terminal)
1236
                    LineEdit.push_undo(s) # when the last line is incomplete
1237
                end
1238
                oldpos = pos
1239
                firstline = false
1240
            end
1241
        end,
1242

1243
        # Open the editor at the location of a stackframe or method
1244
        # This is accessing a contextual variable that gets set in
1245
        # the show_backtrace and show_method_table functions.
1246
        "^Q" => (s::MIState, o...) -> begin
1247
            linfos = repl.last_shown_line_infos
1248
            str = String(take!(LineEdit.buffer(s)))
1249
            n = tryparse(Int, str)
1250
            n === nothing && @goto writeback
1251
            if n <= 0 || n > length(linfos) || startswith(linfos[n][1], "REPL[")
1252
                @goto writeback
1253
            end
1254
            try
1255
                InteractiveUtils.edit(Base.fixup_stdlib_path(linfos[n][1]), linfos[n][2])
1256
            catch ex
1257
                ex isa ProcessFailedException || ex isa Base.IOError || ex isa SystemError || rethrow()
1258
                @info "edit failed" _exception=ex
1259
            end
1260
            LineEdit.refresh_line(s)
1261
            return
1262
            @label writeback
1263
            write(LineEdit.buffer(s), str)
1264
            return
1265
        end,
1266
    )
1267

1268
    prefix_prompt, prefix_keymap = LineEdit.setup_prefix_keymap(hp, julia_prompt)
1269

1270
    a = Dict{Any,Any}[skeymap, repl_keymap, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
1271
    prepend!(a, extra_repl_keymap)
1272

1273
    julia_prompt.keymap_dict = LineEdit.keymap(a)
1274

1275
    mk = mode_keymap(julia_prompt)
1276

1277
    b = Dict{Any,Any}[skeymap, mk, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
1278
    prepend!(b, extra_repl_keymap)
1279

1280
    shell_mode.keymap_dict = help_mode.keymap_dict = LineEdit.keymap(b)
1281

1282
    allprompts = LineEdit.TextInterface[julia_prompt, shell_mode, help_mode, search_prompt, prefix_prompt]
1283
    return ModalInterface(allprompts)
1284
end
1285

1286
function run_frontend(repl::LineEditREPL, backend::REPLBackendRef)
1287
    repl.frontend_task = current_task()
1288
    d = REPLDisplay(repl)
1289
    dopushdisplay = repl.specialdisplay === nothing && !in(d,Base.Multimedia.displays)
1290
    dopushdisplay && pushdisplay(d)
1291
    if !isdefined(repl,:interface)
1292
        interface = repl.interface = setup_interface(repl)
1293
    else
1294
        interface = repl.interface
1295
    end
1296
    repl.backendref = backend
1297
    repl.mistate = LineEdit.init_state(terminal(repl), interface)
1298
    run_interface(terminal(repl), interface, repl.mistate)
1299
    # Terminate Backend
1300
    put!(backend.repl_channel, (nothing, -1))
1301
    dopushdisplay && popdisplay(d)
1302
    nothing
1303
end
1304

1305
## StreamREPL ##
1306

1307
mutable struct StreamREPL <: AbstractREPL
1308
    stream::IO
1309
    prompt_color::String
1310
    input_color::String
1311
    answer_color::String
1312
    waserror::Bool
1313
    frontend_task::Task
1314
    StreamREPL(stream,pc,ic,ac) = new(stream,pc,ic,ac,false)
1315
end
1316
StreamREPL(stream::IO) = StreamREPL(stream, Base.text_colors[:green], Base.input_color(), Base.answer_color())
1317
run_repl(stream::IO) = run_repl(StreamREPL(stream))
1318

1319
outstream(s::StreamREPL) = s.stream
1320
hascolor(s::StreamREPL) = get(s.stream, :color, false)::Bool
1321

1322
answer_color(r::LineEditREPL) = r.envcolors ? Base.answer_color() : r.answer_color
1323
answer_color(r::StreamREPL) = r.answer_color
1324
input_color(r::LineEditREPL) = r.envcolors ? Base.input_color() : r.input_color
1325
input_color(r::StreamREPL) = r.input_color
1326

1327
let matchend = Dict("\"" => r"\"", "\"\"\"" => r"\"\"\"", "'" => r"'",
1328
    "`" => r"`", "```" => r"```", "#" => r"$"m, "#=" => r"=#|#=")
1329
    global _rm_strings_and_comments
1330
    function _rm_strings_and_comments(code::Union{String,SubString{String}})
1331
        buf = IOBuffer(sizehint = sizeof(code))
1332
        pos = 1
1333
        while true
1334
            i = findnext(r"\"(?!\"\")|\"\"\"|'|`(?!``)|```|#(?!=)|#=", code, pos)
1335
            isnothing(i) && break
1336
            match = SubString(code, i)
1337
            j = findnext(matchend[match]::Regex, code, nextind(code, last(i)))
1338
            if match == "#=" # possibly nested
1339
                nested = 1
1340
                while j !== nothing
1341
                    nested += SubString(code, j) == "#=" ? +1 : -1
1342
                    iszero(nested) && break
1343
                    j = findnext(r"=#|#=", code, nextind(code, last(j)))
1344
                end
1345
            elseif match[1] != '#' # quote match: check non-escaped
1346
                while j !== nothing
1347
                    notbackslash = findprev(!=('\\'), code, prevind(code, first(j)))::Int
1348
                    isodd(first(j) - notbackslash) && break # not escaped
1349
                    j = findnext(matchend[match]::Regex, code, nextind(code, first(j)))
1350
                end
1351
            end
1352
            isnothing(j) && break
1353
            if match[1] == '#'
1354
                print(buf, SubString(code, pos, prevind(code, first(i))))
1355
            else
1356
                print(buf, SubString(code, pos, last(i)), ' ', SubString(code, j))
1357
            end
1358
            pos = nextind(code, last(j))
1359
        end
1360
        print(buf, SubString(code, pos, lastindex(code)))
1361
        return String(take!(buf))
1362
    end
1363
end
1364

1365
# heuristic function to decide if the presence of a semicolon
1366
# at the end of the expression was intended for suppressing output
1367
ends_with_semicolon(code::AbstractString) = ends_with_semicolon(String(code))
1368
ends_with_semicolon(code::Union{String,SubString{String}}) =
1369
    contains(_rm_strings_and_comments(code), r";\s*$")
1370

1371
function run_frontend(repl::StreamREPL, backend::REPLBackendRef)
1372
    repl.frontend_task = current_task()
1373
    have_color = hascolor(repl)
1374
    Base.banner(repl.stream)
1375
    d = REPLDisplay(repl)
1376
    dopushdisplay = !in(d,Base.Multimedia.displays)
1377
    dopushdisplay && pushdisplay(d)
1378
    while !eof(repl.stream)::Bool
1379
        if have_color
1380
            print(repl.stream,repl.prompt_color)
1381
        end
1382
        print(repl.stream, "julia> ")
1383
        if have_color
1384
            print(repl.stream, input_color(repl))
1385
        end
1386
        line = readline(repl.stream, keep=true)
1387
        if !isempty(line)
1388
            ast = Base.parse_input_line(line)
1389
            if have_color
1390
                print(repl.stream, Base.color_normal)
1391
            end
1392
            response = eval_with_backend(ast, backend)
1393
            print_response(repl, response, !ends_with_semicolon(line), have_color)
1394
        end
1395
    end
1396
    # Terminate Backend
1397
    put!(backend.repl_channel, (nothing, -1))
1398
    dopushdisplay && popdisplay(d)
1399
    nothing
1400
end
1401

1402
module IPython
1403

1404
using ..REPL
1405

1406
__current_ast_transforms() = isdefined(Base, :active_repl_backend) ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
1407

1408
function repl_eval_counter(hp)
1409
    return length(hp.history) - hp.start_idx
1410
end
1411

1412
function out_transform(@nospecialize(x), n::Ref{Int})
1413
    return quote
1414
        let __temp_val_a72df459 = $x
1415
            $capture_result($n, __temp_val_a72df459)
1416
            __temp_val_a72df459
1417
        end
1418
    end
1419
end
1420

1421
function capture_result(n::Ref{Int}, @nospecialize(x))
1422
    n = n[]
1423
    mod = REPL.active_module()
1424
    if !isdefined(mod, :Out)
1425
        setglobal!(mod, :Out, Dict{Int, Any}())
1426
    end
1427
    if x !== getglobal(mod, :Out) && x !== nothing # remove this?
1428
        getglobal(mod, :Out)[n] = x
1429
    end
1430
    nothing
1431
end
1432

1433
function set_prompt(repl::LineEditREPL, n::Ref{Int})
1434
    julia_prompt = repl.interface.modes[1]
1435
    julia_prompt.prompt = function()
1436
        n[] = repl_eval_counter(julia_prompt.hist)+1
1437
        string("In [", n[], "]: ")
1438
    end
1439
    nothing
1440
end
1441

1442
function set_output_prefix(repl::LineEditREPL, n::Ref{Int})
1443
    julia_prompt = repl.interface.modes[1]
1444
    if REPL.hascolor(repl)
1445
        julia_prompt.output_prefix_prefix = Base.text_colors[:red]
1446
    end
1447
    julia_prompt.output_prefix = () -> string("Out[", n[], "]: ")
1448
    nothing
1449
end
1450

1451
function __current_ast_transforms(backend)
1452
    if backend === nothing
1453
        isdefined(Base, :active_repl_backend) ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
1454
    else
1455
        backend.ast_transforms
1456
    end
1457
end
1458

1459

1460
function ipython_mode!(repl::LineEditREPL=Base.active_repl, backend=nothing)
1461
    n = Ref{Int}(0)
1462
    set_prompt(repl, n)
1463
    set_output_prefix(repl, n)
1464
    push!(__current_ast_transforms(backend), @nospecialize(ast) -> out_transform(ast, n))
1465
    return
1466
end
1467
end
1468

1469
import .IPython.ipython_mode!
1470

1471
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