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

JuliaLang / julia / #37849

25 Jul 2024 12:23AM UTC coverage: 87.45% (-0.09%) from 87.54%
#37849

push

local

web-flow
add an assertion to ensure we're not cleaning-up GC state for GC threads (#55233)

GC threads should never hit this branch.

77459 of 88575 relevant lines covered (87.45%)

15906318.46 hits per line

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

81.69
/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
            bnd = ccall(:jl_get_module_binding, Any, (Any, Any, Cint), scope, var, true)::Core.Binding
2✔
37
            if isdefined(bnd, :owner)
2✔
38
                owner = bnd.owner
×
39
                if owner === bnd
×
40
                    print(io, "\nSuggestion: add an appropriate import or assignment. This global was declared but not assigned.")
×
41
                end
42
            else
43
                owner = ccall(:jl_binding_owner, Ptr{Cvoid}, (Any, Any), scope, var)
2✔
44
                if C_NULL == owner
2✔
45
                    # No global of this name exists in this module.
46
                    # This is the common case, so do not print that information.
47
                    # It could be the binding was exported by two modules, which we can detect
48
                    # by the `usingfailed` flag in the binding:
49
                    if isdefined(bnd, :flags) && Bool(bnd.flags >> 4 & 1) # magic location of the `usingfailed` flag
3✔
50
                        print(io, "\nHint: It looks like two or more modules export different ",
1✔
51
                              "bindings with this name, resulting in ambiguity. Try explicitly ",
52
                              "importing it from a particular module, or qualifying the name ",
53
                              "with the module it should come from.")
54
                    else
55
                        print(io, "\nSuggestion: check for spelling errors or missing imports.")
1✔
56
                    end
57
                    owner = bnd
2✔
58
                else
59
                    owner = unsafe_pointer_to_objref(owner)::Core.Binding
×
60
                end
61
            end
62
            if owner !== bnd
2✔
63
                # this could use jl_binding_dbgmodule for the exported location in the message too
64
                print(io, "\nSuggestion: this global was defined as `$(owner.globalref)` but not assigned a value.")
×
65
            end
66
        elseif scope === :static_parameter
×
67
            print(io, "\nSuggestion: run Test.detect_unbound_args to detect method arguments that do not fully constrain a type parameter.")
×
68
        elseif scope === :local
×
69
            print(io, "\nSuggestion: check for an assignment to a local variable that shadows a global of the same name.")
×
70
        end
71
    else
72
        scope = undef
×
73
    end
74
    if scope !== Base && !_UndefVarError_warnfor(io, Base, var)
2✔
75
        warned = false
1✔
76
        for m in Base.loaded_modules_order
1✔
77
            m === Core && continue
23✔
78
            m === Base && continue
22✔
79
            m === Main && continue
21✔
80
            m === scope && continue
20✔
81
            warned |= _UndefVarError_warnfor(io, m, var)
20✔
82
        end
23✔
83
        warned ||
2✔
84
            _UndefVarError_warnfor(io, Core, var) ||
85
            _UndefVarError_warnfor(io, Main, var)
86
    end
87
    return nothing
2✔
88
end
89

90
function _UndefVarError_warnfor(io::IO, m::Module, var::Symbol)
24✔
91
    Base.isbindingresolved(m, var) || return false
46✔
92
    (Base.isexported(m, var) || Base.ispublic(m, var)) || return false
4✔
93
    print(io, "\nHint: a global variable of this name also exists in $m.")
1✔
94
    return true
1✔
95
end
96

97
function __init__()
6✔
98
    Base.REPL_MODULE_REF[] = REPL
6✔
99
    Base.Experimental.register_error_hint(UndefVarError_hint, UndefVarError)
6✔
100
    return nothing
6✔
101
end
102

103
using Base.Meta, Sockets, StyledStrings
104
using JuliaSyntaxHighlighting
105
import InteractiveUtils
106

107
export
108
    AbstractREPL,
109
    BasicREPL,
110
    LineEditREPL,
111
    StreamREPL
112

113
import Base:
114
    AbstractDisplay,
115
    display,
116
    show,
117
    AnyDict,
118
    ==
119

120
_displaysize(io::IO) = displaysize(io)::Tuple{Int,Int}
32✔
121

122
include("Terminals.jl")
123
using .Terminals
124

125
abstract type AbstractREPL end
126

127
include("options.jl")
128

129
include("LineEdit.jl")
130
using .LineEdit
131
import ..LineEdit:
132
    CompletionProvider,
133
    HistoryProvider,
134
    add_history,
135
    complete_line,
136
    history_next,
137
    history_next_prefix,
138
    history_prev,
139
    history_prev_prefix,
140
    history_first,
141
    history_last,
142
    history_search,
143
    setmodifiers!,
144
    terminal,
145
    MIState,
146
    PromptState,
147
    mode_idx
148

149
include("REPLCompletions.jl")
150
using .REPLCompletions
151

152
include("TerminalMenus/TerminalMenus.jl")
153
include("docview.jl")
154

155
include("Pkg_beforeload.jl")
156

157
@nospecialize # use only declared type signatures
158

159
answer_color(::AbstractREPL) = ""
×
160

161
const JULIA_PROMPT = "julia> "
162
const PKG_PROMPT = "pkg> "
163
const SHELL_PROMPT = "shell> "
164
const HELP_PROMPT = "help?> "
165

166
mutable struct REPLBackend
167
    "channel for AST"
168
    repl_channel::Channel{Any}
169
    "channel for results: (value, iserror)"
170
    response_channel::Channel{Any}
171
    "flag indicating the state of this backend"
172
    in_eval::Bool
173
    "transformation functions to apply before evaluating expressions"
174
    ast_transforms::Vector{Any}
175
    "current backend task"
176
    backend_task::Task
177

178
    REPLBackend(repl_channel, response_channel, in_eval, ast_transforms=copy(repl_ast_transforms)) =
52✔
179
        new(repl_channel, response_channel, in_eval, ast_transforms)
180
end
181
REPLBackend() = REPLBackend(Channel(1), Channel(1), false)
26✔
182

183
"""
184
    softscope(ex)
185

186
Return a modified version of the parsed expression `ex` that uses
187
the REPL's "soft" scoping rules for global syntax blocks.
188
"""
189
function softscope(@nospecialize ex)
376✔
190
    if ex isa Expr
376✔
191
        h = ex.head
241✔
192
        if h === :toplevel
241✔
193
            ex′ = Expr(h)
137✔
194
            map!(softscope, resize!(ex′.args, length(ex.args)), ex.args)
137✔
195
            return ex′
137✔
196
        elseif h in (:meta, :import, :using, :export, :module, :error, :incomplete, :thunk)
104✔
197
            return ex
2✔
198
        elseif h === :global && all(x->isa(x, Symbol), ex.args)
103✔
199
            return ex
1✔
200
        else
201
            return Expr(:block, Expr(:softscope, true), ex)
101✔
202
        end
203
    end
204
    return ex
135✔
205
end
206

207
# Temporary alias until Documenter updates
208
const softscope! = softscope
209

210
function print_qualified_access_warning(mod::Module, owner::Module, name::Symbol)
2✔
211
    @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✔
212
end
213

214
function has_ancestor(query::Module, target::Module)
2✔
215
    query == target && return true
38✔
216
    while true
30✔
217
        next = parentmodule(query)
30✔
218
        next == target && return true
30✔
219
        next == query && return false
28✔
220
        query = next
×
221
    end
15✔
222
end
223

224
retrieve_modules(::Module, ::Any) = (nothing,)
6✔
225
function retrieve_modules(current_module::Module, mod_name::Symbol)
96✔
226
    mod = try
96✔
227
        getproperty(current_module, mod_name)
96✔
228
    catch
229
        return (nothing,)
19✔
230
    end
231
    return (mod isa Module ? mod : nothing,)
77✔
232
end
233
retrieve_modules(current_module::Module, mod_name::QuoteNode) = retrieve_modules(current_module, mod_name.value)
41✔
234
function retrieve_modules(current_module::Module, mod_expr::Expr)
41✔
235
    if Meta.isexpr(mod_expr, :., 2)
41✔
236
        current_module = retrieve_modules(current_module, mod_expr.args[1])[1]
41✔
237
        current_module === nothing && return (nothing,)
41✔
238
        return (current_module, retrieve_modules(current_module, mod_expr.args[2])...)
41✔
239
    else
240
        return (nothing,)
×
241
    end
242
end
243

244
add_locals!(locals, ast::Any) = nothing
×
245
function add_locals!(locals, ast::Expr)
14✔
246
    for arg in ast.args
14✔
247
        add_locals!(locals, arg)
23✔
248
    end
23✔
249
    return nothing
14✔
250
end
251
function add_locals!(locals, ast::Symbol)
57✔
252
    push!(locals, ast)
57✔
253
    return nothing
57✔
254
end
255

256
function collect_names_to_warn!(warnings, locals, current_module::Module, ast)
1,173✔
257
    ast isa Expr || return
1,697✔
258

259
    # don't recurse through module definitions
260
    ast.head === :module && return
649✔
261

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

