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

JuliaLang / julia / #37866

10 Aug 2024 04:31AM UTC coverage: 87.778% (+1.7%) from 86.076%
#37866

push

local

web-flow
compiler: apply more accurate effects to return_type_tfunc (#55338)

In extreme cases, the compiler could mark this function for
concrete-eval, even though that is illegal unless the compiler has first
deleted this instruction. Otherwise the attempt to concrete-eval will
re-run the function repeatedly until it hits a StackOverflow.

Workaround to fix #55147

@aviatesk You might know how to solve this even better, using
post-optimization effect refinements? Since we should actually only
apply the refinement of terminates=false => terminates=true (and thus
allowing concrete eval) if the optimization occurs, and not just in
inference thinks the optimization would be legal.

---------

Co-authored-by: Shuhei Kadowaki <aviatesk@gmail.com>

25 of 30 new or added lines in 8 files covered. (83.33%)

191 existing lines in 12 files now uncovered.

78056 of 88924 relevant lines covered (87.78%)

16403789.58 hits per line

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

81.19
/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
24✔
78
            m === Base && continue
23✔
79
            m === Main && continue
22✔
80
            m === scope && continue
21✔
81
            warned |= _UndefVarError_warnfor(io, m, var)
21✔
82
        end
24✔
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)
25✔
91
    Base.isbindingresolved(m, var) || return false
48✔
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__()
5✔
98
    Base.REPL_MODULE_REF[] = REPL
5✔
99
    Base.Experimental.register_error_hint(UndefVarError_hint, UndefVarError)
5✔
100
    return nothing
5✔
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
public TerminalMenus
114

115
import Base:
116
    AbstractDisplay,
117
    display,
118
    show,
119
    AnyDict,
120
    ==
121

122
_displaysize(io::IO) = displaysize(io)::Tuple{Int,Int}
32✔
123

124
include("Terminals.jl")
125
using .Terminals
126

127
abstract type AbstractREPL end
128

129
include("options.jl")
130

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

151
include("REPLCompletions.jl")
152
using .REPLCompletions
153

154
include("TerminalMenus/TerminalMenus.jl")
155
include("docview.jl")
156

157
include("Pkg_beforeload.jl")
158

159
@nospecialize # use only declared type signatures
160

161
answer_color(::AbstractREPL) = ""
×
162

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

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

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

185
"""
186
    softscope(ex)
187

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

209
# Temporary alias until Documenter updates
210
const softscope! = softscope
211

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

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

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

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

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

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

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

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

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

312
    return nothing
541✔
313
end
314

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

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

334
const repl_ast_transforms = Any[softscope, warn_on_non_owning_accesses] # defaults for new REPL backends
335

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

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

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

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

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

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

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

445
"""
446
    start_repl_backend(repl_channel::Channel, response_channel::Channel)
447

448
    Starts loop for REPL backend
449
    Returns a REPLBackend with backend_task assigned
450

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

462
"""
463
    start_repl_backend(backend::REPLBackend)
464

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

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

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

493
struct REPLDisplay{Repl<:AbstractREPL} <: AbstractDisplay
494
    repl::Repl
35✔
495
end
496

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

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

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

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

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

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

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

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

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

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

618
    Main function to start the REPL
619

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

646
## BasicREPL ##
647

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

655
outstream(r::BasicREPL) = r.terminal
6✔
656
hascolor(r::BasicREPL) = hascolor(r.terminal)
×
657

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

705
## LineEditREPL ##
706

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

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

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

760
mutable struct ShellCompletionProvider <: CompletionProvider end
25✔
761
struct LatexCompletions <: CompletionProvider end
762

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

772
setmodifiers!(c::CompletionProvider, m::LineEdit.Modifiers) = nothing
×
773

774
setmodifiers!(c::REPLCompletionProvider, m::LineEdit.Modifiers) = c.modifiers = m
×
775

776
"""
777
    activate(mod::Module=Main)
778

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

965
    return :ok
78✔
966
end
967

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

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

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

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

1023
history_last(s::LineEdit.MIState, hist::REPLHistoryProvider) =
4✔
1024
    history_next(s, hist, length(hist.history) - hist.cur_idx + 1)
