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

JuliaLang / julia / #37513

pending completion
#37513

push

local

web-flow
Fix inference of one-arg `return_type` method (#49407)

* Fix inference of one-arg `return_type` method

`Core.Compiler.return_type` has two methods:
 - return_type(f, args::Type{<:Tuple})
 - return_type(args::Type{<:Tuple})

Our inference code was only catching the first one.
Expand it to support both.

* Update test/compiler/inference.jl

---------

Co-authored-by: Shuhei Kadowaki <40514306+aviatesk@users.noreply.github.com>

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

72382 of 83401 relevant lines covered (86.79%)

36300167.39 hits per line

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

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

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

6
Example minimal code
7

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

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

20
using Base.Meta, Sockets
21
import InteractiveUtils
22

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

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

36
_displaysize(io::IO) = displaysize(io)::Tuple{Int,Int}
30✔
37

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

41
abstract type AbstractREPL end
42

43
include("options.jl")
44

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

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

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

73
@nospecialize # use only declared type signatures
74

75
answer_color(::AbstractREPL) = ""
×
76

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

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

94
    REPLBackend(repl_channel, response_channel, in_eval, ast_transforms=copy(repl_ast_transforms)) =
42✔
95
        new(repl_channel, response_channel, in_eval, ast_transforms)
96
end
97
REPLBackend() = REPLBackend(Channel(1), Channel(1), false)
21✔
98

99
"""
100
    softscope(ex)
101

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

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

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

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

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

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

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

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

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

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

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

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

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

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

251
struct REPLDisplay{R<:AbstractREPL} <: AbstractDisplay
252
    repl::R
30✔
253
end
254

255
==(a::REPLDisplay, b::REPLDisplay) = a.repl === b.repl
13✔
256

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

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

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

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

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

358
    Main function to start the REPL
359

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

386
## BasicREPL ##
387

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

395
outstream(r::BasicREPL) = r.terminal
6✔
396
hascolor(r::BasicREPL) = hascolor(r.terminal)
×
397

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

445
## LineEditREPL ##
446

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

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

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

498
mutable struct ShellCompletionProvider <: CompletionProvider end
20✔
499
struct LatexCompletions <: CompletionProvider end
500

501
function active_module() # this method is also called from Base
65,551✔
502
    isdefined(Base, :active_repl) || return Main
131,102✔
503
    return active_module(Base.active_repl::AbstractREPL)
×
504
end
505
active_module((; mistate)::LineEditREPL) = mistate === nothing ? Main : mistate.active_module
3,231✔
506
active_module(::AbstractREPL) = Main
10✔
507
active_module(d::REPLDisplay) = active_module(d.repl)
115✔
508

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

700
    return :ok
78✔
701
end
702

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

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

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

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

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

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

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

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

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

818
    searchstart = backwards ? b : a
28✔
819
    if searchdata == response_str[a:b]
44✔
820
        if skip_current
10✔
821
            searchstart = backwards ? prevind(response_str, b) : nextind(response_str, a)
4✔
822
        else
823
            return true
6✔
824
        end
825
    end
826

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

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

852
    return false
4✔
853
end
854

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

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

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

873
backend(r::AbstractREPL) = r.backendref
92✔
874

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

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

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

910
function prepare_next(repl::LineEditREPL)
108✔
911
    println(terminal(repl))
108✔
912
end
913

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

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

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

942
function contextual_prompt(repl::LineEditREPL, prompt::Union{String,Function})
×
943
    function ()
1,418✔
944
        mod = active_module(repl)
2,749✔
945
        prefix = mod == Main ? "" : string('(', mod, ") ")
1,406✔
946
        pr = prompt isa String ? prompt : prompt()
1,378✔
947
        prefix * pr
1,378✔
948
    end
949
end
950

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

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

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

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

989
    ############################### Stage I ################################
990

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

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

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

1015

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

1033

1034
    ################################# Stage II #############################
1035

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

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

1066

1067
    search_prompt, skeymap = LineEdit.setup_search_keymap(hp)
20✔
1068
    search_prompt.complete = LatexCompletions()
20✔
1069

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

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

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

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

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

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

1267
    prefix_prompt, prefix_keymap = LineEdit.setup_prefix_keymap(hp, julia_prompt)
20✔
1268

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

1272
    julia_prompt.keymap_dict = LineEdit.keymap(a)
20✔
1273

1274
    mk = mode_keymap(julia_prompt)
20✔
1275

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

1279
    shell_mode.keymap_dict = help_mode.keymap_dict = LineEdit.keymap(b)
20✔
1280

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

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

1304
## StreamREPL ##
1305

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

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

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

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

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

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

1401
module Numbered
1402

1403
using ..REPL
1404

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

1407
function repl_eval_counter(hp)
503✔
1408
    return length(hp.history) - hp.start_idx
503✔
1409
end
1410

1411
function out_transform(@nospecialize(x), n::Ref{Int})
14✔
1412
    return Expr(:toplevel, get_usings!([], x)..., quote
14✔
1413
        let __temp_val_a72df459 = $x
1414
            $capture_result($n, __temp_val_a72df459)
1415
            __temp_val_a72df459
1416
        end
1417
    end)
1418
end
1419

1420
function get_usings!(usings, ex)
22✔
1421
    # get all `using` and `import` statements which are at the top level
1422
    for (i, arg) in enumerate(ex.args)
44✔
1423
        if Base.isexpr(arg, :toplevel)
65✔
1424
            get_usings!(usings, arg)
8✔
1425
        elseif Base.isexpr(arg, [:using, :import])
58✔
1426
            push!(usings, popat!(ex.args, i))
2✔
1427
        end
1428
    end
64✔
1429
    return usings
22✔
1430
end
1431

1432
function capture_result(n::Ref{Int}, @nospecialize(x))
14✔
1433
    n = n[]
14✔
1434
    mod = Base.MainInclude
14✔
1435
    if !isdefined(mod, :Out)
14✔
1436
        @eval mod global Out
1✔
1437
        @eval mod export Out
1✔
1438
        setglobal!(mod, :Out, Dict{Int, Any}())
1✔
1439
    end
1440
    if x !== getglobal(mod, :Out) && x !== nothing # remove this?
14✔
1441
        getglobal(mod, :Out)[n] = x
13✔
1442
    end
1443
    nothing
14✔
1444
end
1445

1446
function set_prompt(repl::LineEditREPL, n::Ref{Int})
1✔
1447
    julia_prompt = repl.interface.modes[1]
1✔
1448
    julia_prompt.prompt = function()
504✔
1449
        n[] = repl_eval_counter(julia_prompt.hist)+1
503✔
1450
        string("In [", n[], "]: ")
503✔
1451
    end
1452
    nothing
1✔
1453
end
1454

1455
function set_output_prefix(repl::LineEditREPL, n::Ref{Int})
1✔
1456
    julia_prompt = repl.interface.modes[1]
1✔
1457
    if REPL.hascolor(repl)
1✔
1458
        julia_prompt.output_prefix_prefix = Base.text_colors[:red]
1✔
1459
    end
1460
    julia_prompt.output_prefix = () -> string("Out[", n[], "]: ")
14✔
1461
    nothing
1✔
1462
end
1463

1464
function __current_ast_transforms(backend)
1✔
1465
    if backend === nothing
1✔
1466
        isdefined(Base, :active_repl_backend) ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1467
    else
1468
        backend.ast_transforms
1✔
1469
    end
1470
end
1471

1472

1473
function numbered_prompt!(repl::LineEditREPL=Base.active_repl, backend=nothing)
1✔
1474
    n = Ref{Int}(0)
1✔
1475
    set_prompt(repl, n)
1✔
1476
    set_output_prefix(repl, n)
1✔
1477
    push!(__current_ast_transforms(backend), @nospecialize(ast) -> out_transform(ast, n))
15✔
1478
    return
1✔
1479
end
1480

1481
"""
1482
    Out[n]
1483

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

1487
See also [`ans`](@ref).
1488
"""
1489
Base.MainInclude.Out
1490

1491
end
1492

1493
import .Numbered.numbered_prompt!
1494

1495
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