297
        if Meta.isexpr(ast.args[1], :call, 2)
3✔
298
            func_name, func_args = ast.args[1].args
2✔
299
            # here we have a function definition and are inspecting it's arguments for local variables.
300
            # we will error on the conservative side by adding all symbols we find (regardless if they are local variables or possibly-global default values)
301
            add_locals!(locals, func_args)
2✔
302
        end
303
        # fall through to general recursion
304
    end
305

306
    for arg in ast.args
541✔
307
        collect_names_to_warn!(warnings, locals, current_module, arg)
1,004✔
308
    end
1,004✔
309

310
    return nothing
541✔
311
end
312

313
function collect_qualified_access_warnings(current_mod, ast)
122✔
314
    warnings = Set()
122✔
315
    locals = Set{Symbol}()
122✔
316
    collect_names_to_warn!(warnings, locals, current_mod, ast)
122✔
317
    filter!(warnings) do (; outer_mod)
122✔
318
        nameof(outer_mod) ∉ locals
11✔
319
    end
320
    return warnings
122✔
321
end
322

323
function warn_on_non_owning_accesses(current_mod, ast)
110✔
324
    warnings = collect_qualified_access_warnings(current_mod, ast)
110✔
325
    for (; outer_mod, mod, owner, name_being_accessed) in warnings
220✔
326
        print_qualified_access_warning(mod, owner, name_being_accessed)
2✔
327
    end
4✔
328
    return ast
110✔
329
end
330
warn_on_non_owning_accesses(ast) = warn_on_non_owning_accesses(REPL.active_module(), ast)
108✔
331

332
const repl_ast_transforms = Any[softscope, warn_on_non_owning_accesses] # defaults for new REPL backends
333

334
# Allows an external package to add hooks into the code loading.
335
# The hook should take a Vector{Symbol} of package names and
336
# return true if all packages could be installed, false if not
337
# to e.g. install packages on demand
338
const install_packages_hooks = Any[]
339

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

348
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✔
349
    if !isexpr(ast, :toplevel)
453✔
350
        ast = __repl_entry_lower_with_loc(mod, ast, toplevel_file, toplevel_line)
217✔
351
        check_for_missing_packages_and_run_hooks(ast)
217✔
352
        return __repl_entry_eval_expanded_with_loc(mod, ast, toplevel_file, toplevel_line)
216✔
353
    end
354
    local value=nothing
128✔
355
    for i = 1:length(ast.args)
128✔
356
        value = toplevel_eval_with_hooks(mod, ast.args[i], toplevel_file, toplevel_line)
237✔
357
    end
339✔
358
    return value
121✔
359
end
360

361
function eval_user_input(@nospecialize(ast), backend::REPLBackend, mod::Module)
108✔
362
    lasterr = nothing
×
363
    Base.sigatomic_begin()
108✔
364
    while true
113✔
365
        try
113✔
366
            Base.sigatomic_end()
113✔
367
            if lasterr !== nothing
113✔
368
                put!(backend.response_channel, Pair{Any, Bool}(lasterr, true))
5✔
369
            else
370
                backend.in_eval = true
108✔
371
                for xf in backend.ast_transforms
108✔
372
                    ast = Base.invokelatest(xf, ast)
234✔
373
                end
234✔
374
                value = toplevel_eval_with_hooks(mod, ast)
108✔
375
                backend.in_eval = false
102✔
376
                setglobal!(Base.MainInclude, :ans, value)
102✔
377
                put!(backend.response_channel, Pair{Any, Bool}(value, false))
102✔
378
            end
379
            break
112✔
380
        catch err
381
            if lasterr !== nothing
5✔
382
                println("SYSTEM ERROR: Failed to report error to REPL frontend")
×
383
                println(err)
×
384
            end
385
            lasterr = current_exceptions()
5✔
386
        end
387
    end
5✔
388
    Base.sigatomic_end()
107✔
389
    nothing
390
end
391

392
function check_for_missing_packages_and_run_hooks(ast)
217✔
393
    isa(ast, Expr) || return
327✔
394
    mods = modules_to_be_loaded(ast)
107✔
395
    filter!(mod -> isnothing(Base.identify_package(String(mod))), mods) # keep missing modules
112✔
396
    if !isempty(mods)
107✔
397
        isempty(install_packages_hooks) && load_pkg()
1✔
398
        for f in install_packages_hooks
1✔
399
            Base.invokelatest(f, mods) && return
1✔
400
        end
×
401
    end
402
end
403

404
function _modules_to_be_loaded!(ast::Expr, mods::Vector{Symbol})
663✔
405
    ast.head === :quote && return mods # don't search if it's not going to be run during this eval
663✔
406
    if ast.head === :using || ast.head === :import
1,309✔
407
        for arg in ast.args
22✔
408
            arg = arg::Expr
26✔
409
            arg1 = first(arg.args)
26✔
410
            if arg1 isa Symbol # i.e. `Foo`
26✔
411
                if arg1 != :. # don't include local import `import .Foo`
21✔
412
                    push!(mods, arg1)
20✔
413
                end
414
            else # i.e. `Foo: bar`
415
                sym = first((arg1::Expr).args)::Symbol
5✔
416
                if sym != :. # don't include local import `import .Foo: a`
5✔
417
                    push!(mods, sym)
3✔
418
                end
419
            end
420
        end
26✔
421
    end
422
    if ast.head !== :thunk
663✔
423
        for arg in ast.args
553✔
424
            if isexpr(arg, (:block, :if, :using, :import))
1,339✔
425
                _modules_to_be_loaded!(arg, mods)
33✔
426
            end
427
        end
1,339✔
428
    else
429
        code = ast.args[1]
110✔
430
        for arg in code.code
110✔
431
            isa(arg, Expr) || continue
875✔
432
            _modules_to_be_loaded!(arg, mods)
499✔
433
        end
875✔
434
    end
435
end
436

437
function modules_to_be_loaded(ast::Expr, mods::Vector{Symbol} = Symbol[])
24✔
438
    _modules_to_be_loaded!(ast, mods)
262✔
439
    filter!(mod::Symbol -> !in(mod, (:Base, :Main, :Core)), mods) # Exclude special non-package modules
154✔
440
    return unique(mods)
131✔
441
end
442

443
"""
444
    start_repl_backend(repl_channel::Channel, response_channel::Channel)
445

446
    Starts loop for REPL backend
447
    Returns a REPLBackend with backend_task assigned
448

449
    Deprecated since sync / async behavior cannot be selected
450
"""
451
function start_repl_backend(repl_channel::Channel{Any}, response_channel::Channel{Any}
×
452
                            ; get_module::Function = ()->Main)
453
    # Maintain legacy behavior of asynchronous backend
454
    backend = REPLBackend(repl_channel, response_channel, false)
×
455
    # Assignment will be made twice, but will be immediately available
456
    backend.backend_task = @async start_repl_backend(backend; get_module)
×
457
    return backend
×
458
end
459

460
"""
461
    start_repl_backend(backend::REPLBackend)
462

463
    Call directly to run backend loop on current Task.
464
    Use @async for run backend on new Task.
465

466
    Does not return backend until loop is finished.
467
"""
468
function start_repl_backend(backend::REPLBackend,  @nospecialize(consumer = x -> nothing); get_module::Function = ()->Main)
56✔
469
    backend.backend_task = Base.current_task()
26✔
470
    consumer(backend)
26✔
471
    repl_backend_loop(backend, get_module)
26✔
472
    return backend
25✔
473
end
474

475
function repl_backend_loop(backend::REPLBackend, get_module::Function)
26✔
476
    # include looks at this to determine the relative include path
477
    # nothing means cwd
478
    while true
×
479
        tls = task_local_storage()
133✔
480
        tls[:SOURCE_PATH] = nothing
133✔
481
        ast, show_value = take!(backend.repl_channel)
133✔
482
        if show_value == -1
133✔
483
            # exit flag
484
            break
25✔
485
        end
486
        eval_user_input(ast, backend, get_module())
108✔
487
    end
107✔
488
    return nothing
25✔
489
end
490

491
struct REPLDisplay{Repl<:AbstractREPL} <: AbstractDisplay
492
    repl::Repl
35✔
493
end
494

495
function display(d::REPLDisplay, mime::MIME"text/plain", x)
63✔
496
    x = Ref{Any}(x)
63✔
497
    with_repl_linfo(d.repl) do io
63✔
498
        io = IOContext(io, :limit => true, :module => active_module(d)::Module)
123✔
499
        if d.repl isa LineEditREPL
3✔
500
            mistate = d.repl.mistate
60✔
501
            mode = LineEdit.mode(mistate)
60✔
502
            if mode isa LineEdit.Prompt
60✔
503
                LineEdit.write_output_prefix(io, mode, get(io, :color, false)::Bool)
240✔
504
            end
505
        end
506
        get(io, :color, false)::Bool && write(io, answer_color(d.repl))
69✔
507
        if isdefined(d.repl, :options) && isdefined(d.repl.options, :iocontext)
3✔
508
            # this can override the :limit property set initially
509
            io = foldl(IOContext, d.repl.options.iocontext, init=io)
