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

JuliaLang / julia / #37836

11 Jul 2024 11:31AM UTC coverage: 85.36% (-2.2%) from 87.516%
#37836

push

local

web-flow
fix loading of repeated/concurrent modules (#55066)

More followup to fix issues with require. There was an accidental
variable reuse (build_id) that caused it to be unable to load cache
files in many cases. There was also missing check for a dependency
already being loaded, resulting in trying to load it twice. Finally, the
start_loading code may drop the require_lock, but the surrounding code
was not prepared for that. Now integrate the necessary checks into
start_loading, instead of needing to duplicate them before and
afterwards.

Fixes #53983
Fixes #54940
Closes #55064

74 of 89 new or added lines in 1 file covered. (83.15%)

2133 existing lines in 46 files now uncovered.

73916 of 86593 relevant lines covered (85.36%)

15331627.57 hits per line

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

0.59
/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

UNCOV
20
function UndefVarError_hint(io::IO, ex::UndefVarError)
×
UNCOV
21
    var = ex.var
×
UNCOV
22
    if var === :or
×
23
        print(io, "\nSuggestion: Use `||` for short-circuiting boolean OR.")
×
UNCOV
24
    elseif var === :and
×
25
        print(io, "\nSuggestion: Use `&&` for short-circuiting boolean AND.")
×
UNCOV
26
    elseif var === :help
×
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]))
×
UNCOV
30
    elseif var === :quit
×
31
        print(io, "\nSuggestion: To exit Julia, use Ctrl-D, or type exit() and press enter.")
×
32
    end
UNCOV
33
    if isdefined(ex, :scope)
×
UNCOV
34
        scope = ex.scope
×
UNCOV
35
        if scope isa Module
×
UNCOV
36
            bnd = ccall(:jl_get_module_binding, Any, (Any, Any, Cint), scope, var, true)::Core.Binding
×
UNCOV
37
            if isdefined(bnd, :owner)
×
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
UNCOV
43
                owner = ccall(:jl_binding_owner, Ptr{Cvoid}, (Any, Any), scope, var)
×
UNCOV
44
                if C_NULL == owner
×
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:
UNCOV
49
                    if isdefined(bnd, :flags) && Bool(bnd.flags >> 4 & 1) # magic location of the `usingfailed` flag
