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

JuliaLang / julia / #37440

pending completion
#37440

push

local

web-flow
Update checksums for llvm 14.0.6+2 (#48659)

69011 of 80481 relevant lines covered (85.75%)

40171151.98 hits per line

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

0.26
/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
answer_color(::AbstractREPL) = ""
×
75

76
const JULIA_PROMPT = "julia> "
77
const PKG_PROMPT = "pkg> "
78
const SHELL_PROMPT = "shell> "
79
const HELP_PROMPT = "help?> "
80

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

93
    REPLBackend(repl_channel, response_channel, in_eval, ast_transforms=copy(repl_ast_transforms)) =
×
94
        new(repl_channel, response_channel, in_eval, ast_transforms)
95
end
96
REPLBackend() = REPLBackend(Channel(1), Channel(1), false)
×
97

98
"""
99
    softscope(ex)
100

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

122
# Temporary alias until Documenter updates
123
const softscope! = softscope
124

125
const repl_ast_transforms = Any[softscope] # defaults for new REPL backends
126

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

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

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

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

202
"""
203
    start_repl_backend(repl_channel::Channel, response_channel::Channel)
204

205
    Starts loop for REPL backend
206
    Returns a REPLBackend with backend_task assigned
207

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

219
"""
220
    start_repl_backend(backend::REPLBackend)
221

222
    Call directly to run backend loop on current Task.
223
    Use @async for run backend on new Task.
224

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

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

250
struct REPLDisplay{R<:AbstractREPL} <: AbstractDisplay
251
    repl::R
252
end
253

254
==(a::REPLDisplay, b::REPLDisplay) = a.repl === b.repl
×
255

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

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

335
# A reference to a backend that is not mutable
336
struct REPLBackendRef
337
    repl_channel::Channel{Any}
338
    response_channel::Channel{Any}
339
end
340
REPLBackendRef(backend::REPLBackend) = REPLBackendRef(backend.repl_channel, backend.response_channel)
×
341

342
function destroy(ref::REPLBackendRef, state::Task)
×
343
    if istaskfailed(state)
×
344
        close(ref.repl_channel, TaskFailedException(state))
×
345
        close(ref.response_channel, TaskFailedException(state))
×
346
    end
347
    close(ref.repl_channel)
×
348
    close(ref.response_channel)
×
349
end
350

351
"""
352
    run_repl(repl::AbstractREPL)
353
    run_repl(repl, consumer = backend->nothing; backend_on_current_task = true)
354

355
    Main function to start the REPL
356

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

383
## BasicREPL ##
384

385
mutable struct BasicREPL <: AbstractREPL
386
    terminal::TextTerminal
387
    waserror::Bool
388
    frontend_task::Task
389
    BasicREPL(t) = new(t, false)
×
390
end
391

392
outstream(r::BasicREPL) = r.terminal
×
393
hascolor(r::BasicREPL) = hascolor(r.terminal)
×
394

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

442
## LineEditREPL ##
443

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

480
LineEditREPL(t::TextTerminal, hascolor::Bool, envcolors::Bool=false) =
×
481
    LineEditREPL(t, hascolor,
482
        hascolor ? Base.text_colors[:green] : "",
483
        hascolor ? Base.input_color() : "",
484
        hascolor ? Base.answer_color() : "",
485
        hascolor ? Base.text_colors[:red] : "",
486
        hascolor ? Base.text_colors[:yellow] : "",
487
        false, false, false, envcolors
488
    )
489

490
mutable struct REPLCompletionProvider <: CompletionProvider
491
    modifiers::LineEdit.Modifiers
492
end
493
REPLCompletionProvider() = REPLCompletionProvider(LineEdit.Modifiers())
×
494

495
mutable struct ShellCompletionProvider <: CompletionProvider end
496
struct LatexCompletions <: CompletionProvider end
497

498
function active_module() # this method is also called from Base
52,046✔
499
    isdefined(Base, :active_repl) || return Main
104,092✔
500
    return active_module(Base.active_repl::AbstractREPL)
×
501
end
502
active_module((; mistate)::LineEditREPL) = mistate === nothing ? Main : mistate.active_module
×
503
active_module(::AbstractREPL) = Main
×
504
active_module(d::REPLDisplay) = active_module(d.repl)
×
505

506
setmodifiers!(c::REPLCompletionProvider, m::LineEdit.Modifiers) = c.modifiers = m
×
507

508
"""
509
    activate(mod::Module=Main)
510

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

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

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

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

540
function complete_line(c::LatexCompletions, s)
×
541
    partial = beforecursor(LineEdit.buffer(s))
×
542
    full = LineEdit.input_string(s)::String
×
543
    ret, range, should_complete = bslash_completions(full, lastindex(partial))[2]
×
544
    return unique!(map(completion_text, ret)), partial[range], should_complete
×
545
end
546

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

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

574
invalid_history_message(path::String) = """
×
575
Invalid history file ($path) format:
576
If you have a history file left over from an older version of Julia,
577
try renaming or deleting it.
578
Invalid character: """
579

580
munged_history_message(path::String) = """
×
581
Invalid history file ($path) format:
582
An editor may have converted tabs to spaces at line """
583

584
function hist_open_file(hp::REPLHistoryProvider)
×
585
    f = open(hp.file_path, read=true, write=true, create=true)
×
586
    hp.history_file = f
×
587
    seekend(f)
×
588
end
589

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

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

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

663
function history_move(s::Union{LineEdit.MIState,LineEdit.PrefixSearchState}, hist::REPLHistoryProvider, idx::Int, save_idx::Int = hist.cur_idx)
×
664
    max_idx = length(hist.history) + 1
×
665
    @assert 1 <= hist.cur_idx <= max_idx
×
666
    (1 <= idx <= max_idx) || return :none
×
667
    idx != hist.cur_idx || return :none
×
668

669
    # save the current line
670
    if save_idx == max_idx
×
671
        hist.last_mode = LineEdit.mode(s)
×
672
        hist.last_buffer = copy(LineEdit.buffer(s))
×
673
    else
674
        hist.history[save_idx] = LineEdit.input_string(s)
×
675
        hist.modes[save_idx] = mode_idx(hist, LineEdit.mode(s))
×
676
    end
677

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

697
    return :ok
×
698
end
699

700
# REPL History can also transitions modes
701
function LineEdit.accept_result_newmode(hist::REPLHistoryProvider)
×
702
    if 1 <= hist.cur_idx <= length(hist.modes)
×
703
        return hist.mode_mapping[hist.modes[hist.cur_idx]]
×
704
    end
705
    return nothing
×
706
end
707

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

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

751
history_first(s::LineEdit.MIState, hist::REPLHistoryProvider) =
×
752
    history_prev(s, hist, hist.cur_idx - 1 -
753
                 (hist.cur_idx > hist.start_idx+1 ? hist.start_idx : 0))
754

755
history_last(s::LineEdit.MIState, hist::REPLHistoryProvider) =
×
756
    history_next(s, hist, length(hist.history) - hist.cur_idx + 1)
757

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

799
function history_search(hist::REPLHistoryProvider, query_buffer::IOBuffer, response_buffer::IOBuffer,
×
800
                        backwards::Bool=false, skip_current::Bool=false)
801

802
    qpos = position(query_buffer)
×
803
    qpos > 0 || return true
×
804
    searchdata = beforecursor(query_buffer)
×
805
    response_str = String(take!(copy(response_buffer)))
×
806

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

815
    searchstart = backwards ? b : a
×
816
    if searchdata == response_str[a:b]
×
817
        if skip_current
×
818
            searchstart = backwards ? prevind(response_str, b) : nextind(response_str, a)
×
819
        else
820
            return true
×
821
        end
822
    end
823

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

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

849
    return false
×
850
end
851

852
function history_reset_state(hist::REPLHistoryProvider)
×
853
    if hist.cur_idx != length(hist.history) + 1
×
854
        hist.last_idx = hist.cur_idx
×
855
        hist.cur_idx = length(hist.history) + 1
×
856
    end
857
    nothing
×
858
end
859
LineEdit.reset_state(hist::REPLHistoryProvider) = history_reset_state(hist)
×
860

861
function return_callback(s)
×
862
    ast = Base.parse_input_line(String(take!(copy(LineEdit.buffer(s)))), depwarn=false)
×
863
    return !(isa(ast, Expr) && ast.head === :incomplete)
×
864
end
865

866
find_hist_file() = get(ENV, "JULIA_HISTORY",
×
867
                       !isempty(DEPOT_PATH) ? joinpath(DEPOT_PATH[1], "logs", "repl_history.jl") :
868
                       error("DEPOT_PATH is empty and and ENV[\"JULIA_HISTORY\"] not set."))
869

870
backend(r::AbstractREPL) = r.backendref
×
871

872
function eval_with_backend(ast, backend::REPLBackendRef)
×
873
    put!(backend.repl_channel, (ast, 1))
×
874
    return take!(backend.response_channel) # (val, iserr)
×
875
end
876

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

901
function reset(repl::LineEditREPL)
×
902
    raw!(repl.t, false)
×
903
    hascolor(repl) && print(repl.t, Base.text_colors[:normal])
×
904
    nothing
×
905
end
906

907
function prepare_next(repl::LineEditREPL)
×
908
    println(terminal(repl))
×
909
end
910

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

933
repl_filename(repl, hp::REPLHistoryProvider) = "REPL[$(max(length(hp.history)-hp.start_idx, 1))]"
×
934
repl_filename(repl, hp) = "REPL"
×
935

936
const JL_PROMPT_PASTE = Ref(true)
937
enable_promptpaste(v::Bool) = JL_PROMPT_PASTE[] = v
×
938

939
function contextual_prompt(repl::LineEditREPL, prompt::Union{String,Function})
×
940
    function ()
×
941
        mod = active_module(repl)
×
942
        prefix = mod == Main ? "" : string('(', mod, ") ")
×
943
        pr = prompt isa String ? prompt : prompt()
×
944
        prefix * pr
×
945
    end
946
end
947

948
setup_interface(
949
    repl::LineEditREPL;
950
    # those keyword arguments may be deprecated eventually in favor of the Options mechanism
951
    hascolor::Bool = repl.options.hascolor,
952
    extra_repl_keymap::Any = repl.options.extra_keymap
953
) = setup_interface(repl, hascolor, extra_repl_keymap)
×
954

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

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

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

986
    ############################### Stage I ################################
987

988
    # This will provide completions for REPL and help mode
989
    replc = REPLCompletionProvider()
×
990

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

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

1012

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

1030

1031
    ################################# Stage II #############################
1032

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

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

1063

1064
    search_prompt, skeymap = LineEdit.setup_search_keymap(hp)
×
1065
    search_prompt.complete = LatexCompletions()
×
1066

1067
    shell_prompt_len = length(SHELL_PROMPT)
×
1068
    help_prompt_len = length(HELP_PROMPT)
×
1069
    jl_prompt_regex = r"^In \[[0-9]+\]: |^(?:\(.+\) )?julia> "
×
1070
    pkg_prompt_regex = r"^(?:\(.+\) )?pkg> "
×
1071

1072
    # Canonicalize user keymap input
1073
    if isa(extra_repl_keymap, Dict)
×
1074
        extra_repl_keymap = AnyDict[extra_repl_keymap]
×
1075
    end
1076

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

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

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

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

1264
    prefix_prompt, prefix_keymap = LineEdit.setup_prefix_keymap(hp, julia_prompt)
×
1265

1266
    a = Dict{Any,Any}[skeymap, repl_keymap, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
×
1267
    prepend!(a, extra_repl_keymap)
×
1268

1269
    julia_prompt.keymap_dict = LineEdit.keymap(a)
×
1270

1271
    mk = mode_keymap(julia_prompt)
×
1272

1273
    b = Dict{Any,Any}[skeymap, mk, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
×
1274
    prepend!(b, extra_repl_keymap)
×
1275

1276
    shell_mode.keymap_dict = help_mode.keymap_dict = LineEdit.keymap(b)
×
1277

1278
    allprompts = LineEdit.TextInterface[julia_prompt, shell_mode, help_mode, search_prompt, prefix_prompt]
×
1279
    return ModalInterface(allprompts)
×
1280
end
1281

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

1301
## StreamREPL ##
1302

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

1315
outstream(s::StreamREPL) = s.stream
×
1316
hascolor(s::StreamREPL) = get(s.stream, :color, false)::Bool
×
1317

1318
answer_color(r::LineEditREPL) = r.envcolors ? Base.answer_color() : r.answer_color
×
1319
answer_color(r::StreamREPL) = r.answer_color
×
1320
input_color(r::LineEditREPL) = r.envcolors ? Base.input_color() : r.input_color
×
1321
input_color(r::StreamREPL) = r.input_color
×
1322

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

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

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

1398
module IPython
1399

1400
using ..REPL
1401

1402
__current_ast_transforms() = isdefined(Base, :active_repl_backend) ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1403

1404
function repl_eval_counter(hp)
×
1405
    return length(hp.history) - hp.start_idx
×
1406
end
1407

1408
function out_transform(@nospecialize(x), n::Ref{Int})
×
1409
    return quote
×
1410
        let __temp_val_a72df459 = $x
×
1411
            $capture_result($n, __temp_val_a72df459)
×
1412
            __temp_val_a72df459
×
1413
        end
1414
    end
1415
end
1416

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

1429
function set_prompt(repl::LineEditREPL, n::Ref{Int})
×
1430
    julia_prompt = repl.interface.modes[1]
×
1431
    julia_prompt.prompt = function()
×
1432
        n[] = repl_eval_counter(julia_prompt.hist)+1
×
1433
        string("In [", n[], "]: ")
×
1434
    end
1435
    nothing
×
1436
end
1437

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

1447
function __current_ast_transforms(backend)
×
1448
    if backend === nothing
×
1449
        isdefined(Base, :active_repl_backend) ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1450
    else
1451
        backend.ast_transforms
×
1452
    end
1453
end
1454

1455

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

1465
import .IPython.ipython_mode!
1466

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

© 2026 Coveralls, Inc