60✔
510
        end
511
        show_repl(io, mime, x[])
63✔
512
        println(io)
62✔
513
    end
514
    return nothing
62✔
515
end
516

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

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

521
show_repl(io::IO, ::MIME"text/plain", ex::Expr) =
2✔
522
    print(io, JuliaSyntaxHighlighting.highlight(
523
        sprint(show, ex, context=IOContext(io, :color => false))))
524

525
function print_response(repl::AbstractREPL, response, show_value::Bool, have_color::Bool)
104✔
526
    repl.waserror = response[2]
104✔
527
    with_repl_linfo(repl) do io
104✔
528
        io = IOContext(io, :module => active_module(repl)::Module)
205✔
529
        print_response(io, response, show_value, have_color, specialdisplay(repl))
104✔
530
    end
531
    return nothing
104✔
532
end
533

534
function repl_display_error(errio::IO, @nospecialize errval)
9✔
535
    # this will be set to true if types in the stacktrace are truncated
536
    limitflag = Ref(false)
9✔
537
    errio = IOContext(errio, :stacktrace_types_limited => limitflag)
9✔
538
    Base.invokelatest(Base.display_error, errio, errval)
9✔
539
    if limitflag[]
7✔
540
        print(errio, "Some type information was truncated. Use `show(err)` to see complete types.")
2✔
541
        println(errio)
2✔
542
    end
543
    return nothing
7✔
544
end
545

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

596
# A reference to a backend that is not mutable
597
struct REPLBackendRef
598
    repl_channel::Channel{Any}
24✔
599
    response_channel::Channel{Any}
600
end
601
REPLBackendRef(backend::REPLBackend) = REPLBackendRef(backend.repl_channel, backend.response_channel)
24✔
602

603
function destroy(ref::REPLBackendRef, state::Task)
23✔
604
    if istaskfailed(state)
23✔
605
        close(ref.repl_channel, TaskFailedException(state))
×
606
        close(ref.response_channel, TaskFailedException(state))
×
607
    end
608
    close(ref.repl_channel)
23✔
609
    close(ref.response_channel)
23✔
610
end
611

612
"""
613
    run_repl(repl::AbstractREPL)
614
    run_repl(repl, consumer = backend->nothing; backend_on_current_task = true)
615

616
    Main function to start the REPL
617

618
    consumer is an optional function that takes a REPLBackend as an argument
619
"""
620
function run_repl(repl::AbstractREPL, @nospecialize(consumer = x -> nothing); backend_on_current_task::Bool = true, backend = REPLBackend())
71✔
621
    backend_ref = REPLBackendRef(backend)
24✔
622
    cleanup = @task try
47✔
623
            destroy(backend_ref, t)
23✔
624
        catch e
625
            Core.print(Core.stderr, "\nINTERNAL ERROR: ")
×
626
            Core.println(Core.stderr, e)
×
627
            Core.println(Core.stderr, catch_backtrace())
×
628
        end
629
    get_module = () -> active_module(repl)
128✔
630
    if backend_on_current_task
24✔
631
        t = @async run_frontend(repl, backend_ref)
48✔
632
        errormonitor(t)
24✔
633
        Base._wait2(t, cleanup)
24✔
634
        start_repl_backend(backend, consumer; get_module)
24✔
635
    else
636
        t = @async start_repl_backend(backend, consumer; get_module)
×
637
        errormonitor(t)
×
638
        Base._wait2(t, cleanup)
×
639
        run_frontend(repl, backend_ref)
×
640
    end
641
    return backend
23✔
642
end
643

644
## BasicREPL ##
645

646
mutable struct BasicREPL <: AbstractREPL
647
    terminal::TextTerminal
648
    waserror::Bool
649
    frontend_task::Task
650
    BasicREPL(t) = new(t, false)
3✔
651
end
652

653
outstream(r::BasicREPL) = r.terminal
6✔
654
hascolor(r::BasicREPL) = hascolor(r.terminal)
×
655

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

703
## LineEditREPL ##
704

705
mutable struct LineEditREPL <: AbstractREPL
706
    t::TextTerminal
707
    hascolor::Bool
708
    prompt_color::String
709
    input_color::String
710
    answer_color::String
711
    shell_color::String
712
    help_color::String
713
    pkg_color::String
714
    history_file::Bool
715
    in_shell::Bool
716
    in_help::Bool
717
    envcolors::Bool
718
    waserror::Bool
719
    specialdisplay::Union{Nothing,AbstractDisplay}
720
    options::Options
721
    mistate::Union{MIState,Nothing}
722
    last_shown_line_infos::Vector{Tuple{String,Int}}
723
    interface::ModalInterface
724
    backendref::REPLBackendRef
725
    frontend_task::Task
726
    function LineEditREPL(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,pkg_color,history_file,in_shell,in_help,envcolors)
28✔
727
        opts = Options()
28✔
728
        opts.hascolor = hascolor
28✔
729
        if !hascolor
28✔
730
            opts.beep_colors = [""]
×
731
        end
732
        new(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,pkg_color,history_file,in_shell,
28✔
733
            in_help,envcolors,false,nothing, opts, nothing, Tuple{String,Int}[])
734
    end
735
end
736
outstream(r::LineEditREPL) = (t = r.t; t isa TTYTerminal ? t.out_stream : t)
171✔
737
specialdisplay(r::LineEditREPL) = r.specialdisplay
101✔
738
specialdisplay(r::AbstractREPL) = nothing
×
739
terminal(r::LineEditREPL) = r.t
158✔
740
hascolor(r::LineEditREPL) = r.hascolor
203✔
741

742
LineEditREPL(t::TextTerminal, hascolor::Bool, envcolors::Bool=false) =
56✔
743
    LineEditREPL(t, hascolor,
744
        hascolor ? Base.text_colors[:green] : "",
745
        hascolor ? Base.input_color() : "",
746
        hascolor ? Base.answer_color() : "",
747
        hascolor ? Base.text_colors[:red] : "",
748
        hascolor ? Base.text_colors[:yellow] : "",
749
        hascolor ? Base.text_colors[:blue] : "",
750
        false, false, false, envcolors
751
    )
752

753
mutable struct REPLCompletionProvider <: CompletionProvider
754
    modifiers::LineEdit.Modifiers
25✔
755
end
756
REPLCompletionProvider() = REPLCompletionProvider(LineEdit.Modifiers())
25✔
757

758
mutable struct ShellCompletionProvider <: CompletionProvider end
25✔
759
struct LatexCompletions <: CompletionProvider end
760

761
function active_module() # this method is also called from Base
23,160✔
762
    isdefined(Base, :active_repl) || return Main
46,318✔
763
    return active_module(Base.active_repl::AbstractREPL)
2✔
764
end
765
active_module((; mistate)::LineEditREPL) = mistate === nothing ? Main : mistate.active_module
4,773✔
766
active_module(::AbstractREPL) = Main
12✔
767
active_module(d::REPLDisplay) = active_module(d.repl)
123✔
768

769
setmodifiers!(c::CompletionProvider, m::LineEdit.Modifiers) = nothing
×
770

771
setmodifiers!(c::REPLCompletionProvider, m::LineEdit.Modifiers) = c.modifiers = m
×
772

773
"""
774
    activate(mod::Module=Main)
775

776
Set `mod` as the default contextual module in the REPL,
777
both for evaluating expressions and printing them.
778
"""
779
function activate(mod::Module=Main)
×
780
    mistate = (Base.active_repl::LineEditREPL).mistate
×
781
    mistate === nothing && return nothing
×
782
    mistate.active_module = mod
×
783
    Base.load_InteractiveUtils(mod)
×
784
    return nothing
×
785
end
786

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

789
function complete_line(c::REPLCompletionProvider, s::PromptState, mod::Module; hint::Bool=false)
3,524✔
790
    partial = beforecursor(s.input_buffer)
3,524✔
791
    full = LineEdit.input_string(s)
1,762✔
792
    ret, range, should_complete = completions(full, lastindex(partial), mod, c.modifiers.shift, hint)
3,524✔
793
    c.modifiers = LineEdit.Modifiers()
1,762✔
794
    return unique!(String[completion_text(x) for x in ret]), partial[range], should_complete
1,762✔
795
end
796

797
function complete_line(c::ShellCompletionProvider, s::PromptState; hint::Bool=false)
844✔
798
    # First parse everything up to the current position
799
    partial = beforecursor(s.input_buffer)
844✔
800
    full = LineEdit.input_string(s)
422✔
801
    ret, range, should_complete = shell_completions(full, lastindex(partial), hint)
844✔
802
    return unique!(String[completion_text(x) for x in ret]), partial[range], should_complete
422✔
803
end
804

805
function complete_line(c::LatexCompletions, s; hint::Bool=false)
×
806
    partial = beforecursor(LineEdit.buffer(s))
×
807
    full = LineEdit.input_string(s)::String
×
808
    ret, range, should_complete = bslash_completions(full, lastindex(partial), hint)[2]
×
809
    return unique!(String[completion_text(x) for x in ret]), partial[range], should_complete
×
810
end
811