×
UNCOV
50
                        print(io, "\nHint: It looks like two or more modules export different ",
×
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
UNCOV
55
                        print(io, "\nSuggestion: check for spelling errors or missing imports.")
×
56
                    end
UNCOV
57
                    owner = bnd
×
58
                else
59
                    owner = unsafe_pointer_to_objref(owner)::Core.Binding
×
60
                end
61
            end
UNCOV
62
            if owner !== bnd
×
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
UNCOV
74
    if scope !== Base && !_UndefVarError_warnfor(io, Base, var)
×
UNCOV
75
        warned = false
×
UNCOV
76
        for m in Base.loaded_modules_order
×
UNCOV
77
            m === Core && continue
×
UNCOV
78
            m === Base && continue
×
UNCOV
79
            m === Main && continue
×
UNCOV
80
            m === scope && continue
×
UNCOV
81
            warned |= _UndefVarError_warnfor(io, m, var)
×
UNCOV
82
        end
×
UNCOV
83
        warned ||
×
84
            _UndefVarError_warnfor(io, Core, var) ||
85
            _UndefVarError_warnfor(io, Main, var)
86
    end
UNCOV
87
    return nothing
×
88
end
89

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

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

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

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

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

UNCOV
119
_displaysize(io::IO) = displaysize(io)::Tuple{Int,Int}
×
120

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

124
abstract type AbstractREPL end
125

126
include("options.jl")
127

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

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

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

154
include("Pkg_beforeload.jl")
155

156
@nospecialize # use only declared type signatures
157

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

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

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

UNCOV
177
    REPLBackend(repl_channel, response_channel, in_eval, ast_transforms=copy(repl_ast_transforms)) =
×
178
        new(repl_channel, response_channel, in_eval, ast_transforms)
179
end
UNCOV
180
REPLBackend() = REPLBackend(Channel(1), Channel(1), false)
×
181

182
"""
183
    softscope(ex)
184

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

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

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

UNCOV
213
function has_ancestor(query::Module, target::Module)
×
UNCOV
214
    query == target && return true
×
UNCOV
215
    while true
×
UNCOV
216
        next = parentmodule(query)
×
UNCOV
217
        next == target && return true
×
UNCOV
218
        next == query && return false
×
219
        query = next
×
UNCOV
220
    end
×
221
end
222

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

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

UNCOV
255
function collect_names_to_warn!(warnings, locals, current_module::Module, ast)
×
UNCOV
256
    ast isa Expr || return
×
257

258
    # don't recurse through module definitions
UNCOV
259
    ast.head === :module && return
×
260

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

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

UNCOV
305
    for arg in ast.args
×
UNCOV
306
        collect_names_to_warn!(warnings, locals, current_module, arg)
×
UNCOV
307
    end
×
308

UNCOV
309
    return nothing
×
310
end
311

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

UNCOV
322
function warn_on_non_owning_accesses(current_mod, ast)
×
UNCOV
323
    warnings = collect_qualified_access_warnings(current_mod, ast)
×
UNCOV
324
    for (; outer_mod, mod, owner, name_being_accessed) in warnings
×
UNCOV
325
        print_qualified_access_warning(mod, owner, name_being_accessed)
×
UNCOV
326
    end
×
UNCOV
327
    return ast
×
328
end
UNCOV
329
warn_on_non_owning_accesses(ast) = warn_on_non_owning_accesses(REPL.active_module(), ast)
×
330

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

490
struct REPLDisplay{Repl<:AbstractREPL} <: AbstractDisplay
491
    repl::Repl
492
end
493

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

UNCOV
517
function print_response(repl::AbstractREPL, response, show_value::Bool, have_color::Bool)
×
UNCOV
518
    repl.waserror = response[2]
×
UNCOV
519
    with_repl_linfo(repl) do io
×
UNCOV
520
        io = IOContext(io, :module => active_module(repl)::Module)
×
UNCOV
521
        print_response(io, response, show_value, have_color, specialdisplay(repl))
×
522
    end
UNCOV
523
    return nothing
×
524
end
525

UNCOV
526
function repl_display_error(errio::IO, @nospecialize errval)
×
527
    # this will be set to true if types in the stacktrace are truncated
UNCOV
528
    limitflag = Ref(false)
×
UNCOV
529
    errio = IOContext(errio, :stacktrace_types_limited => limitflag)
×
UNCOV
530
    Base.invokelatest(Base.display_error, errio, errval)
×
UNCOV
531
    if limitflag[]
×
UNCOV
532
        print(errio, "Some type information was truncated. Use `show(err)` to see complete types.")
×
UNCOV
533
        println(errio)
×
534
    end
UNCOV
535
    return nothing
×
536
end
537

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

588
# A reference to a backend that is not mutable
589
struct REPLBackendRef
590
    repl_channel::Channel{Any}
591
    response_channel::Channel{Any}
592
end
UNCOV
593
REPLBackendRef(backend::REPLBackend) = REPLBackendRef(backend.repl_channel, backend.response_channel)
×
594

UNCOV
595
function destroy(ref::REPLBackendRef, state::Task)
×
UNCOV
596
    if istaskfailed(state)
×
597
        close(ref.repl_channel, TaskFailedException(state))
×
598
        close(ref.response_channel, TaskFailedException(state))
×
599
    end
UNCOV
600
    close(ref.repl_channel)
×
UNCOV
601
    close(ref.response_channel)
×
602
end
603

604
"""
605
    run_repl(repl::AbstractREPL)
606
    run_repl(repl, consumer = backend->nothing; backend_on_current_task = true)
607

608
    Main function to start the REPL
609

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

636
## BasicREPL ##
637

638
mutable struct BasicREPL <: AbstractREPL
639
    terminal::TextTerminal
640
    waserror::Bool
641
    frontend_task::Task
UNCOV
642
    BasicREPL(t) = new(t, false)
×
643
end
644

UNCOV
645
outstream(r::BasicREPL) = r.terminal
×
646
hascolor(r::BasicREPL) = hascolor(r.terminal)
×
647

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

695
## LineEditREPL ##
696

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

UNCOV
734
LineEditREPL(t::TextTerminal, hascolor::Bool, envcolors::Bool=false) =
×
735
    LineEditREPL(t, hascolor,
736
        hascolor ? Base.text_colors[:green] : "",
737
        hascolor ? Base.input_color() : "",
738
        hascolor ? Base.answer_color() : "",
739
        hascolor ? Base.text_colors[:red] : "",
740
        hascolor ? Base.text_colors[:yellow] : "",
741
        hascolor ? Base.text_colors[:blue] : "",
742
        false, false, false, envcolors
743
    )
744

745
mutable struct REPLCompletionProvider <: CompletionProvider
746
    modifiers::LineEdit.Modifiers
747
end
UNCOV
748
REPLCompletionProvider() = REPLCompletionProvider(LineEdit.Modifiers())
×
749

750
mutable struct ShellCompletionProvider <: CompletionProvider end
751
struct LatexCompletions <: CompletionProvider end
752

753
function active_module() # this method is also called from Base
14,080✔
754
    isdefined(Base, :active_repl) || return Main
28,160✔
UNCOV
755
    return active_module(Base.active_repl::AbstractREPL)
×
756
end
UNCOV
757
active_module((; mistate)::LineEditREPL) = mistate === nothing ? Main : mistate.active_module
×
UNCOV
758
active_module(::AbstractREPL) = Main
×
UNCOV
759
active_module(d::REPLDisplay) = active_module(d.repl)
×
760

761
setmodifiers!(c::CompletionProvider, m::LineEdit.Modifiers) = nothing
×
762

763
setmodifiers!(c::REPLCompletionProvider, m::LineEdit.Modifiers) = c.modifiers = m
×
764

765
"""
766
    activate(mod::Module=Main)
767

768
Set `mod` as the default contextual module in the REPL,
769
both for evaluating expressions and printing them.
770
"""
771
function activate(mod::Module=Main)
×
772
    mistate = (Base.active_repl::LineEditREPL).mistate
×
773
    mistate === nothing && return nothing
×
774
    mistate.active_module = mod
×
775
    Base.load_InteractiveUtils(mod)
×
776
    return nothing
×
777
end
778

UNCOV
779
beforecursor(buf::IOBuffer) = String(buf.data[1:buf.ptr-1])
×
780

UNCOV
781
function complete_line(c::REPLCompletionProvider, s::PromptState, mod::Module; hint::Bool=false)
×
UNCOV
782
    partial = beforecursor(s.input_buffer)
×
UNCOV
783
    full = LineEdit.input_string(s)
×
UNCOV
784
    ret, range, should_complete = completions(full, lastindex(partial), mod, c.modifiers.shift, hint)
×
UNCOV
785
    c.modifiers = LineEdit.Modifiers()
×
UNCOV
786
    return unique!(String[completion_text(x) for x in ret]), partial[range], should_complete
×
787
end
788

UNCOV
789
function complete_line(c::ShellCompletionProvider, s::PromptState; hint::Bool=false)
×
790
    # First parse everything up to the current position
UNCOV
791
    partial = beforecursor(s.input_buffer)
×
UNCOV
792
    full = LineEdit.input_string(s)
×
UNCOV
793
    ret, range, should_complete = shell_completions(full, lastindex(partial), hint)
×
UNCOV
794
    return unique!(String[completion_text(x) for x in ret]), partial[range], should_complete
×
795
end
796

797
function complete_line(c::LatexCompletions, s; hint::Bool=false)
×
798
    partial = beforecursor(LineEdit.buffer(s))
×
799
    full = LineEdit.input_string(s)::String
×
800
    ret, range, should_complete = bslash_completions(full, lastindex(partial), hint)[2]
×
801
    return unique!(String[completion_text(x) for x in ret]), partial[range], should_complete
×
802
end
803

UNCOV
804
with_repl_linfo(f, repl) = f(outstream(repl))
×
UNCOV
805
function with_repl_linfo(f, repl::LineEditREPL)
×
UNCOV
806
    linfos = Tuple{String,Int}[]
×
UNCOV
807
    io = IOContext(outstream(repl), :last_shown_line_infos => linfos)
×
UNCOV
808
    f(io)
×
UNCOV
809
    if !isempty(linfos)
×
UNCOV
810
        repl.last_shown_line_infos = linfos
×
811
    end
UNCOV
812
    nothing
×
813
end
814

815
mutable struct REPLHistoryProvider <: HistoryProvider
816
    history::Vector{String}
817
    file_path::String
818
    history_file::Union{Nothing,IO}
819
    start_idx::Int
820
    cur_idx::Int
821
    last_idx::Int
822
    last_buffer::IOBuffer
823
    last_mode::Union{Nothing,Prompt}
824
    mode_mapping::Dict{Symbol,Prompt}
825
    modes::Vector{Symbol}
826
end
UNCOV
827
REPLHistoryProvider(mode_mapping::Dict{Symbol}) =
×
828
    REPLHistoryProvider(String[], "", nothing, 0, 0, -1, IOBuffer(),
829
                        nothing, mode_mapping, UInt8[])
830

831
invalid_history_message(path::String) = """
×
832
Invalid history file ($path) format:
833
If you have a history file left over from an older version of Julia,
834
try renaming or deleting it.
835
Invalid character: """
836

837
munged_history_message(path::String) = """
×
838
Invalid history file ($path) format:
839
An editor may have converted tabs to spaces at line """
840

UNCOV
841
function hist_open_file(hp::REPLHistoryProvider)
×
UNCOV
842
    f = open(hp.file_path, read=true, write=true, create=true)
×
UNCOV
843
    hp.history_file = f
×
UNCOV
844
    seekend(f)
×
845
end
846

UNCOV
847
function hist_from_file(hp::REPLHistoryProvider, path::String)
×
UNCOV
848
    getline(lines, i) = i > length(lines) ? "" : lines[i]
×
UNCOV
849
    file_lines = readlines(path)
×
850
    countlines = 0
×
UNCOV
851
    while true
×
852
        # First parse the metadata that starts with '#' in particular the REPL mode
UNCOV
853
        countlines += 1
×
UNCOV
854
        line = getline(file_lines, countlines)
×
855
        mode = :julia
×
UNCOV
856
        isempty(line) && break
×
UNCOV
857
        line[1] != '#' &&
×
858
            error(invalid_history_message(path), repr(line[1]), " at line ", countlines)
UNCOV
859
        while !isempty(line)
×
UNCOV
860
            startswith(line, '#') || break
×
UNCOV
861
            if startswith(line, "# mode: ")
×
UNCOV
862
                mode = Symbol(SubString(line, 9))
×
863
            end
UNCOV
864
            countlines += 1
×
UNCOV
865
            line = getline(file_lines, countlines)
×
UNCOV
866
        end
×
UNCOV
867
        isempty(line) && break
×
868

869
        # Now parse the code for the current REPL mode
UNCOV
870
        line[1] == ' '  &&
×
871
            error(munged_history_message(path), countlines)
UNCOV
872
        line[1] != '\t' &&
×
873
            error(invalid_history_message(path), repr(line[1]), " at line ", countlines)
UNCOV
874
        lines = String[]
×
UNCOV
875
        while !isempty(line)
×
UNCOV
876
            push!(lines, chomp(SubString(line, 2)))
×
UNCOV
877
            next_line = getline(file_lines, countlines+1)
×
UNCOV
878
            isempty(next_line) && break
×
UNCOV
879
            first(next_line) == ' '  && error(munged_history_message(path), countlines)
×
880
            # A line not starting with a tab means we are done with code for this entry
UNCOV
881
            first(next_line) != '\t' && break
×
882
            countlines += 1
×
883
            line = getline(file_lines, countlines)
×
884
        end
×
UNCOV
885
        push!(hp.modes, mode)
×
UNCOV
886
        push!(hp.history, join(lines, '\n'))
×
UNCOV
887
    end
×
UNCOV
888
    hp.start_idx = length(hp.history)
×
UNCOV
889
    return hp
×
890
end
891

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

UNCOV
920
function history_move(s::Union{LineEdit.MIState,LineEdit.PrefixSearchState}, hist::REPLHistoryProvider, idx::Int, save_idx::Int = hist.cur_idx)
×
UNCOV
921
    max_idx = length(hist.history) + 1
×
UNCOV
922
    @assert 1 <= hist.cur_idx <= max_idx
×
UNCOV
923
    (1 <= idx <= max_idx) || return :none
×
UNCOV
924
    idx != hist.cur_idx || return :none
×
925

926
    # save the current line
UNCOV
927
    if save_idx == max_idx
×
UNCOV
928
        hist.last_mode = LineEdit.mode(s)
×
UNCOV
929
        hist.last_buffer = copy(LineEdit.buffer(s))
×
930
    else
UNCOV
931
        hist.history[save_idx] = LineEdit.input_string(s)
×
UNCOV
932
        hist.modes[save_idx] = mode_idx(hist, LineEdit.mode(s))
×
933
    end
934

935
    # load the saved line
UNCOV
936
    if idx == max_idx
×
UNCOV
937
        last_buffer = hist.last_buffer
×
UNCOV
938
        LineEdit.transition(s, hist.last_mode) do
×
UNCOV
939
            LineEdit.replace_line(s, last_buffer)
×
940
        end
UNCOV
941
        hist.last_mode = nothing
×
UNCOV
942
        hist.last_buffer = IOBuffer()
×
943
    else
UNCOV
944
        if haskey(hist.mode_mapping, hist.modes[idx])
×
UNCOV
945
            LineEdit.transition(s, hist.mode_mapping[hist.modes[idx]]) do
×
UNCOV
946
                LineEdit.replace_line(s, hist.history[idx])
×
947
            end
948
        else
UNCOV
949
            return :skip
×
950
        end
951
    end
UNCOV
952
    hist.cur_idx = idx
×
953

UNCOV
954
    return :ok
×
955
end
956

957
# REPL History can also transitions modes
UNCOV
958
function LineEdit.accept_result_newmode(hist::REPLHistoryProvider)
×
UNCOV
959
    if 1 <= hist.cur_idx <= length(hist.modes)
×
UNCOV
960
        return hist.mode_mapping[hist.modes[hist.cur_idx]]
×
961
    end
UNCOV
962
    return nothing
×
963
end
964

UNCOV
965
function history_prev(s::LineEdit.MIState, hist::REPLHistoryProvider,
×
966
                      num::Int=1, save_idx::Int = hist.cur_idx)
UNCOV
967
    num <= 0 && return history_next(s, hist, -num, save_idx)
×
UNCOV
968
    hist.last_idx = -1
×
UNCOV
969
    m = history_move(s, hist, hist.cur_idx-num, save_idx)
×
UNCOV
970
    if m === :ok
×
UNCOV
971
        LineEdit.move_input_start(s)
×
UNCOV
972
        LineEdit.reset_key_repeats(s) do
×
UNCOV
973
            LineEdit.move_line_end(s)
×
974
        end
UNCOV
975
        return LineEdit.refresh_line(s)
×
UNCOV
976
    elseif m === :skip
×
UNCOV
977
        return history_prev(s, hist, num+1, save_idx)
×
978
    else
979
        return Terminals.beep(s)
×
980
    end
981
end
982

UNCOV
983
function history_next(s::LineEdit.MIState, hist::REPLHistoryProvider,
×
984
                      num::Int=1, save_idx::Int = hist.cur_idx)
UNCOV
985
    if num == 0
×
986
        Terminals.beep(s)
×
987
        return
×
988
    end
UNCOV
989
    num < 0 && return history_prev(s, hist, -num, save_idx)
×
UNCOV
990
    cur_idx = hist.cur_idx
×
UNCOV
991
    max_idx = length(hist.history) + 1
×
UNCOV
992
    if cur_idx == max_idx && 0 < hist.last_idx
×
993
        # issue #6312
994
        cur_idx = hist.last_idx
×
995
        hist.last_idx = -1
×
996
    end
UNCOV
997
    m = history_move(s, hist, cur_idx+num, save_idx)
×
UNCOV
998
    if m === :ok
×
UNCOV
999
        LineEdit.move_input_end(s)
×
UNCOV
1000
        return LineEdit.refresh_line(s)
×
UNCOV
1001
    elseif m === :skip
×
UNCOV
1002
        return history_next(s, hist, num+1, save_idx)
×
1003
    else
UNCOV
1004
        return Terminals.beep(s)
×
1005
    end
1006
end
1007

UNCOV
1008
history_first(s::LineEdit.MIState, hist::REPLHistoryProvider) =
×
1009
    history_prev(s, hist, hist.cur_idx - 1 -
1010
                 (hist.cur_idx > hist.start_idx+1 ? hist.start_idx : 0))
1011

UNCOV
1012
history_last(s::LineEdit.MIState, hist::REPLHistoryProvider) =
×
1013
    history_next(s, hist, length(hist.history) - hist.cur_idx + 1)
1014

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

UNCOV
1056
function history_search(hist::REPLHistoryProvider, query_buffer::IOBuffer, response_buffer::IOBuffer,
×
1057
                        backwards::Bool=false, skip_current::Bool=false)
1058

UNCOV
1059
    qpos = position(query_buffer)
×
UNCOV
1060
    qpos > 0 || return true
×
UNCOV
1061
    searchdata = beforecursor(query_buffer)
×
UNCOV
1062
    response_str = String(take!(copy(response_buffer)))
×
1063

1064
    # Alright, first try to see if the current match still works
UNCOV
1065
    a = position(response_buffer) + 1 # position is zero-indexed
×
1066
    # FIXME: I'm pretty sure this is broken since it uses an index
1067
    # into the search data to index into the response string
UNCOV
1068
    b = a + sizeof(searchdata)
×
UNCOV
1069
    b = b ≤ ncodeunits(response_str) ? prevind(response_str, b) : b-1
×
UNCOV
1070
    b = min(lastindex(response_str), b) # ensure that b is valid
×
1071

UNCOV
1072
    searchstart = backwards ? b : a
×
UNCOV
1073
    if searchdata == response_str[a:b]
×
UNCOV
1074
        if skip_current
×
UNCOV
1075
            searchstart = backwards ? prevind(response_str, b) : nextind(response_str, a)
×
1076
        else
UNCOV
1077
            return true
×
1078
        end
1079
    end
1080

1081
    # Start searching
1082
    # First the current response buffer
UNCOV
1083
    if 1 <= searchstart <= lastindex(response_str)
×
UNCOV
1084
        match = backwards ? findprev(searchdata, response_str, searchstart) :
×
1085
                            findnext(searchdata, response_str, searchstart)
UNCOV
1086
        if match !== nothing
×
UNCOV
1087
            seek(response_buffer, first(match) - 1)
×
UNCOV
1088
            return true
×
1089
        end
1090
    end
1091

1092
    # Now search all the other buffers
UNCOV
1093
    idxs = backwards ? ((hist.cur_idx-1):-1:1) : ((hist.cur_idx+1):1:length(hist.history))
×
UNCOV
1094
    for idx in idxs
×
UNCOV
1095
        h = hist.history[idx]
×
UNCOV
1096
        match = backwards ? findlast(searchdata, h) : findfirst(searchdata, h)
×
UNCOV
1097
        if match !== nothing && h != response_str && haskey(hist.mode_mapping, hist.modes[idx])
×
UNCOV
1098
            truncate(response_buffer, 0)
×
UNCOV
1099
            write(response_buffer, h)
×
UNCOV
1100
            seek(response_buffer, first(match) - 1)
×
UNCOV
1101
            hist.cur_idx = idx
×
UNCOV
1102
            return true
×
1103
        end
UNCOV
1104
    end
×
1105

UNCOV
1106
    return false
×
1107
end
1108

UNCOV
1109
function history_reset_state(hist::REPLHistoryProvider)
×
UNCOV
1110
    if hist.cur_idx != length(hist.history) + 1
×
UNCOV
1111
        hist.last_idx = hist.cur_idx
×
UNCOV
1112
        hist.cur_idx = length(hist.history) + 1
×
1113
    end
UNCOV
1114
    nothing
×
1115
end
UNCOV
1116
LineEdit.reset_state(hist::REPLHistoryProvider) = history_reset_state(hist)
×
1117

UNCOV
1118
function return_callback(s)
×
UNCOV
1119
    ast = Base.parse_input_line(String(take!(copy(LineEdit.buffer(s)))), depwarn=false)
×
UNCOV
1120
    return !(isa(ast, Expr) && ast.head === :incomplete)
×
1121
end
1122

UNCOV
1123
find_hist_file() = get(ENV, "JULIA_HISTORY",
×
1124
                       !isempty(DEPOT_PATH) ? joinpath(DEPOT_PATH[1], "logs", "repl_history.jl") :
1125
                       error("DEPOT_PATH is empty and ENV[\"JULIA_HISTORY\"] not set."))
1126

UNCOV
1127
backend(r::AbstractREPL) = r.backendref
×
1128

UNCOV
1129
function eval_with_backend(ast, backend::REPLBackendRef)
×
UNCOV
1130
    put!(backend.repl_channel, (ast, 1))
×
UNCOV
1131
    return take!(backend.response_channel) # (val, iserr)
×
1132
end
1133

UNCOV
1134
function respond(f, repl, main; pass_empty::Bool = false, suppress_on_semicolon::Bool = true)
×
UNCOV
1135
    return function do_respond(s::MIState, buf, ok::Bool)
×
UNCOV
1136
        if !ok
×
UNCOV
1137
            return transition(s, :abort)
×
1138
        end
UNCOV
1139
        line = String(take!(buf)::Vector{UInt8})
×
UNCOV
1140
        if !isempty(line) || pass_empty
×
UNCOV
1141
            reset(repl)
×
UNCOV
1142
            local response
×
UNCOV
1143
            try
×
UNCOV
1144
                ast = Base.invokelatest(f, line)
×
UNCOV
1145
                response = eval_with_backend(ast, backend(repl))
×
1146
            catch
UNCOV
1147
                response = Pair{Any, Bool}(current_exceptions(), true)
×
1148
            end
UNCOV
1149
            hide_output = suppress_on_semicolon && ends_with_semicolon(line)
×
UNCOV
1150
            print_response(repl, response, !hide_output, hascolor(repl))
×
1151
        end
UNCOV
1152
        prepare_next(repl)
×
UNCOV
1153
        reset_state(s)
×
UNCOV
1154
        return s.current_mode.sticky ? true : transition(s, main)
×
1155
    end
1156
end
1157

UNCOV
1158
function reset(repl::LineEditREPL)
×
UNCOV
1159
    raw!(repl.t, false)
×
UNCOV
1160
    hascolor(repl) && print(repl.t, Base.text_colors[:normal])
×
UNCOV
1161
    nothing
×
1162
end
1163

UNCOV
1164
function prepare_next(repl::LineEditREPL)
×
UNCOV
1165
    println(terminal(repl))
×
1166
end
1167

UNCOV
1168
function mode_keymap(julia_prompt::Prompt)
×
UNCOV
1169
    AnyDict(
×
UNCOV
1170
    '\b' => function (s::MIState,o...)
×
UNCOV
1171
        if isempty(s) || position(LineEdit.buffer(s)) == 0
×
UNCOV
1172
            buf = copy(LineEdit.buffer(s))
×
UNCOV
1173
            transition(s, julia_prompt) do
×
UNCOV
1174
                LineEdit.state(s, julia_prompt).input_buffer = buf
×
1175
            end
1176
        else
1177
            LineEdit.edit_backspace(s)
×
1178
        end
1179
    end,
UNCOV
1180
    "^C" => function (s::MIState,o...)
×
UNCOV
1181
        LineEdit.move_input_end(s)
×
UNCOV
1182
        LineEdit.refresh_line(s)
×
UNCOV
1183
        print(LineEdit.terminal(s), "^C\n\n")
×
UNCOV
1184
        transition(s, julia_prompt)
×
UNCOV
1185
        transition(s, :reset)
×
UNCOV
1186
        LineEdit.refresh_line(s)
×
1187
    end)
1188
end
1189

UNCOV
1190
repl_filename(repl, hp::REPLHistoryProvider) = "REPL[$(max(length(hp.history)-hp.start_idx, 1))]"
×
1191
repl_filename(repl, hp) = "REPL"
×
1192

1193
const JL_PROMPT_PASTE = Ref(true)
1194
enable_promptpaste(v::Bool) = JL_PROMPT_PASTE[] = v
×
1195

UNCOV
1196
function contextual_prompt(repl::LineEditREPL, prompt::Union{String,Function})
×
UNCOV
1197
    function ()
×
UNCOV
1198
        mod = active_module(repl)
×
UNCOV
1199
        prefix = mod == Main ? "" : string('(', mod, ") ")
×
UNCOV
1200
        pr = prompt isa String ? prompt : prompt()
×
UNCOV
1201
        prefix * pr
×
1202
    end
1203
end
1204

UNCOV
1205
setup_interface(
×
1206
    repl::LineEditREPL;
1207
    # those keyword arguments may be deprecated eventually in favor of the Options mechanism
1208
    hascolor::Bool = repl.options.hascolor,
1209
    extra_repl_keymap::Any = repl.options.extra_keymap
1210
) = setup_interface(repl, hascolor, extra_repl_keymap)
1211

1212

1213
# This non keyword method can be precompiled which is important
UNCOV
1214
function setup_interface(
×
1215
    repl::LineEditREPL,
1216
    hascolor::Bool,
1217
    extra_repl_keymap::Any, # Union{Dict,Vector{<:Dict}},
1218
)
1219
    # The precompile statement emitter has problem outputting valid syntax for the
1220
    # type of `Union{Dict,Vector{<:Dict}}` (see #28808).
1221
    # This function is however important to precompile for REPL startup time, therefore,
1222
    # make the type Any and just assert that we have the correct type below.
UNCOV
1223
    @assert extra_repl_keymap isa Union{Dict,Vector{<:Dict}}
×
1224

1225
    ###
1226
    #
1227
    # This function returns the main interface that describes the REPL
1228
    # functionality, it is called internally by functions that setup a
1229
    # Terminal-based REPL frontend.
1230
    #
1231
    # See run_frontend(repl::LineEditREPL, backend::REPLBackendRef)
1232
    # for usage
1233
    #
1234
    ###
1235

1236
    ###
1237
    # We setup the interface in two stages.
1238
    # First, we set up all components (prompt,rsearch,shell,help)
1239
    # Second, we create keymaps with appropriate transitions between them
1240
    #   and assign them to the components
1241
    #
1242
    ###
1243

1244
    ############################### Stage I ################################
1245

1246
    # This will provide completions for REPL and help mode
UNCOV
1247
    replc = REPLCompletionProvider()
×
1248

1249
    # Set up the main Julia prompt
UNCOV
1250
    julia_prompt = Prompt(contextual_prompt(repl, JULIA_PROMPT);
×
1251
        # Copy colors from the prompt object
1252
        prompt_prefix = hascolor ? repl.prompt_color : "",
1253
        prompt_suffix = hascolor ?
1254
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1255
        repl = repl,
1256
        complete = replc,
1257
        on_enter = return_callback)
1258

1259
    # Setup help mode
UNCOV
1260
    help_mode = Prompt(contextual_prompt(repl, "help?> "),
×
1261
        prompt_prefix = hascolor ? repl.help_color : "",
1262
        prompt_suffix = hascolor ?
1263
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1264
        repl = repl,
1265
        complete = replc,
1266
        # When we're done transform the entered line into a call to helpmode function
UNCOV
1267
        on_done = respond(line::String->helpmode(outstream(repl), line, repl.mistate.active_module),
×
1268
                          repl, julia_prompt, pass_empty=true, suppress_on_semicolon=false))
1269

1270

1271
    # Set up shell mode
UNCOV
1272
    shell_mode = Prompt(SHELL_PROMPT;
×
1273
        prompt_prefix = hascolor ? repl.shell_color : "",
1274
        prompt_suffix = hascolor ?
1275
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1276
        repl = repl,
1277
        complete = ShellCompletionProvider(),
1278
        # Transform "foo bar baz" into `foo bar baz` (shell quoting)
1279
        # and pass into Base.repl_cmd for processing (handles `ls` and `cd`
1280
        # special)
1281
        on_done = respond(repl, julia_prompt) do line
UNCOV
1282
            Expr(:call, :(Base.repl_cmd),
×
1283
                :(Base.cmd_gen($(Base.shell_parse(line::String)[1]))),
1284
                outstream(repl))
1285
        end,
1286
        sticky = true)
1287

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

1316

1317
    ################################# Stage II #############################
1318

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

UNCOV
1349
    julia_prompt.on_done = respond(x->Base.parse_input_line(x,filename=repl_filename(repl,hp)), repl, julia_prompt)
×
1350

1351

UNCOV
1352
    search_prompt, skeymap = LineEdit.setup_search_keymap(hp)
×
UNCOV
1353
    search_prompt.complete = LatexCompletions()
×
1354

1355
    shell_prompt_len = length(SHELL_PROMPT)
×
1356
    help_prompt_len = length(HELP_PROMPT)
×
1357
    jl_prompt_regex = r"^In \[[0-9]+\]: |^(?:\(.+\) )?julia> "
×
1358
    pkg_prompt_regex = r"^(?:\(.+\) )?pkg> "
×
1359

1360
    # Canonicalize user keymap input
UNCOV
1361
    if isa(extra_repl_keymap, Dict)
×
1362
        extra_repl_keymap = AnyDict[extra_repl_keymap]
×
1363
    end
1364

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

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

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

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

UNCOV
1588
    prefix_prompt, prefix_keymap = LineEdit.setup_prefix_keymap(hp, julia_prompt)
×
1589

UNCOV
1590
    a = Dict{Any,Any}[skeymap, repl_keymap, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
×
UNCOV
1591
    prepend!(a, extra_repl_keymap)
×
1592

UNCOV
1593
    julia_prompt.keymap_dict = LineEdit.keymap(a)
×
1594

UNCOV
1595
    mk = mode_keymap(julia_prompt)
×
1596

UNCOV
1597
    b = Dict{Any,Any}[skeymap, mk, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
×
UNCOV
1598
    prepend!(b, extra_repl_keymap)
×
1599

UNCOV
1600
    shell_mode.keymap_dict = help_mode.keymap_dict = dummy_pkg_mode.keymap_dict = LineEdit.keymap(b)
×
1601

UNCOV
1602
    allprompts = LineEdit.TextInterface[julia_prompt, shell_mode, help_mode, dummy_pkg_mode, search_prompt, prefix_prompt]
×
UNCOV
1603
    return ModalInterface(allprompts)
×
1604
end
1605

UNCOV
1606
function run_frontend(repl::LineEditREPL, backend::REPLBackendRef)
×
UNCOV
1607
    repl.frontend_task = current_task()
×
UNCOV
1608
    d = REPLDisplay(repl)
×
UNCOV
1609
    dopushdisplay = repl.specialdisplay === nothing && !in(d,Base.Multimedia.displays)
×
UNCOV
1610
    dopushdisplay && pushdisplay(d)
×
UNCOV
1611
    if !isdefined(repl,:interface)
×
UNCOV
1612
        interface = repl.interface = setup_interface(repl)
×
1613
    else
UNCOV
1614
        interface = repl.interface
×
1615
    end
UNCOV
1616
    repl.backendref = backend
×
UNCOV
1617
    repl.mistate = LineEdit.init_state(terminal(repl), interface)
×
UNCOV
1618
    run_interface(terminal(repl), interface, repl.mistate)
×
1619
    # Terminate Backend
UNCOV
1620
    put!(backend.repl_channel, (nothing, -1))
×
UNCOV
1621
    dopushdisplay && popdisplay(d)
×
UNCOV
1622
    nothing
×
1623
end
1624

1625
## StreamREPL ##
1626

1627
mutable struct StreamREPL <: AbstractREPL
1628
    stream::IO
1629
    prompt_color::String
1630
    input_color::String
1631
    answer_color::String
1632
    waserror::Bool
1633
    frontend_task::Task
1634
    StreamREPL(stream,pc,ic,ac) = new(stream,pc,ic,ac,false)
×
1635
end
1636
StreamREPL(stream::IO) = StreamREPL(stream, Base.text_colors[:green], Base.input_color(), Base.answer_color())
×
1637
run_repl(stream::IO) = run_repl(StreamREPL(stream))
×
1638

1639
outstream(s::StreamREPL) = s.stream
×
1640
hascolor(s::StreamREPL) = get(s.stream, :color, false)::Bool
×
1641

1642
answer_color(r::LineEditREPL) = r.envcolors ? Base.answer_color() : r.answer_color
×
1643
answer_color(r::StreamREPL) = r.answer_color
×
1644
input_color(r::LineEditREPL) = r.envcolors ? Base.input_color() : r.input_color
×
1645
input_color(r::StreamREPL) = r.input_color
×
1646

1647
let matchend = Dict("\"" => r"\"", "\"\"\"" => r"\"\"\"", "'" => r"'",
1648
    "`" => r"`", "```" => r"```", "#" => r"$"m, "#=" => r"=#|#=")
1649
    global _rm_strings_and_comments
UNCOV
1650
    function _rm_strings_and_comments(code::Union{String,SubString{String}})
×
UNCOV
1651
        buf = IOBuffer(sizehint = sizeof(code))
×
1652
        pos = 1
×
UNCOV
1653
        while true
×
UNCOV
1654
            i = findnext(r"\"(?!\"\")|\"\"\"|'|`(?!``)|```|#(?!=)|#=", code, pos)
×
UNCOV
1655
            isnothing(i) && break
×
UNCOV
1656
            match = SubString(code, i)
×
UNCOV
1657
            j = findnext(matchend[match]::Regex, code, nextind(code, last(i)))
×
UNCOV
1658
            if match == "#=" # possibly nested
×
1659
                nested = 1
×
UNCOV
1660
                while j !== nothing
×
UNCOV
1661
                    nested += SubString(code, j) == "#=" ? +1 : -1
×
UNCOV
1662
                    iszero(nested) && break
×
UNCOV
1663
                    j = findnext(r"=#|#=", code, nextind(code, last(j)))
×
UNCOV
1664
                end
×
UNCOV
1665
            elseif match[1] != '#' # quote match: check non-escaped
×
UNCOV
1666
                while j !== nothing
×
UNCOV
1667
                    notbackslash = findprev(!=('\\'), code, prevind(code, first(j)))::Int
×
UNCOV
1668
                    isodd(first(j) - notbackslash) && break # not escaped
×
UNCOV
1669
                    j = findnext(matchend[match]::Regex, code, nextind(code, first(j)))
×
UNCOV
1670
                end
×
1671
            end
UNCOV
1672
            isnothing(j) && break
×
UNCOV
1673
            if match[1] == '#'
×
UNCOV
1674
                print(buf, SubString(code, pos, prevind(code, first(i))))
×
1675
            else
UNCOV
1676
                print(buf, SubString(code, pos, last(i)), ' ', SubString(code, j))
×
1677
            end
UNCOV
1678
            pos = nextind(code, last(j))
×
UNCOV
1679
        end
×
UNCOV
1680
        print(buf, SubString(code, pos, lastindex(code)))
×
UNCOV
1681
        return String(take!(buf))
×
1682
    end
1683
end
1684

1685
# heuristic function to decide if the presence of a semicolon
1686
# at the end of the expression was intended for suppressing output
1687
ends_with_semicolon(code::AbstractString) = ends_with_semicolon(String(code))
×
UNCOV
1688
ends_with_semicolon(code::Union{String,SubString{String}}) =
×
UNCOV
1689
    contains(_rm_strings_and_comments(code), r";\s*$")
×
1690

UNCOV
1691
function banner(io::IO = stdout; short = false)
×
1692
    if Base.GIT_VERSION_INFO.tagged_commit
×
1693
        commit_string = Base.TAGGED_RELEASE_BANNER
×
1694
    elseif isempty(Base.GIT_VERSION_INFO.commit)
×
1695
        commit_string = ""
×
1696
    else
UNCOV
1697
        days = Int(floor((ccall(:jl_clock_now, Float64, ()) - Base.GIT_VERSION_INFO.fork_master_timestamp) / (60 * 60 * 24)))
×
UNCOV
1698
        days = max(0, days)
×
UNCOV
1699
        unit = days == 1 ? "day" : "days"
×
1700
        distance = Base.GIT_VERSION_INFO.fork_master_distance
×
UNCOV
1701
        commit = Base.GIT_VERSION_INFO.commit_short
×
1702

1703
        if distance == 0
×
UNCOV
1704
            commit_string = "Commit $(commit) ($(days) $(unit) old master)"
×
1705
        else
1706
            branch = Base.GIT_VERSION_INFO.branch
×
1707
            commit_string = "$(branch)/$(commit) (fork: $(distance) commits, $(days) $(unit))"
×
1708
        end
1709
    end
1710

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

UNCOV
1713
    if get(io, :color, false)::Bool
×
1714
        c = Base.text_colors
×
1715
        tx = c[:normal] # text
×
1716
        jl = c[:normal] # julia
×
1717
        d1 = c[:bold] * c[:blue]    # first dot
×
1718
        d2 = c[:bold] * c[:red]     # second dot
×
1719
        d3 = c[:bold] * c[:green]   # third dot
×
1720
        d4 = c[:bold] * c[:magenta] # fourth dot
×
1721

1722
        if short
×
1723
            print(io,"""
×
1724
              $(d3)o$(tx)  | Version $(VERSION)$(commit_date)
1725
             $(d2)o$(tx) $(d4)o$(tx) | $(commit_string)
1726
            """)
1727
        else
1728
            print(io,"""               $(d3)_$(tx)
×
1729
               $(d1)_$(tx)       $(jl)_$(tx) $(d2)_$(d3)(_)$(d4)_$(tx)     |  Documentation: https://docs.julialang.org
1730
              $(d1)(_)$(jl)     | $(d2)(_)$(tx) $(d4)(_)$(tx)    |
1731
               $(jl)_ _   _| |_  __ _$(tx)   |  Type \"?\" for help, \"]?\" for Pkg help.
1732
              $(jl)| | | | | | |/ _` |$(tx)  |
1733
              $(jl)| | |_| | | | (_| |$(tx)  |  Version $(VERSION)$(commit_date)
1734
             $(jl)_/ |\\__'_|_|_|\\__'_|$(tx)  |  $(commit_string)
1735
            $(jl)|__/$(tx)                   |
1736

1737
            """)
1738
        end
1739
    else
UNCOV
1740
        if short
×
UNCOV
1741
            print(io,"""
×
1742
              o  |  Version $(VERSION)$(commit_date)
1743
             o o |  $(commit_string)
1744
            """)
1745
        else
UNCOV
1746
            print(io,"""
×
1747
                           _
1748
               _       _ _(_)_     |  Documentation: https://docs.julialang.org
1749
              (_)     | (_) (_)    |
1750
               _ _   _| |_  __ _   |  Type \"?\" for help, \"]?\" for Pkg help.
1751
              | | | | | | |/ _` |  |
1752
              | | |_| | | | (_| |  |  Version $(VERSION)$(commit_date)
1753
             _/ |\\__'_|_|_|\\__'_|  |  $(commit_string)
1754
            |__/                   |
1755

1756
            """)
1757
        end
1758
    end
1759
end
1760

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

1792
module Numbered
1793

1794
using ..REPL
1795

1796
__current_ast_transforms() = isdefined(Base, :active_repl_backend) ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1797

UNCOV
1798
function repl_eval_counter(hp)
×
UNCOV
1799
    return length(hp.history) - hp.start_idx
×
1800
end
1801

UNCOV
1802
function out_transform(@nospecialize(x), n::Ref{Int})
×
UNCOV
1803
    return Expr(:toplevel, get_usings!([], x)..., quote
×
UNCOV
1804
        let __temp_val_a72df459 = $x
×
UNCOV
1805
            $capture_result($n, __temp_val_a72df459)
×
UNCOV
1806
            __temp_val_a72df459
×
1807
        end
1808
    end)
1809
end
1810

UNCOV
1811
function get_usings!(usings, ex)
×
UNCOV
1812
    ex isa Expr || return usings
×
1813
    # get all `using` and `import` statements which are at the top level
UNCOV
1814
    for (i, arg) in enumerate(ex.args)
×
UNCOV
1815
        if Base.isexpr(arg, :toplevel)
×
UNCOV
1816
            get_usings!(usings, arg)
×
UNCOV
1817
        elseif Base.isexpr(arg, [:using, :import])
×
UNCOV
1818
            push!(usings, popat!(ex.args, i))
×
1819
        end
UNCOV
1820
    end
×
UNCOV
1821
    return usings
×
1822
end
1823

UNCOV
1824
function capture_result(n::Ref{Int}, @nospecialize(x))
×
UNCOV
1825
    n = n[]
×
UNCOV
1826
    mod = Base.MainInclude
×
UNCOV
1827
    if !isdefined(mod, :Out)
×
UNCOV
1828
        @eval mod global Out
×
UNCOV
1829
        @eval mod export Out
×
UNCOV
1830
        setglobal!(mod, :Out, Dict{Int, Any}())
×
1831
    end
UNCOV
1832
    if x !== getglobal(mod, :Out) && x !== nothing # remove this?
×
UNCOV
1833
        getglobal(mod, :Out)[n] = x
×
1834
    end
UNCOV
1835
    nothing
×
1836
end
1837

UNCOV
1838
function set_prompt(repl::LineEditREPL, n::Ref{Int})
×
UNCOV
1839
    julia_prompt = repl.interface.modes[1]
×
UNCOV
1840
    julia_prompt.prompt = function()
×
UNCOV
1841
        n[] = repl_eval_counter(julia_prompt.hist)+1
×
UNCOV
1842
        string("In [", n[], "]: ")
×
1843
    end
UNCOV
1844
    nothing
×
1845
end
1846

UNCOV
1847
function set_output_prefix(repl::LineEditREPL, n::Ref{Int})
×
UNCOV
1848
    julia_prompt = repl.interface.modes[1]
×
UNCOV
1849
    if REPL.hascolor(repl)
×
UNCOV
1850
        julia_prompt.output_prefix_prefix = Base.text_colors[:red]
×
1851
    end
UNCOV
1852
    julia_prompt.output_prefix = () -> string("Out[", n[], "]: ")
×
UNCOV
1853
    nothing
×
1854
end
1855

UNCOV
1856
function __current_ast_transforms(backend)
×
UNCOV
1857
    if backend === nothing
×
1858
        isdefined(Base, :active_repl_backend) ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1859
    else
UNCOV
1860
        backend.ast_transforms
×
1861
    end
1862
end
1863

UNCOV
1864
function numbered_prompt!(repl::LineEditREPL=Base.active_repl, backend=nothing)
×
UNCOV
1865
    n = Ref{Int}(0)
×
UNCOV
1866
    set_prompt(repl, n)
×
UNCOV
1867
    set_output_prefix(repl, n)
×
UNCOV
1868
    push!(__current_ast_transforms(backend), @nospecialize(ast) -> out_transform(ast, n))
×
UNCOV
1869
    return
×
1870
end
1871

1872
"""
1873
    Out[n]
1874

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

1878
See also [`ans`](@ref).
1879
"""
1880
Base.MainInclude.Out
1881

1882
end
1883

1884
import .Numbered.numbered_prompt!
1885

1886
# this assignment won't survive precompilation,
1887
# but will stick if REPL is baked into a sysimg.
1888
# Needs to occur after this module is finished.
1889
Base.REPL_MODULE_REF[] = REPL
1890

1891
if Base.generating_output()
1892
    include("precompile.jl")
1893
end
1894

1895
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