1025

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

1067
function history_search(hist::REPLHistoryProvider, query_buffer::IOBuffer, response_buffer::IOBuffer,
28✔
1068
                        backwards::Bool=false, skip_current::Bool=false)
1069

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

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

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

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

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

1117
    return false
4✔
1118
end
1119

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

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

1134
find_hist_file() = get(ENV, "JULIA_HISTORY",
4✔
1135
                       !isempty(DEPOT_PATH) ? joinpath(DEPOT_PATH[1], "logs", "repl_history.jl") :
1136
                       error("DEPOT_PATH is empty and ENV[\"JULIA_HISTORY\"] not set."))
1137

1138
backend(r::AbstractREPL) = r.backendref
100✔
1139

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

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

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

1175
function prepare_next(repl::LineEditREPL)
116✔
1176
    println(terminal(repl))
116✔
1177
end
1178

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

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

1204
const JL_PROMPT_PASTE = Ref(true)
1205
enable_promptpaste(v::Bool) = JL_PROMPT_PASTE[] = v
×
1206

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

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

1223

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

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

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

1255
    ############################### Stage I ################################
1256

1257
    # This will provide completions for REPL and help mode
1258
    replc = REPLCompletionProvider()
25✔
1259

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

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

1281

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

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

1327

1328
    ################################# Stage II #############################
1329

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

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

1362

1363
    search_prompt, skeymap = LineEdit.setup_search_keymap(hp)
25✔
1364
    search_prompt.complete = LatexCompletions()
25✔
1365

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

1371
    # Canonicalize user keymap input
1372
    if isa(extra_repl_keymap, Dict)
25✔
1373
        extra_repl_keymap = AnyDict[extra_repl_keymap]
×
1374
    end
1375

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

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

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

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

1599
    prefix_prompt, prefix_keymap = LineEdit.setup_prefix_keymap(hp, julia_prompt)
25✔
1600

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

1604
    julia_prompt.keymap_dict = LineEdit.keymap(a)
25✔
1605

1606
    mk = mode_keymap(julia_prompt)
25✔
1607

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

1611
    shell_mode.keymap_dict = help_mode.keymap_dict = dummy_pkg_mode.keymap_dict = LineEdit.keymap(b)
25✔
1612

1613
    allprompts = LineEdit.TextInterface[julia_prompt, shell_mode, help_mode, dummy_pkg_mode, search_prompt, prefix_prompt]
25✔
1614
    return ModalInterface(allprompts)
25✔
1615
end
1616

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

1636
## StreamREPL ##
1637

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

1650
outstream(s::StreamREPL) = s.stream
×
1651
hascolor(s::StreamREPL) = get(s.stream, :color, false)::Bool
×
1652

1653
answer_color(r::LineEditREPL) = r.envcolors ? Base.answer_color() : r.answer_color
×
1654
answer_color(r::StreamREPL) = r.answer_color
×
1655
input_color(r::LineEditREPL) = r.envcolors ? Base.input_color() : r.input_color
×
1656
input_color(r::StreamREPL) = r.input_color
×
1657

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

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

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

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

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

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

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

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

1767
            """)
1768
        end
1769
    end
1770
end
1771

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

1803
module Numbered
1804

1805
using ..REPL
1806

1807
__current_ast_transforms() = Base.active_repl_backend !== nothing ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1808

1809
function repl_eval_counter(hp)
765✔
1810
    return length(hp.history) - hp.start_idx
765✔
1811
end
1812

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

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

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

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

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

1867
function __current_ast_transforms(backend)
1868
    if backend === nothing
1✔
1869
        Base.active_repl_backend !== nothing ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1870
    else
1871
        backend.ast_transforms
1✔
1872
    end
1873
end
1874

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

1883
"""
1884
    Out[n]
1885

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

1889
See also [`ans`](@ref).
1890
"""
1891
Base.MainInclude.Out
1892

1893
end
1894

1895
import .Numbered.numbered_prompt!
1896

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

1902
if Base.generating_output()
1903
    include("precompile.jl")
1904
end
1905

1906
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