812
with_repl_linfo(f, repl) = f(outstream(repl))
6✔
813
function with_repl_linfo(f, repl::LineEditREPL)
161✔
814
    linfos = Tuple{String,Int}[]
161✔
815
    io = IOContext(outstream(repl), :last_shown_line_infos => linfos)
161✔
816
    f(io)
161✔
817
    if !isempty(linfos)
160✔
818
        repl.last_shown_line_infos = linfos
6✔
819
    end
820
    nothing
821
end
822

823
mutable struct REPLHistoryProvider <: HistoryProvider
824
    history::Vector{String}
30✔
825
    file_path::String
826
    history_file::Union{Nothing,IO}
827
    start_idx::Int
828
    cur_idx::Int
829
    last_idx::Int
830
    last_buffer::IOBuffer
831
    last_mode::Union{Nothing,Prompt}
832
    mode_mapping::Dict{Symbol,Prompt}
833
    modes::Vector{Symbol}
834
end
835
REPLHistoryProvider(mode_mapping::Dict{Symbol}) =
30✔
836
    REPLHistoryProvider(String[], "", nothing, 0, 0, -1, IOBuffer(),
837
                        nothing, mode_mapping, UInt8[])
838

839
invalid_history_message(path::String) = """
×
840
Invalid history file ($path) format:
841
If you have a history file left over from an older version of Julia,
842
try renaming or deleting it.
843
Invalid character: """
844

845
munged_history_message(path::String) = """
×
846
Invalid history file ($path) format:
847
An editor may have converted tabs to spaces at line """
848

849
function hist_open_file(hp::REPLHistoryProvider)
850
    f = open(hp.file_path, read=true, write=true, create=true)
4✔
851
    hp.history_file = f
4✔
852
    seekend(f)
4✔
853
end
854

855
function hist_from_file(hp::REPLHistoryProvider, path::String)
8✔
856
    getline(lines, i) = i > length(lines) ? "" : lines[i]
276✔
857
    file_lines = readlines(path)
8✔
858
    countlines = 0
×
859
    while true
42✔
860
        # First parse the metadata that starts with '#' in particular the REPL mode
861
        countlines += 1
42✔
862
        line = getline(file_lines, countlines)
76✔
863
        mode = :julia
×
864
        isempty(line) && break
42✔
865
        line[1] != '#' &&
68✔
866
            error(invalid_history_message(path), repr(line[1]), " at line ", countlines)
867
        while !isempty(line)
102✔
868
            startswith(line, '#') || break
204✔
869
            if startswith(line, "# mode: ")
68✔
870
                mode = Symbol(SubString(line, 9))
68✔
871
            end
872
            countlines += 1
68✔
873
            line = getline(file_lines, countlines)
136✔
874
        end
68✔
875
        isempty(line) && break
34✔
876

877
        # Now parse the code for the current REPL mode
878
        line[1] == ' '  &&
68✔
879
            error(munged_history_message(path), countlines)
880
        line[1] != '\t' &&
68✔
881
            error(invalid_history_message(path), repr(line[1]), " at line ", countlines)
882
        lines = String[]
34✔
883
        while !isempty(line)
34✔
884
            push!(lines, chomp(SubString(line, 2)))
68✔
885
            next_line = getline(file_lines, countlines+1)
64✔
886
            isempty(next_line) && break
34✔
887
            first(next_line) == ' '  && error(munged_history_message(path), countlines)
60✔
888
            # A line not starting with a tab means we are done with code for this entry
889
            first(next_line) != '\t' && break
60✔
890
            countlines += 1
×
891
            line = getline(file_lines, countlines)
×
892
        end
×
893
        push!(hp.modes, mode)
34✔
894
        push!(hp.history, join(lines, '\n'))
34✔
895
    end
34✔
896
    hp.start_idx = length(hp.history)
8✔
897
    return hp
8✔
898
end
899

900
function add_history(hist::REPLHistoryProvider, s::PromptState)
119✔
901
    str = rstrip(String(take!(copy(s.input_buffer))))
223✔
902
    isempty(strip(str)) && return
119✔
903
    mode = mode_idx(hist, LineEdit.mode(s))
101✔
904
    !isempty(hist.history) &&
101✔
905
        isequal(mode, hist.modes[end]) && str == hist.history[end] && return
906
    push!(hist.modes, mode)
95✔
907
    push!(hist.history, str)
95✔
908
    hist.history_file === nothing && return
95✔
909
    entry = """
16✔
910
    # time: $(Libc.strftime("%Y-%m-%d %H:%M:%S %Z", time()))
911
    # mode: $mode
912
    $(replace(str, r"^"ms => "\t"))
913
    """
914
    # TODO: write-lock history file
915
    try
16✔
916
        seekend(hist.history_file)
16✔
917
    catch err
918
        (err isa SystemError) || rethrow()
×
919
        # File handle might get stale after a while, especially under network file systems
920
        # If this doesn't fix it (e.g. when file is deleted), we'll end up rethrowing anyway
921
        hist_open_file(hist)
×
922
    end
923
    print(hist.history_file, entry)
32✔
924
    flush(hist.history_file)
16✔
925
    nothing
926
end
927

928
function history_move(s::Union{LineEdit.MIState,LineEdit.PrefixSearchState}, hist::REPLHistoryProvider, idx::Int, save_idx::Int = hist.cur_idx)
96✔
929
    max_idx = length(hist.history) + 1
136✔
930
    @assert 1 <= hist.cur_idx <= max_idx
96✔
931
    (1 <= idx <= max_idx) || return :none
98✔
932
    idx != hist.cur_idx || return :none
94✔
933

934
    # save the current line
935
    if save_idx == max_idx
94✔
936
        hist.last_mode = LineEdit.mode(s)
46✔
937
        hist.last_buffer = copy(LineEdit.buffer(s))
46✔
938
    else
939
        hist.history[save_idx] = LineEdit.input_string(s)
84✔
940
        hist.modes[save_idx] = mode_idx(hist, LineEdit.mode(s))
63✔
941
    end
942

943
    # load the saved line
944
    if idx == max_idx
94✔
945
        last_buffer = hist.last_buffer
10✔
946
        LineEdit.transition(s, hist.last_mode) do
10✔
947
            LineEdit.replace_line(s, last_buffer)
10✔
948
        end
949
        hist.last_mode = nothing
10✔
950
        hist.last_buffer = IOBuffer()
10✔
951
    else
952
        if haskey(hist.mode_mapping, hist.modes[idx])
168✔
953
            LineEdit.transition(s, hist.mode_mapping[hist.modes[idx]]) do
68✔
954
                LineEdit.replace_line(s, hist.history[idx])
68✔
955
            end
956
        else
957
            return :skip
16✔
958
        end
959
    end
960
    hist.cur_idx = idx
78✔
961

962
    return :ok
78✔
963
end
964

965
# REPL History can also transitions modes
966
function LineEdit.accept_result_newmode(hist::REPLHistoryProvider)
27✔
967
    if 1 <= hist.cur_idx <= length(hist.modes)
27✔
968
        return hist.mode_mapping[hist.modes[hist.cur_idx]]
23✔
969
    end
970
    return nothing
4✔
971
end
972

973
function history_prev(s::LineEdit.MIState, hist::REPLHistoryProvider,
34✔
974
                      num::Int=1, save_idx::Int = hist.cur_idx)
975
    num <= 0 && return history_next(s, hist, -num, save_idx)
58✔
976
    hist.last_idx = -1
32✔
977
    m = history_move(s, hist, hist.cur_idx-num, save_idx)
32✔
978
    if m === :ok
32✔
979
        LineEdit.move_input_start(s)
24✔
980
        LineEdit.reset_key_repeats(s) do
24✔
981
            LineEdit.move_line_end(s)
24✔
982
        end
983
        return LineEdit.refresh_line(s)
24✔
984
    elseif m === :skip
8✔
985
        return history_prev(s, hist, num+1, save_idx)
8✔
986
    else
987
        return Terminals.beep(s)
×
988
    end
989
end
990

991
function history_next(s::LineEdit.MIState, hist::REPLHistoryProvider,
26✔
992
                      num::Int=1, save_idx::Int = hist.cur_idx)
993
    if num == 0
44✔
994
        Terminals.beep(s)
×
995
        return
×
996
    end
997
    num < 0 && return history_prev(s, hist, -num, save_idx)
26✔
998
    cur_idx = hist.cur_idx
24✔
999
    max_idx = length(hist.history) + 1
24✔
1000
    if cur_idx == max_idx && 0 < hist.last_idx
24✔
1001
        # issue #6312
1002
        cur_idx = hist.last_idx
×
1003
        hist.last_idx = -1
×
1004
    end
1005
    m = history_move(s, hist, cur_idx+num, save_idx)
24✔
1006
    if m === :ok
24✔
1007
        LineEdit.move_input_end(s)
16✔
1008
        return LineEdit.refresh_line(s)
16✔
1009
    elseif m === :skip
8✔
1010
        return history_next(s, hist, num+1, save_idx)
6✔
1011
    else
1012
        return Terminals.beep(s)
2✔
1013
    end
1014
end
1015

