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

JuliaLang / julia / #37907

17 Sep 2024 11:47PM UTC coverage: 87.689% (+0.8%) from 86.849%
#37907

push

local

web-flow
codegen: fix bits compare for UnionAll (#55770)

Fixes #55768 in two parts: one is making the type computation in
emit_bits_compare agree with the parent function and two is not using
the optimized egal code for UnionAll kinds, which is different from how
the egal code itself works for kinds.

78275 of 89264 relevant lines covered (87.69%)

16631630.93 hits per line

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

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

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

6
Example minimal code
7

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

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

20
function UndefVarError_hint(io::IO, ex::UndefVarError)
2✔
21
    var = ex.var
2✔
22
    if var === :or
2✔
23
        print(io, "\nSuggestion: Use `||` for short-circuiting boolean OR.")
×
24
    elseif var === :and
2✔
25
        print(io, "\nSuggestion: Use `&&` for short-circuiting boolean AND.")
×
26
    elseif var === :help
2✔
27
        println(io)
×
28
        # Show friendly help message when user types help or help() and help is undefined
29
        show(io, MIME("text/plain"), Base.Docs.parsedoc(Base.Docs.keywords[:help]))
×
30
    elseif var === :quit
2✔
31
        print(io, "\nSuggestion: To exit Julia, use Ctrl-D, or type exit() and press enter.")
×
32
    end
33
    if isdefined(ex, :scope)
2✔
34
        scope = ex.scope
2✔
35
        if scope isa Module
2✔
36
            bpart = Base.lookup_binding_partition(Base.get_world_counter(), GlobalRef(scope, var))
2✔
37
            kind = Base.binding_kind(bpart)
2✔
38
            if kind === Base.BINDING_KIND_GLOBAL || kind === Base.BINDING_KIND_CONST || kind == Base.BINDING_KIND_DECLARED
4✔
39
                print(io, "\nSuggestion: add an appropriate import or assignment. This global was declared but not assigned.")
×
40
            elseif kind === Base.BINDING_KIND_FAILED
2✔
41
                print(io, "\nHint: It looks like two or more modules export different ",
1✔
42
                "bindings with this name, resulting in ambiguity. Try explicitly ",
43
                "importing it from a particular module, or qualifying the name ",
44
                "with the module it should come from.")
45
            elseif kind === Base.BINDING_KIND_GUARD
1✔
46
                print(io, "\nSuggestion: check for spelling errors or missing imports.")
1✔
47
            else
48
                print(io, "\nSuggestion: this global was defined as `$(bpart.restriction.globalref)` but not assigned a value.")
×
49
            end
50
        elseif scope === :static_parameter
×
51
            print(io, "\nSuggestion: run Test.detect_unbound_args to detect method arguments that do not fully constrain a type parameter.")
×
52
        elseif scope === :local
×
53
            print(io, "\nSuggestion: check for an assignment to a local variable that shadows a global of the same name.")
×
54
        end
55
    else
56
        scope = undef
×
57
    end
58
    if scope !== Base && !_UndefVarError_warnfor(io, Base, var)
2✔
59
        warned = false
1✔
60
        for m in Base.loaded_modules_order
1✔
61
            m === Core && continue
23✔
62
            m === Base && continue
22✔
63
            m === Main && continue
21✔
64
            m === scope && continue
20✔
65
            warned |= _UndefVarError_warnfor(io, m, var)
20✔
66
        end
23✔
67
        warned ||
2✔
68
            _UndefVarError_warnfor(io, Core, var) ||
69
            _UndefVarError_warnfor(io, Main, var)
70
    end
71
    return nothing
2✔
72
end
73

74
function _UndefVarError_warnfor(io::IO, m::Module, var::Symbol)
24✔
75
    Base.isbindingresolved(m, var) || return false
46✔
76
    (Base.isexported(m, var) || Base.ispublic(m, var)) || return false
4✔
77
    print(io, "\nHint: a global variable of this name also exists in $m.")
1✔
78
    return true
1✔
79
end
80

81
function __init__()
5✔
82
    Base.REPL_MODULE_REF[] = REPL
5✔
83
    Base.Experimental.register_error_hint(UndefVarError_hint, UndefVarError)
5✔
84
    return nothing
5✔
85
end
86

87
using Base.Meta, Sockets, StyledStrings
88
using JuliaSyntaxHighlighting
89
import InteractiveUtils
90

91
export
92
    AbstractREPL,
93
    BasicREPL,
94
    LineEditREPL,
95
    StreamREPL
96

97
public TerminalMenus
98

99
import Base:
100
    AbstractDisplay,
101
    display,
102
    show,
103
    AnyDict,
104
    ==
105

106
_displaysize(io::IO) = displaysize(io)::Tuple{Int,Int}
32✔
107

108
include("Terminals.jl")
109
using .Terminals
110

111
abstract type AbstractREPL end
112

113
include("options.jl")
114

115
include("LineEdit.jl")
116
using .LineEdit
117
import .LineEdit:
118
    CompletionProvider,
119
    HistoryProvider,
120
    add_history,
121
    complete_line,
122
    history_next,
123
    history_next_prefix,
124
    history_prev,
125
    history_prev_prefix,
126
    history_first,
127
    history_last,
128
    history_search,
129
    setmodifiers!,
130
    terminal,
131
    MIState,
132
    PromptState,
133
    mode_idx
134

135
include("REPLCompletions.jl")
136
using .REPLCompletions
137

138
include("TerminalMenus/TerminalMenus.jl")
139
include("docview.jl")
140

141
include("Pkg_beforeload.jl")
142

143
@nospecialize # use only declared type signatures
144

145
answer_color(::AbstractREPL) = ""
×
146

147
const JULIA_PROMPT = "julia> "
148
const PKG_PROMPT = "pkg> "
149
const SHELL_PROMPT = "shell> "
150
const HELP_PROMPT = "help?> "
151

152
mutable struct REPLBackend
153
    "channel for AST"
154
    repl_channel::Channel{Any}
155
    "channel for results: (value, iserror)"
156
    response_channel::Channel{Any}
157
    "flag indicating the state of this backend"
158
    in_eval::Bool
159
    "transformation functions to apply before evaluating expressions"
160
    ast_transforms::Vector{Any}
161
    "current backend task"
162
    backend_task::Task
163

164
    REPLBackend(repl_channel, response_channel, in_eval, ast_transforms=copy(repl_ast_transforms)) =
52✔
165
        new(repl_channel, response_channel, in_eval, ast_transforms)
166
end
167
REPLBackend() = REPLBackend(Channel(1), Channel(1), false)
26✔
168

169
"""
170
    softscope(ex)
171

172
Return a modified version of the parsed expression `ex` that uses
173
the REPL's "soft" scoping rules for global syntax blocks.
174
"""
175
function softscope(@nospecialize ex)
376✔
176
    if ex isa Expr
376✔
177
        h = ex.head
241✔
178
        if h === :toplevel
241✔
179
            ex′ = Expr(h)
137✔
180
            map!(softscope, resize!(ex′.args, length(ex.args)), ex.args)
137✔
181
            return ex′
137✔
182
        elseif h in (:meta, :import, :using, :export, :module, :error, :incomplete, :thunk)
104✔
183
            return ex
2✔
184
        elseif h === :global && all(x->isa(x, Symbol), ex.args)
103✔
185
            return ex
1✔
186
        else
187
            return Expr(:block, Expr(:softscope, true), ex)
101✔
188
        end
189
    end
190
    return ex
135✔
191
end
192

193
# Temporary alias until Documenter updates
194
const softscope! = softscope
195

196
function print_qualified_access_warning(mod::Module, owner::Module, name::Symbol)
2✔
197
    @warn string(name, " is defined in ", owner, " and is not public in ", mod) maxlog = 1 _id = string("repl-warning-", mod, "-", owner, "-", name) _line = nothing _file = nothing _module = nothing
2✔
198
end
199

200
function has_ancestor(query::Module, target::Module)
2✔
201
    query == target && return true
38✔
202
    while true
30✔
203
        next = parentmodule(query)
30✔
204
        next == target && return true
30✔
205
        next == query && return false
28✔
206
        query = next
×
207
    end
15✔
208
end
209

210
retrieve_modules(::Module, ::Any) = (nothing,)
×
211
function retrieve_modules(current_module::Module, mod_name::Symbol)
96✔
212
    mod = try
96✔
213
        getproperty(current_module, mod_name)
96✔
214
    catch
215
        return (nothing,)
19✔
216
    end
217
    return (mod isa Module ? mod : nothing,)
77✔
218
end
219
retrieve_modules(current_module::Module, mod_name::QuoteNode) = retrieve_modules(current_module, mod_name.value)
41✔
220
function retrieve_modules(current_module::Module, mod_expr::Expr)
41✔
221
    if Meta.isexpr(mod_expr, :., 2)
41✔
222
        current_module = retrieve_modules(current_module, mod_expr.args[1])[1]
41✔
223
        current_module === nothing && return (nothing,)
41✔
224
        return (current_module, retrieve_modules(current_module, mod_expr.args[2])...)
41✔
225
    else
226
        return (nothing,)
×
227
    end
228
end
229

230
add_locals!(locals, ast::Any) = nothing
×
231
function add_locals!(locals, ast::Expr)
14✔
232
    for arg in ast.args
14✔
233
        add_locals!(locals, arg)
23✔
234
    end
23✔
235
    return nothing
14✔
236
end
237
function add_locals!(locals, ast::Symbol)
57✔
238
    push!(locals, ast)
57✔
239
    return nothing
57✔
240
end
241

242
function collect_names_to_warn!(warnings, locals, current_module::Module, ast)
1,173✔
243
    ast isa Expr || return
1,697✔
244

245
    # don't recurse through module definitions
246
    ast.head === :module && return
649✔
247

248
    if Meta.isexpr(ast, :., 2)
649✔
249
        mod_name, name_being_accessed = ast.args
61✔
250
        # retrieve the (possibly-nested) module being named here
251
        mods = retrieve_modules(current_module, mod_name)
61✔
252
        all(x -> x isa Module, mods) || return
87✔
253
        outer_mod = first(mods)
36✔
254
        mod = last(mods)
36✔
255
        if name_being_accessed isa QuoteNode
36✔
256
            name_being_accessed = name_being_accessed.value
36✔
257
        end
258
        name_being_accessed isa Symbol || return
36✔
259
        owner = try
36✔
260
            which(mod, name_being_accessed)
36✔
261
        catch
262
            return
×
263
        end
264
        # if `owner` is a submodule of `mod`, then don't warn. E.g. the name `parse` is present in the module `JSON`
265
        # but is owned by `JSON.Parser`; we don't warn if it is accessed as `JSON.parse`.
266
        has_ancestor(owner, mod) && return
61✔
267
        # Don't warn if the name is public in the module we are accessing it
268
        Base.ispublic(mod, name_being_accessed) && return
12✔
269
        # Don't warn if accessing names defined in Core from Base if they are present in Base (e.g. `Base.throw`).
270
        mod === Base && Base.ispublic(Core, name_being_accessed) && return
11✔
271
        push!(warnings, (; outer_mod, mod, owner, name_being_accessed))
11✔
272
        # no recursion
273
        return
11✔
274
    elseif Meta.isexpr(ast, :(=), 2)
588✔
275
        lhs, rhs = ast.args
47✔
276
        # any symbols we find on the LHS we will count as local. This can potentially be overzealous,
277
        # but we want to avoid false positives (unnecessary warnings) more than false negatives.
278
        add_locals!(locals, lhs)
47✔
279
        # we'll recurse into the RHS only
280
        return collect_names_to_warn!(warnings, locals, current_module, rhs)
47✔
281
    elseif Meta.isexpr(ast, :function) && length(ast.args) >= 1
541✔
282

283
        if Meta.isexpr(ast.args[1], :call, 2)
3✔
284
            func_name, func_args = ast.args[1].args
2✔
285
            # here we have a function definition and are inspecting it's arguments for local variables.
286
            # we will error on the conservative side by adding all symbols we find (regardless if they are local variables or possibly-global default values)
287
            add_locals!(locals, func_args)
2✔
288
        end
289
        # fall through to general recursion
290
    end
291

292
    for arg in ast.args
541✔
293
        collect_names_to_warn!(warnings, locals, current_module, arg)
1,004✔
294
    end
1,004✔
295

296
    return nothing
541✔
297
end
298

299
function collect_qualified_access_warnings(current_mod, ast)
12✔
300
    warnings = Set()
122✔
301
    locals = Set{Symbol}()
122✔
302
    collect_names_to_warn!(warnings, locals, current_mod, ast)
122✔
303
    filter!(warnings) do (; outer_mod)
122✔
304
        nameof(outer_mod) ∉ locals
11✔
305
    end
306
    return warnings
122✔
307
end
308

309
function warn_on_non_owning_accesses(current_mod, ast)
110✔
310
    warnings = collect_qualified_access_warnings(current_mod, ast)
110✔
311
    for (; outer_mod, mod, owner, name_being_accessed) in warnings
220✔
312
        print_qualified_access_warning(mod, owner, name_being_accessed)
2✔
313
    end
4✔
314
    return ast
110✔
315
end
316
warn_on_non_owning_accesses(ast) = warn_on_non_owning_accesses(Base.active_module(), ast)
108✔
317

318
const repl_ast_transforms = Any[softscope, warn_on_non_owning_accesses] # defaults for new REPL backends
319

320
# Allows an external package to add hooks into the code loading.
321
# The hook should take a Vector{Symbol} of package names and
322
# return true if all packages could be installed, false if not
323
# to e.g. install packages on demand
324
const install_packages_hooks = Any[]
325

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

334
function toplevel_eval_with_hooks(mod::Module, @nospecialize(ast), toplevel_file=Ref{Ptr{UInt8}}(Base.unsafe_convert(Ptr{UInt8}, :REPL)), toplevel_line=Ref{Cint}(1))
345✔
335
    if !isexpr(ast, :toplevel)
453✔
336
        ast = __repl_entry_lower_with_loc(mod, ast, toplevel_file, toplevel_line)
217✔
337
        check_for_missing_packages_and_run_hooks(ast)
217✔
338
        return __repl_entry_eval_expanded_with_loc(mod, ast, toplevel_file, toplevel_line)
216✔
339
    end
340
    local value=nothing
128✔
341
    for i = 1:length(ast.args)
128✔
342
        value = toplevel_eval_with_hooks(mod, ast.args[i], toplevel_file, toplevel_line)
237✔
343
    end
339✔
344
    return value
121✔
345
end
346

347
function eval_user_input(@nospecialize(ast), backend::REPLBackend, mod::Module)
108✔
348
    lasterr = nothing
×
349
    Base.sigatomic_begin()
108✔
350
    while true
113✔
351
        try
113✔
352
            Base.sigatomic_end()
113✔
353
            if lasterr !== nothing
113✔
354
                put!(backend.response_channel, Pair{Any, Bool}(lasterr, true))
5✔
355
            else
356
                backend.in_eval = true
108✔
357
                for xf in backend.ast_transforms
108✔
358
                    ast = Base.invokelatest(xf, ast)
234✔
359
                end
234✔
360
                value = toplevel_eval_with_hooks(mod, ast)
108✔
361
                backend.in_eval = false
102✔
362
                setglobal!(Base.MainInclude, :ans, value)
102✔
363
                put!(backend.response_channel, Pair{Any, Bool}(value, false))
102✔
364
            end
365
            break
112✔
366
        catch err
367
            if lasterr !== nothing
5✔
368
                println("SYSTEM ERROR: Failed to report error to REPL frontend")
×
369
                println(err)
×
370
            end
371
            lasterr = current_exceptions()
5✔
372
        end
373
    end
5✔
374
    Base.sigatomic_end()
107✔
375
    nothing
376
end
377

378
function check_for_missing_packages_and_run_hooks(ast)
217✔
379
    isa(ast, Expr) || return
327✔
380
    mods = modules_to_be_loaded(ast)
107✔
381
    filter!(mod -> isnothing(Base.identify_package(String(mod))), mods) # keep missing modules
112✔
382
    if !isempty(mods)
107✔
383
        isempty(install_packages_hooks) && load_pkg()
1✔
384
        for f in install_packages_hooks
1✔
385
            Base.invokelatest(f, mods) && return
1✔
386
        end
×
387
    end
388
end
389

390
function _modules_to_be_loaded!(ast::Expr, mods::Vector{Symbol})
663✔
391
    ast.head === :quote && return mods # don't search if it's not going to be run during this eval
663✔
392
    if ast.head === :using || ast.head === :import
1,309✔
393
        for arg in ast.args
22✔
394
            arg = arg::Expr
26✔
395
            arg1 = first(arg.args)
26✔
396
            if arg1 isa Symbol # i.e. `Foo`
26✔
397
                if arg1 != :. # don't include local import `import .Foo`
21✔
398
                    push!(mods, arg1)
20✔
399
                end
400
            else # i.e. `Foo: bar`
401
                sym = first((arg1::Expr).args)::Symbol
5✔
402
                if sym != :. # don't include local import `import .Foo: a`
5✔
403
                    push!(mods, sym)
3✔
404
                end
405
            end
406
        end
26✔
407
    end
408
    if ast.head !== :thunk
663✔
409
        for arg in ast.args
553✔
410
            if isexpr(arg, (:block, :if, :using, :import))
1,339✔
411
                _modules_to_be_loaded!(arg, mods)
33✔
412
            end
413
        end
1,339✔
414
    else
415
        code = ast.args[1]
110✔
416
        for arg in code.code
110✔
417
            isa(arg, Expr) || continue
875✔
418
            _modules_to_be_loaded!(arg, mods)
499✔
419
        end
875✔
420
    end
421
end
422

423
function modules_to_be_loaded(ast::Expr, mods::Vector{Symbol} = Symbol[])
24✔
424
    _modules_to_be_loaded!(ast, mods)
262✔
425
    filter!(mod::Symbol -> !in(mod, (:Base, :Main, :Core)), mods) # Exclude special non-package modules
154✔
426
    return unique(mods)
131✔
427
end
428

429
"""
430
    start_repl_backend(repl_channel::Channel, response_channel::Channel)
431

432
    Starts loop for REPL backend
433
    Returns a REPLBackend with backend_task assigned
434

435
    Deprecated since sync / async behavior cannot be selected
436
"""
437
function start_repl_backend(repl_channel::Channel{Any}, response_channel::Channel{Any}
×
438
                            ; get_module::Function = ()->Main)
439
    # Maintain legacy behavior of asynchronous backend
440
    backend = REPLBackend(repl_channel, response_channel, false)
×
441
    # Assignment will be made twice, but will be immediately available
442
    backend.backend_task = @async start_repl_backend(backend; get_module)
×
443
    return backend
×
444
end
445

446
"""
447
    start_repl_backend(backend::REPLBackend)
448

449
    Call directly to run backend loop on current Task.
450
    Use @async for run backend on new Task.
451

452
    Does not return backend until loop is finished.
453
"""
454
function start_repl_backend(backend::REPLBackend,  @nospecialize(consumer = x -> nothing); get_module::Function = ()->Main)
56✔
455
    backend.backend_task = Base.current_task()
26✔
456
    consumer(backend)
26✔
457
    repl_backend_loop(backend, get_module)
26✔
458
    return backend
25✔
459
end
460

461
function repl_backend_loop(backend::REPLBackend, get_module::Function)
26✔
462
    # include looks at this to determine the relative include path
463
    # nothing means cwd
464
    while true
×
465
        tls = task_local_storage()
133✔
466
        tls[:SOURCE_PATH] = nothing
133✔
467
        ast, show_value = take!(backend.repl_channel)
133✔
468
        if show_value == -1
133✔
469
            # exit flag
470
            break
25✔
471
        end
472
        eval_user_input(ast, backend, get_module())
108✔
473
    end
107✔
474
    return nothing
25✔
475
end
476

477
struct REPLDisplay{Repl<:AbstractREPL} <: AbstractDisplay
478
    repl::Repl
35✔
479
end
480

481
function display(d::REPLDisplay, mime::MIME"text/plain", x)
63✔
482
    x = Ref{Any}(x)
63✔
483
    with_repl_linfo(d.repl) do io
63✔
484
        io = IOContext(io, :limit => true, :module => Base.active_module(d)::Module)
123✔
485
        if d.repl isa LineEditREPL
3✔
486
            mistate = d.repl.mistate
60✔
487
            mode = LineEdit.mode(mistate)
60✔
488
            if mode isa LineEdit.Prompt
60✔
489
                LineEdit.write_output_prefix(io, mode, get(io, :color, false)::Bool)
240✔
490
            end
491
        end
492
        get(io, :color, false)::Bool && write(io, answer_color(d.repl))
69✔
493
        if isdefined(d.repl, :options) && isdefined(d.repl.options, :iocontext)
3✔
494
            # this can override the :limit property set initially
495
            io = foldl(IOContext, d.repl.options.iocontext, init=io)
60✔
496
        end
497
        show_repl(io, mime, x[])
63✔
498
        println(io)
62✔
499
    end
500
    return nothing
62✔
501
end
502

503
display(d::REPLDisplay, x) = display(d, MIME("text/plain"), x)
63✔
504

505
show_repl(io::IO, mime::MIME"text/plain", x) = show(io, mime, x)
61✔
506

507
show_repl(io::IO, ::MIME"text/plain", ex::Expr) =
2✔
508
    print(io, JuliaSyntaxHighlighting.highlight(
509
        sprint(show, ex, context=IOContext(io, :color => false))))
510

511
function print_response(repl::AbstractREPL, response, show_value::Bool, have_color::Bool)
104✔
512
    repl.waserror = response[2]
104✔
513
    with_repl_linfo(repl) do io
104✔
514
        io = IOContext(io, :module => Base.active_module(repl)::Module)
205✔
515
        print_response(io, response, show_value, have_color, specialdisplay(repl))
104✔
516
    end
517
    return nothing
104✔
518
end
519

520
function repl_display_error(errio::IO, @nospecialize errval)
9✔
521
    # this will be set to true if types in the stacktrace are truncated
522
    limitflag = Ref(false)
9✔
523
    errio = IOContext(errio, :stacktrace_types_limited => limitflag)
9✔
524
    Base.invokelatest(Base.display_error, errio, errval)
9✔
525
    if limitflag[]
7✔
526
        print(errio, "Some type information was truncated. Use `show(err)` to see complete types.")
2✔
527
        println(errio)
2✔
528
    end
529
    return nothing
7✔
530
end
531

532
function print_response(errio::IO, response, show_value::Bool, have_color::Bool, specialdisplay::Union{AbstractDisplay,Nothing}=nothing)
105✔
533
    Base.sigatomic_begin()
105✔
534
    val, iserr = response
105✔
535
    while true
106✔
536
        try
106✔
537
            Base.sigatomic_end()
106✔
538
            if iserr
106✔
539
                val = Base.scrub_repl_backtrace(val)
8✔
540
                Base.istrivialerror(val) || setglobal!(Base.MainInclude, :err, val)
13✔
541
                repl_display_error(errio, val)
8✔
542
            else
543
                if val !== nothing && show_value
98✔
544
                    try
63✔
545
                        if specialdisplay === nothing
63✔
546
                            Base.invokelatest(display, val)
49✔
547
                        else
548
                            Base.invokelatest(display, specialdisplay, val)
62✔
549
                        end
550
                    catch
551
                        println(errio, "Error showing value of type ", typeof(val), ":")
1✔
552
                        rethrow()
1✔
553
                    end
554
                end
555
            end
556
            break
106✔
557
        catch ex
558
            if iserr
2✔
559
                println(errio) # an error during printing is likely to leave us mid-line
1✔
560
                println(errio, "SYSTEM (REPL): showing an error caused an error")
1✔
561
                try
1✔
562
                    excs = Base.scrub_repl_backtrace(current_exceptions())
1✔
563
                    setglobal!(Base.MainInclude, :err, excs)
1✔
564
                    repl_display_error(errio, excs)
2✔
565
                catch e
566
                    # at this point, only print the name of the type as a Symbol to
567
                    # minimize the possibility of further errors.
568
                    println(errio)
1✔
569
                    println(errio, "SYSTEM (REPL): caught exception of type ", typeof(e).name.name,
1✔
570
                            " while trying to handle a nested exception; giving up")
571
                end
572
                break
1✔
573
            end
574
            val = current_exceptions()
1✔
575
            iserr = true
1✔
576
        end
577
    end
1✔
578
    Base.sigatomic_end()
105✔
579
    nothing
580
end
581

582
# A reference to a backend that is not mutable
583
struct REPLBackendRef
584
    repl_channel::Channel{Any}
24✔
585
    response_channel::Channel{Any}
586
end
587
REPLBackendRef(backend::REPLBackend) = REPLBackendRef(backend.repl_channel, backend.response_channel)
24✔
588

589
function destroy(ref::REPLBackendRef, state::Task)
590
    if istaskfailed(state)
23✔
591
        close(ref.repl_channel, TaskFailedException(state))
×
592
        close(ref.response_channel, TaskFailedException(state))
×
593
    end
594
    close(ref.repl_channel)
23✔
595
    close(ref.response_channel)
23✔
596
end
597

598
"""
599
    run_repl(repl::AbstractREPL)
600
    run_repl(repl, consumer = backend->nothing; backend_on_current_task = true)
601

602
    Main function to start the REPL
603

604
    consumer is an optional function that takes a REPLBackend as an argument
605
"""
606
function run_repl(repl::AbstractREPL, @nospecialize(consumer = x -> nothing); backend_on_current_task::Bool = true, backend = REPLBackend())
71✔
607
    backend_ref = REPLBackendRef(backend)
24✔
608
    cleanup = @task try
47✔
609
            destroy(backend_ref, t)
23✔
610
        catch e
611
            Core.print(Core.stderr, "\nINTERNAL ERROR: ")
×
612
            Core.println(Core.stderr, e)
×
613
            Core.println(Core.stderr, catch_backtrace())
×
614
        end
615
    get_module = () -> Base.active_module(repl)
124✔
616
    if backend_on_current_task
24✔
617
        t = @async run_frontend(repl, backend_ref)
48✔
618
        errormonitor(t)
24✔
619
        Base._wait2(t, cleanup)
24✔
620
        start_repl_backend(backend, consumer; get_module)
24✔
621
    else
622
        t = @async start_repl_backend(backend, consumer; get_module)
×
623
        errormonitor(t)
×
624
        Base._wait2(t, cleanup)
×
625
        run_frontend(repl, backend_ref)
×
626
    end
627
    return backend
23✔
628
end
629

630
## BasicREPL ##
631

632
mutable struct BasicREPL <: AbstractREPL
633
    terminal::TextTerminal
634
    waserror::Bool
635
    frontend_task::Task
636
    BasicREPL(t) = new(t, false)
3✔
637
end
638

639
outstream(r::BasicREPL) = r.terminal
6✔
640
hascolor(r::BasicREPL) = hascolor(r.terminal)
×
641

642
function run_frontend(repl::BasicREPL, backend::REPLBackendRef)
3✔
643
    repl.frontend_task = current_task()
3✔
644
    d = REPLDisplay(repl)
3✔
645
    dopushdisplay = !in(d,Base.Multimedia.displays)
6✔
646
    dopushdisplay && pushdisplay(d)
3✔
647
    hit_eof = false
×
648
    while true
6✔
649
        Base.reseteof(repl.terminal)
6✔
650
        write(repl.terminal, JULIA_PROMPT)
6✔
651
        line = ""
×
652
        ast = nothing
×
653
        interrupted = false
×
654
        while true
6✔
655
            try
6✔
656
                line *= readline(repl.terminal, keep=true)
6✔
657
            catch e
658
                if isa(e,InterruptException)
×
659
                    try # raise the debugger if present
×
660
                        ccall(:jl_raise_debugger, Int, ())
×
661
                    catch
×
662
                    end
663
                    line = ""
×
664
                    interrupted = true
×
665
                    break
×
666
                elseif isa(e,EOFError)
×
667
                    hit_eof = true
×
668
                    break
×
669
                else
670
                    rethrow()
×
671
                end
672
            end
673
            ast = Base.parse_input_line(line)
12✔
674
            (isa(ast,Expr) && ast.head === :incomplete) || break
12✔
675
        end
×
676
        if !isempty(line)
6✔
677
            response = eval_with_backend(ast, backend)
4✔
678
            print_response(repl, response, !ends_with_semicolon(line), false)
3✔
679
        end
680
        write(repl.terminal, '\n')
5✔
681
        ((!interrupted && isempty(line)) || hit_eof) && break
8✔
682
    end
3✔
683
    # terminate backend
684
    put!(backend.repl_channel, (nothing, -1))
2✔
685
    dopushdisplay && popdisplay(d)
2✔
686
    nothing
687
end
688

689
## LineEditREPL ##
690

691
mutable struct LineEditREPL <: AbstractREPL
692
    t::TextTerminal
693
    hascolor::Bool
694
    prompt_color::String
695
    input_color::String
696
    answer_color::String
697
    shell_color::String
698
    help_color::String
699
    pkg_color::String
700
    history_file::Bool
701
    in_shell::Bool
702
    in_help::Bool
703
    envcolors::Bool
704
    waserror::Bool
705
    specialdisplay::Union{Nothing,AbstractDisplay}
706
    options::Options
707
    mistate::Union{MIState,Nothing}
708
    last_shown_line_infos::Vector{Tuple{String,Int}}
709
    interface::ModalInterface
710
    backendref::REPLBackendRef
711
    frontend_task::Task
712
    function LineEditREPL(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,pkg_color,history_file,in_shell,in_help,envcolors)
28✔
713
        opts = Options()
28✔
714
        opts.hascolor = hascolor
28✔
715
        if !hascolor
28✔
716
            opts.beep_colors = [""]
×
717
        end
718
        new(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,pkg_color,history_file,in_shell,
28✔
719
            in_help,envcolors,false,nothing, opts, nothing, Tuple{String,Int}[])
720
    end
721
end
722
outstream(r::LineEditREPL) = (t = r.t; t isa TTYTerminal ? t.out_stream : t)
171✔
723
specialdisplay(r::LineEditREPL) = r.specialdisplay
101✔
724
specialdisplay(r::AbstractREPL) = nothing
×
725
terminal(r::LineEditREPL) = r.t
158✔
726
hascolor(r::LineEditREPL) = r.hascolor
203✔
727

728
LineEditREPL(t::TextTerminal, hascolor::Bool, envcolors::Bool=false) =
56✔
729
    LineEditREPL(t, hascolor,
730
        hascolor ? Base.text_colors[:green] : "",
731
        hascolor ? Base.input_color() : "",
732
        hascolor ? Base.answer_color() : "",
733
        hascolor ? Base.text_colors[:red] : "",
734
        hascolor ? Base.text_colors[:yellow] : "",
735
        hascolor ? Base.text_colors[:blue] : "",
736
        false, false, false, envcolors
737
    )
738

739
mutable struct REPLCompletionProvider <: CompletionProvider
740
    modifiers::LineEdit.Modifiers
25✔
741
end
742
REPLCompletionProvider() = REPLCompletionProvider(LineEdit.Modifiers())
25✔
743

744
mutable struct ShellCompletionProvider <: CompletionProvider end
25✔
745
struct LatexCompletions <: CompletionProvider end
746

747
Base.active_module((; mistate)::LineEditREPL) = mistate === nothing ? Main : mistate.active_module
4,795✔
748
Base.active_module(::AbstractREPL) = Main
2✔
749
Base.active_module(d::REPLDisplay) = Base.active_module(d.repl)
123✔
750

751
setmodifiers!(c::CompletionProvider, m::LineEdit.Modifiers) = nothing
×
752

753
setmodifiers!(c::REPLCompletionProvider, m::LineEdit.Modifiers) = c.modifiers = m
×
754

755
"""
756
    activate(mod::Module=Main)
757

758
Set `mod` as the default contextual module in the REPL,
759
both for evaluating expressions and printing them.
760
"""
761
function activate(mod::Module=Main; interactive_utils::Bool=true)
×
762
    mistate = (Base.active_repl::LineEditREPL).mistate
×
763
    mistate === nothing && return nothing
×
764
    mistate.active_module = mod
×
765
    interactive_utils && Base.load_InteractiveUtils(mod)
×
766
    return nothing
×
767
end
768

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

771
function complete_line(c::REPLCompletionProvider, s::PromptState, mod::Module; hint::Bool=false)
3,524✔
772
    partial = beforecursor(s.input_buffer)
3,524✔
773
    full = LineEdit.input_string(s)
1,762✔
774
    ret, range, should_complete = completions(full, lastindex(partial), mod, c.modifiers.shift, hint)
3,524✔
775
    c.modifiers = LineEdit.Modifiers()
1,762✔
776
    return unique!(String[completion_text(x) for x in ret]), partial[range], should_complete
1,762✔
777
end
778

779
function complete_line(c::ShellCompletionProvider, s::PromptState; hint::Bool=false)
844✔
780
    # First parse everything up to the current position
781
    partial = beforecursor(s.input_buffer)
844✔
782
    full = LineEdit.input_string(s)
422✔
783
    ret, range, should_complete = shell_completions(full, lastindex(partial), hint)
844✔
784
    return unique!(String[completion_text(x) for x in ret]), partial[range], should_complete
422✔
785
end
786

787
function complete_line(c::LatexCompletions, s; hint::Bool=false)
×
788
    partial = beforecursor(LineEdit.buffer(s))
×
789
    full = LineEdit.input_string(s)::String
×
790
    ret, range, should_complete = bslash_completions(full, lastindex(partial), hint)[2]
×
791
    return unique!(String[completion_text(x) for x in ret]), partial[range], should_complete
×
792
end
793

794
with_repl_linfo(f, repl) = f(outstream(repl))
6✔
795
function with_repl_linfo(f, repl::LineEditREPL)
161✔
796
    linfos = Tuple{String,Int}[]
161✔
797
    io = IOContext(outstream(repl), :last_shown_line_infos => linfos)
161✔
798
    f(io)
161✔
799
    if !isempty(linfos)
160✔
800
        repl.last_shown_line_infos = linfos
6✔
801
    end
802
    nothing
803
end
804

805
mutable struct REPLHistoryProvider <: HistoryProvider
806
    history::Vector{String}
30✔
807
    file_path::String
808
    history_file::Union{Nothing,IO}
809
    start_idx::Int
810
    cur_idx::Int
811
    last_idx::Int
812
    last_buffer::IOBuffer
813
    last_mode::Union{Nothing,Prompt}
814
    mode_mapping::Dict{Symbol,Prompt}
815
    modes::Vector{Symbol}
816
end
817
REPLHistoryProvider(mode_mapping::Dict{Symbol}) =
30✔
818
    REPLHistoryProvider(String[], "", nothing, 0, 0, -1, IOBuffer(),
819
                        nothing, mode_mapping, UInt8[])
820

821
invalid_history_message(path::String) = """
×
822
Invalid history file ($path) format:
823
If you have a history file left over from an older version of Julia,
824
try renaming or deleting it.
825
Invalid character: """
826

827
munged_history_message(path::String) = """
×
828
Invalid history file ($path) format:
829
An editor may have converted tabs to spaces at line """
830

831
function hist_open_file(hp::REPLHistoryProvider)
832
    f = open(hp.file_path, read=true, write=true, create=true)
4✔
833
    hp.history_file = f
4✔
834
    seekend(f)
4✔
835
end
836

837
function hist_from_file(hp::REPLHistoryProvider, path::String)
8✔
838
    getline(lines, i) = i > length(lines) ? "" : lines[i]
276✔
839
    file_lines = readlines(path)
8✔
840
    countlines = 0
×
841
    while true
42✔
842
        # First parse the metadata that starts with '#' in particular the REPL mode
843
        countlines += 1
42✔
844
        line = getline(file_lines, countlines)
76✔
845
        mode = :julia
×
846
        isempty(line) && break
42✔
847
        line[1] != '#' &&
68✔
848
            error(invalid_history_message(path), repr(line[1]), " at line ", countlines)
849
        while !isempty(line)
102✔
850
            startswith(line, '#') || break
204✔
851
            if startswith(line, "# mode: ")
68✔
852
                mode = Symbol(SubString(line, 9))
68✔
853
            end
854
            countlines += 1
68✔
855
            line = getline(file_lines, countlines)
136✔
856
        end
68✔
857
        isempty(line) && break
34✔
858

859
        # Now parse the code for the current REPL mode
860
        line[1] == ' '  &&
68✔
861
            error(munged_history_message(path), countlines)
862
        line[1] != '\t' &&
68✔
863
            error(invalid_history_message(path), repr(line[1]), " at line ", countlines)
864
        lines = String[]
34✔
865
        while !isempty(line)
34✔
866
            push!(lines, chomp(SubString(line, 2)))
68✔
867
            next_line = getline(file_lines, countlines+1)
64✔
868
            isempty(next_line) && break
34✔
869
            first(next_line) == ' '  && error(munged_history_message(path), countlines)
60✔
870
            # A line not starting with a tab means we are done with code for this entry
871
            first(next_line) != '\t' && break
60✔
872
            countlines += 1
×
873
            line = getline(file_lines, countlines)
×
874
        end
×
875
        push!(hp.modes, mode)
34✔
876
        push!(hp.history, join(lines, '\n'))
34✔
877
    end
34✔
878
    hp.start_idx = length(hp.history)
8✔
879
    return hp
8✔
880
end
881

882
function add_history(hist::REPLHistoryProvider, s::PromptState)
119✔
883
    str = rstrip(String(take!(copy(s.input_buffer))))
223✔
884
    isempty(strip(str)) && return
119✔
885
    mode = mode_idx(hist, LineEdit.mode(s))
101✔
886
    !isempty(hist.history) &&
101✔
887
        isequal(mode, hist.modes[end]) && str == hist.history[end] && return
888
    push!(hist.modes, mode)
95✔
889
    push!(hist.history, str)
95✔
890
    hist.history_file === nothing && return
95✔
891
    entry = """
16✔
892
    # time: $(Libc.strftime("%Y-%m-%d %H:%M:%S %Z", time()))
893
    # mode: $mode
894
    $(replace(str, r"^"ms => "\t"))
895
    """
896
    # TODO: write-lock history file
897
    try
16✔
898
        seekend(hist.history_file)
16✔
899
    catch err
900
        (err isa SystemError) || rethrow()
×
901
        # File handle might get stale after a while, especially under network file systems
902
        # If this doesn't fix it (e.g. when file is deleted), we'll end up rethrowing anyway
903
        hist_open_file(hist)
×
904
    end
905
    print(hist.history_file, entry)
32✔
906
    flush(hist.history_file)
16✔
907
    nothing
908
end
909

910
function history_move(s::Union{LineEdit.MIState,LineEdit.PrefixSearchState}, hist::REPLHistoryProvider, idx::Int, save_idx::Int = hist.cur_idx)
96✔
911
    max_idx = length(hist.history) + 1
136✔
912
    @assert 1 <= hist.cur_idx <= max_idx
96✔
913
    (1 <= idx <= max_idx) || return :none
98✔
914
    idx != hist.cur_idx || return :none
94✔
915

916
    # save the current line
917
    if save_idx == max_idx
94✔
918
        hist.last_mode = LineEdit.mode(s)
31✔
919
        hist.last_buffer = copy(LineEdit.buffer(s))
46✔
920
    else
921
        hist.history[save_idx] = LineEdit.input_string(s)
84✔
922
        hist.modes[save_idx] = mode_idx(hist, LineEdit.mode(s))
63✔
923
    end
924

925
    # load the saved line
926
    if idx == max_idx
94✔
927
        last_buffer = hist.last_buffer
10✔
928
        LineEdit.transition(s, hist.last_mode) do
14✔
929
            LineEdit.replace_line(s, last_buffer)
10✔
930
        end
931
        hist.last_mode = nothing
10✔
932
        hist.last_buffer = IOBuffer()
10✔
933
    else
934
        if haskey(hist.mode_mapping, hist.modes[idx])
168✔
935
            LineEdit.transition(s, hist.mode_mapping[hist.modes[idx]]) do
68✔
936
                LineEdit.replace_line(s, hist.history[idx])
68✔
937
            end
938
        else
939
            return :skip
16✔
940
        end
941
    end
942
    hist.cur_idx = idx
78✔
943

944
    return :ok
78✔
945
end
946

947
# REPL History can also transitions modes
948
function LineEdit.accept_result_newmode(hist::REPLHistoryProvider)
27✔
949
    if 1 <= hist.cur_idx <= length(hist.modes)
27✔
950
        return hist.mode_mapping[hist.modes[hist.cur_idx]]
23✔
951
    end
952
    return nothing
4✔
953
end
954

955
function history_prev(s::LineEdit.MIState, hist::REPLHistoryProvider,
34✔
956
                      num::Int=1, save_idx::Int = hist.cur_idx)
957
    num <= 0 && return history_next(s, hist, -num, save_idx)
58✔
958
    hist.last_idx = -1
32✔
959
    m = history_move(s, hist, hist.cur_idx-num, save_idx)
32✔
960
    if m === :ok
32✔
961
        LineEdit.move_input_start(s)
48✔
962
        LineEdit.reset_key_repeats(s) do
24✔
963
            LineEdit.move_line_end(s)
24✔
964
        end
965
        return LineEdit.refresh_line(s)
24✔
966
    elseif m === :skip
8✔
967
        return history_prev(s, hist, num+1, save_idx)
8✔
968
    else
969
        return Terminals.beep(s)
×
970
    end
971
end
972

973
function history_next(s::LineEdit.MIState, hist::REPLHistoryProvider,
26✔
974
                      num::Int=1, save_idx::Int = hist.cur_idx)
975
    if num == 0
44✔
976
        Terminals.beep(s)
×
977
        return
×
978
    end
979
    num < 0 && return history_prev(s, hist, -num, save_idx)
26✔
980
    cur_idx = hist.cur_idx
24✔
981
    max_idx = length(hist.history) + 1
24✔
982
    if cur_idx == max_idx && 0 < hist.last_idx
24✔
983
        # issue #6312
984
        cur_idx = hist.last_idx
×
985
        hist.last_idx = -1
×
986
    end
987
    m = history_move(s, hist, cur_idx+num, save_idx)
24✔
988
    if m === :ok
24✔
989
        LineEdit.move_input_end(s)
16✔
990
        return LineEdit.refresh_line(s)
16✔
991
    elseif m === :skip
8✔
992
        return history_next(s, hist, num+1, save_idx)
6✔
993
    else
994
        return Terminals.beep(s)
2✔
995
    end
996
end
997

998
history_first(s::LineEdit.MIState, hist::REPLHistoryProvider) =
6✔
999
    history_prev(s, hist, hist.cur_idx - 1 -
1000
                 (hist.cur_idx > hist.start_idx+1 ? hist.start_idx : 0))
1001

1002
history_last(s::LineEdit.MIState, hist::REPLHistoryProvider) =
4✔
1003
    history_next(s, hist, length(hist.history) - hist.cur_idx + 1)
1004

1005
function history_move_prefix(s::LineEdit.PrefixSearchState,
38✔
1006
                             hist::REPLHistoryProvider,
1007
                             prefix::AbstractString,
1008
                             backwards::Bool,
1009
                             cur_idx::Int = hist.cur_idx)
1010
    cur_response = String(take!(copy(LineEdit.buffer(s))))
101✔
1011
    # when searching forward, start at last_idx
1012
    if !backwards && hist.last_idx > 0
38✔
1013
        cur_idx = hist.last_idx
1✔
1014
    end
1015
    hist.last_idx = -1
38✔
1016
    max_idx = length(hist.history)+1
38✔
1017
    idxs = backwards ? ((cur_idx-1):-1:1) : ((cur_idx+1):1:max_idx)
65✔
1018
    for idx in idxs
38✔
1019
        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✔
1020
            m = history_move(s, hist, idx)
36✔
1021
            if m === :ok
36✔
1022
                if idx == max_idx
34✔
1023
                    # on resuming the in-progress edit, leave the cursor where the user last had it
1024
                elseif isempty(prefix)
30✔
1025
                    # on empty prefix search, move cursor to the end
1026
                    LineEdit.move_input_end(s)
14✔
1027
                else
1028
                    # otherwise, keep cursor at the prefix position as a visual cue
1029
                    seek(LineEdit.buffer(s), sizeof(prefix))
16✔
1030
                end
1031
                LineEdit.refresh_line(s)
34✔
1032
                return :ok
34✔
1033
            elseif m === :skip
2✔
1034
                return history_move_prefix(s,hist,prefix,backwards,idx)
2✔
1035
            end
1036
        end
1037
    end
62✔
1038
    Terminals.beep(s)
×
1039
    nothing
1040
end
1041
history_next_prefix(s::LineEdit.PrefixSearchState, hist::REPLHistoryProvider, prefix::AbstractString) =
5✔
1042
    history_move_prefix(s, hist, prefix, false)
1043
history_prev_prefix(s::LineEdit.PrefixSearchState, hist::REPLHistoryProvider, prefix::AbstractString) =
31✔
1044
    history_move_prefix(s, hist, prefix, true)
1045

1046
function history_search(hist::REPLHistoryProvider, query_buffer::IOBuffer, response_buffer::IOBuffer,
28✔
1047
                        backwards::Bool=false, skip_current::Bool=false)
1048

1049
    qpos = position(query_buffer)
28✔
1050
    qpos > 0 || return true
28✔
1051
    searchdata = beforecursor(query_buffer)
56✔
1052
    response_str = String(take!(copy(response_buffer)))
50✔
1053

1054
    # Alright, first try to see if the current match still works
1055
    a = position(response_buffer) + 1 # position is zero-indexed
28✔
1056
    # FIXME: I'm pretty sure this is broken since it uses an index
1057
    # into the search data to index into the response string
1058
    b = a + sizeof(searchdata)
28✔
1059
    b = b ≤ ncodeunits(response_str) ? prevind(response_str, b) : b-1
48✔
1060
    b = min(lastindex(response_str), b) # ensure that b is valid
50✔
1061

1062
    searchstart = backwards ? b : a
28✔
1063
    if searchdata == response_str[a:b]
44✔
1064
        if skip_current
10✔
1065
            searchstart = backwards ? prevind(response_str, b) : nextind(response_str, a)
4✔
1066
        else
1067
            return true
6✔
1068
        end
1069
    end
1070

1071
    # Start searching
1072
    # First the current response buffer
1073
    if 1 <= searchstart <= lastindex(response_str)
36✔
1074
        match = backwards ? findprev(searchdata, response_str, searchstart) :
14✔
1075
                            findnext(searchdata, response_str, searchstart)
1076
        if match !== nothing
14✔
1077
            seek(response_buffer, first(match) - 1)
12✔
1078
            return true
6✔
1079
        end
1080
    end
1081

1082
    # Now search all the other buffers
1083
    idxs = backwards ? ((hist.cur_idx-1):-1:1) : ((hist.cur_idx+1):1:length(hist.history))
32✔
1084
    for idx in idxs
16✔
1085
        h = hist.history[idx]
40✔
1086
        match = backwards ? findlast(searchdata, h) : findfirst(searchdata, h)
80✔
1087
        if match !== nothing && h != response_str && haskey(hist.mode_mapping, hist.modes[idx])
54✔
1088
            truncate(response_buffer, 0)
12✔
1089
            write(response_buffer, h)
12✔
1090
            seek(response_buffer, first(match) - 1)
24✔
1091
            hist.cur_idx = idx
12✔
1092
            return true
12✔
1093
        end
1094
    end
52✔
1095

1096
    return false
4✔
1097
end
1098

1099
function history_reset_state(hist::REPLHistoryProvider)
1100
    if hist.cur_idx != length(hist.history) + 1
276✔
1101
        hist.last_idx = hist.cur_idx
133✔
1102
        hist.cur_idx = length(hist.history) + 1
133✔
1103
    end
1104
    nothing
1105
end
1106
LineEdit.reset_state(hist::REPLHistoryProvider) = history_reset_state(hist)
244✔
1107

1108
function return_callback(s)
98✔
1109
    ast = Base.parse_input_line(String(take!(copy(LineEdit.buffer(s)))), depwarn=false)
181✔
1110
    return !(isa(ast, Expr) && ast.head === :incomplete)
98✔
1111
end
1112

1113
find_hist_file() = get(ENV, "JULIA_HISTORY",
4✔
1114
                       !isempty(DEPOT_PATH) ? joinpath(DEPOT_PATH[1], "logs", "repl_history.jl") :
1115
                       error("DEPOT_PATH is empty and ENV[\"JULIA_HISTORY\"] not set."))
1116

1117
backend(r::AbstractREPL) = r.backendref
100✔
1118

1119
function eval_with_backend(ast, backend::REPLBackendRef)
104✔
1120
    put!(backend.repl_channel, (ast, 1))
104✔
1121
    return take!(backend.response_channel) # (val, iserr)
104✔
1122
end
1123

1124
function respond(f, repl, main; pass_empty::Bool = false, suppress_on_semicolon::Bool = true)
100✔
1125
    return function do_respond(s::MIState, buf, ok::Bool)
237✔
1126
        if !ok
137✔
1127
            return transition(s, :abort)
21✔
1128
        end
1129
        line = String(take!(buf)::Vector{UInt8})
217✔
1130
        if !isempty(line) || pass_empty
131✔
1131
            reset(repl)
101✔
1132
            local response
1133
            try
101✔
1134
                ast = Base.invokelatest(f, line)
101✔
1135
                response = eval_with_backend(ast, backend(repl))
100✔
1136
            catch
1137
                response = Pair{Any, Bool}(current_exceptions(), true)
1✔
1138
            end
1139
            hide_output = suppress_on_semicolon && ends_with_semicolon(line)
101✔
1140
            print_response(repl, response, !hide_output, hascolor(repl))
101✔
1141
        end
1142
        prepare_next(repl)
116✔
1143
        reset_state(s)
116✔
1144
        return s.current_mode.sticky ? true : transition(s, main)
116✔
1145
    end
1146
end
1147

1148
function reset(repl::LineEditREPL)
101✔
1149
    raw!(repl.t, false)
101✔
1150
    hascolor(repl) && print(repl.t, Base.text_colors[:normal])
101✔
1151
    nothing
1152
end
1153

1154
function prepare_next(repl::LineEditREPL)
116✔
1155
    println(terminal(repl))
116✔
1156
end
1157

1158
function mode_keymap(julia_prompt::Prompt)
1159
    AnyDict(
27✔
1160
    '\b' => function (s::MIState,o...)
7✔
1161
        if isempty(s) || position(LineEdit.buffer(s)) == 0
7✔
1162
            buf = copy(LineEdit.buffer(s))
7✔
1163
            transition(s, julia_prompt) do
7✔
1164
                LineEdit.state(s, julia_prompt).input_buffer = buf
7✔
1165
            end
1166
        else
1167
            LineEdit.edit_backspace(s)
×
1168
        end
1169
    end,
1170
    "^C" => function (s::MIState,o...)
1171
        LineEdit.move_input_end(s)
1172
        LineEdit.refresh_line(s)
1173
        print(LineEdit.terminal(s), "^C\n\n")
1174
        transition(s, julia_prompt)
1175
        transition(s, :reset)
1176
        LineEdit.refresh_line(s)
1177
    end)
1178
end
1179

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

1183
const JL_PROMPT_PASTE = Ref(true)
1184
enable_promptpaste(v::Bool) = JL_PROMPT_PASTE[] = v
×
1185

1186
function contextual_prompt(repl::LineEditREPL, prompt::Union{String,Function})
1187
    function ()
2,190✔
1188
        mod = Base.active_module(repl)
4,273✔
1189
        prefix = mod == Main ? "" : string('(', mod, ") ")
2,172✔
1190
        pr = prompt isa String ? prompt : prompt()
2,140✔
1191
        prefix * pr
2,140✔
1192
    end
1193
end
1194

1195
setup_interface(
71✔
1196
    repl::LineEditREPL;
1197
    # those keyword arguments may be deprecated eventually in favor of the Options mechanism
1198
    hascolor::Bool = repl.options.hascolor,
1199
    extra_repl_keymap::Any = repl.options.extra_keymap
1200
) = setup_interface(repl, hascolor, extra_repl_keymap)
1201

1202

1203
# This non keyword method can be precompiled which is important
1204
function setup_interface(
25✔
1205
    repl::LineEditREPL,
1206
    hascolor::Bool,
1207
    extra_repl_keymap::Any, # Union{Dict,Vector{<:Dict}},
1208
)
1209
    # The precompile statement emitter has problem outputting valid syntax for the
1210
    # type of `Union{Dict,Vector{<:Dict}}` (see #28808).
1211
    # This function is however important to precompile for REPL startup time, therefore,
1212
    # make the type Any and just assert that we have the correct type below.
1213
    @assert extra_repl_keymap isa Union{Dict,Vector{<:Dict}}
25✔
1214

1215
    ###
1216
    #
1217
    # This function returns the main interface that describes the REPL
1218
    # functionality, it is called internally by functions that setup a
1219
    # Terminal-based REPL frontend.
1220
    #
1221
    # See run_frontend(repl::LineEditREPL, backend::REPLBackendRef)
1222
    # for usage
1223
    #
1224
    ###
1225

1226
    ###
1227
    # We setup the interface in two stages.
1228
    # First, we set up all components (prompt,rsearch,shell,help)
1229
    # Second, we create keymaps with appropriate transitions between them
1230
    #   and assign them to the components
1231
    #
1232
    ###
1233

1234
    ############################### Stage I ################################
1235

1236
    # This will provide completions for REPL and help mode
1237
    replc = REPLCompletionProvider()
25✔
1238

1239
    # Set up the main Julia prompt
1240
    julia_prompt = Prompt(contextual_prompt(repl, JULIA_PROMPT);
50✔
1241
        # Copy colors from the prompt object
1242
        prompt_prefix = hascolor ? repl.prompt_color : "",
1243
        prompt_suffix = hascolor ?
1244
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1245
        repl = repl,
1246
        complete = replc,
1247
        on_enter = return_callback)
1248

1249
    # Setup help mode
1250
    help_mode = Prompt(contextual_prompt(repl, HELP_PROMPT),
50✔
1251
        prompt_prefix = hascolor ? repl.help_color : "",
1252
        prompt_suffix = hascolor ?
1253
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1254
        repl = repl,
1255
        complete = replc,
1256
        # When we're done transform the entered line into a call to helpmode function
1257
        on_done = respond(line::String->helpmode(outstream(repl), line, repl.mistate.active_module),
2✔
1258
                          repl, julia_prompt, pass_empty=true, suppress_on_semicolon=false))
1259

1260

1261
    # Set up shell mode
1262
    shell_mode = Prompt(SHELL_PROMPT;
50✔
1263
        prompt_prefix = hascolor ? repl.shell_color : "",
1264
        prompt_suffix = hascolor ?
1265
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1266
        repl = repl,
1267
        complete = ShellCompletionProvider(),
1268
        # Transform "foo bar baz" into `foo bar baz` (shell quoting)
1269
        # and pass into Base.repl_cmd for processing (handles `ls` and `cd`
1270
        # special)
1271
        on_done = respond(repl, julia_prompt) do line
1272
            Expr(:call, :(Base.repl_cmd),
9✔
1273
                :(Base.cmd_gen($(Base.shell_parse(line::String)[1]))),
1274
                outstream(repl))
1275
        end,
1276
        sticky = true)
1277

1278
    # Set up dummy Pkg mode that will be replaced once Pkg is loaded
1279
    # use 6 dots to occupy the same space as the most likely "@v1.xx" env name
1280
    dummy_pkg_mode = Prompt(Pkg_promptf,
50✔
1281
        prompt_prefix = hascolor ? repl.pkg_color : "",
1282
        prompt_suffix = hascolor ?
1283
        (repl.envcolors ? Base.input_color : repl.input_color) : "",
1284
        repl = repl,
1285
        complete = LineEdit.EmptyCompletionProvider(),
1286
        on_done = respond(line->nothing, repl, julia_prompt),
1287
        on_enter = function (s::MIState)
1288
                # This is hit when the user tries to execute a command before the real Pkg mode has been
1289
                # switched to. Ok to do this even if Pkg is loading on the other task because of the loading lock.
1290
                REPLExt = load_pkg()
1291
                if REPLExt isa Module && isdefined(REPLExt, :PkgCompletionProvider)
1292
                    for mode in repl.interface.modes
1293
                        if mode isa LineEdit.Prompt && mode.complete isa REPLExt.PkgCompletionProvider
1294
                            # pkg mode
1295
                            buf = copy(LineEdit.buffer(s))
1296
                            transition(s, mode) do
1297
                                LineEdit.state(s, mode).input_buffer = buf
1298
                            end
1299
                        end
1300
                    end
1301
                end
1302
                return true
1303
            end,
1304
        sticky = true)
1305

1306

1307
    ################################# Stage II #############################
1308

1309
    # Setup history
1310
    # We will have a unified history for all REPL modes
1311
    hp = REPLHistoryProvider(Dict{Symbol,Prompt}(:julia => julia_prompt,
25✔
1312
                                                 :shell => shell_mode,
1313
                                                 :help  => help_mode,
1314
                                                 :pkg  => dummy_pkg_mode))
1315
    if repl.history_file
25✔
1316
        try
4✔
1317
            hist_path = find_hist_file()
4✔
1318
            mkpath(dirname(hist_path))
4✔
1319
            hp.file_path = hist_path
4✔
1320
            hist_open_file(hp)
4✔
1321
            finalizer(replc) do replc
4✔
1322
                close(hp.history_file)
4✔
1323
            end
1324
            hist_from_file(hp, hist_path)
4✔
1325
        catch
1326
            # use REPL.hascolor to avoid using the local variable with the same name
1327
            print_response(repl, Pair{Any, Bool}(current_exceptions(), true), true, REPL.hascolor(repl))
×
1328
            println(outstream(repl))
×
1329
            @info "Disabling history file for this session"
×
1330
            repl.history_file = false
×
1331
        end
1332
    end
1333
    history_reset_state(hp)
25✔
1334
    julia_prompt.hist = hp
25✔
1335
    shell_mode.hist = hp
25✔
1336
    help_mode.hist = hp
25✔
1337
    dummy_pkg_mode.hist = hp
25✔
1338

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

1341

1342
    search_prompt, skeymap = LineEdit.setup_search_keymap(hp)
25✔
1343
    search_prompt.complete = LatexCompletions()
25✔
1344

1345
    shell_prompt_len = length(SHELL_PROMPT)
×
1346
    help_prompt_len = length(HELP_PROMPT)
×
1347
    jl_prompt_regex = Regex("^In \\[[0-9]+\\]: |^(?:\\(.+\\) )?$JULIA_PROMPT")
25✔
1348
    pkg_prompt_regex = Regex("^(?:\\(.+\\) )?$PKG_PROMPT")
25✔
1349

1350
    # Canonicalize user keymap input
1351
    if isa(extra_repl_keymap, Dict)
25✔
1352
        extra_repl_keymap = AnyDict[extra_repl_keymap]
×
1353
    end
1354

1355
    repl_keymap = AnyDict(
25✔
1356
        ';' => function (s::MIState,o...)
55✔
1357
            if isempty(s) || position(LineEdit.buffer(s)) == 0
103✔
1358
                buf = copy(LineEdit.buffer(s))
7✔
1359
                transition(s, shell_mode) do
7✔
1360
                    LineEdit.state(s, shell_mode).input_buffer = buf
7✔
1361
                end
1362
            else
1363
                edit_insert(s, ';')
48✔
1364
            end
1365
        end,
1366
        '?' => function (s::MIState,o...)
1✔
1367
            if isempty(s) || position(LineEdit.buffer(s)) == 0
1✔
1368
                buf = copy(LineEdit.buffer(s))
1✔
1369
                transition(s, help_mode) do
1✔
1370
                    LineEdit.state(s, help_mode).input_buffer = buf
1✔
1371
                end
1372
            else
1373
                edit_insert(s, '?')
×
1374
            end
1375
        end,
1376
        ']' => function (s::MIState,o...)
3✔
1377
            if isempty(s) || position(LineEdit.buffer(s)) == 0
6✔
1378
                buf = copy(LineEdit.buffer(s))
×
1379
                transition(s, dummy_pkg_mode) do
×
1380
                    LineEdit.state(s, dummy_pkg_mode).input_buffer = buf
1381
                end
1382
                # load Pkg on another thread if available so that typing in the dummy Pkg prompt
1383
                # isn't blocked, but instruct the main REPL task to do the transition via s.async_channel
1384
                t_replswitch = Threads.@spawn begin
×
1385
                    REPLExt = load_pkg()
1386
                    if REPLExt isa Module && isdefined(REPLExt, :PkgCompletionProvider)
1387
                        put!(s.async_channel,
1388
                            function (s::MIState)
1389
                                LineEdit.mode(s) === dummy_pkg_mode || return :ok
1390
                                for mode in repl.interface.modes
1391
                                    if mode isa LineEdit.Prompt && mode.complete isa REPLExt.PkgCompletionProvider
1392
                                        buf = copy(LineEdit.buffer(s))
1393
                                        transition(s, mode) do
1394
                                            LineEdit.state(s, mode).input_buffer = buf
1395
                                        end
1396
                                        if !isempty(s) && @invokelatest(LineEdit.check_for_hint(s))
1397
                                            @invokelatest(LineEdit.refresh_line(s))
1398
                                        end
1399
                                        break
1400
                                    end
1401
                                end
1402
                                return :ok
1403
                            end
1404
                        )
1405
                    end
1406
                end
1407
                Base.errormonitor(t_replswitch)
×
1408
            else
1409
                edit_insert(s, ']')
3✔
1410
            end
1411
        end,
1412

1413
        # Bracketed Paste Mode
1414
        "\e[200~" => (s::MIState,o...)->begin
8✔
1415
            input = LineEdit.bracketed_paste(s) # read directly from s until reaching the end-bracketed-paste marker
8✔
1416
            sbuffer = LineEdit.buffer(s)
8✔
1417
            curspos = position(sbuffer)
8✔
1418
            seek(sbuffer, 0)
16✔
1419
            shouldeval = (bytesavailable(sbuffer) == curspos && !occursin(UInt8('\n'), sbuffer))
8✔
1420
            seek(sbuffer, curspos)
16✔
1421
            if curspos == 0
8✔
1422
                # if pasting at the beginning, strip leading whitespace
1423
                input = lstrip(input)
7✔
1424
            end
1425
            if !shouldeval
8✔
1426
                # when pasting in the middle of input, just paste in place
1427
                # don't try to execute all the WIP, since that's rather confusing
1428
                # and is often ill-defined how it should behave
1429
                edit_insert(s, input)
×
1430
                return
×
1431
            end
1432
            LineEdit.push_undo(s)
8✔
1433
            edit_insert(sbuffer, input)
8✔
1434
            input = String(take!(sbuffer))
16✔
1435
            oldpos = firstindex(input)
×
1436
            firstline = true
×
1437
            isprompt_paste = false
×
1438
            curr_prompt_len = 0
×
1439
            pasting_help = false
×
1440

1441
            while oldpos <= lastindex(input) # loop until all lines have been executed
50✔
1442
                if JL_PROMPT_PASTE[]
24✔
1443
                    # Check if the next statement starts with a prompt i.e. "julia> ", in that case
1444
                    # skip it. But first skip whitespace unless pasting in a docstring which may have
1445
                    # indented prompt examples that we don't want to execute
1446
                    while input[oldpos] in (pasting_help ? ('\n') : ('\n', ' ', '\t'))
66✔
1447
                        oldpos = nextind(input, oldpos)
28✔
1448
                        oldpos >= sizeof(input) && return
14✔
1449
                    end
14✔
1450
                    substr = SubString(input, oldpos)
48✔
1451
                    # Check if input line starts with "julia> ", remove it if we are in prompt paste mode
1452
                    if (firstline || isprompt_paste) && startswith(substr, jl_prompt_regex)
24✔
1453
                        detected_jl_prompt = match(jl_prompt_regex, substr).match
11✔
1454
                        isprompt_paste = true
×
1455
                        curr_prompt_len = sizeof(detected_jl_prompt)
11✔
1456
                        oldpos += curr_prompt_len
11✔
1457
                        transition(s, julia_prompt)
11✔
1458
                        pasting_help = false
×
1459
                    # Check if input line starts with "pkg> " or "(...) pkg> ", remove it if we are in prompt paste mode and switch mode
1460
                    elseif (firstline || isprompt_paste) && startswith(substr, pkg_prompt_regex)
13✔
1461
                        detected_pkg_prompt = match(pkg_prompt_regex, substr).match
×
1462
                        isprompt_paste = true
×
1463
                        curr_prompt_len = sizeof(detected_pkg_prompt)
×
1464
                        oldpos += curr_prompt_len
×
1465
                        Base.active_repl.interface.modes[1].keymap_dict[']'](s, o...)
×
1466
                        pasting_help = false
×
1467
                    # Check if input line starts with "shell> ", remove it if we are in prompt paste mode and switch mode
1468
                    elseif (firstline || isprompt_paste) && startswith(substr, SHELL_PROMPT)
13✔
1469
                        isprompt_paste = true
×
1470
                        oldpos += shell_prompt_len
2✔
1471
                        curr_prompt_len = shell_prompt_len
2✔
1472
                        transition(s, shell_mode)
2✔
1473
                        pasting_help = false
×
1474
                    # Check if input line starts with "help?> ", remove it if we are in prompt paste mode and switch mode
1475
                    elseif (firstline || isprompt_paste) && startswith(substr, HELP_PROMPT)
11✔
1476
                        isprompt_paste = true
×
1477
                        oldpos += help_prompt_len
1✔
1478
                        curr_prompt_len = help_prompt_len
1✔
1479
                        transition(s, help_mode)
1✔
1480
                        pasting_help = true
×
1481
                    # If we are prompt pasting and current statement does not begin with a mode prefix, skip to next line
1482
                    elseif isprompt_paste
10✔
1483
                        while input[oldpos] != '\n'
316✔
1484
                            oldpos = nextind(input, oldpos)
302✔
1485
                            oldpos >= sizeof(input) && return
151✔
1486
                        end
149✔
1487
                        continue
7✔
1488
                    end
1489
                end
1490
                dump_tail = false
15✔
1491
                nl_pos = findfirst('\n', input[oldpos:end])
30✔
1492
                if s.current_mode == julia_prompt
15✔
1493
                    ast, pos = Meta.parse(input, oldpos, raise=false, depwarn=false)
12✔
1494
                    if (isa(ast, Expr) && (ast.head === :error || ast.head === :incomplete)) ||
22✔
1495
                            (pos > ncodeunits(input) && !endswith(input, '\n'))
1496
                        # remaining text is incomplete (an error, or parser ran to the end but didn't stop with a newline):
1497
                        # Insert all the remaining text as one line (might be empty)
1498
                        dump_tail = true
×
1499
                    end
1500
                elseif isnothing(nl_pos) # no newline at end, so just dump the tail into the prompt and don't execute
6✔
1501
                    dump_tail = true
×
1502
                elseif s.current_mode == shell_mode # handle multiline shell commands
3✔
1503
                    lines = split(input[oldpos:end], '\n')
4✔
1504
                    pos = oldpos + sizeof(lines[1]) + 1
2✔
1505
                    if length(lines) > 1
2✔
1506
                        for line in lines[2:end]
2✔
1507
                            # to be recognized as a multiline shell command, the lines must be indented to the
1508
                            # same prompt position
1509
                            if !startswith(line, ' '^curr_prompt_len)
3✔
1510
                                break
2✔
1511
                            end
1512
                            pos += sizeof(line) + 1
1✔
1513
                        end
1✔
1514
                    end
1515
                else
1516
                    pos = oldpos + nl_pos
1✔
1517
                end
1518
                if dump_tail
15✔
1519
                    tail = input[oldpos:end]
10✔
1520
                    if !firstline
5✔
1521
                        # strip leading whitespace, but only if it was the result of executing something
1522
                        # (avoids modifying the user's current leading wip line)
1523
                        tail = lstrip(tail)
1✔
1524
                    end
1525
                    if isprompt_paste # remove indentation spaces corresponding to the prompt
5✔
1526
                        tail = replace(tail, r"^"m * ' '^curr_prompt_len => "")
7✔
1527
                    end
1528
                    LineEdit.replace_line(s, tail, true)
5✔
1529
                    LineEdit.refresh_line(s)
5✔
1530
                    break
5✔
1531
                end
1532
                # get the line and strip leading and trailing whitespace
1533
                line = strip(input[oldpos:prevind(input, pos)])
20✔
1534
                if !isempty(line)
10✔
1535
                    if isprompt_paste # remove indentation spaces corresponding to the prompt
10✔
1536
                        line = replace(line, r"^"m * ' '^curr_prompt_len => "")
10✔
1537
                    end
1538
                    # put the line on the screen and history
1539
                    LineEdit.replace_line(s, line)
20✔
1540
                    LineEdit.commit_line(s)
10✔
1541
                    # execute the statement
1542
                    terminal = LineEdit.terminal(s) # This is slightly ugly but ok for now
10✔
1543
                    raw!(terminal, false) && disable_bracketed_paste(terminal)
10✔
1544
                    @invokelatest LineEdit.mode(s).on_done(s, LineEdit.buffer(s), true)
10✔
1545
                    raw!(terminal, true) && enable_bracketed_paste(terminal)
10✔
1546
                    LineEdit.push_undo(s) # when the last line is incomplete
10✔
1547
                end
1548
                oldpos = pos
10✔
1549
                firstline = false
×
1550
            end
17✔
1551
        end,
1552

1553
        # Open the editor at the location of a stackframe or method
1554
        # This is accessing a contextual variable that gets set in
1555
        # the show_backtrace and show_method_table functions.
1556
        "^Q" => (s::MIState, o...) -> begin
1557
            linfos = repl.last_shown_line_infos
1558
            str = String(take!(LineEdit.buffer(s)))
1559
            n = tryparse(Int, str)
1560
            n === nothing && @goto writeback
1561
            if n <= 0 || n > length(linfos) || startswith(linfos[n][1], "REPL[")
1562
                @goto writeback
1563
            end
1564
            try
1565
                InteractiveUtils.edit(Base.fixup_stdlib_path(linfos[n][1]), linfos[n][2])
1566
            catch ex
1567
                ex isa ProcessFailedException || ex isa Base.IOError || ex isa SystemError || rethrow()
1568
                @info "edit failed" _exception=ex
1569
            end
1570
            LineEdit.refresh_line(s)
1571
            return
1572
            @label writeback
1573
            write(LineEdit.buffer(s), str)
1574
            return
1575
        end,
1576
    )
1577

1578
    prefix_prompt, prefix_keymap = LineEdit.setup_prefix_keymap(hp, julia_prompt)
25✔
1579

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

1583
    julia_prompt.keymap_dict = LineEdit.keymap(a)
25✔
1584

1585
    mk = mode_keymap(julia_prompt)
25✔
1586

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

1590
    shell_mode.keymap_dict = help_mode.keymap_dict = dummy_pkg_mode.keymap_dict = LineEdit.keymap(b)
25✔
1591

1592
    allprompts = LineEdit.TextInterface[julia_prompt, shell_mode, help_mode, dummy_pkg_mode, search_prompt, prefix_prompt]
25✔
1593
    return ModalInterface(allprompts)
25✔
1594
end
1595

1596
function run_frontend(repl::LineEditREPL, backend::REPLBackendRef)
21✔
1597
    repl.frontend_task = current_task()
21✔
1598
    d = REPLDisplay(repl)
21✔
1599
    dopushdisplay = repl.specialdisplay === nothing && !in(d,Base.Multimedia.displays)
36✔
1600
    dopushdisplay && pushdisplay(d)
21✔
1601
    if !isdefined(repl,:interface)
21✔
1602
        interface = repl.interface = setup_interface(repl)
26✔
1603
    else
1604
        interface = repl.interface
8✔
1605
    end
1606
    repl.backendref = backend
21✔
1607
    repl.mistate = LineEdit.init_state(terminal(repl), interface)
21✔
1608
    run_interface(terminal(repl), interface, repl.mistate)
21✔
1609
    # Terminate Backend
1610
    put!(backend.repl_channel, (nothing, -1))
21✔
1611
    dopushdisplay && popdisplay(d)
21✔
1612
    nothing
1613
end
1614

1615
## StreamREPL ##
1616

1617
mutable struct StreamREPL <: AbstractREPL
1618
    stream::IO
1619
    prompt_color::String
1620
    input_color::String
1621
    answer_color::String
1622
    waserror::Bool
1623
    frontend_task::Task
1624
    StreamREPL(stream,pc,ic,ac) = new(stream,pc,ic,ac,false)
×
1625
end
1626
StreamREPL(stream::IO) = StreamREPL(stream, Base.text_colors[:green], Base.input_color(), Base.answer_color())
×
1627
run_repl(stream::IO) = run_repl(StreamREPL(stream))
×
1628

1629
outstream(s::StreamREPL) = s.stream
×
1630
hascolor(s::StreamREPL) = get(s.stream, :color, false)::Bool
×
1631

1632
answer_color(r::LineEditREPL) = r.envcolors ? Base.answer_color() : r.answer_color
×
1633
answer_color(r::StreamREPL) = r.answer_color
×
1634
input_color(r::LineEditREPL) = r.envcolors ? Base.input_color() : r.input_color
×
1635
input_color(r::StreamREPL) = r.input_color
×
1636

1637
let matchend = Dict("\"" => r"\"", "\"\"\"" => r"\"\"\"", "'" => r"'",
1638
    "`" => r"`", "```" => r"```", "#" => r"$"m, "#=" => r"=#|#=")
1639
    global _rm_strings_and_comments
1640
    function _rm_strings_and_comments(code::Union{String,SubString{String}})
126✔
1641
        buf = IOBuffer(sizehint = sizeof(code))
252✔
1642
        pos = 1
×
1643
        while true
166✔
1644
            i = findnext(r"\"(?!\"\")|\"\"\"|'|`(?!``)|```|#(?!=)|#=", code, pos)
332✔
1645
            isnothing(i) && break
212✔
1646
            match = SubString(code, i)
46✔
1647
            j = findnext(matchend[match]::Regex, code, nextind(code, last(i)))
92✔
1648
            if match == "#=" # possibly nested
46✔
1649
                nested = 1
×
1650
                while j !== nothing
11✔
1651
                    nested += SubString(code, j) == "#=" ? +1 : -1
10✔
1652
                    iszero(nested) && break
10✔
1653
                    j = findnext(r"=#|#=", code, nextind(code, last(j)))
12✔
1654
                end
6✔
1655
            elseif match[1] != '#' # quote match: check non-escaped
82✔
1656
                while j !== nothing
38✔
1657
                    notbackslash = findprev(!=('\\'), code, prevind(code, first(j)))::Int
66✔
1658
                    isodd(first(j) - notbackslash) && break # not escaped
33✔
1659
                    j = findnext(matchend[match]::Regex, code, nextind(code, first(j)))
14✔
1660
                end
7✔
1661
            end
1662
            isnothing(j) && break
86✔
1663
            if match[1] == '#'
80✔
1664
                print(buf, SubString(code, pos, prevind(code, first(i))))
14✔
1665
            else
1666
                print(buf, SubString(code, pos, last(i)), ' ', SubString(code, j))
26✔
1667
            end
1668
            pos = nextind(code, last(j))
40✔
1669
        end
40✔
1670
        print(buf, SubString(code, pos, lastindex(code)))
126✔
1671
        return String(take!(buf))
126✔
1672
    end
1673
end
1674

1675
# heuristic function to decide if the presence of a semicolon
1676
# at the end of the expression was intended for suppressing output
1677
ends_with_semicolon(code::AbstractString) = ends_with_semicolon(String(code))
×
1678
ends_with_semicolon(code::Union{String,SubString{String}}) =
126✔
1679
    contains(_rm_strings_and_comments(code), r";\s*$")
1680

1681
function banner(io::IO = stdout; short = false)
4✔
1682
    if Base.GIT_VERSION_INFO.tagged_commit
×
1683
        commit_string = Base.TAGGED_RELEASE_BANNER
×
1684
    elseif isempty(Base.GIT_VERSION_INFO.commit)
×
1685
        commit_string = ""
×
1686
    else
1687
        days = Int(floor((ccall(:jl_clock_now, Float64, ()) - Base.GIT_VERSION_INFO.fork_master_timestamp) / (60 * 60 * 24)))
2✔
1688
        days = max(0, days)
2✔
1689
        unit = days == 1 ? "day" : "days"
2✔
1690
        distance = Base.GIT_VERSION_INFO.fork_master_distance
×
1691
        commit = Base.GIT_VERSION_INFO.commit_short
2✔
1692

1693
        if distance == 0
×
1694
            commit_string = "Commit $(commit) ($(days) $(unit) old master)"
2✔
1695
        else
1696
            branch = Base.GIT_VERSION_INFO.branch
×
1697
            commit_string = "$(branch)/$(commit) (fork: $(distance) commits, $(days) $(unit))"
×
1698
        end
1699
    end
1700

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

1703
    if get(io, :color, false)::Bool
2✔
1704
        c = Base.text_colors
×
1705
        tx = c[:normal] # text
×
1706
        jl = c[:normal] # julia
×
1707
        d1 = c[:bold] * c[:blue]    # first dot
×
1708
        d2 = c[:bold] * c[:red]     # second dot
×
1709
        d3 = c[:bold] * c[:green]   # third dot
×
1710
        d4 = c[:bold] * c[:magenta] # fourth dot
×
1711

1712
        if short
×
1713
            print(io,"""
×
1714
              $(d3)o$(tx)  | Version $(VERSION)$(commit_date)
1715
             $(d2)o$(tx) $(d4)o$(tx) | $(commit_string)
1716
            """)
1717
        else
1718
            print(io,"""               $(d3)_$(tx)
×
1719
               $(d1)_$(tx)       $(jl)_$(tx) $(d2)_$(d3)(_)$(d4)_$(tx)     |  Documentation: https://docs.julialang.org
1720
              $(d1)(_)$(jl)     | $(d2)(_)$(tx) $(d4)(_)$(tx)    |
1721
               $(jl)_ _   _| |_  __ _$(tx)   |  Type \"?\" for help, \"]?\" for Pkg help.
1722
              $(jl)| | | | | | |/ _` |$(tx)  |
1723
              $(jl)| | |_| | | | (_| |$(tx)  |  Version $(VERSION)$(commit_date)
1724
             $(jl)_/ |\\__'_|_|_|\\__'_|$(tx)  |  $(commit_string)
1725
            $(jl)|__/$(tx)                   |
1726

1727
            """)
1728
        end
1729
    else
1730
        if short
2✔
1731
            print(io,"""
1✔
1732
              o  |  Version $(VERSION)$(commit_date)
1733
             o o |  $(commit_string)
1734
            """)
1735
        else
1736
            print(io,"""
1✔
1737
                           _
1738
               _       _ _(_)_     |  Documentation: https://docs.julialang.org
1739
              (_)     | (_) (_)    |
1740
               _ _   _| |_  __ _   |  Type \"?\" for help, \"]?\" for Pkg help.
1741
              | | | | | | |/ _` |  |
1742
              | | |_| | | | (_| |  |  Version $(VERSION)$(commit_date)
1743
             _/ |\\__'_|_|_|\\__'_|  |  $(commit_string)
1744
            |__/                   |
1745

1746
            """)
1747
        end
1748
    end
1749
end
1750

1751
function run_frontend(repl::StreamREPL, backend::REPLBackendRef)
×
1752
    repl.frontend_task = current_task()
×
1753
    have_color = hascolor(repl)
×
1754
    banner(repl.stream)
×
1755
    d = REPLDisplay(repl)
×
1756
    dopushdisplay = !in(d,Base.Multimedia.displays)
×
1757
    dopushdisplay && pushdisplay(d)
×
1758
    while !eof(repl.stream)::Bool
×
1759
        if have_color
×
1760
            print(repl.stream,repl.prompt_color)
×
1761
        end
1762
        print(repl.stream, JULIA_PROMPT)
×
1763
        if have_color
×
1764
            print(repl.stream, input_color(repl))
×
1765
        end
1766
        line = readline(repl.stream, keep=true)
×
1767
        if !isempty(line)
×
1768
            ast = Base.parse_input_line(line)
×
1769
            if have_color
×
1770
                print(repl.stream, Base.color_normal)
×
1771
            end
1772
            response = eval_with_backend(ast, backend)
×
1773
            print_response(repl, response, !ends_with_semicolon(line), have_color)
×
1774
        end
1775
    end
×
1776
    # Terminate Backend
1777
    put!(backend.repl_channel, (nothing, -1))
×
1778
    dopushdisplay && popdisplay(d)
×
1779
    nothing
×
1780
end
1781

1782
module Numbered
1783

1784
using ..REPL
1785

1786
__current_ast_transforms() = Base.active_repl_backend !== nothing ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1787

1788
function repl_eval_counter(hp)
773✔
1789
    return length(hp.history) - hp.start_idx
773✔
1790
end
1791

1792
function out_transform(@nospecialize(x), n::Ref{Int})
16✔
1793
    return Expr(:toplevel, get_usings!([], x)..., quote
16✔
1794
        let __temp_val_a72df459 = $x
1795
            $capture_result($n, __temp_val_a72df459)
1796
            __temp_val_a72df459
1797
        end
1798
    end)
1799
end
1800

1801
function get_usings!(usings, ex)
25✔
1802
    ex isa Expr || return usings
25✔
1803
    # get all `using` and `import` statements which are at the top level
1804
    for (i, arg) in enumerate(ex.args)
25✔
1805
        if Base.isexpr(arg, :toplevel)
48✔
1806
            get_usings!(usings, arg)
9✔
1807
        elseif Base.isexpr(arg, [:using, :import])
64✔
1808
            push!(usings, popat!(ex.args, i))
2✔
1809
        end
1810
    end
71✔
1811
    return usings
25✔
1812
end
1813

1814
function capture_result(n::Ref{Int}, @nospecialize(x))
16✔
1815
    n = n[]
16✔
1816
    mod = Base.MainInclude
16✔
1817
    if !isdefined(mod, :Out)
16✔
1818
        @eval mod global Out
1✔
1819
        @eval mod export Out
1✔
1820
        setglobal!(mod, :Out, Dict{Int, Any}())
1✔
1821
    end
1822
    if x !== getglobal(mod, :Out) && x !== nothing # remove this?
16✔
1823
        getglobal(mod, :Out)[n] = x
14✔
1824
    end
1825
    nothing
16✔
1826
end
1827

1828
function set_prompt(repl::LineEditREPL, n::Ref{Int})
1✔
1829
    julia_prompt = repl.interface.modes[1]
1✔
1830
    julia_prompt.prompt = function()
774✔
1831
        n[] = repl_eval_counter(julia_prompt.hist)+1
773✔
1832
        string("In [", n[], "]: ")
773✔
1833
    end
1834
    nothing
1✔
1835
end
1836

1837
function set_output_prefix(repl::LineEditREPL, n::Ref{Int})
1✔
1838
    julia_prompt = repl.interface.modes[1]
1✔
1839
    if REPL.hascolor(repl)
1✔
1840
        julia_prompt.output_prefix_prefix = Base.text_colors[:red]
1✔
1841
    end
1842
    julia_prompt.output_prefix = () -> string("Out[", n[], "]: ")
15✔
1843
    nothing
1✔
1844
end
1845

1846
function __current_ast_transforms(backend)
1847
    if backend === nothing
1✔
1848
        Base.active_repl_backend !== nothing ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1849
    else
1850
        backend.ast_transforms
1✔
1851
    end
1852
end
1853

1854
function numbered_prompt!(repl::LineEditREPL=Base.active_repl::LineEditREPL, backend=nothing)
1855
    n = Ref{Int}(0)
1✔
1856
    set_prompt(repl, n)
1✔
1857
    set_output_prefix(repl, n)
1✔
1858
    push!(__current_ast_transforms(backend), @nospecialize(ast) -> out_transform(ast, n))
17✔
1859
    return
1✔
1860
end
1861

1862
"""
1863
    Out[n]
1864

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

1868
See also [`ans`](@ref).
1869
"""
1870
Base.MainInclude.Out
1871

1872
end
1873

1874
import .Numbered.numbered_prompt!
1875

1876
# this assignment won't survive precompilation,
1877
# but will stick if REPL is baked into a sysimg.
1878
# Needs to occur after this module is finished.
1879
Base.REPL_MODULE_REF[] = REPL
1880

1881
if Base.generating_output()
1882
    include("precompile.jl")
1883
end
1884

1885
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