1016
history_first(s::LineEdit.MIState, hist::REPLHistoryProvider) =
6✔
1017
    history_prev(s, hist, hist.cur_idx - 1 -
1018
                 (hist.cur_idx > hist.start_idx+1 ? hist.start_idx : 0))
1019

1020
history_last(s::LineEdit.MIState, hist::REPLHistoryProvider) =
4✔
1021
    history_next(s, hist, length(hist.history) - hist.cur_idx + 1)
1022

1023
function history_move_prefix(s::LineEdit.PrefixSearchState,
38✔
1024
                             hist::REPLHistoryProvider,
1025
                             prefix::AbstractString,
1026
                             backwards::Bool,
1027
                             cur_idx::Int = hist.cur_idx)
1028
    cur_response = String(take!(copy(LineEdit.buffer(s))))
101✔
1029
    # when searching forward, start at last_idx
1030
    if !backwards && hist.last_idx > 0
38✔
1031
        cur_idx = hist.last_idx
1✔
1032
    end
1033
    hist.last_idx = -1
38✔
1034
    max_idx = length(hist.history)+1
38✔
1035
    idxs = backwards ? ((cur_idx-1):-1:1) : ((cur_idx+1):1:max_idx)
65✔
1036
    for idx in idxs
38✔
1037
        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✔
1038
            m = history_move(s, hist, idx)
36✔
1039
            if m === :ok
36✔
1040
                if idx == max_idx
34✔
1041
                    # on resuming the in-progress edit, leave the cursor where the user last had it
1042
                elseif isempty(prefix)
30✔
1043
                    # on empty prefix search, move cursor to the end
1044
                    LineEdit.move_input_end(s)
14✔
1045
                else
1046
                    # otherwise, keep cursor at the prefix position as a visual cue
1047
                    seek(LineEdit.buffer(s), sizeof(prefix))
16✔
1048
                end
1049
                LineEdit.refresh_line(s)
34✔
1050
                return :ok
34✔
1051
            elseif m === :skip
2✔
1052
                return history_move_prefix(s,hist,prefix,backwards,idx)
2✔
1053
            end
1054
        end
1055
    end
62✔
1056
    Terminals.beep(s)
×
1057
    nothing
1058
end
1059
history_next_prefix(s::LineEdit.PrefixSearchState, hist::REPLHistoryProvider, prefix::AbstractString) =
5✔
1060
    history_move_prefix(s, hist, prefix, false)
1061
history_prev_prefix(s::LineEdit.PrefixSearchState, hist::REPLHistoryProvider, prefix::AbstractString) =
31✔
1062
    history_move_prefix(s, hist, prefix, true)
1063

1064
function history_search(hist::REPLHistoryProvider, query_buffer::IOBuffer, response_buffer::IOBuffer,
28✔
1065
                        backwards::Bool=false, skip_current::Bool=false)
1066

1067
    qpos = position(query_buffer)
28✔
1068
    qpos > 0 || return true
28✔
1069
    searchdata = beforecursor(query_buffer)
56✔
1070
    response_str = String(take!(copy(response_buffer)))
50✔
1071

1072
    # Alright, first try to see if the current match still works
1073
    a = position(response_buffer) + 1 # position is zero-indexed
28✔
1074
    # FIXME: I'm pretty sure this is broken since it uses an index
1075
    # into the search data to index into the response string
1076
    b = a + sizeof(searchdata)
28✔
1077
    b = b ≤ ncodeunits(response_str) ? prevind(response_str, b) : b-1
48✔
1078
    b = min(lastindex(response_str), b) # ensure that b is valid
50✔
1079

1080
    searchstart = backwards ? b : a
28✔
1081
    if searchdata == response_str[a:b]
44✔
1082
        if skip_current
10✔
1083
            searchstart = backwards ? prevind(response_str, b) : nextind(response_str, a)
4✔
1084
        else
1085
            return true
6✔
1086
        end
1087
    end
1088

1089
    # Start searching
1090
    # First the current response buffer
1091
    if 1 <= searchstart <= lastindex(response_str)
36✔
1092
        match = backwards ? findprev(searchdata, response_str, searchstart) :
14✔
1093
                            findnext(searchdata, response_str, searchstart)
1094
        if match !== nothing
14✔
1095
            seek(response_buffer, first(match) - 1)
6✔
1096
            return true
6✔
1097
        end
1098
    end
1099

1100
    # Now search all the other buffers
1101
    idxs = backwards ? ((hist.cur_idx-1):-1:1) : ((hist.cur_idx+1):1:length(hist.history))
32✔
1102
    for idx in idxs
16✔
1103
        h = hist.history[idx]
40✔
1104
        match = backwards ? findlast(searchdata, h) : findfirst(searchdata, h)
80✔
1105
        if match !== nothing && h != response_str && haskey(hist.mode_mapping, hist.modes[idx])
54✔
1106
            truncate(response_buffer, 0)
12✔
1107
            write(response_buffer, h)
12✔
1108
            seek(response_buffer, first(match) - 1)
12✔
1109
            hist.cur_idx = idx
12✔
1110
            return true
12✔
1111
        end
1112
    end
52✔
1113

1114
    return false
4✔
1115
end
1116

1117
function history_reset_state(hist::REPLHistoryProvider)
1118
    if hist.cur_idx != length(hist.history) + 1
276✔
1119
        hist.last_idx = hist.cur_idx
133✔
1120
        hist.cur_idx = length(hist.history) + 1
133✔
1121
    end
1122
    nothing
1123
end
1124
LineEdit.reset_state(hist::REPLHistoryProvider) = history_reset_state(hist)
244✔
1125

1126
function return_callback(s)
98✔
1127
    ast = Base.parse_input_line(String(take!(copy(LineEdit.buffer(s)))), depwarn=false)
181✔
1128
    return !(isa(ast, Expr) && ast.head === :incomplete)
98✔
1129
end
1130

1131
find_hist_file() = get(ENV, "JULIA_HISTORY",
8✔
1132
                       !isempty(DEPOT_PATH) ? joinpath(DEPOT_PATH[1], "logs", "repl_history.jl") :
1133
                       error("DEPOT_PATH is empty and ENV[\"JULIA_HISTORY\"] not set."))
1134

1135
backend(r::AbstractREPL) = r.backendref
100✔
1136

1137
function eval_with_backend(ast, backend::REPLBackendRef)
104✔
1138
    put!(backend.repl_channel, (ast, 1))
104✔
1139
    return take!(backend.response_channel) # (val, iserr)
104✔
1140
end
1141

1142
function respond(f, repl, main; pass_empty::Bool = false, suppress_on_semicolon::Bool = true)
100✔
1143
    return function do_respond(s::MIState, buf, ok::Bool)
237✔
1144
        if !ok
137✔
1145
            return transition(s, :abort)
21✔
1146
        end
1147
        line = String(take!(buf)::Vector{UInt8})
217✔
1148
        if !isempty(line) || pass_empty
131✔
1149
            reset(repl)
101✔
1150
            local response
1151
            try
101✔
1152
                ast = Base.invokelatest(f, line)
101✔
1153
                response = eval_with_backend(ast, backend(repl))
100✔
1154
            catch
1155
                response = Pair{Any, Bool}(current_exceptions(), true)
1✔
1156
            end
1157
            hide_output = suppress_on_semicolon && ends_with_semicolon(line)
101✔
1158
            print_response(repl, response, !hide_output, hascolor(repl))
101✔
1159
        end
1160
        prepare_next(repl)
116✔
1161
        reset_state(s)
116✔
1162
        return s.current_mode.sticky ? true : transition(s, main)
116✔
1163
    end
1164
end
1165

1166
function reset(repl::LineEditREPL)
101✔
1167
    raw!(repl.t, false)
101✔
1168
    hascolor(repl) && print(repl.t, Base.text_colors[:normal])
101✔
1169
    nothing
1170
end
1171

1172
function prepare_next(repl::LineEditREPL)
116✔
1173
    println(terminal(repl))
116✔
1174
end
1175

1176
function mode_keymap(julia_prompt::Prompt)
1177
    AnyDict(
27✔
1178
    '\b' => function (s::MIState,o...)
7✔
1179
        if isempty(s) || position(LineEdit.buffer(s)) == 0
7✔
1180
            buf = copy(LineEdit.buffer(s))
7✔
1181
            transition(s, julia_prompt) do
7✔
1182
                LineEdit.state(s, julia_prompt).input_buffer = buf
7✔
1183
            end
1184
        else
1185
            LineEdit.edit_backspace(s)
×
1186
        end
1187
    end,
1188
    "^C" => function (s::MIState,o...)
1189
        LineEdit.move_input_end(s)
1190
        LineEdit.refresh_line(s)
1191
        print(LineEdit.terminal(s), "^C\n\n")
1192
        transition(s, julia_prompt)
1193
        transition(s, :reset)
1194
        LineEdit.refresh_line(s)
1195
    end)
1196
end
1197

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

1201
const JL_PROMPT_PASTE = Ref(true)
1202
enable_promptpaste(v::Bool) = JL_PROMPT_PASTE[] = v
×
1203

1204
function contextual_prompt(repl::LineEditREPL, prompt::Union{String,Function})
1205
    function ()
2,179✔
1206
        mod = active_module(repl)
4,251✔
1207
        prefix = mod == Main ? "" : string('(', mod, ") ")
2,161✔
1208
        pr = prompt isa String ? prompt : prompt()
2,129✔
1209
        prefix * pr
2,129✔
1210
    end
1211
end
1212

1213
setup_interface(
71✔
1214
    repl::LineEditREPL;
1215
    # those keyword arguments may be deprecated eventually in favor of the Options mechanism
1216
    hascolor::Bool = repl.options.hascolor,
1217
    extra_repl_keymap::Any = repl.options.extra_keymap
1218
) = setup_interface(repl, hascolor, extra_repl_keymap)
1219

1220

1221
# This non keyword method can be precompiled which is important
1222
function setup_interface(
25✔
1223
    repl::LineEditREPL,
1224
    hascolor::Bool,
1225
    extra_repl_keymap::Any, # Union{Dict,Vector{<:Dict}},
1226
)
1227
    # The precompile statement emitter has problem outputting valid syntax for the
1228
    # type of `Union{Dict,Vector{<:Dict}}` (see #28808).
1229
    # This function is however important to precompile for REPL startup time, therefore,
1230
    # make the type Any and just assert that we have the correct type below.
1231
    @assert extra_repl_keymap isa Union{Dict,Vector{<:Dict}}
25✔
1232

1233
    ###
1234
    #
1235
    # This function returns the main interface that describes the REPL
1236
    # functionality, it is called internally by functions that setup a
1237
    # Terminal-based REPL frontend.
1238
    #
1239
    # See run_frontend(repl::LineEditREPL, backend::REPLBackendRef)
1240
    # for usage
1241
    #
1242
    ###
1243

1244
    ###
1245
    # We setup the interface in two stages.
1246
    # First, we set up all components (prompt,rsearch,shell,help)
1247
    # Second, we create keymaps with appropriate transitions between them
1248
    #   and assign them to the components
1249
    #
1250
    ###
1251

1252
    ############################### Stage I ################################
1253

1254
    # This will provide completions for REPL and help mode
1255
    replc = REPLCompletionProvider()
25✔
1256

1257
    # Set up the main Julia prompt
1258
    julia_prompt = Prompt(contextual_prompt(repl, JULIA_PROMPT);
50✔
1259
        # Copy colors from the prompt object
1260
        prompt_prefix = hascolor ? repl.prompt_color : "",
1261
        prompt_suffix = hascolor ?
1262
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1263
        repl = repl,
1264
        complete = replc,
1265
        on_enter = return_callback)
1266

1267
    # Setup help mode
1268
    help_mode = Prompt(contextual_prompt(repl, HELP_PROMPT),
50✔
1269
        prompt_prefix = hascolor ? repl.help_color : "",
1270
        prompt_suffix = hascolor ?
1271
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1272
        repl = repl,
1273
        complete = replc,
1274
        # When we're done transform the entered line into a call to helpmode function
1275
        on_done = respond(line::String->helpmode(outstream(repl), line, repl.mistate.active_module),
2✔
1276
                          repl, julia_prompt, pass_empty=true, suppress_on_semicolon=false))
1277

1278

1279
    # Set up shell mode
1280
    shell_mode = Prompt(SHELL_PROMPT;
50✔
1281
        prompt_prefix = hascolor ? repl.shell_color : "",
1282
        prompt_suffix = hascolor ?
1283
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1284
        repl = repl,
1285
        complete = ShellCompletionProvider(),
1286
        # Transform "foo bar baz" into `foo bar baz` (shell quoting)
1287
        # and pass into Base.repl_cmd for processing (handles `ls` and `cd`
1288
        # special)
1289
        on_done = respond(repl, julia_prompt) do line
1290
            Expr(:call, :(Base.repl_cmd),
9✔
1291
                :(Base.cmd_gen($(Base.shell_parse(line::String)[1]))),
1292
                outstream(repl))
1293
        end,
1294
        sticky = true)
1295

1296
    # Set up dummy Pkg mode that will be replaced once Pkg is loaded
1297
    # use 6 dots to occupy the same space as the most likely "@v1.xx" env name
1298
    dummy_pkg_mode = Prompt(Pkg_promptf,
50✔
1299
        prompt_prefix = hascolor ? repl.pkg_color : "",
1300
        prompt_suffix = hascolor ?
1301
        (repl.envcolors ? Base.input_color : repl.input_color) : "",
1302
        repl = repl,
1303
        complete = LineEdit.EmptyCompletionProvider(),
1304
        on_done = respond(line->nothing, repl, julia_prompt),
1305
        on_enter = function (s::MIState)
1306
                # This is hit when the user tries to execute a command before the real Pkg mode has been
1307
                # switched to. Ok to do this even if Pkg is loading on the other task because of the loading lock.
1308
                REPLExt = load_pkg()
1309
                if REPLExt isa Module && isdefined(REPLExt, :PkgCompletionProvider)
1310
                    for mode in repl.interface.modes
1311
                        if mode isa LineEdit.Prompt && mode.complete isa REPLExt.PkgCompletionProvider
1312
                            # pkg mode
1313
                            buf = copy(LineEdit.buffer(s))
1314
                            transition(s, mode) do
1315
                                LineEdit.state(s, mode).input_buffer = buf
1316
                            end
1317
                        end
1318
                    end
1319
                end
1320
                return true
1321
            end,
1322
        sticky = true)
1323

1324

1325
    ################################# Stage II #############################
1326

1327
    # Setup history
1328
    # We will have a unified history for all REPL modes
1329
    hp = REPLHistoryProvider(Dict{Symbol,Prompt}(:julia => julia_prompt,
25✔
1330
                                                 :shell => shell_mode,
1331
                                                 :help  => help_mode,
1332
                                                 :pkg  => dummy_pkg_mode))
1333
    if repl.history_file
25✔
1334
        try
4✔
1335
            hist_path = find_hist_file()
8✔
1336
            mkpath(dirname(hist_path))
4✔
1337
            hp.file_path = hist_path
4✔
1338
            hist_open_file(hp)
4✔
1339
            finalizer(replc) do replc
4✔
1340
                close(hp.history_file)
4✔
1341
            end
1342
            hist_from_file(hp, hist_path)
4✔
1343
        catch
1344
            # use REPL.hascolor to avoid using the local variable with the same name
1345
            print_response(repl, Pair{Any, Bool}(current_exceptions(), true), true, REPL.hascolor(repl))
×
1346
            println(outstream(repl))
×
1347
            @info "Disabling history file for this session"
×
1348
            repl.history_file = false
×
1349
        end
1350
    end
1351
    history_reset_state(hp)
25✔
1352
    julia_prompt.hist = hp
25✔
1353
    shell_mode.hist = hp
25✔
1354
    help_mode.hist = hp
25✔
1355
    dummy_pkg_mode.hist = hp
25✔
1356

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

1359

1360
    search_prompt, skeymap = LineEdit.setup_search_keymap(hp)
25✔
1361
    search_prompt.complete = LatexCompletions()
25✔
1362

1363
    shell_prompt_len = length(SHELL_PROMPT)
×
1364
    help_prompt_len = length(HELP_PROMPT)
×
1365
    jl_prompt_regex = Regex("^In \\[[0-9]+\\]: |^(?:\\(.+\\) )?$JULIA_PROMPT")
25✔
1366
    pkg_prompt_regex = Regex("^(?:\\(.+\\) )?$PKG_PROMPT")
25✔
1367

1368
    # Canonicalize user keymap input
1369
    if isa(extra_repl_keymap, Dict)
25✔
1370
        extra_repl_keymap = AnyDict[extra_repl_keymap]
×
1371
    end
1372

1373
    repl_keymap = AnyDict(
25✔
1374
        ';' => function (s::MIState,o...)
55✔
1375
            if isempty(s) || position(LineEdit.buffer(s)) == 0
103✔
1376
                buf = copy(LineEdit.buffer(s))
7✔
1377
                transition(s, shell_mode) do
7✔
1378
                    LineEdit.state(s, shell_mode).input_buffer = buf
7✔
1379
                end
1380
            else
1381
                edit_insert(s, ';')
48✔
1382
            end
1383
        end,
1384
        '?' => function (s::MIState,o...)
1✔
1385
            if isempty(s) || position(LineEdit.buffer(s)) == 0
1✔
1386
                buf = copy(LineEdit.buffer(s))
1✔
1387
                transition(s, help_mode) do
1✔
1388
                    LineEdit.state(s, help_mode).input_buffer = buf
1✔
1389
                end
1390
            else
1391
                edit_insert(s, '?')
×
1392
            end
1393
        end,
1394
        ']' => function (s::MIState,o...)
3✔
1395
            if isempty(s) || position(LineEdit.buffer(s)) == 0
6✔
1396
                buf = copy(LineEdit.buffer(s))
×
1397
                transition(s, dummy_pkg_mode) do
×
1398
                    LineEdit.state(s, dummy_pkg_mode).input_buffer = buf
1399
                end
1400
                # load Pkg on another thread if available so that typing in the dummy Pkg prompt
1401
                # isn't blocked, but instruct the main REPL task to do the transition via s.async_channel
1402
                t_replswitch = Threads.@spawn begin
×
1403
                    REPLExt = load_pkg()
1404
                    if REPLExt isa Module && isdefined(REPLExt, :PkgCompletionProvider)
1405
                        put!(s.async_channel,
1406
                            function (s::MIState)
1407
                                LineEdit.mode(s) === dummy_pkg_mode || return :ok
1408
                                for mode in repl.interface.modes
1409
                                    if mode isa LineEdit.Prompt && mode.complete isa REPLExt.PkgCompletionProvider
1410
                                        buf = copy(LineEdit.buffer(s))
1411
                                        transition(s, mode) do
1412
                                            LineEdit.state(s, mode).input_buffer = buf
1413
                                        end
1414
                                        if !isempty(s) && @invokelatest(LineEdit.check_for_hint(s))
1415
                                            @invokelatest(LineEdit.refresh_line(s))
1416
                                        end
1417
                                        break
1418
                                    end
1419
                                end
1420
                                return :ok
1421
                            end
1422
                        )
1423
                    end
1424
                end
1425
                Base.errormonitor(t_replswitch)
×
1426
            else
1427
                edit_insert(s, ']')
3✔
1428
            end
1429
        end,
1430

1431
        # Bracketed Paste Mode
1432
        "\e[200~" => (s::MIState,o...)->begin
8✔
1433
            input = LineEdit.bracketed_paste(s) # read directly from s until reaching the end-bracketed-paste marker
8✔
1434
            sbuffer = LineEdit.buffer(s)
8✔
1435
            curspos = position(sbuffer)
8✔
1436
            seek(sbuffer, 0)
8✔
1437
            shouldeval = (bytesavailable(sbuffer) == curspos && !occursin(UInt8('\n'), sbuffer))
8✔
1438
            seek(sbuffer, curspos)
8✔
1439
            if curspos == 0
8✔
1440
                # if pasting at the beginning, strip leading whitespace
1441
                input = lstrip(input)
7✔
1442
            end
1443
            if !shouldeval
8✔
1444
                # when pasting in the middle of input, just paste in place
1445
                # don't try to execute all the WIP, since that's rather confusing
1446
                # and is often ill-defined how it should behave
1447
                edit_insert(s, input)
×
1448
                return
×
1449
            end
1450
            LineEdit.push_undo(s)
8✔
1451
            edit_insert(sbuffer, input)
8✔
1452
            input = String(take!(sbuffer))
16✔
1453
            oldpos = firstindex(input)
×
1454
            firstline = true
×
1455
            isprompt_paste = false
×
1456
            curr_prompt_len = 0
×
1457
            pasting_help = false
×
1458

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

1571
        # Open the editor at the location of a stackframe or method
1572
        # This is accessing a contextual variable that gets set in
1573
        # the show_backtrace and show_method_table functions.
1574
        "^Q" => (s::MIState, o...) -> begin
1575
            linfos = repl.last_shown_line_infos
1576
            str = String(take!(LineEdit.buffer(s)))
1577
            n = tryparse(Int, str)
1578
            n === nothing && @goto writeback
1579
            if n <= 0 || n > length(linfos) || startswith(linfos[n][1], "REPL[")
1580
                @goto writeback
1581
            end
1582
            try
1583
                InteractiveUtils.edit(Base.fixup_stdlib_path(linfos[n][1]), linfos[n][2])
1584
            catch ex
1585
                ex isa ProcessFailedException || ex isa Base.IOError || ex isa SystemError || rethrow()
1586
                @info "edit failed" _exception=ex
1587
            end
1588
            LineEdit.refresh_line(s)
1589
            return
1590
            @label writeback
1591
            write(LineEdit.buffer(s), str)
1592
            return
1593
        end,
1594
    )
1595

1596
    prefix_prompt, prefix_keymap = LineEdit.setup_prefix_keymap(hp, julia_prompt)
25✔
1597

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

1601
    julia_prompt.keymap_dict = LineEdit.keymap(a)
25✔
1602

1603
    mk = mode_keymap(julia_prompt)
25✔
1604

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

1608
    shell_mode.keymap_dict = help_mode.keymap_dict = dummy_pkg_mode.keymap_dict = LineEdit.keymap(b)
25✔
1609

1610
    allprompts = LineEdit.TextInterface[julia_prompt, shell_mode, help_mode, dummy_pkg_mode, search_prompt, prefix_prompt]
25✔
1611
    return ModalInterface(allprompts)
25✔
1612
end
1613

1614
function run_frontend(repl::LineEditREPL, backend::REPLBackendRef)
21✔
1615
    repl.frontend_task = current_task()
21✔
1616
    d = REPLDisplay(repl)
21✔
1617
    dopushdisplay = repl.specialdisplay === nothing && !in(d,Base.Multimedia.displays)
36✔
1618
    dopushdisplay && pushdisplay(d)
21✔
1619
    if !isdefined(repl,:interface)
21✔
1620
        interface = repl.interface = setup_interface(repl)
26✔
1621
    else
1622
        interface = repl.interface
8✔
1623
    end
1624
    repl.backendref = backend
21✔
1625
    repl.mistate = LineEdit.init_state(terminal(repl), interface)
21✔
1626
    run_interface(terminal(repl), interface, repl.mistate)
21✔
1627
    # Terminate Backend
1628
    put!(backend.repl_channel, (nothing, -1))
21✔
1629
    dopushdisplay && popdisplay(d)
21✔
1630
    nothing
1631
end
1632

1633
## StreamREPL ##
1634

1635
mutable struct StreamREPL <: AbstractREPL
1636
    stream::IO
1637
    prompt_color::String
1638
    input_color::String
1639
    answer_color::String
1640
    waserror::Bool
1641
    frontend_task::Task
1642
    StreamREPL(stream,pc,ic,ac) = new(stream,pc,ic,ac,false)
×
1643
end
1644
StreamREPL(stream::IO) = StreamREPL(stream, Base.text_colors[:green], Base.input_color(), Base.answer_color())
×
1645
run_repl(stream::IO) = run_repl(StreamREPL(stream))
×
1646

1647
outstream(s::StreamREPL) = s.stream
×
1648
hascolor(s::StreamREPL) = get(s.stream, :color, false)::Bool
×
1649

1650
answer_color(r::LineEditREPL) = r.envcolors ? Base.answer_color() : r.answer_color
×
1651
answer_color(r::StreamREPL) = r.answer_color
×
1652
input_color(r::LineEditREPL) = r.envcolors ? Base.input_color() : r.input_color
×
1653
input_color(r::StreamREPL) = r.input_color
×
1654

1655
let matchend = Dict("\"" => r"\"", "\"\"\"" => r"\"\"\"", "'" => r"'",
1656
    "`" => r"`", "```" => r"```", "#" => r"$"m, "#=" => r"=#|#=")
1657
    global _rm_strings_and_comments
1658
    function _rm_strings_and_comments(code::Union{String,SubString{String}})
126✔
1659
        buf = IOBuffer(sizehint = sizeof(code))
252✔
1660
        pos = 1
×
1661
        while true
166✔
1662
            i = findnext(r"\"(?!\"\")|\"\"\"|'|`(?!``)|```|#(?!=)|#=", code, pos)
332✔
1663
            isnothing(i) && break
212✔
1664
            match = SubString(code, i)
46✔
1665
            j = findnext(matchend[match]::Regex, code, nextind(code, last(i)))
92✔
1666
            if match == "#=" # possibly nested
46✔
1667
                nested = 1
×
1668
                while j !== nothing
11✔
1669
                    nested += SubString(code, j) == "#=" ? +1 : -1
10✔
1670
                    iszero(nested) && break
10✔
1671
                    j = findnext(r"=#|#=", code, nextind(code, last(j)))
12✔
1672
                end
6✔
1673
            elseif match[1] != '#' # quote match: check non-escaped
82✔
1674
                while j !== nothing
38✔
1675
                    notbackslash = findprev(!=('\\'), code, prevind(code, first(j)))::Int
66✔
1676
                    isodd(first(j) - notbackslash) && break # not escaped
33✔
1677
                    j = findnext(matchend[match]::Regex, code, nextind(code, first(j)))
14✔
1678
                end
7✔
1679
            end
1680
            isnothing(j) && break
86✔
1681
            if match[1] == '#'
80✔
1682
                print(buf, SubString(code, pos, prevind(code, first(i))))
14✔
1683
            else
1684
                print(buf, SubString(code, pos, last(i)), ' ', SubString(code, j))
26✔
1685
            end
1686
            pos = nextind(code, last(j))
40✔
1687
        end
40✔
1688
        print(buf, SubString(code, pos, lastindex(code)))
126✔
1689
        return String(take!(buf))
126✔
1690
    end
1691
end
1692

1693
# heuristic function to decide if the presence of a semicolon
1694
# at the end of the expression was intended for suppressing output
1695
ends_with_semicolon(code::AbstractString) = ends_with_semicolon(String(code))
×
1696
ends_with_semicolon(code::Union{String,SubString{String}}) =
126✔
1697
    contains(_rm_strings_and_comments(code), r";\s*$")
1698

1699
function banner(io::IO = stdout; short = false)
4✔
1700
    if Base.GIT_VERSION_INFO.tagged_commit
×
1701
        commit_string = Base.TAGGED_RELEASE_BANNER
×
1702
    elseif isempty(Base.GIT_VERSION_INFO.commit)
×
1703
        commit_string = ""
×
1704
    else
1705
        days = Int(floor((ccall(:jl_clock_now, Float64, ()) - Base.GIT_VERSION_INFO.fork_master_timestamp) / (60 * 60 * 24)))
2✔
1706
        days = max(0, days)
2✔
1707
        unit = days == 1 ? "day" : "days"
2✔
1708
        distance = Base.GIT_VERSION_INFO.fork_master_distance
×
1709
        commit = Base.GIT_VERSION_INFO.commit_short
2✔
1710

1711
        if distance == 0
×
1712
            commit_string = "Commit $(commit) ($(days) $(unit) old master)"
2✔
1713
        else
1714
            branch = Base.GIT_VERSION_INFO.branch
×
1715
            commit_string = "$(branch)/$(commit) (fork: $(distance) commits, $(days) $(unit))"
×
1716
        end
1717
    end
1718

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

1721
    if get(io, :color, false)::Bool
2✔
1722
        c = Base.text_colors
×
1723
        tx = c[:normal] # text
×
1724
        jl = c[:normal] # julia
×
1725
        d1 = c[:bold] * c[:blue]    # first dot
×
1726
        d2 = c[:bold] * c[:red]     # second dot
×
1727
        d3 = c[:bold] * c[:green]   # third dot
×
1728
        d4 = c[:bold] * c[:magenta] # fourth dot
×
1729

1730
        if short
×
1731
            print(io,"""
×
1732
              $(d3)o$(tx)  | Version $(VERSION)$(commit_date)
1733
             $(d2)o$(tx) $(d4)o$(tx) | $(commit_string)
1734
            """)
1735
        else
1736
            print(io,"""               $(d3)_$(tx)
×
1737
               $(d1)_$(tx)       $(jl)_$(tx) $(d2)_$(d3)(_)$(d4)_$(tx)     |  Documentation: https://docs.julialang.org
1738
              $(d1)(_)$(jl)     | $(d2)(_)$(tx) $(d4)(_)$(tx)    |
1739
               $(jl)_ _   _| |_  __ _$(tx)   |  Type \"?\" for help, \"]?\" for Pkg help.
1740
              $(jl)| | | | | | |/ _` |$(tx)  |
1741
              $(jl)| | |_| | | | (_| |$(tx)  |  Version $(VERSION)$(commit_date)
1742
             $(jl)_/ |\\__'_|_|_|\\__'_|$(tx)  |  $(commit_string)
1743
            $(jl)|__/$(tx)                   |
1744

1745
            """)
1746
        end
1747
    else
1748
        if short
2✔
1749
            print(io,"""
1✔
1750
              o  |  Version $(VERSION)$(commit_date)
1751
             o o |  $(commit_string)
1752
            """)
1753
        else
1754
            print(io,"""
1✔
1755
                           _
1756
               _       _ _(_)_     |  Documentation: https://docs.julialang.org
1757
              (_)     | (_) (_)    |
1758
               _ _   _| |_  __ _   |  Type \"?\" for help, \"]?\" for Pkg help.
1759
              | | | | | | |/ _` |  |
1760
              | | |_| | | | (_| |  |  Version $(VERSION)$(commit_date)
1761
             _/ |\\__'_|_|_|\\__'_|  |  $(commit_string)
1762
            |__/                   |
1763

1764
            """)
1765
        end
1766
    end
1767
end
1768

1769
function run_frontend(repl::StreamREPL, backend::REPLBackendRef)
×
1770
    repl.frontend_task = current_task()
×
1771
    have_color = hascolor(repl)
×
1772
    banner(repl.stream)
×
1773
    d = REPLDisplay(repl)
×
1774
    dopushdisplay = !in(d,Base.Multimedia.displays)
×
1775
    dopushdisplay && pushdisplay(d)
×
1776
    while !eof(repl.stream)::Bool
×
1777
        if have_color
×
1778
            print(repl.stream,repl.prompt_color)
×
1779
        end
1780
        print(repl.stream, JULIA_PROMPT)
×
1781
        if have_color
×
1782
            print(repl.stream, input_color(repl))
×
1783
        end
1784
        line = readline(repl.stream, keep=true)
×
1785
        if !isempty(line)
×
1786
            ast = Base.parse_input_line(line)
×
1787
            if have_color
×
1788
                print(repl.stream, Base.color_normal)
×
1789
            end
1790
            response = eval_with_backend(ast, backend)
×
1791
            print_response(repl, response, !ends_with_semicolon(line), have_color)
×
1792
        end
1793
    end
×
1794
    # Terminate Backend
1795
    put!(backend.repl_channel, (nothing, -1))
×
1796
    dopushdisplay && popdisplay(d)
×
1797
    nothing
×
1798
end
1799

1800
module Numbered
1801

1802
using ..REPL
1803

1804
__current_ast_transforms() = isdefined(Base, :active_repl_backend) ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1805

1806
function repl_eval_counter(hp)
765✔
1807
    return length(hp.history) - hp.start_idx
765✔
1808
end
1809

1810
function out_transform(@nospecialize(x), n::Ref{Int})
16✔
1811
    return Expr(:toplevel, get_usings!([], x)..., quote
16✔
1812
        let __temp_val_a72df459 = $x
1813
            $capture_result($n, __temp_val_a72df459)
1814
            __temp_val_a72df459
1815
        end
1816
    end)
1817
end
1818

1819
function get_usings!(usings, ex)
25✔
1820
    ex isa Expr || return usings
25✔
1821
    # get all `using` and `import` statements which are at the top level
1822
    for (i, arg) in enumerate(ex.args)
25✔
1823
        if Base.isexpr(arg, :toplevel)
48✔
1824
            get_usings!(usings, arg)
9✔
1825
        elseif Base.isexpr(arg, [:using, :import])
64✔
1826
            push!(usings, popat!(ex.args, i))
2✔
1827
        end
1828
    end
71✔
1829
    return usings
25✔
1830
end
1831

1832
function capture_result(n::Ref{Int}, @nospecialize(x))
16✔
1833
    n = n[]
16✔
1834
    mod = Base.MainInclude
16✔
1835
    if !isdefined(mod, :Out)
16✔
1836
        @eval mod global Out
1✔
1837
        @eval mod export Out
1✔
1838
        setglobal!(mod, :Out, Dict{Int, Any}())
1✔
1839
    end
1840
    if x !== getglobal(mod, :Out) && x !== nothing # remove this?
16✔
1841
        getglobal(mod, :Out)[n] = x
14✔
1842
    end
1843
    nothing
16✔
1844
end
1845

1846
function set_prompt(repl::LineEditREPL, n::Ref{Int})
1✔
1847
    julia_prompt = repl.interface.modes[1]
1✔
1848
    julia_prompt.prompt = function()
766✔
1849
        n[] = repl_eval_counter(julia_prompt.hist)+1
765✔
1850
        string("In [", n[], "]: ")
765✔
1851
    end
1852
    nothing
1✔
1853
end
1854

1855
function set_output_prefix(repl::LineEditREPL, n::Ref{Int})
1✔
1856
    julia_prompt = repl.interface.modes[1]
1✔
1857
    if REPL.hascolor(repl)
1✔
1858
        julia_prompt.output_prefix_prefix = Base.text_colors[:red]
1✔
1859
    end
1860
    julia_prompt.output_prefix = () -> string("Out[", n[], "]: ")
15✔
1861
    nothing
1✔
1862
end
1863

1864
function __current_ast_transforms(backend)
1865
    if backend === nothing
1✔
1866
        isdefined(Base, :active_repl_backend) ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1867
    else
1868
        backend.ast_transforms
1✔
1869
    end
1870
end
1871

1872
function numbered_prompt!(repl::LineEditREPL=Base.active_repl, backend=nothing)
1873
    n = Ref{Int}(0)
1✔
1874
    set_prompt(repl, n)
1✔
1875
    set_output_prefix(repl, n)
1✔
1876
    push!(__current_ast_transforms(backend), @nospecialize(ast) -> out_transform(ast, n))
17✔
1877
    return
1✔
1878
end
1879

1880
"""
1881
    Out[n]
1882

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

1886
See also [`ans`](@ref).
1887
"""
1888
Base.MainInclude.Out
1889

1890
end
1891

1892
import .Numbered.numbered_prompt!
1893

1894
# this assignment won't survive precompilation,
1895
# but will stick if REPL is baked into a sysimg.
1896
# Needs to occur after this module is finished.
1897
Base.REPL_MODULE_REF[] = REPL
1898

1899
if Base.generating_output()
1900
    include("precompile.jl")
1901
end
1902

1903
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